/* 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() } }) } }) /* * Add method for adding a client or server. */ noAuthRouter.postAsync('/clients', async (req, res) => { let client = req.body.client if (typeof client === 'string') client = JSON.parse(client) let ipxe = req.body.ipxe if (typeof ipxe === 'string') ipxe = true if (!client.type) client.type = 'CLIENT' if (!client.name) client.name = 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 = [] // TODO: Save all IPs? Maybe only primary ip? const createClient = { name: client.name, description: client.type, ip: client.networks[0].ip, mac: client.networks[0].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.networks[0].ip) client.parents.forEach(pid => { newClient.addGroup(pid) }) log({ category: 'REGISTRATION', description: 'Client 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) }) noAuthRouter.postAsync('/clients/:uuid', async (req, res) => { let client = req.body.client // Add the name to the ram modules. for (let ram of client.ram.modules) { ram.name = ram.formfactor if (client.ram.isEcc === 'Single-bit ECC') ram.name += '-ECC' } const clientDb = await db.client.findOne({ where: { uuid: client.uuid } }) if (!clientDb) return res.status(404).send({ error: 'CLIENT_NOT_FOUND', message: 'There is no client matching the provided uuid.' }) if (client.name) clientDb.update({ name: client.name }) client.id = clientDb.id // System data. Sometime just string with whitespaces only. if (!/\S/.test(client.system.manufacturer)) client.system.manufacturer = 'unavailable' if (!/\S/.test(client.system.model)) client.system.model = 'unavailable' if (!/\S/.test(client.system.serialnumber)) client.system.serialnumber = 'unavailable' const result = await backendHelper.updateClient(client) 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.update({ 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\n` menuscript += `param client { "name": "\${clientname}", "type": "CLIENT", "uuid": "\${uuid}", "purpose": "Pool PC", "networks": [{ "ip": "\${net0/ip}", "mac": "\${net0/mac}" }] }\r\n` menuscript += 'chain --replace ' + basUrl + '/api/registration/clients##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 }