summaryrefslogtreecommitdiffstats
path: root/server/lib/confighelper.js
blob: 6e6f152308590c50467095593fe751dbccde8f72 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
/* global __appdir */
const path = require('path')
const fs = require('fs')
const db = require(path.join(__appdir, 'lib', 'sequelize'))
const eventHelper = require(path.join(__appdir, 'lib', 'eventhelper'))
const serverConfig = require(path.join(__appdir, 'config', 'config'))

async function getConfig (client, list) {
  let configPath = []

  // client not known, start registration
  if (client === null) {
    let config = { id: 'REGISTRATION', name: 'Client Registration' }
    if (!list) {
      config.script = fs.readFileSync(path.join(__appdir, 'ipxe', 'registration.ipxe'), 'utf8')
      return config
    }
    configPath.push(config)
    return configPath
  }

  // Client is in db, check for registration hooks.
  if (client.registrationState !== null) {
    // client is in registration state, load scripts
    var hook = await db.registrationhook.findOne({ where: { id: client.registrationState } })
    let config = { id: 'HOOK' + hook.id, name: hook.name, source: { type: hook.type + '_HOOK', id: hook.id, name: hook.name } }
    if (!list) {
      if (hook.type === 'IPXE') {
        config.script = hook.script
      } else if (hook.type === 'BASH') {
        config.script = fs.readFileSync(path.join(__appdir, 'ipxe', 'minilinux.ipxe'), 'utf8')
      }
      return config
    }
    if (hook.type === 'BASH') config.name = 'Minilinux'
    else config.name = hook.name
    configPath.push(config)
  }

  // Check for event or direct configs
  const configs = await _checkParentConfigs(client.id, !list)

  if (configs.length) {
    if (!list) return configs[0]
    else configPath.push(...configs)
  }

  // No config found, use default config
  let config = await prepareConfig({ id: 'DEFAULT', source: { type: 'DEFAULT' } })
  if (config === null) config = { id: 'DEFAULT', name: 'Fallback Default', script: getDefaultConfig(client), source: { type: 'DEFAULT' } }

  if (!list) return config

  configPath.push(config)
  return configPath
}

async function getGroupConfig (group, list) {
  if (!group) return
  let configPath = []

  // Check for event or direct configs
  const configs = await _checkParentConfigs(group.id, !list, false)

  if (!list) {
    if (configs.length) return configs[0]
  } else {
    configPath.push(...configs)
  }

  // No config found, use default config
  let config = await prepareConfig({ id: 'DEFAULT', source: { type: 'DEFAULT' } })
  if (config === null) config = { id: 'DEFAULT', name: 'Fallback Default', script: getDefaultConfig({}), source: { type: 'DEFAULT' } }

  if (!list) return config

  configPath.push(config)
  return configPath
}

async function _checkParentConfigs (id, breakOnImportant, isClient = true) {
  const importantList = []
  const eventList = []
  const configList = []
  const blackList = []
  const seenGroupsList = isClient ? [] : [id]
  let parents = isClient ? [] : [id]

  do {
    let events = []
    let configs = []

    if (isClient) {
      events = await db.event.findAll({ where: { '$clients.id$': id }, include: ['clients', 'config'] })
      let client = await db.client.findOne({ where: { id: id }, include: ['config'] })
      if (client && client.configId) configs = [{ id: client.configId, source: { type: 'CLIENT', id: client.id, name: client.name } }]
    } else {
      events = await db.event.findAll({ where: { '$groups.id$': parents }, include: ['groups', 'config'] })
      let groups = await db.group.findAll({ where: { id: parents }, include: ['config'] })
      groups.forEach(group => {
        if (group.configId) configs.push({ id: group.configId, source: { type: 'GROUP', id: group.id, name: group.name } })
      })
    }

    let importants = []
    let nonImportants = []

    events.forEach(event => {
      if (!eventHelper.isActive(event)) return

      if ((event.groups && event.groups.some(group => group.group_x_event.blacklist)) ||
       (event.clients && event.clients.length && event.clients[0].client_x_event.blacklist)) {
        return blackList.push(event.id)
      }

      if (blackList.includes(event.id) || !event.configId) return

      let config = {
        id: event.configId,
        source: {
          type: event.important ? 'IMPORTANT_EVENT' : 'EVENT',
          id: event.id,
          name: event.name
        }
      }

      if (event.groups) config.source.groups = event.groups.map(x => ({ id: x.id, name: x.name }))

      if (event.important) importants.push(config)
      else nonImportants.push(config)
    })

    if (importants.length > 1) importantList.push(await _createDynamicMenu(importants))
    else if (importants.length === 1) importantList.push(await prepareConfig(importants[0]))

    if (breakOnImportant && importantList.length) break

    if (nonImportants.length > 1) eventList.push(await _createDynamicMenu(nonImportants))
    else if (nonImportants.length === 1) eventList.push(await prepareConfig(nonImportants[0]))

    if (configs.length > 1) configList.push(await _createDynamicMenu(configs))
    else if (configs.length === 1) configList.push(await prepareConfig(configs[0]))

    if (isClient) {
      parents = (await db.group.findAll({ where: { '$clients.id$': id }, include: ['clients'] })).map(x => x.id)
      isClient = false
    } else {
      parents = (await db.group.findAll({ where: { '$subgroups.id$': parents }, include: ['subgroups'] })).map(x => x.id)
    }

    for (let i = parents.length - 1; i >= 0; i--) {
      if (seenGroupsList.includes(parents[i])) parents.splice(i, 1)
    }

    seenGroupsList.push(...parents)
  } while (parents.length)

  return [...importantList, ...eventList, ...configList]
}

// create the config script from database
async function prepareConfig (configInfo, noScript) {
  let id = configInfo.id

  // If no id is given, find the default config
  const where = {}
  if (id === 'DEFAULT') where.isDefault = true
  else where.id = id

  const config = await db.config.findOne({ where, include: ['entries'], order: [[db.config.associations.entries, db.config.associations.entries.through, 'sortValue', 'ASC']] })
  if (!config) return null

  const result = { id: config.id, name: config.name, source: configInfo.source }

  if (noScript) return result
  if (config.script !== null && config.script !== '') {
    result.script = config.script
    return result
  }
  var script = ''
  var menuscript = ''
  script += '#!ipxe\r\n\r\n'
  script += 'set img https://bas.intra.uni-freiburg.de/files/ipxe_wallpaper.png || goto start\r\n'
  script += `console --picture \${img} --x 800 --y 600 ||\r\n\r\n`
  script += ':start\r\n'
  script += 'menu ' + config.name + '\r\n'
  config.entries.forEach(entry => {
    script += 'item'
    if (entry.config_x_entry.keyBind !== null) {
      script += ' --key ' + entry.config_x_entry.keyBind
    }
    script += ' menuentry' + entry.id + ' '
    if (entry.config_x_entry.customName !== null && entry.config_x_entry.customName !== '') {
      script += entry.config_x_entry.customName
    } else {
      script += entry.name
    }
    script += '\r\n'
    menuscript += ':' + 'menuentry' + entry.id + '\r\n'
    menuscript += entry.script + '\r\n\r\n'
  })
  script += 'choose '
  if (config.defaultEntry !== null && config.timeout !== null) {
    script += '--default menuentry' + config.defaultEntry + ' --timeout ' + config.timeout + ' '
  }
  script += `target && goto \${target}\r\n\r\n`
  script += menuscript

  result.script = script
  return result
}

// create dynamic menu to load the different given configs for a client
async function _createDynamicMenu (configInfos, noScript) {
  const ids = []
  const idSourceMap = {}

  for (let i in configInfos) {
    let id = configInfos[i].id
    let source = configInfos[i].source

    if (!ids.includes(id)) ids.push(id)

    if (idSourceMap[id] === undefined) idSourceMap[id] = source
    else if (Array.isArray(idSourceMap[id])) idSourceMap[id].push(source)
    else idSourceMap[id] = [idSourceMap[id], source]
  }

  if (ids.length === 1) {
    return prepareConfig({
      id: ids[0],
      source: idSourceMap[ids[0]]
    })
  }

  const configs = await db.config.findAll({ where: { id: ids } })

  const result = {
    merged: true,
    id: JSON.stringify(configs.map(x => x.id)),
    name: configs.map(x => x.name).join(' + '),
    configs: configs.map(x => ({ id: x.id, name: x.name, source: idSourceMap[x.id] }))
  }
  if (noScript) return result

  var script = ''
  var menuscript = ''
  var defaultentry = ''
  script += '#!ipxe\r\n\r\n'
  script += 'set img https://bas.intra.uni-freiburg.de/files/ipxe_wallpaper.png || goto start\r\n'
  script += `console --picture \${img} --x 800 --y 600 ||\r\n\r\n`
  script += ':start\r\n'
  script += 'menu ' + 'Choose one configuration to boot' + '\r\n'
  configs.forEach(config => {
    script += 'item '
    script += 'menuentry' + config.id + ' '
    script += config.name
    script += '\r\n'

    // Last script processed is default script
    defaultentry = 'menuentry' + config.id

    menuscript += ':' + 'menuentry' + config.id + '\r\n'
    menuscript += 'chain ' + 'https://' + serverConfig.https.host + '/api/configloader/configs/' + config.id + '\r\n\r\n'
  })
  script += 'choose --default ' + defaultentry + ' '
  script += '--timeout 15000 '
  script += `target && goto \${target}\r\n\r\n`
  script += menuscript

  result.script = script
  return result
}

// Creates a default overview config, when there is no default config in the frontend.
function getDefaultConfig (client) {
  let script = '#!ipxe\r\n\r\n'
  script += 'set img https://bas.intra.uni-freiburg.de/files/ipxe_wallpaper.png || goto start\r\n'
  script += `console --picture \${img} --x 800 --y 600 ||\r\n\r\n`
  script += ':start\r\n'
  script += 'menu ' + 'Client is successfully registered' + '\r\n'
  script += 'item --gap BAS-ID: ' + client.id + '\r\n'
  script += 'item --gap Name: ' + client.name + '\r\n'
  script += 'item --gap\r\n'
  script += 'item --gap IP: ' + client.ip + '\r\n'
  script += 'item --gap MAC: ' + client.mac + '\r\n'
  script += 'item --gap UUID: ' + client.uuid + '\r\n'
  script += 'item --gap\r\n'
  script += 'item --gap\r\n'
  script += 'item --gap ' + 'No config is set for the client or any of its parents!' + '\r\n'
  script += 'item --gap ' + 'Please assign a config to the client or a parent first! (e.g. in the frontend)' + '\r\n'
  script += 'item --gap\r\n'
  script += 'item ' + 'poweroff Power Off' + '\r\n'
  script += 'item ' + 'reboot Reboot' + '\r\n'
  script += `choose --default poweroff --timeout 25000 target && goto \${target}\r\n\r\n`

  script += '\r\n'
  script += ':poweroff\r\n'
  script += 'poweroff\r\n'
  script += '\r\n'
  script += ':reboot\r\n'
  script += 'reboot\r\n'
  script += '\r\n'

  return script
}

module.exports = { getConfig, getGroupConfig, getDefaultConfig, prepareConfig }