diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index d01b92f75..0619d9ea0 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -8,6 +8,7 @@ > Users must be able to say: “Nice enhancement, I'm eager to test it” - [New network] Ability for pool's admin to create a new network within the pool (PR [#5873](https://github.com/vatesfr/xen-orchestra/pull/5873)) +- [Netbox] Synchronize primary IPv4 and IPv6 addresses [#5633](https://github.com/vatesfr/xen-orchestra/issues/5633) (PR [#5879](https://github.com/vatesfr/xen-orchestra/pull/5879)) ### Bug fixes @@ -39,5 +40,6 @@ - @xen-orchestra/log minor - @xen-orchestra/mixins patch - xo-server-auth-ldap patch +- xo-server-netbox minor - xo-server minor - xo-web minor diff --git a/packages/xo-server-netbox/src/index.js b/packages/xo-server-netbox/src/index.js index f8a1dd47f..6d98a8285 100644 --- a/packages/xo-server-netbox/src/index.js +++ b/packages/xo-server-netbox/src/index.js @@ -1,7 +1,7 @@ import assert from 'assert' import ipaddr from 'ipaddr.js' import { createLogger } from '@xen-orchestra/log' -import { find, flatten, forEach, groupBy, isEmpty, keyBy, mapValues, trimEnd, zipObject } from 'lodash' +import { find, flatten, forEach, groupBy, isEmpty, keyBy, mapValues, omit, trimEnd, zipObject } from 'lodash' const log = createLogger('xo:netbox') @@ -253,14 +253,26 @@ class Netbox { // Build collections for later const netboxVms = {} // VM UUID → Netbox VM - const vifsByVm = {} // VM UUID → VIF + const vifsByVm = {} // VM UUID → VIF UUID[] const ipsByDeviceByVm = {} // VM UUID → (VIF device → IP) + const primaryIpsByVm = {} // VM UUID → { ipv4, ipv6 } const vmsToCreate = [] - const vmsToUpdate = [] + let vmsToUpdate = [] // will be reused for primary IPs for (const vm of Object.values(vms)) { vifsByVm[vm.uuid] = vm.VIFs const vmIpsByDevice = (ipsByDeviceByVm[vm.uuid] = {}) + + if (primaryIpsByVm[vm.uuid] === undefined) { + primaryIpsByVm[vm.uuid] = {} + } + if (vm.addresses['0/ipv4/0'] !== undefined) { + primaryIpsByVm[vm.uuid].ipv4 = vm.addresses['0/ipv4/0'] + } + if (vm.addresses['0/ipv6/0'] !== undefined) { + primaryIpsByVm[vm.uuid].ipv6 = ipaddr.parse(vm.addresses['0/ipv6/0']).toString() + } + forEach(vm.addresses, (address, key) => { const device = key.split('/')[0] if (vmIpsByDevice[device] === undefined) { @@ -483,6 +495,7 @@ class Netbox { const ipsToDelete = [] const ipsToCreate = [] const ignoredIps = [] + const netboxIpsByVif = {} for (const [vmUuid, vifs] of Object.entries(vifsByVm)) { const vmIpsByDevice = ipsByDeviceByVm[vmUuid] if (vmIpsByDevice === undefined) { @@ -495,6 +508,8 @@ class Netbox { continue } + netboxIpsByVif[vifId] = [] + const interface_ = interfaces[vif.uuid] const interfaceOldIps = oldNetboxIps[interface_.id] ?? [] @@ -508,6 +523,7 @@ class Netbox { netboxIp => ipaddr.parse(netboxIp.address.split('/')[0]).toString() === ipCompactNotation ) if (netboxIpIndex >= 0) { + netboxIpsByVif[vifId].push(interfaceOldIps[netboxIpIndex]) interfaceOldIps.splice(netboxIpIndex, 1) } else { const prefix = prefixes.find(({ prefix }) => { @@ -524,6 +540,7 @@ class Netbox { address: `${ip}/${prefix.prefix.split('/')[1]}`, assigned_object_type: 'virtualization.vminterface', assigned_object_id: interface_.id, + vifId, // needed to populate netboxIpsByVif with newly created IPs }) } } @@ -537,9 +554,61 @@ class Netbox { await Promise.all([ ipsToDelete.length !== 0 && this.#makeRequest('/ipam/ip-addresses/', 'DELETE', ipsToDelete), - ipsToCreate.length !== 0 && this.#makeRequest('/ipam/ip-addresses/', 'POST', ipsToCreate), + ipsToCreate.length !== 0 && + this.#makeRequest( + '/ipam/ip-addresses/', + 'POST', + ipsToCreate.map(ip => omit(ip, 'vifId')) + ).then(newNetboxIps => { + newNetboxIps.forEach((newNetboxIp, i) => { + const { vifId } = ipsToCreate[i] + if (netboxIpsByVif[vifId] === undefined) { + netboxIpsByVif[vifId] = [] + } + netboxIpsByVif[vifId].push(newNetboxIp) + }) + }), ]) + // Primary IPs + vmsToUpdate = [] + Object.entries(netboxVms).forEach(([vmId, netboxVm]) => { + if (netboxVm.primary_ip4 !== null && netboxVm.primary_ip6 !== null) { + return + } + const newNetboxVm = { id: netboxVm.id } + const vifs = vifsByVm[vmId] + vifs.forEach(vifId => { + const netboxIps = netboxIpsByVif[vifId] + const vmMainIps = primaryIpsByVm[vmId] + + netboxIps?.forEach(netboxIp => { + const address = netboxIp.address.split('/')[0] + if ( + newNetboxVm.primary_ip4 === undefined && + address === vmMainIps.ipv4 && + netboxVm.primary_ip4?.address !== netboxIp.address + ) { + newNetboxVm.primary_ip4 = netboxIp.id + } + if ( + newNetboxVm.primary_ip6 === undefined && + address === vmMainIps.ipv6 && + netboxVm.primary_ip6?.address !== netboxIp.address + ) { + newNetboxVm.primary_ip6 = netboxIp.id + } + }) + }) + if (newNetboxVm.primary_ip4 !== undefined || newNetboxVm.primary_ip6 !== undefined) { + vmsToUpdate.push(newNetboxVm) + } + }) + + if (vmsToUpdate.length > 0) { + await this.#makeRequest('/virtualization/virtual-machines/', 'PATCH', vmsToUpdate) + } + log.debug('synchronized') }