summaryrefslogblamecommitdiffstats
path: root/server/api/registration.js
blob: 11de700d586d2b65d331b8252ac8296b26adacd5 (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()
      }
    })
  }
})

/*
 * Reworked add method for adding a client or server.
 */
noAuthRouter.postAsync('/', async (req, res) => {
  let client = req.body.client
  if (typeof client === 'string') client = JSON.parse(client)

  const ipxe = req.body.ipxe
  if (typeof ipxe === 'string') ipxe === 'true' ? true : false

  if (!client.type) client.type = 'CLIENT'
  if (!client.title) client.title = client.type + '_' + client.uuid

  // If the client already exists return the configloader ipxe script.
  const clientDb = await db.client.findOne({ where: { uuid: client.uuid } })
  if (clientDb) {
    if (ipxe) return res.send(`#!ipxe\nchain https://` + url + `/api/configloader/\${uuid}`)
    else return res.send({ error: 'CLIENT_ALREADY_EXISTS', msg: 'A client with the provided UUID does already exist.' })
  }

  // Client does not exist.
  if (!client.parents) client.parents = []
  const createClient = { name: client.title, description: client.type, ip: client.network.ip, mac: client.network.mac, uuid: client.uuid }
  if (client.type === 'CLIENT') createClient.registrationState = await getNextHookScript(client.parents)
  const newClient = await db.client.create(createClient)
  client.id = newClient.id

  // Add groups to the client.
  if (client.parents.length === 0) client.parents = await ipHelper.getGroups(client.network.ip)
  client.parents.forEach(pid => { newClient.addGroup(pid) })
  log({ category: 'CLIENT_REGISTRATION', description: client.type + ' added successfully.', clientId: newClient.id })

  // Add the client to the backends.
  const result = await backendHelper.addClient(client)
  if (ipxe) return res.send(`#!ipxe\nchain https://` + url + `/api/configloader/\${uuid}`)
  else return res.send(result)
})

/*
 * Adds the client to the database and set parents if a parent was selected. Calls addClient for all external-backends.
 */
noAuthRouter.postAsync('/add', async (req, res) => {
  const feedback = req.body.feedback
  const mac = req.body.mac
  const uuid = req.body.uuid
  const ip = req.body.ip
  let name = req.body.name
  const parentId = parseInt(req.body.id)
  const purpose = req.body.purpose
  let parentIds = []

  if (!name) name = 'Client_' + uuid

  // If the client already exists return the configloader ipxe script.
  const client = await db.client.findOne({ where: { uuid: uuid } })
  if (client) return res.send(`#!ipxe\nchain https://` + url + `/api/configloader/\${uuid}`)

  // Else (Client does not exist)
  var groupids = []
  if (parentId) groupids = [parentId]
  const resId = await getNextHookScript(groupids)
  const newClient = await db.client.create({ name: name, description: 'Client', ip: ip, mac: mac, uuid: uuid, registrationState: resId })
  if (parentId) {
    newClient.addGroup(parentId)
    parentIds.push(parentId)
  } else {
    // Filtered list with all group we will add the client
    parentIds = await ipHelper.getGroups(ip)
    parentIds.forEach(pid => { newClient.addGroup(pid) })
  }
  log({ category: 'CLIENT_REGISTRATION', description: 'Client added successfully.', clientId: newClient.id })

  // Add the client to the backends.
  var c = { id: newClient.id, title: name, uuid: uuid, network: { mac: mac, ip: ip }, type: req.body.type }
  if (parentIds.length > 0) c.parents = parentIds
  if (purpose) c.purpose = purpose

  var result = await backendHelper.addClient(c)

  if (feedback) res.send(result)
  else res.send(`#!ipxe\nchain https://` + url + `/api/configloader/\${uuid}`)
})

noAuthRouter.postAsync('/:uuid/update', async (req, res) => {
  const uuid = req.params.uuid
  const name = req.body.name
  const parentId = req.body.id

  // System
  const sysManufacturer = req.body.sys_manufacturer
  const sysModel = req.body.sys_model
  const sysSerial = req.body.sys_serial

  // CPU
  const cpuModel = req.body.cpu_model
  const cpuManufacturer = req.body.cpu_manufacturer
  const cpuType = req.body.cpu_type
  var cpuFrequency = req.body.cpu_frequency / 1000
  const cpuCores = req.body.cpu_cores

  // RAM
  if (req.body.ram_size) {
    const ramSize = req.body.ram_size.split('\n')
    const ramManufacturer = req.body.ram_manufacturer.split('\n')
    const ramFormfactor = req.body.ram_formfactor.split('\n')
    const ramType = req.body.ram_type.split('\n')
    const ramIsEcc = req.body.ram_isecc.replace('Error Correction Type: ', '')
    var ramModules = []

    // Build ram array
    for (let ram in ramSize) {
      if (ramSize[ram].replace('Size: ', '') !== 'No Module Installed') {
        const size = ramSize[ram].replace('Size: ', '').split(' ')
        let title = ramFormfactor[ram].replace('Form Factor: ', '')
        if (ramIsEcc === 'Single-bit ECC') title += '-ECC'

        const ramModule = {
          capacity: size[0],
          unit: size[1],
          manufacturer: ramManufacturer[ram].replace('Manufacturer: ', ''),
          title: title,
          type: ramType[ram].replace('Type: ', '')
        }
        ramModules.push(ramModule)
      }
    }
  }

  // SSD / HDD
  if (req.body.drives) {
    const drivesRaw = req.body.drives.split('%OBJECT_SPLITTER%')
    var drives = []
    for (let driveRaw in drivesRaw) {
      if (drivesRaw[driveRaw].length > 0) {
        const dRaw = drivesRaw[driveRaw].split('%ATTRIBUTE_SPLITTER%')
        const drive = {
          model: dRaw[0].trim().replace('Device Model: ', ''),
          serial: dRaw[1].trim().replace('Serial Number: ', ''),
          capacity: dRaw[2].trim().split(' ')[0],
          unit: dRaw[2].trim().split(' ')[1],
          type: dRaw[3].trim().replace('Rotation Rate: ', ''),
          formfactor: dRaw[4].trim().replace('Form Factor: ', ''),
          connection: dRaw[5].trim().replace('SATA Version is: ', '')
        }
        drives.push(drive)
      }
    }
  }

  const client = await db.client.findOne({ where: { uuid: uuid } })
  if (!client) return res.status(404).send({ error: 'CLIENT_NOT_FOUND', message: 'There is no client matching the provided uuid.' })

  client.update({ name: name })
  var c = { uuid: uuid, id: client.id }
  if (name) c.title = name
  if (parentId) c.parentId = parentId

  // System data. Sometime just string with whitespaces only.
  c.system = {}
  if (/\S/.test(sysManufacturer)) c.system.manufacturer = sysManufacturer
  else c.system.manufacturer = 'Not set'

  if (/\S/.test(sysModel)) c.system.model = sysModel
  else c.system.model = 'Not set'

  if (/\S/.test(sysSerial)) c.system.serialnumber = sysSerial
  else c.system.serialnumber = 'Not set'

  // TODO: MULTI GPU's ?!
  c.cpu = { model: cpuModel, manufacturer: cpuManufacturer, type: cpuType, frequency: cpuFrequency, cores: cpuCores }
  if (ramModules) c.ram = ramModules
  if (drives) c.drives = drives

  const result = await backendHelper.updateClient(c)
  res.send(result)
})

/*
 * Mehtod for uploading the tpm key and stuff.
 */
noAuthRouter.putAsync('/:uuid/files', async (req, res) => {
  const client = await db.client.findOne({ where: { uuid: req.params.uuid } })
  const result = await backendHelper.uploadFiles(client.id, req.files)
  res.send(result)
})

/*
 * Open api method for setting the registration state of a given uuid.
 */
noAuthRouter.post('/:uuid/success', (req, res) => {
  const uuid = req.params.uuid
  const id = parseInt(req.body.id)
  db.client.findOne({ where: { uuid: uuid }, include: ['groups'] }).then(client => {
    // Client not found handling
    if (client === null) {
      res.status(404).send({ status: 'INVALID_UUID', error: 'There is no client with the provided UUID.' })
      return
    }

    // Check if the finished script id (id) matches the current state of the client.
    if (client.registrationState !== id) {
      res.status(400).send({ status: 'INVALID_SCRIPT', error: 'This script should not have been executed.' })
      return
    }

    // If it matches, search for the next script and update the clients registrationState.
    // Get all group id's of the client.
    var groupids = []
    client.groups.forEach(g => {
      groupids = [...groupids, g.id]
    })

    // Get the sort value of the current hook.
    db.registrationhook.findOne({ where: { id: client.registrationState } }).then(hook => {
      getNextHookScript(groupids, hook.sortvalue).then(resID => {
        // Update the client's registration state
        client.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\nparam name \${clientname}\r\n`
  menuscript += 'param id ' + id + `\r\nparam mac \${net0/mac}\r\nparam uuid \${uuid}\r\nparam ip \${net0/ip}\r\n`
  menuscript += 'chain --replace ' + basUrl + '/api/registration/add##params\r\n\r\n'

  // Goto start menu
  if (id !== '0') {
    script += 'item reset Go to start\r\n'
    menuscript += ':reset\r\nparams\r\nparam id ' + 0 + '\r\nchain --replace ' + basUrl + '/api/registration/group##params\r\n\r\n'
  }

  // Exit menu
  script += 'item exit Exit manual registration\r\n'
  menuscript += ':exit\r\nexit 1\r\n\r\n'

  // Concat script + menuscript and return it.
  script += `choose target && goto \${target}\r\n\r\n`
  script += menuscript
  return script
}

function toAscii (string) {
  string = string.replace('ü', 'ue')
  string = string.replace('ö', 'oe')
  string = string.replace('ä', 'ae')
  return ascii(string)
}

/* eslint-disable */
var escapable = /[\\"\x00-\x1f\x7f-\uffff]/g
/* eslint-enable */
var meta = { // table of character substitutions
  '\b': '\\b',
  '\t': '\\t',
  '\n': '\\n',
  '\f': '\\f',
  '\r': '\\r',
  '"': '\\"',
  '\\': '\\\\'
}

function ascii (string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.

  escapable.lastIndex = 0
  return escapable.test(string)
    ? string.replace(escapable, function (a) {
      var c = meta[a]
      return typeof c === 'string' ? c
        : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4)
    }) : string
}