Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,7 @@ config.json
logs

# No screenshots
screenshots
screenshots

# No schedules
schedules.json
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ Central repository for macless client configurations without having to keep trac
You can also pre-create devices and assign configs yourself if needed.

## Features
- Custom config assignments
- Screenshot preview
- Device logging
- Device endpoint tooling
- and more...
- Custom config assignments
- Screenshot preview
- Device logging
- Device endpoint tooling
- and more...

## Installation
1.) Clone repository `git clone https://github.com/versx/DeviceConfigManager`
Expand All @@ -20,6 +20,9 @@ You can also pre-create devices and assign configs yourself if needed.
5.) Run `npm run start`
6.) Access via http://machineip:port/ using username: `root` and password `pass123!`

## Notes
If you use HAProxy, make sure to set `option forwardfor` in your haproxy.cfg if you are not passing the x-forward-for header so the correct IP addresses are saved.

## PM2 (recommended)
Once everything is setup and running appropriately, you can add this to PM2 ecosystem.config.js file so it is automatically started:
```
Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"moment-timezone": "^0.5.28",
"express-session": "^1.17.1",
"multer": "^1.4.2",
"mustache": "^4.0.1",
Expand Down
4 changes: 3 additions & 1 deletion src/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ function query(sql, args) {
// The Promise constructor should catch any errors thrown on
// this tick. Alternately, try/catch and reject(err) on catch.
var conn = getConnection();
/* eslint-disable no-unused-vars */
conn.query(sql, args, function(err, rows, fields) {
/* eslint-enable no-unused-vars */
// Call reject on error states,
// call resolve with results
if (err) {
Expand All @@ -40,7 +42,7 @@ function query(sql, args) {
resolve(rows);
conn.end(function(err, args) {
if (err) {
console.error('Failed to close mysql connection.');
console.error('Failed to close mysql connection:', args);
return;
}
});
Expand Down
58 changes: 58 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ const config = require('./config.json');
const Device = require('./models/device.js');
const Config = require('./models/config.js');
const Migrator = require('./migrator.js');
const ScheduleManager = require('./models/schedule-manager.js');
const apiRoutes = require('./routes/api.js');

const timezones = require('../static/data/timezones.json');

// TODO: Create route classes
// TODO: Error checking/handling

Expand All @@ -32,6 +35,7 @@ app.set('view engine', 'mustache');
app.set('views', path.resolve(__dirname, 'views'));
app.engine('mustache', mustacheExpress());
app.use(bodyParser.urlencoded({ extended: false, limit: '50mb' })); // for parsing application/x-www-form-urlencoded
//app.use(bodyParser.raw({ type: 'application/x-www-form-urlencoded' }));
app.use(express.static(path.resolve(__dirname, '../static')));
app.use('/screenshots', express.static(path.resolve(__dirname, '../screenshots')));

Expand Down Expand Up @@ -208,6 +212,60 @@ app.get('/config/delete/:name', function(req, res) {
res.render('config-delete', data);
});

// Schedule UI Routes
app.get('/schedules', function(req, res) {
res.render('schedules', defaultData);
});

app.get('/schedule/new', async function(req, res) {
var data = defaultData;
var configs = await Config.getAll();
var devices = await Device.getAll();
data.configs = configs;
data.devices = devices;
data.timezones = timezones;
res.render('schedule-new', data);
});

app.get('/schedule/edit/:name', async function(req, res) {
var name = req.params.name;
var data = defaultData;
var configs = await Config.getAll();
var devices = await Device.getAll();
var schedule = ScheduleManager.getByName(name);
if (configs) {
configs.forEach(function(cfg) {
cfg.selected = cfg.name === schedule.config;
cfg.next_config_selected = cfg.name === schedule.next_config;
});
}
if (devices) {
devices.forEach(function(device) {
device.selected = schedule.uuids.includes(device.uuid);
});
}
data.old_name = name;
data.name = schedule.name;
data.configs = configs;
data.devices = devices;
data.start_time = schedule.start_time;
data.end_time = schedule.end_time;
data.timezone = schedule.timezone;
data.next_config = schedule.next_config;
data.enabled = schedule.enabled === 1 ? 'checked' : '';
timezones.forEach(function(timezone) {
timezone.selected = timezone.value === schedule.timezone;
});
data.timezones = timezones;
res.render('schedule-edit', data);
});

app.get('/schedule/delete/:name', function(req, res) {
var data = defaultData;
data.name = req.params.name;
res.render('schedule-delete', data);
});

// Settings UI Routes
app.get('/settings', function(req, res) {
res.render('settings', defaultData);
Expand Down
13 changes: 7 additions & 6 deletions src/migrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,9 @@ class Migrator {
var done = false;
while (!done) {
if (query === undefined || query === null) {
var message = `Failed to connect to database (as root) while initializing. Try: ${count}/10`;
console.error(`[DBController] Failed to connect to database (as root) while initializing. Try: ${count}/10`);
if (count === 10) {
console.error('[DBController]', message);
process.exit(-1);
} else {
console.error('[DBController]', message);
}
count++;
await utils.snooze(2500);
Expand Down Expand Up @@ -88,7 +85,7 @@ class Migrator {
let msql = sql.replace('&semi', ';').trim();
if (msql !== '') {
console.log('[DBController] Executing:', msql);
let results = await query(msql)
var result = await query(msql)
.then(x => x)
.catch(async err => {
console.error('[DBController] Migration failed:', err, '\r\nExecuting SQL statement:', msql);
Expand All @@ -109,6 +106,7 @@ class Migrator {
return null;
*/
});
console.log('[DBController] Migration execution result:', result);
}
});

Expand All @@ -121,9 +119,12 @@ class Migrator {
.then(x => x)
.catch(err => {
console.error('[DBController] Migration failed:', err);
process.exit(-1);
process.exit(
});
console.log('[DBController] Migration successful');
if (newVersion === toVersion) {
console.log('[DBController] Migration done');
}
this.migrate(newVersion, toVersion);
}
}
Expand Down
1 change: 0 additions & 1 deletion src/models/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ class Config {
if (result.length === 0) {
return null;
}
// TODO: Error checking
var c = result[0];
var data = new Config(
name,
Expand Down
165 changes: 165 additions & 0 deletions src/models/schedule-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
'use strict';

const fs = require('fs');
const path = require('path');
const moment = require('moment');

const Device = require('./device.js');
const utils = require('../utils.js');

const schedulesFile = path.resolve(__dirname, '../schedules.json');
const scheduleCheckInterval = 60 * 1000;

var lastUpdate = -2;

if (!fs.existsSync(schedulesFile)) {
fs.writeFileSync(schedulesFile, '{}');
}

class ScheduleManager {
static getAll() {
var json = fs.readFileSync(schedulesFile);
var schedules = JSON.parse(json, null, 2);
return schedules;
}
static create(name, config, uuids, startTime, endTime, timezone, nextConfig, enabled) {
var schedules = this.getAll();
schedules[name] = {
name: name,
config: config,
uuids: uuids,
start_time: startTime,
end_time: endTime,
timezone: parseInt(timezone),
next_config: nextConfig,
enabled: enabled
};
var result = this.save(schedules);
return result;
}
static getByName(name) {
var schedules = this.getAll();
if (schedules) {
return schedules[name];
}
return null;
}
static update(oldName, name, config, uuids, startTime, endTime, timezone, nextConfig, enabled) {
var result = this.delete(oldName);
if (result) {
result = this.create(name, config, uuids, startTime, endTime, timezone, nextConfig, enabled);
return result;
}
return false;
}
static delete(name) {
var schedules = this.getAll();
delete schedules[name];
var result = this.save(schedules);
return result;
}
static deleteAll() {
var result = this.save({});
return result;
}
static save(schedules) {
try {
var json = JSON.stringify(schedules, null, 2);
fs.writeFileSync(schedulesFile, json);
console.log('Schedules list updated');
return true;
} catch (e) {
console.error('save:', e);
}
return false;
}
static async checkSchedules() {
var now = todaySeconds();
if (lastUpdate === -2) {
utils.snooze(5000);
lastUpdate = parseInt(now);
return;
} else if (lastUpdate > now) {
lastUpdate = -1;
}

var schedules = ScheduleManager.getAll();
var values = Object.values(schedules);
values.forEach(async function(schedule) {
var startTimeSeconds = timeToSeconds(schedule.start_time);
var endTimeSeconds = timeToSeconds(schedule.end_time);
console.log('Now:', now, 'Last Update:', lastUpdate, 'Start:', startTimeSeconds, 'End:', endTimeSeconds);
console.log('Triggering schedule', schedule.name, 'in', startTimeSeconds - now, 'seconds');
if (schedule.enabled) {
if (startTimeSeconds !== 0 &&
endTimeSeconds !== 0 &&
now >= startTimeSeconds &&
now <= endTimeSeconds &&
lastUpdate < startTimeSeconds) {
await ScheduleManager.triggerSchedule(schedule, schedule.config);
} else if (startTimeSeconds !== 0 &&
endTimeSeconds !== 0 &&
now < startTimeSeconds &&
now > endTimeSeconds &&
lastUpdate < endTimeSeconds) {
await ScheduleManager.triggerSchedule(schedule, schedule.next_config);
}
}
});

utils.snooze(5000);
lastUpdate = parseInt(now);
}
static async triggerSchedule(schedule, config) {
console.log('Running schedule for', schedule, 'to assign config', config);
var uuids = schedule.uuids.split(',');
if (uuids) {
uuids.forEach(async function(uuid) {
var device = await Device.getByName(uuid);
// Check if the device config is not already set to the scheduled config to assign.
if (device.config !== config) {
device.config = config;
var result = await device.save();
if (result) {
// Success
console.log('Device', uuid, 'assigned config', config, 'successfully');
} else {
console.error('Failed to assign device', uuid, 'config', config);
}
}
});
}
}
}

function timeToSeconds(time) {
if (time) {
var split = time.split(':');
if (split.length === 3) {
var hours = parseInt(split[0]);
var minutes = parseInt(split[1]);
var seconds = parseInt(split[2]);
var timeNew = parseInt(hours * 3600 + minutes * 60 + seconds);
return timeNew;
}
}
return 0;
}

function todaySeconds() {
var date = moment();
var formattedDate = date.format('HH:mm:ss');
var split = formattedDate.split(':');
if (split.length >= 3) {
var hour = parseInt(split[0]) || 0;
var minute = parseInt(split[1]) || 0;
var second = parseInt(split[2]) || 0;
return hour * 3600 + minute * 60 + second;
} else {
return 0;
}
}

setInterval(ScheduleManager.checkSchedules, scheduleCheckInterval);

module.exports = ScheduleManager;
Loading