summaryrefslogblamecommitdiffstats
path: root/server/api/registration.js
blob: 7ba0a48b5af70a043f2e70ae2016f216fdc4a022 (plain) (tree)
1
2
3
4
5
6
7
8
9
10

                          
                                


                                                   
                                                         
                                                                                               

                                                                

                                                                              
                                                      
                                                                        

                                                      
                                         
 

















                                                                                                                                                           


                




                                                      
                                                                                                                                                            








                                                                 
                                                    




                                                 
                                                                    




                                                                      






                                                                                                                                                                      


           
                                                                








                                                                              













                                                                                                      



                                                                 










                                                                                                     







                                   

                                                                               
 
















                                                                                   

  


                                                                               






                              
                                                     
  
                                            
   

                                           
                  
                                                              


                                                                                                
                                                       






                                                                                                         
                                                                         





                             
 
  
                                            
   
                                                        


                                                                 
                          
                                                              
                  
 


                                                     






                                                                                                                          







































                                                                                                                          
                    












                                                                                                                                    

             






























                                                                                                                                           
                
                            
         
 


                                                                                           
 


                                                                                                                                   
 


















                                                                                                                                                                                                                                                         
         
       


     

                                          


                                                                 
                                               
                                                                                                                                                 



                                                                                                        
                             
                                                                                                       
 
                              
                                                                                                   
                                                            




                                              

                                    

                                                                                                   
                                    

  
                                                              


                                                  
                                                     










                                                                     
     
   
 
                                                                                


                                                                                                                                      
 





                                                                             






                                                                                          
                                                         
                  

  
  
                                              
   



                                                                              


  

                                                                      
                                                   
                              
                                  
                                                                                    





                                                                                                           

                                                                                    




                                                                                                             




                                        
 

                                                                                           

                                                                 
                       
                                  
          
                                       

        

















                                                                                                     
                                               





                             
                                          
 
  







                                                                                                     

                                                                                                                                
                                                                







                                                  





















                                                                                                   

                                                                  
                                                
                
                  
                                                                 












                                              


                   
   
                                        















                                                                                                                     



                                                                             


                                     
                            










                                                                              
                                   











                                                                    
                                                                                             





                
                                


                          
                     









                                                                                                                     


                                       
                                                   

                                                       









                                              

                                            
                               
                                                     





                                          
                                                                            


                                    




                                                                                                                     

     



                                                                                   
 
                             


             









                                                                  






                                                


                           







                                                                 
                                 



                                                  
                                    
                               


                                                                                


                                    


                                                                    
       
                                   



            

                                   
                                          




                                       
                 










                                                                                                               



                                               
                                                  

                                                                                                                                                         
                          

                                     


                              





            

                                                  

                                         










                                                                                                                    


                                               






                                                  


                                    




          
               


  







                                                                      
                               
                           


                                                                                     
                  
                                      








                                                                 

         
                     

                                                                          



                             

                                                                                    

    


                                                                                                                      
                                                                                       
 
                                                         

                           

                                                                                                                           
                                                                                         

             
 


                            
                    
                                                                 
                                                                                    
                                                                                                                                                                                                                                              
                                     
                                                                                         

                    
                   
                                          
                                                                                                                                   
   

              

                                                    

                                              

                                                      

               
 
                                                                    

                                 

                                                                                                                  

                        
                                                      
                          
                                        

                                                                                                 
                                       






                                                                                           



















                                                                                                     
                                                                

                                 




                                    


                                                                         
                                 









                                                                                     
                        

                                                      

                                

                                                                                                                 





                                                                      

                                                      

                                                                

                                          
                                                             

                                                                                                           


                                                       
 
















                                                                                     



                                                                                     

































                                                                            
/* 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: <GROUP_ID>, name: <GROUP_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
}