From a3e2cb36b0cbceb025753c7aeae0553de043b968 Mon Sep 17 00:00:00 2001 From: Jannik Schönartz Date: Sun, 7 Apr 2019 23:42:35 +0000 Subject: [server/backends] Add infoblox ipxe ip selection stuff --- server/api/backends.js | 12 ++- server/api/registration.js | 82 +++++++++++++- server/ipxe/registration.ipxe | 1 + server/lib/external-backends/backendhelper.js | 28 ++++- .../external-backends/backends/infoblox-backend.js | 118 ++++++++++++++++----- server/lib/external-backends/index.js | 12 +++ 6 files changed, 223 insertions(+), 30 deletions(-) diff --git a/server/api/backends.js b/server/api/backends.js index 838467a..e92999a 100644 --- a/server/api/backends.js +++ b/server/api/backends.js @@ -9,6 +9,16 @@ var noAuthRouter = decorateApp(express.Router()) // GET requests. +// TODO DELETE +noAuthRouter.getAsync('/:id/test', async (req, res) => { + const id = req.params.id + const backend = await db.backend.findOne({ where: { id: id } }) + const externalBackends = new ExternalBackends() + const instance = externalBackends.getInstance(backend.type) + const result = await instance.test(backend.credentials) + res.send(result) +}) + /* * @return: Returns a list of all backends saved in the db. */ @@ -31,7 +41,7 @@ router.getAsync('/:id', async (req, res) => { const instance = externalBackends.getInstance(backend.type) let credentialTypes = instance.getCredentials() - // Get the ids of the 'password' fieldds + // Get the ids of the 'password' fields let censorIds = [] credentialTypes.forEach(function f (element) { if (element.type === 'switch') { diff --git a/server/api/registration.js b/server/api/registration.js index e3ca350..dc18bf1 100644 --- a/server/api/registration.js +++ b/server/api/registration.js @@ -115,9 +115,8 @@ noAuthRouter.postAsync('/clients', async (req, res) => { let ipxe = req.body.ipxe if (typeof ipxe === 'string') ipxe = true - - if (!client.type) client.type = 'CLIENT' - if (!client.name) client.name = client.type + '_' + client.uuid + let automatic = req.body.automatic + if (typeof automatic === 'string' && automatic === 'true') automatic = true // If the client already exists return the configloader ipxe script. const clientDb = await db.client.findOne({ where: { uuid: client.uuid } }) @@ -126,6 +125,55 @@ noAuthRouter.postAsync('/clients', async (req, res) => { 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] + if (client.networks.length >= 1) { + const network = client.networks[0] + // Get the dhcp backend. Only one dhcp backend can exist else -> conflict. + const dhcp = await backendHelper.getDhcp() + 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, client.name, true) + + // 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}. Error: ${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)) + const setIp = await dhcp.instance.setIp(dhcp.backend.credentials, network.dhcp, network.mac, client.name) + // Check for errors. + if (setIp.error) { + // Setting the client ip failed + log({ category: 'ERROR_DHCP', description: `[${dhcp.backend.id}] Error setting ip ${network.ip} for mac ${network.mac}. Error: ${setIp.msg}` }) + } else { + // Client ip set successfully + client.networks[0].ip = network.dhcp + } + } else { + // 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 && !ipCheck.error) return res.send(buildSelectIpIpxeMenu(client, ipCheck)) + else if (!ipxe && ipCheck && !ipCheck.error) return res.send({ client: client, ipList: ipCheck }) + } + } + } + // Client does not exist. if (!client.parents) client.parents = [] // TODO: Save all IPs? Maybe only primary ip? @@ -356,6 +404,34 @@ function buildIpxeMenu (id, name, groups, parents) { return script } +function buildSelectIpIpxeMenu (client, ipList) { + const basUrl = 'https://' + url + let script = '#!ipxe\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 += '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 += 'chain --replace ' + basUrl + '/api/registration/clients##params\r\n\r\n' + return script +} + function toAscii (string) { string = string.replace('ü', 'ue') string = string.replace('ö', 'oe') diff --git a/server/ipxe/registration.ipxe b/server/ipxe/registration.ipxe index 505d72f..621921d 100644 --- a/server/ipxe/registration.ipxe +++ b/server/ipxe/registration.ipxe @@ -28,6 +28,7 @@ goto start :automatic params param ipxe true +param automatic true param client { "type": "CLIENT", "uuid": "${uuid}", "purpose": "Pool PC", "networks": [{ "ip": "${net0/ip}", "mac": "${net0/mac}" }] } chain https://bas.intra.uni-freiburg.de/api/registration/clients##params diff --git a/server/lib/external-backends/backendhelper.js b/server/lib/external-backends/backendhelper.js index d8d4b24..b7a8a9b 100644 --- a/server/lib/external-backends/backendhelper.js +++ b/server/lib/external-backends/backendhelper.js @@ -4,7 +4,7 @@ const ExternalBackends = require(path.join(__appdir, 'lib', 'external-backends') const db = require(path.join(__appdir, 'lib', 'sequelize')) const log = require(path.join(__appdir, 'lib', 'log')) -module.exports = { addClient, updateClient, deleteClients, uploadFiles } +module.exports = { addClient, getDhcp, updateClient, deleteClients, uploadFiles } async function addClient (client) { // Get all backends and call addClient for each instance. @@ -123,3 +123,29 @@ async function uploadFiles (clientId, files) { } return results } + +async function getDhcp () { + const backends = await db.backend.findAll() + + let dhcp = false + for (let b in backends) { + const backend = backends[b] + const ba = new ExternalBackends() + const instance = ba.getInstance(backend.type) + + const isDHCP = await instance.isDhcp(backend.credentials) + if (isDHCP) { + if (!dhcp) dhcp = { instance: instance, backend: backend } + else { + // Conflict occured! + const conflict = await db.conflict.create({ description: 'Multiple dhcp backends found' }) + + // Add both backends to the conflict. + conflict.createObject({ objectType: 'BACKEND', objectId: backend.id }) + conflict.createObject({ objectType: 'BACKEND', objectId: dhcp.backend.id }) + return false + } + } + } + return dhcp +} diff --git a/server/lib/external-backends/backends/infoblox-backend.js b/server/lib/external-backends/backends/infoblox-backend.js index 2b2c6d1..27aa0b4 100644 --- a/server/lib/external-backends/backends/infoblox-backend.js +++ b/server/lib/external-backends/backends/infoblox-backend.js @@ -64,6 +64,64 @@ class InfobloxBackend extends ExternalBackends { }) } + async checkIp (credentials, ipv4) { + const c = this.mapCredentials(credentials) + + // Login + const ipam = new Infoblox({ + ip: c.url, + apiVersion: c.version + }) + const login = await ipam.login(c.username, c.password) + if (!login) return false + + // Get the host and check the leased state + let host = JSON.parse(await ipam.getHost(ipv4))[0] + if (host.lease_state && host.lease_state === 'ACTIVE') { + // If leased return the next 20 free ips of the subnet. + const dhcpNetwork = JSON.parse(await ipam.getNetworkFromIp(ipv4)) + const nextIps = await ipam.getNext(dhcpNetwork[0]._ref, 20) + return nextIps + } + return false + } + + async setIp (credentials, ipv4, mac, name, setNextIp = false) { + const c = this.mapCredentials(credentials) + + const ipam = new Infoblox({ + ip: c.url, + apiVersion: c.version + }) + const login = await ipam.login(c.username, c.password) + if (!login) return { error: 'LOGIN_FAILED' } + + // If setNextIp is true, use the next available ip from the subnet + if (setNextIp) { + const network = JSON.parse(await ipam.getNetworkFromIp(ipv4)) + ipv4 = 'func:nextavailableip:' + network[0].network + } + const domain = await ipam.getDomain() + const createHost = await ipam.create('record:host?_return_fields%2B=ipv4addrs&_return_as_object=1', { + 'name': name + '.' + domain, + 'ipv4addrs': [ + { + 'ipv4addr': ipv4, + 'mac': mac + } + ] + }) + + // Return error if there is one + if (createHost.Error) { + return { error: 'ERROR_INFOBLOX', msg: createHost.text } + } else return { host: createHost.result.ipv4addrs[0].host, ip: createHost.result.ipv4addrs[0].ipv4addr } + } + + isDhcp () { + return true + } + // ############################################################################ // ####################### helper/optional functions ######################### // Helper function, to map the array of credential objects into a single js object. @@ -80,38 +138,48 @@ class InfobloxBackend extends ExternalBackends { } // #### TEST FUNCTIONS ### - test (credentials) { - var c = this.mapCredentials(credentials) + async test (credentials) { + const c = this.mapCredentials(credentials) - var ipam = new Infoblox({ + const ipam = new Infoblox({ ip: c.url, apiVersion: c.version }) - var promise = new Promise((resolve, reject) => { - ipam.login(c.username, c.password).then(response => { - if (response) { - // ipam.create('record:host?_return_fields%2B=na13me,ipv4addrs&_return_as_object=1', {'name': 'wapiTest.lp.privat', 'ipv4addrs': [{'ipv4addr': '10.21.9.141', 'mac': 'aa:bb:cc:11:22:21'}]}).then(function (response) { - // return response - // }) - // Get the network from the ip. - // ipam.getHost('10.21.9.78').then(function (response) { - ipam.getNetworkFromIp('10.21.9.78').then(response => { - // ipam.getIpsFromSubnet('10.21.9.0/24').then(function (response) { - // ipam.list('record:host').then(function (response) { - // ipam.getDomain().then(function (response) { - response = JSON.parse(response) - // ipam.getNext(response[0]._ref, 1).then(r => { - resolve(response) - // }) - // res.send(JSON.parse(response)) - }) - } else { - return { success: false, error: 'TEST' } + const login = await ipam.login(c.username, c.password) + let result = {} + if (!login) return { error: 'LOGIN_FAILED' } + // Get network from ip + result['getNetworkFromIp'] = JSON.parse(await ipam.getNetworkFromIp('10.21.9.78')) + // Get host + // result["getHost.78"] = JSON.parse(await ipam.getHost('10.21.9.78')) + // result["getHost.219"] = JSON.parse(await ipam.getHost('10.21.9.219')) + // Get ips from subnet + // result["getIpsFromSubnet"] = JSON.parse(await ipam.getIpsFromSubnet('10.21.9.0/24')) + // List records + // result["list"] = JSON.parse(await ipam.list('record:host')) + // Get Domain + result['getDomain'] = await ipam.getDomain() + // Get next ip + // result["getNextIp.1"] = await ipam.getNext(result["getNetworkFromIp"][0]._ref, 1) + // Get next 20 ips + // result["getNextIp.20"] = await ipam.getNext(result["getNetworkFromIp"][0]._ref, 20) + + // Create Host + // result['createHost'] = await ipam.create('record:host?_return_fields%2B=ipv4addrs&_return_as_object=1', {'name': 'wapiTest2.lp.privat', 'ipv4addrs': [{'ipv4addr': '10.21.9.218', 'mac': 'aa:bb:cc:11:22:21'}]}) + /* + result['create'] = await ipam.create('record:host?_return_fields%2B=ipv4addrs&_return_as_object=1', { + 'name': 'test.lp.privat', + 'ipv4addrs': [ + { + 'ipv4addr': 'func:nextavailableip:' + result['getNetworkFromIp'][0].network, + 'mac': 'aa:bb:cc:11:22:21' } - }) + ] }) - return promise + result['create'] + */ + return result } } diff --git a/server/lib/external-backends/index.js b/server/lib/external-backends/index.js index 7acaa85..8dcae5a 100644 --- a/server/lib/external-backends/index.js +++ b/server/lib/external-backends/index.js @@ -166,6 +166,18 @@ class ExternalBackends { async getFile (credentials, externalId, filename) { return { error: 'NOT_IMPLEMENTED_EXCEPTION', message: 'The provided backend does not have an getFile method' } } + async checkIp (credentials, ipv4) { + return { error: 'NOT_IMPLEMENTED_EXCEPTION', message: 'The provided backend does not have a checkIp method' } + } + async setIp (credentials, ipv4, mac) { + return { error: 'NOT_IMPLEMENTED_EXCEPTION', message: 'The provided backend does not have a setIp method' } + } + /* + * Only one dhcp backend should be configures or it causes problems in the registration. + */ + isDhcp () { + return false + } } module.exports = ExternalBackends -- cgit v1.2.3-55-g7522