/* 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')) // Ipxe needs the url without the port because ipxe can't handle port requests const url = config.https.host // + ':' + config.https.port const log = require(path.join(__appdir, 'lib', 'log')) const HttpResponse = require(path.join(__appdir, 'lib', 'httpresponse')) // This is needed for parsing vendor/product codes const pci = require(path.join(__appdir, 'lib', 'pci')) const EdidReader = require('edid-reader') // Permission check middleware router.all(['', '/hooks', '/:y', '/hooks/:x'], async (req, res, next) => { switch (req.method) { case 'GET': if (!await req.user.hasPermission('registration.view')) return res.status(403).send({ error: 'Missing permission', permission: 'registration.view' }) break case 'POST': case 'DELETE': if (!await req.user.hasPermission('registration.edit')) return res.status(403).send({ error: 'Missing permission', permission: 'registration.edit' }) break default: return res.status(400).send() } next() }) // 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() const oldOrder = hooks.sort((a, b) => (a.sortvalue > b.sortvalue)) var promises = [] hooks.forEach(hook => { promises.push(hook.update({ sortvalue: idSortvalueMap[hook.id] })) }) await Promise.all(promises) log({ category: 'REGISTATIONHOOK_EDIT_ORDER', description: 'Registration hook order successfully edited.\n' + 'Old-Order: ' + '\n\t' + oldOrder.map(x => { return '[' + x.id + '] ' + x.name }).toString().replace(/,/g, '\n\t') + '\n' + 'New-Order: ' + '\n\t' + req.body.ids.map(x => { return '[' + x + '] ' + oldOrder.filter(y => y.id === x)[0].name }).toString().replace(/,/g, '\n\t') }) 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) log({ category: 'REGISTATIONHOOK_EDIT', description: '[' + hook.id + '] ' + hook.name + ': Registration hook successfully edited.\n' + 'ID: ' + hook.id + '\n' + 'Name: ' + hook.name + '\n' + 'Type: ' + hook.type + '\n' + 'Description: ' + hook.description + '\n' + 'Sortvalue: ' + hook.sortvalue + '\n' + 'Groups: ' + req.body.groups, userId: req.user.id }) } } else { var maxSortvalue = await db.registrationhook.max('sortvalue') item.sortvalue = maxSortvalue ? maxSortvalue + 1 : 1 hook = await db.registrationhook.create(item) log({ category: 'REGISTATIONHOOK_CREATE', description: '[' + hook.id + '] ' + hook.name + ': Registration hook successfully created.\n' + 'ID: ' + hook.id + '\n' + 'Name: ' + hook.name + '\n' + 'Type: ' + hook.type + '\n' + 'Description: ' + hook.description + '\n' + 'Sortvalue: ' + hook.sortvalue + '\n' + 'Groups: ' + req.body.groups, userId: req.user.id }) } if (hook) { hook.setGroups(req.body.groups) res.send({ id: hook.id }) } res.end() }) // ############################################################################ // ########################## DELETE requests ############################### router.deleteAsync('/hooks/:id', async (req, res) => { if (!(req.params.id > 0)) return HttpResponse.invalidId().send(res) const hook = await db.registrationhook.findOne({ where: { id: req.params.id } }) const count = await db.registrationhook.destroy({ where: { id: req.params.id } }) if (count) { log({ category: 'REGISTATIONHOOK_DELETE', description: 'Registration hook successfully deleted.\n' + 'ID: ' + hook.id + '\n' + 'Name: ' + hook.name + '\n' + 'Type: ' + hook.type + '\n' + 'Sortvalue: ' + hook.sortvalue, userId: req.user.id }) HttpResponse.success('deleted', 'hook', req.params.id).send(res) } else HttpResponse.notFound(req.params.id).send(res) }) // ############################################################################ // ############################################################################ 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 = {} // Defines weather the answer is a ipxe script or json response let ipxe = req.body.ipxe if (typeof ipxe === 'string' && ipxe === 'true') ipxe = true let dhcp = false if (req.body.version && req.body.version >= 2) { /* New hardware collection script */ client = await parseHardwareInformation(req.body) // 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.' }) } } else { client = req.body.client if (typeof client === 'string') client = JSON.parse(client) let automatic = req.body.automatic if (typeof automatic === 'string' && automatic === 'true') automatic = true let confirmation = req.body.confirmation if (typeof confirmation === 'string' && confirmation === 'true') confirmation = true // 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.' }) } // DHCP network stuff: // TODO: Multiip / backend problems // * Multiple backends possible ? multiple dhcp's? if set ip differentiates which one should we save in the bas db? // * Only servers have multiple ips? No multi leased ips possible? // If there is no ip, we don't need DHCP checks. // Only the first ip address is checked! client.networks[0] if (client.networks.length >= 1 && client.type === 'CLIENT') { const network = client.networks[0] // Get the dhcp backend. Only one dhcp backend can exist else -> conflict. dhcp = await backendHelper.getDhcp() let ipSelection = false let setIpError if (dhcp) { if (automatic) { // Set the name of the client if it's not set if (!client.name) { // Check the ip state in the dhcp const ipCheck = await dhcp.instance.checkIp(dhcp.backend.credentials, network.ip) // If it's not leased, set the hostname as clientname if (!ipCheck.error && !ipCheck.leased && ipCheck.name !== '') { if (ipCheck.name) client.name = ipCheck.name if (ipCheck.id) dhcp.ref = ipCheck.id } else { client.name = client.type + '_' + client.uuid const setIp = await dhcp.instance.setIp(dhcp.backend.credentials, network.ip, undefined, network.mac, undefined, true) // Check for errors. if (!setIp.error) { dhcp.ref = setIp.ref // Client ip set successfully client.networks[0].ip = setIp.ip } else { log({ category: 'ERROR_DHCP', description: `[${dhcp.backend.id}] Error setting ip ${network.ip} for mac ${network.mac}\nError: ${setIp.msg}` }) } } } } else if (network.dhcp) { // If networks.dhcp is set the user already choose the ip and we have to set it now. if (!network.dhcp.domain) { // Check if there are multiple domains. const domainList = await dhcp.instance.checkDomain(dhcp.backend.credentials) if (domainList.length > 1) return res.send(buildSelectDomainIpxeMenu(client, domainList)) else network.dhcp.domain = domainList[0] } if (!client.name) return res.send(buildNameClientIpxeMenu(client)) if (confirmation) return res.send(buildOverviewIpxeMenu(client)) const setIp = await dhcp.instance.setIp(dhcp.backend.credentials, network.dhcp.ip, network.dhcp.domain, network.mac, client.name) dhcp.ref = setIp.id // Check for errors. if (setIp.error) { log({ category: 'ERROR_DHCP', description: `[${dhcp.backend.id}] Error setting ip ${network.ip} for mac ${network.mac}\nError: ${setIp.msg}` }) // Setting the client ip failed ipSelection = true delete client.networks[0].dhcp delete client.name setIpError = setIp.msg } else { // Client ip set successfully client.networks[0].ip = network.dhcp.ip client.networks[0].hostname = client.name client.networks[0].domain = setIp.domain } } else { ipSelection = true } if (ipSelection) { // If not: check if the client has a leased ipv4 address. const ipCheck = await dhcp.instance.checkIp(dhcp.backend.credentials, network.ip) // Build ipxe and return if (ipxe && ipCheck.leased && !ipCheck.error) return res.send(buildSelectIpIpxeMenu(client, ipCheck.nextIps, setIpError)) else if (!ipxe && ipCheck.leased && !ipCheck.error) return res.send({ client: client, ipList: ipCheck.nextIps }) // Set the hostname as clientname if it exists and is not a leased ip. if (!ipCheck.leased && ipCheck.name) { if (ipCheck.name) client.name = ipCheck.name if (ipCheck.id) dhcp.ref = ipCheck.id } else { // Leased ip but no hostname? --> Maybe not waited long enough after DHCP deletion let date = new Date() const tenMin = 1000 * 60 * 10 const fiveMin = 1000 * 60 * 5 // Round up to the next 10-min mark for the error msg // === Add 5 min to the time and round to the nearest one const rounded = new Date(Math.round((date.getTime() + fiveMin) / tenMin) * tenMin) return res.send(buildNameClientIpxeMenu(client, ['Client has a fixed IP but NO hostname was found.', 'Infoblox might not be ready after client deletion.', `Wait until ${rounded.toTimeString()} or enter a name and continue anyways ...`])) } } } else { // End of DHCP Stuff if (automatic) { client.name = client.type + '_' + client.uuid } } } } // Client does not exist. if (!client.parents) client.parents = [] if (!client.name) client.name = client.type + '_' + client.uuid if (!client.type) client.type = 'CLIENT' // 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 dhcp backend mapping if (dhcp && dhcp.ref) dhcp.backend.addMappedClients(newClient, { through: { externalId: dhcp.ref } }) // 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 = backendHelper.addClient(client) if (ipxe) return res.send(`#!ipxe\nsleep 5\nchain https://` + url + `/api/configloader/\${uuid}`) else return res.send(await result) }) noAuthRouter.postAsync('/clients/:uuid', async (req, res) => { let client = {} if (req.body.version && req.body.version >= 2) { /* New hardware collection script */ client = await parseHardwareInformation(req.body) } else { /* OLD SCRIPT */ client = req.body.client if (typeof client === 'string') client = JSON.parse(client) if (client && client.ram && client.ram.modules) { // 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: req.params.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 // If domain gets updated without a hostname, set the hostname as name. for (let index in client.networks) { const network = client.networks[index] if (network.domain && !network.hostname) network.hostname = clientDb.name } if (client && client.system) { // 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) }) } }) } /* * New mehthod for preparing the new json formatted hw information */ async function parseHardwareInformation (data) { let client = { 'parents': [], 'type': data.type ? data.type : 'CLIENT', // SERVER OR CLIENT 'uuid': '', 'networks': [], // { 'mac': '', 'ip': '' } 'system': { 'model': '', 'manufacturer': '', 'serialnumber': '' }, 'cpus': [], 'ram': { 'modules': [], 'isEcc': false }, 'drives': [], 'gpus': [], 'monitors': [], 'contacts': [] } if (data.name) client.name = data.name // TODO: Rack and Bay stuff /* DmiDecode: CPU, RAM, System Information (Serial, Model, ...) */ if (data.dmidecode && data.dmidecode.length > 0) { const filter = [ /* "BIOS Information", "OEM Strings"," Base Board Information", "Chassis Information", "System Power Supply" */ 'System Information', 'Processor Information', 'Memory Device', 'Physical Memory Array' ] const filteredData = data.dmidecode.filter(x => filter.includes(x.name)) for (let entry of filteredData) { switch (entry.name) { case 'System Information': client.system.model = entry.props['Product Name'].values[0] client.system.manufacturer = entry.props['Manufacturer'].values[0] client.system.serialnumber = entry.props['Serial Number'].values[0] client.uuid = entry.props['UUID'].values[0] break case 'Processor Information': client.cpus.push({ 'model': entry.props['Version'].values[0], 'manufacturer': entry.props['Manufacturer'].values[0], 'type': entry.props['Family'].values[0], 'cores': entry.props['Core Count'].values[0], 'frequency': entry.props['Current Speed'].values[0].split(' ')[0], 'unit': entry.props['Current Speed'].values[0].split(' ')[1] }) break case 'Memory Device': if (entry.props['Size'].values[0] === 'No Module Installed') break client.ram.modules.push({ 'capacity': entry.props['Size'].values[0].split(' ')[0], 'unit': entry.props['Size'].values[0].split(' ')[1], 'manufacturer': entry.props['Manufacturer'].values[0], 'model': entry.props['Part Number'].values[0], 'type': entry.props['Type'].values[0], 'formfactor': entry.props['Form Factor'].values[0], 'speed': entry.props['Speed'].values[0], 'serialnumber': entry.props['Serial Number'].values[0] }) break case 'Physical Memory Array': client.ram.isEcc = !!entry.props['Error Correction Type'].values[0].endsWith('ECC') break } } } /* Smartctl */ for (let key in data.drives) { let drive = { 'model': '', 'family': '', // NEW 'firmware': '', 'serial': '', // TODO: Update this to serialnumber to unify (also need to be changed in the idoit backend then) 'capacity': '', 'unit': '', 'type': '', 'formfactor': '', 'connection': '', // CHECK?! 'connection_speed': '' // NEW } let units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] const driveData = data.drives[key] const smartctl = driveData.smartctl /* Figure out if it's HDD, SSD or CD/DVD-ROM */ if (smartctl['user_capacity']) { let capacity = smartctl['user_capacity']['bytes'] let unitcounter = 0 while (capacity > 1000) { if (unitcounter + 1 <= units.length) { capacity = capacity / 1000 unitcounter++ } } drive['capacity'] = Math.round(capacity) drive['unit'] = units[unitcounter] if (smartctl['rotation_rate']) { if (smartctl['rotation_rate'] > 0) { drive['type'] = 'HDD' } else if (smartctl['rotation_rate'] === 0) { drive['type'] = 'SSD' } } } else { const regexCDDVD = /\/dev\/sr[0-9]+/ // Seems to be a CD/DVD-ROM if (driveData.readlink.match(regexCDDVD)) drive['type'] = 'CD/DVD-ROM' else drive['type'] = 'UNKNOWN' } if (smartctl['form_factor']) drive['formfactor'] = smartctl['form_factor'].name if (smartctl['sata_version']) drive['connection'] = smartctl['sata_version'].string if (smartctl['interface_speed']) { if (smartctl['interface_speed'].current) drive['connection_speed'] = smartctl['interface_speed'].current.string else if (smartctl['interface_speed'].max) drive['connection_speed'] = smartctl['interface_speed'].max.string } if (smartctl['model_name']) drive.model = smartctl['model_name'] if (smartctl['model_family']) drive.family = smartctl['model_family'] if (smartctl['serial_number']) drive.serial = smartctl['serial_number'] if (smartctl['firmware_version']) drive.firmware = smartctl['firmware_version'] client.drives.push(drive) } /* lspci */ for (let obj of data.lspci) { /* GPU */ if (obj.class === '0300') { const parsedPci = await pci.parseIds(obj.vendor, obj.device) client.gpus.push({ 'manufacturer': parsedPci.vendor.name, 'model': parsedPci.vendor.device.name }) } } /* ip */ for (let ip of data.ip) { if (ip['link_type'] === 'loopback') continue let network = { 'name': ip.ifname, 'mac': ip.address, 'ip': undefined, 'ipv6': undefined, 'hostname': undefined } for (let addr of ip['addr_info']) { if (addr.scope !== 'global') continue if (addr.family === 'inet') network.ip = addr.local else if (addr.family === 'inet6') network.ipv6 = addr.local } client.networks.push(network) } /* net */ /* Get network information as fallback for ip */ if (client.networks.length <= 0) { for (let key in data.net) { // IP v4 and v6 comes with the netmask, so the last 3 chars need to be cut const ipv4 = data.net[key]['ipv4'] const ipv6 = data.net[key]['ipv6'] let network = { 'name': key, 'mac': data.net[key]['mac'], ...(ipv4 && { 'ip': ipv4.substring(0, ipv4.length - 3) }), ...(ipv6 && { 'ipv6': ipv6.substring(0, ipv6.length - 3) }), 'hostname': undefined } client.networks.push(network) } } /* edid */ for (let port in data.edid) { const rawEdid = data.edid[port] const edid = EdidReader.parse(rawEdid) client.monitors.push({ model: edid.modelName, vendor: edid.vendor, serialnumber: edid.serialNumber, modes: edid.standardDisplayModes, port: port, resolution: { width: edid.dtds[0].horActivePixels, height: edid.dtds[0].vertActivePixels }, dimensions: { width: edid.displaySize[0], height: edid.displaySize[1], inch: Math.round(Math.sqrt(Math.pow(edid.displaySize[0], 2) + Math.pow(edid.displaySize[1], 2)) / 25.4) } }) } /* lshw */ if (data.lshw && data.lshw.length > 0) { /* Get display information (as fallback) */ if (client.gpus && client.gpus.length === 0) { const gpus = data.lshw[0].children.filter(y => y.id === 'core')[0].children.filter(z => z.id === 'pci')[0].children.filter(w => w.id === 'display') for (let gpu of gpus) { client.gpus.push({ 'manufacturer': gpu.vendor, 'model': gpu.product /* 'memory': undefined, 'unit': undefined, */ }) } } } /* Contacts */ if (data.contacts && data.contacts.length > 0) { for (let username of data.contacts) { client.contacts.push(username) } } /* Location */ if (data.location) { // Server might get an name of the rack instead of an id if (typeof data.location.parent === 'string' && isNaN(data.location.parent)) { // Parent is not a number, so get the BAS ID for the object const parent = await db.group.findOne({ where: { name: data.location.parent } }) // findOne only returns the first object with the matching name, so if the name isn't unique id should be used client.parents.push(parent.id) } else if (data.location.parent) { const pid = Number(data.location.parent) if (!isNaN(pid)) client.parents.push(pid) } // Add bay and slot if given if (data.location.bay || data.location.slot) { client.location = { ...(data.location.slot && { slot: data.location.slot, ...(data.location.bay && { bay: data.location.bay }) }) } } } return client } /* * 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 // Cheap way to duplicate the array: 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", "parents": [` + parents[parents.length - 1].id + `], "networks": [{ "ip": "\${net0/ip}", "mac": "\${net0/mac}" }] }\r\n` menuscript += 'param ipxe true\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 buildSelectIpIpxeMenu (client, ipList, error = undefined) { const basUrl = 'https://' + url let script = '#!ipxe\r\n' if (error) script += 'echo\r\necho ' + error + '\r\necho\r\nprompt Press any key to select a new ip address\r\n' let menuscript = '' script += ':start\r\n' script += 'menu Select the ip for this client: \r\n' for (let ip of ipList) { client.networks[0].dhcp = { ip: ip } script += 'item ' + ip + ' ' + ip + '\r\n' menuscript += ':' + ip + '\r\n' + 'params\r\nparam client ' + JSON.stringify(client) + '\r\n' menuscript += 'param ipxe true\r\n' menuscript += 'chain --replace ' + basUrl + '/api/registration/clients##params\r\n\r\n' } script += `choose target && goto \${target}\r\n\r\n` script += menuscript return script } function buildSelectDomainIpxeMenu (client, domainList) { const basUrl = 'https://' + url let script = '#!ipxe\r\n' let menuscript = '' script += ':start\r\n' script += 'menu Select the domain for this client: \r\n' for (let index in domainList) { const domain = domainList[index] client.networks[0].dhcp.domain = domain script += 'item ' + domain + ' ' + domain + '\r\n' menuscript += ':' + domain + '\r\n' + 'params\r\nparam client ' + JSON.stringify(client) + '\r\n' menuscript += 'param ipxe true\r\n' menuscript += 'chain --replace ' + basUrl + '/api/registration/clients##params\r\n\r\n' } script += `choose target && goto \${target}\r\n\r\n` script += menuscript return script } function buildNameClientIpxeMenu (client, showErrorLines = []) { const basUrl = 'https://' + url let script = '#!ipxe\r\n' for (let line of showErrorLines) { script += `echo ${line}\r\n` } script += '\r\necho Enter client name\r\nread clientname\r\nparams\r\n' client.name = `\${clientname}` script += 'param client ' + JSON.stringify(client) + '\r\n' script += 'param ipxe true\r\n' // Trigger the overview ipxe menu script += 'param confirmation true\r\n' script += 'chain --replace ' + basUrl + '/api/registration/clients##params\r\n\r\n' return script } function buildOverviewIpxeMenu (client) { const basUrl = 'https://' + url const c = JSON.stringify(client) let script = '#!ipxe\r\n' script += ':start\r\n' script += 'menu Overview Register Client\r\n' script += 'item --gap Name: ' + client.name + '\r\n' delete client.name if (client.networks[0].dhcp) { if (client.networks[0].dhcp.ip) script += 'item --gap New IP: ' + client.networks[0].dhcp.ip + '\r\n' if (client.networks[0].dhcp.domain) script += 'item --gap Domain: ' + client.networks[0].dhcp.domain + '\r\n' delete client.networks[0].dhcp } script += 'item --gap\r\n' script += 'item --gap Current IP: ' + client.networks[0].ip + '\r\n' script += 'item --gap MAC: ' + client.networks[0].mac + '\r\n' script += 'item --gap UUID: ' + client.uuid + '\r\n' for (let index = 1; index < client.networks.length; index++) { // Only handle the networks with index >= 1 const network = client.networks[index] script += 'item --gap\r\n' script += 'item --gap Current IP: ' + network.ip + '\r\n' if (network.dhcp && network.dhcp.ip) script += 'item --gap New IP: ' + network.dhcp.ip + '\r\n' if (network.dhcp && network.dhcp.domain) script += 'item --gap Domain: ' + network.dhcp.domain + '\r\n' script += 'item --gap MAC: ' + network.mac + '\r\n' if (network.dhcp) delete network.dhcp } script += 'item --gap\r\n' script += 'item default\r\n' script += 'item --key y confirm Confirm [y]es\r\n' script += 'item --key n cancel Cancel [n]o\r\n' script += `choose --default default target && goto \${target}\r\n\r\n` script += ':default\r\n' script += 'goto start\r\n\r\n' script += ':confirm\r\n' script += 'params\r\nparam client ' + c + '\r\n' script += 'param ipxe true\r\n' script += 'chain --replace ' + basUrl + '/api/registration/clients##params\r\n\r\n' script += ':cancel\r\n' script += 'params\r\nparam client ' + JSON.stringify(client) + '\r\n' script += 'param ipxe true\r\n' script += 'chain --replace ' + basUrl + '/api/registration/clients##params\r\n\r\n' 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 }