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

                          
                                


                                                   
                                                         
                                                                                               


                                                                
                                                      



                




                                                      
                                                                                                                                                            








                                                                 
                                                    












                                                                      
                                                                



























                                                                                                                      






                              
                                                     
  
                                            
   

                                           
                  
                                                              


                                                                                                
                                                       






                                                                                                         
                                                                         





                             
 
  
                                            
   
                                                        
                              

                                                             
                          
                                                              

                                                                             

                                                                                      







                                                                                                                        









                                                                                                                     
                  


                                                                              
                                        
                           
                  



                                                                       
                                                                                                                   
                            




                                          
                                                                                                                                                         



                                                                                            
                                                                        
                                                                                                                 
                            


                                         



                                        




                                              



                          

                                                                                         
 
                                



                                                                                                                                 



                                                      



       

                                          
                                               
                                                                                                                                                 



                                                                                                        


                                                                                           
                              
                                                                                                   
                                                            
                                                                                                      






                                                                                          
                                                              
                              
 





                                                                   
   
 



                                                                                                                                      
 






                                                                                          
                                                         
                  

  
  
                                              
   



                                                                              


  

                                                                      
                                                   
                              
                                  
                                                                                    





                                                                                                           

                                                                                    




                                                                                                             




                                        
 

                                                                                           

                                                                 
                       
                                  
          
                                       

        

















                                                                                                     
                                               





                             
                                          
 
  







                                                                                                     

                                                                                                                                
                                                                







                                                  





















                                                                                                   







                                                                      
                               
                           


                                                                                     









                                                                 

         
                     

                                                                          



                             

                                                                                    

    


                                                                                                                      
                                                                                       
 
                                                         

                           

                                                                                                                           
                                                                                         

             
 


                            
                    
                                                                 


                                                                                                                                                                                         

                    
                   
                                          
                                                                                                                                   
   

              

                                                    

                                              

                                                      

               
 
                                                                    

                                 

                                                                                                                  

                        
                                                      




                                                                                                 
                                       












                                                                                           
                                 






































                                                                                     



                                                                                     

































                                                                            
/* 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') ipxe = true
  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.' })
  }

  if (!client.type) client.type = 'CLIENT'

  // 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]
  let dhcp = false
  if (client.networks.length >= 1) {
    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) client.name = client.type + '_' + client.uuid
        const setIp = await dhcp.instance.setIp(dhcp.backend.credentials, network.ip, network.mac, undefined, true)
        dhcp.ref = setIp.ref
        // Check for errors.
        if (!setIp.error) {
          // 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 (!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, network.mac, client.name)
        dhcp.ref = setIp.ref
        // Check for errors.
        if (setIp.error) {
          // 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
        }
      } 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) {
          if (ipCheck.name) client.name = ipCheck.name
          if (ipCheck.id) dhcp.ref = ipCheck.id
        }
      }
    }
  }

  // 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 dhcp backend mapping
  if (dhcp) 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 = 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

  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: 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

  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)
      })
    }
  })
}

/*
 * 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
  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 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 index in ipList) {
    const ip = ipList[index]
    client.networks[0].dhcp = 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 buildNameClientIpxeMenu (client) {
  const basUrl = 'https://' + url
  let script = '#!ipxe\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'
  script += 'menu Overview Register Client\r\n'
  script += 'item --gap Name: ' + client.name + '\r\n'
  script += 'item --gap UUID: ' + client.uuid + '\r\n'

  for (let index in client.networks) {
    const network = client.networks[index]
    script += 'item --gap\r\n'
    script += 'item --gap IP: ' + network.ip + '\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
}