/* global __appdir */ var path = require('path') var express = require('express') const { decorateApp } = require('@awaitjs/express') var router = decorateApp(express.Router()) var noAuthRouter = decorateApp(express.Router()) var db = require(path.join(__appdir, 'lib', 'sequelize')) const backendHelper = require(path.join(__appdir, 'lib', 'external-backends', 'backendhelper')) const ipHelper = require(path.join(__appdir, 'lib', 'iphelper')) const config = require(path.join(__appdir, 'config', 'config')) const url = config.https.host + ':' + config.https.port const log = require(path.join(__appdir, 'lib', 'log')) // GET requests. /* * Returns all registration hooks sorted by sortValue. * * @return: List of registration hooks */ router.get('/hooks', (req, res) => { db.registrationhook.findAll({ order: [ ['sortValue', 'ASC'] ], include: [{ model: db.group, as: 'groups', attributes: ['id', 'name'] }] }).then(hooks => { res.send(hooks) }) }) // POST requests. /* * Reorders the registration hooks based on an array of hook ids. */ router.postAsync('/hookorder', async (req, res) => { var idSortvalueMap = {} req.body.ids.forEach((id, index) => { idSortvalueMap[id] = index }) var hooks = await db.registrationhook.findAll() var promises = [] hooks.forEach(hook => { promises.push(hook.update({ sortvalue: idSortvalueMap[hook.id] })) }) await Promise.all(promises) res.end() }) router.postAsync(['/hooks', '/hooks/:id'], async (req, res) => { var item = { name: req.body.name, description: req.body.description, type: req.body.type, script: req.body.script } var hook = null if (req.params.id > 0) { hook = await db.registrationhook.findOne({ where: { id: req.params.id } }) if (hook) await hook.update(item) } else { var maxSortvalue = await db.registrationhook.max('sortvalue') item.sortvalue = maxSortvalue ? maxSortvalue + 1 : 1 hook = await db.registrationhook.create(item) } if (hook) { hook.setGroups(req.body.groups) res.send({ id: hook.id }) } res.end() }) // DELETE requests. router.delete(['/hooks', '/hooks/:id'], (req, res) => { db.registrationhook.destroy({ where: { id: req.params.id || req.body.ids } }).then(count => { res.send({ count }) }) }) module.exports.router = router // GET requests. // POST requests. /* * Returns all root parents or all childs of a group. * * @return: Ipxe menu with a list of groups. */ noAuthRouter.post('/group', (req, res) => { const id = req.body.id var parents = [] if (req.body.parents) parents = JSON.parse(req.body.parents) if (id === '0') { db.group.findAll({ where: { '$parents.id$': null }, include: ['parents'] }).then(groups => { if (groups) { res.send(buildIpxeMenu(id, 'Root', groups, [])) } else { res.status(404).end() } }) } else { db.group.findOne({ where: { id: id }, include: ['parents', 'subgroups', 'clients'] }).then(group => { if (group) { res.send(buildIpxeMenu(id, group.name, group.subgroups, parents)) } else { res.status(404).end() } }) } }) /* * Reworked add method for adding a client or server. */ noAuthRouter.postAsync('/', async (req, res) => { console.log(req.body) let client = req.body.client const ipxe = req.body.ipxe if (!client.type) client.type = 'CLIENT' if (!client.title) client.title = client.type + '_' + client.uuid // If the client already exists return the configloader ipxe script. const clientDb = await db.client.findOne({ where: { uuid: client.uuid } }) if (clientDb) { if (ipxe) return res.send(`#!ipxe\nchain https://` + url + `/api/configloader/\${uuid}`) else return res.send({ error: 'CLIENT_ALREADY_EXISTS', msg: 'A client with the provided UUID does already exist.' }) } // Client does not exist. if (!client.parents) client.parents = [] const createClient = { name: client.title, description: client.type, ip: client.network.ip, mac: client.network.mac, uuid: client.uuid } if (client.type === 'CLIENT') createClient.registrationState = await getNextHookScript(client.parents) const newClient = await db.client.create(createClient) client.id = newClient.id // Add groups to the client. if (client.parents.length === 0) client.parents = await ipHelper.getGroups(client.network.ip) client.parents.forEach(pid => { newClient.addGroup(pid) }) log({ category: 'CLIENT_REGISTRATION', description: client.type + ' added successfully.', clientId: newClient.id }) // Add the client to the backends. const result = await backendHelper.addClient(client) if (ipxe) return res.send(`#!ipxe\nchain https://` + url + `/api/configloader/\${uuid}`) else return res.send(result) }) /* * Adds the client to the database and set parents if a parent was selected. Calls addClient for all external-backends. */ noAuthRouter.postAsync('/add', async (req, res) => { const feedback = req.body.feedback const mac = req.body.mac const uuid = req.body.uuid const ip = req.body.ip let name = req.body.name const parentId = parseInt(req.body.id) const purpose = req.body.purpose let parentIds = [] if (!name) name = 'Client_' + uuid // If the client already exists return the configloader ipxe script. const client = await db.client.findOne({ where: { uuid: uuid } }) if (client) return res.send(`#!ipxe\nchain https://` + url + `/api/configloader/\${uuid}`) // Else (Client does not exist) var groupids = [] if (parentId) groupids = [parentId] const resId = await getNextHookScript(groupids) const newClient = await db.client.create({ name: name, description: 'Client', ip: ip, mac: mac, uuid: uuid, registrationState: resId }) if (parentId) { newClient.addGroup(parentId) parentIds.push(parentId) } else { // Filtered list with all group we will add the client parentIds = await ipHelper.getGroups(ip) parentIds.forEach(pid => { newClient.addGroup(pid) }) } log({ category: 'CLIENT_REGISTRATION', description: 'Client added successfully.', clientId: newClient.id }) // Add the client to the backends. var c = { id: newClient.id, title: name, uuid: uuid, network: { mac: mac, ip: ip } } if (parentIds.length > 0) c.parents = parentIds if (purpose) c.purpose = purpose var result = await backendHelper.addClient(c) if (feedback) res.send(result) else res.send(`#!ipxe\nchain https://` + url + `/api/configloader/\${uuid}`) }) noAuthRouter.postAsync('/:uuid/update', async (req, res) => { const uuid = req.params.uuid const name = req.body.name const parentId = req.body.id // System const sysManufacturer = req.body.sys_manufacturer const sysModel = req.body.sys_model const sysSerial = req.body.sys_serial // CPU const cpuModel = req.body.cpu_model const cpuManufacturer = req.body.cpu_manufacturer const cpuType = req.body.cpu_type var cpuFrequency = req.body.cpu_frequency / 1000 const cpuCores = req.body.cpu_cores // RAM if (req.body.ram_size) { const ramSize = req.body.ram_size.split('\n') const ramManufacturer = req.body.ram_manufacturer.split('\n') const ramFormfactor = req.body.ram_formfactor.split('\n') const ramType = req.body.ram_type.split('\n') const ramIsEcc = req.body.ram_isecc.replace('Error Correction Type: ', '') var ramModules = [] // Build ram array for (let ram in ramSize) { if (ramSize[ram].replace('Size: ', '') !== 'No Module Installed') { const size = ramSize[ram].replace('Size: ', '').split(' ') let title = ramFormfactor[ram].replace('Form Factor: ', '') if (ramIsEcc === 'Single-bit ECC') title += '-ECC' const ramModule = { capacity: size[0], unit: size[1], manufacturer: ramManufacturer[ram].replace('Manufacturer: ', ''), title: title, type: ramType[ram].replace('Type: ', '') } ramModules.push(ramModule) } } } // SSD / HDD if (req.body.drives) { const drivesRaw = req.body.drives.split('%OBJECT_SPLITTER%') var drives = [] for (let driveRaw in drivesRaw) { if (drivesRaw[driveRaw].length > 0) { const dRaw = drivesRaw[driveRaw].split('%ATTRIBUTE_SPLITTER%') const drive = { model: dRaw[0].trim().replace('Device Model: ', ''), serial: dRaw[1].trim().replace('Serial Number: ', ''), capacity: dRaw[2].trim().split(' ')[0], unit: dRaw[2].trim().split(' ')[1], type: dRaw[3].trim().replace('Rotation Rate: ', ''), formfactor: dRaw[4].trim().replace('Form Factor: ', ''), connection: dRaw[5].trim().replace('SATA Version is: ', '') } drives.push(drive) } } } const client = await db.client.findOne({ where: { uuid: uuid } }) if (!client) return res.status(404).send({ error: 'CLIENT_NOT_FOUND', message: 'There is no client matching the provided uuid.' }) client.update({ name: name }) var c = { uuid: uuid, id: client.id } if (name) c.title = name if (parentId) c.parentId = parentId // System data. Sometime just string with whitespaces only. c.system = {} if (/\S/.test(sysManufacturer)) c.system.manufacturer = sysManufacturer else c.system.manufacturer = 'Not set' if (/\S/.test(sysModel)) c.system.model = sysModel else c.system.model = 'Not set' if (/\S/.test(sysSerial)) c.system.serialnumber = sysSerial else c.system.serialnumber = 'Not set' // TODO: MULTI GPU's ?! c.cpu = { model: cpuModel, manufacturer: cpuManufacturer, type: cpuType, frequency: cpuFrequency, cores: cpuCores } if (ramModules) c.ram = ramModules if (drives) c.drives = drives const result = await backendHelper.updateClient(c) res.send(result) }) /* * Mehtod for uploading the tpm key and stuff. */ noAuthRouter.putAsync('/:uuid/files', async (req, res) => { const client = await db.client.findOne({ where: { uuid: req.params.uuid } }) const result = await backendHelper.uploadFiles(client.id, req.files) res.send(result) }) /* * Open api method for setting the registration state of a given uuid. */ noAuthRouter.post('/:uuid/success', (req, res) => { const uuid = req.params.uuid const id = parseInt(req.body.id) db.client.findOne({ where: { uuid: uuid }, include: ['groups'] }).then(client => { // Client not found handling if (client === null) { res.status(404).send({ status: 'INVALID_UUID', error: 'There is no client with the provided UUID.' }) return } // Check if the finished script id (id) matches the current state of the client. if (client.registrationState !== id) { res.status(400).send({ status: 'INVALID_SCRIPT', error: 'This script should not have been executed.' }) return } // If it matches, search for the next script and update the clients registrationState. // Get all group id's of the client. var groupids = [] client.groups.forEach(g => { groupids = [...groupids, g.id] }) // Get the sort value of the current hook. db.registrationhook.findOne({ where: { id: client.registrationState } }).then(hook => { getNextHookScript(groupids, hook.sortvalue).then(resID => { // Update the client's registration state client.updateAttributes({ registrationState: resID }) res.send({ status: 'SUCCESS' }) }) }) }) }) /* * Returns the next bash script for the minilinux. Else empty script. (Empty = bash will make reboot) */ noAuthRouter.get('/:uuid/nexthook', (req, res) => { const uuid = req.params.uuid db.client.findOne({ where: { uuid: uuid } }).then(client => { // Return 404 if the client doesn't exist or it has no registration state. if (client === null || client.registrationState === null) { res.status(404).send() return } db.registrationhook.findOne({ where: { id: client.registrationState } }).then(hook => { if (hook.type !== 'BASH') { res.send() } else { res.set('id', client.registrationState) res.send(hook.script) } }) }) }) module.exports.noAuthRouter = noAuthRouter /* * parentIds: * sortvalue: * */ function getNextHookScript (groupids, sortvalue) { // Gets the list of all groupids inclusive the recursive parents. return getRecursiveParents(groupids).then(gids => { // Get the list of all hooks where the parent is null or those who fullfill the group dependency. var options = { where: { '$groups.id$': { [db.Op.or]: [null, gids] } }, include: ['groups'], order: [['sortvalue', 'ASC']] } if (sortvalue !== undefined) options.where.sortvalue = { [db.Op.gt]: sortvalue } return db.registrationhook.findAll(options).then(result => { var resID = null if (result.length >= 1) resID = result[0].id return resID }) }) } /* * groupids: Array of group ids to get the parents from. * * Returns a list of the grop ids and all recursive ids of their parents. */ function getRecursiveParents (groupIds) { var gids = [] return db.group.findAll({ where: { id: groupIds }, include: ['parents'] }).then(groups => { groups.forEach(group => { group.parents.forEach(parent => { if (!groupIds.includes(parent.id) && !gids.includes(parent.id)) gids = [...gids, parent.id] }) }) if (gids.length === 0) return groupIds else { return getRecursiveParents(gids).then(r => { return groupIds.concat(r) }) } }) } /* * id: id of the current selected location. * name: Name of the current selected location * groups: List of group [{ id: , name: }, ...] * * Build the ipxe menu out of the list of groups. * Used by the manual registration. */ function buildIpxeMenu (id, name, groups, parents) { var basUrl = 'https://' + url var script = '#!ipxe\r\n' // script = script.concat(`console --picture \${img} --x 800 --y 600 || shell\r\n`) // Add parent to keep track of the path we clicked through. var parentId = 0 var oldParents = parents.slice(0) if (parents.length > 0) { parentId = oldParents[oldParents.length - 1].id oldParents.length = oldParents.length - 1 } parents.push({ id: id, name: toAscii(name) }) script += `set space:hex 20:20\r\n` script += `set space \${space:string}\r\n` script += `set parents ` + JSON.stringify(parents) + '\r\n\r\n' // Menu var menuscript = '' script += ':start\r\n' script += 'menu Choose the group you want the client to be saved in\r\n' // Parent menu entries. var spacer = '' parents.forEach(parent => { script += 'item --gap ' + spacer + '[' + parent.id + '] ' + parent.name + '\r\n' spacer += `\${space}` }) // Back button script += 'item --key b back ' + spacer + '..\r\n' menuscript += ':back\r\nparams\r\nparam id ' + parentId + '\r\nparam parents ' + JSON.stringify(oldParents) + '\r\n' menuscript += 'chain --replace ' + basUrl + '/api/registration/group##params\r\n\r\n' // Group menu entries. First 1-9 are pressable via key? var counter = '1' groups.forEach(group => { script += 'item --key ' + counter + ' ' + counter + ' ' + spacer + '[' + group.id + '] ' + toAscii(group.name) + '\r\n' menuscript += ':' + counter + '\r\n' + 'params\r\nparam id ' + group.id + `\r\nparam parents \${parents}\r\n` menuscript += 'chain --replace ' + basUrl + '/api/registration/group##params\r\n\r\n' counter++ }) // Menu seperator script += 'item --gap\r\n' // Add client menu script += 'item select Add client to ' + toAscii(name) + '\r\n' menuscript += `:select\r\necho Enter client name\r\nread clientname\r\nparams\r\nparam name \${clientname}\r\n` menuscript += 'param id ' + id + `\r\nparam mac \${net0/mac}\r\nparam uuid \${uuid}\r\nparam ip \${net0/ip}\r\n` menuscript += 'chain --replace ' + basUrl + '/api/registration/add##params\r\n\r\n' // Goto start menu if (id !== '0') { script += 'item reset Go to start\r\n' menuscript += ':reset\r\nparams\r\nparam id ' + 0 + '\r\nchain --replace ' + basUrl + '/api/registration/group##params\r\n\r\n' } // Exit menu script += 'item exit Exit manual registration\r\n' menuscript += ':exit\r\nexit 1\r\n\r\n' // Concat script + menuscript and return it. script += `choose target && goto \${target}\r\n\r\n` script += menuscript return script } function toAscii (string) { string = string.replace('ü', 'ue') string = string.replace('ö', 'oe') string = string.replace('ä', 'ae') return ascii(string) } /* eslint-disable */ var escapable = /[\\"\x00-\x1f\x7f-\uffff]/g /* eslint-enable */ var meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"': '\\"', '\\': '\\\\' } function ascii (string) { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. escapable.lastIndex = 0 return escapable.test(string) ? string.replace(escapable, function (a) { var c = meta[a] return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4) }) : string }