Skip to content

Commit

Permalink
feat(netbox): sync XO users as Netbox tenants
Browse files Browse the repository at this point in the history
See Zammad#11356
See Zammad#17364
See Zammad#18409
  • Loading branch information
pdonias committed Nov 7, 2023
1 parent 9886e06 commit be4db78
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 7 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [Netbox] Ability to synchronize XO users as Netbox tenants (PR [#7158](https://github.com/vatesfr/xen-orchestra/pull/7158))

### Bug fixes

> Users must be able to say: “I had this issue, happy to know it's fixed”
Expand All @@ -27,4 +29,6 @@
<!--packages-start-->

- xo-server-netbox minor

<!--packages-end-->
5 changes: 5 additions & 0 deletions packages/xo-server-netbox/src/configuration-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ const configurationSchema = {
$type: 'pool',
},
},
syncUsers: {
type: 'boolean',
title: 'Synchronize users',
description: 'Synchronize XO users as Netbox tenants and bind VM creators. For this to work, you need to assign the `uuid` custom field to the type "tenancy > tenant".'
},
syncInterval: {
type: 'number',
title: 'Interval',
Expand Down
136 changes: 129 additions & 7 deletions packages/xo-server-netbox/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Netbox {
#xoPools
#removeApiMethods
#syncInterval
#syncUsers
#token
#xo

Expand All @@ -64,6 +65,7 @@ class Netbox {
this.#endpoint = 'http://' + this.#endpoint
}
this.#allowUnauthorized = configuration.allowUnauthorized ?? false
this.#syncUsers = configuration.syncUsers ?? false
this.#token = configuration.token
this.#xoPools = configuration.pools
this.#syncInterval = configuration.syncInterval && configuration.syncInterval * 60 * 60 * 1e3
Expand Down Expand Up @@ -206,8 +208,12 @@ class Netbox {
throw new Error('UUID custom field was not found. Please create it manually from your Netbox interface.')
}
const { content_types: types } = uuidCustomField
if (TYPES_WITH_UUID.some(type => !types.includes(type))) {
throw new Error('UUID custom field must be assigned to types ' + TYPES_WITH_UUID.join(', '))
const typesWithUuid = TYPES_WITH_UUID
if (this.#syncUsers) {
typesWithUuid.push('tenancy.tenant')
}
if (typesWithUuid.some(type => !types.includes(type))) {
throw new Error('UUID custom field must be assigned to types ' + typesWithUuid.join(', '))
}
}

Expand All @@ -233,6 +239,114 @@ class Netbox {

log.info(`Synchronizing ${xoPools.length} pools with Netbox`, { pools: xoPools })

// Tenants -----------------------------------------------------------------

let nbTenants
if (this.#syncUsers) {
log.info('Synchronizing users')

const createNbTenant = xoUser => {
const name = xoUser.email.slice(0, NAME_MAX_LENGTH)
return {
custom_fields: { uuid: xoUser.id },
name,
slug: slugify(name),
description: 'XO user',
}
}

const xoUsers = await this.#xo.getAllUsers()

nbTenants = keyBy(await this.#request('/tenancy/tenants/'), 'custom_fields.uuid')
delete nbTenants.null // Ignore tenants that don't have a UUID

const nbTenantsToCheck = { ...nbTenants }

const tenantsToUpdate = []
const tenantsToCreate = []
for (const xoUser of xoUsers) {
const nbTenant = nbTenants[xoUser.id]
delete nbTenantsToCheck[xoUser.id]

const updatedTenant = createNbTenant(xoUser)

if (nbTenant !== undefined) {
// Tenant was found in Netbox: update it
const patch = diff(updatedTenant, nbTenant)
if (patch !== undefined) {
tenantsToUpdate.push(patch)
}
} else {
// Tenant wasn't found: create it
tenantsToCreate.push(updatedTenant)
}
}

// Delete all the other tenants that weren't found in XO
const tenantsToDelete = Object.values(nbTenantsToCheck)

// Tenants that are bound to a VM can only be deleted after we update the
// VM's tenancy and tenants that CAN be deleted now MUST be deleted now to
// avoid conflicts with new tenants
// const {
// nonDeletable: nonDeletableTenants,
// dependent: dependentTenants,
// nonDependent: nonDependentTenants,
// } = splitDependentObjects(tenantsToDelete)

const nonDeletableTenants = []
const dependentTenants = []
const nonDependentTenants = []
for (const nbTenant of tenantsToDelete) {
if (
(nbTenant.circuit_count ?? 0) +
(nbTenant.device_count ?? 0) +
(nbTenant.ipaddress_count ?? 0) +
(nbTenant.prefix_count ?? 0) +
(nbTenant.rack_count ?? 0) +
(nbTenant.site_count ?? 0) +
(nbTenant.vlan_count ?? 0) +
(nbTenant.vrf_count ?? 0) +
(nbTenant.cluster_count ?? 0) >
0
) {
nonDeletableTenants.push(nbTenant)
} else if ((nbTenant.virtualmachine_count ?? 0) > 0) {
dependentTenants.push(nbTenant)
} else {
nonDependentTenants.push(nbTenant)
}
}

if (nonDeletableTenants.length > 0) {
log.warn(`Could not delete ${nonDeletableTenants.length} tenants because dependent object count is not 0`, {
tenant: nonDeletableTenants[0],
})
}

const nbVms = await this.#request('/virtualization/virtual-machines/')

const vmsToUpdate = []
for (const nbVm of nbVms) {
if (find(dependentTenants, { id: nbVm.tenant?.id }) !== undefined) {
vmsToUpdate.push({ id: nbVm.id, tenant: null })
}
}

// Perform calls to Netbox
await this.#request('/virtualization/virtual-machines/', 'PATCH', vmsToUpdate)
await this.#request(
'/tenancy/tenants/',
'DELETE',
dependentTenants.concat(nonDependentTenants).map(nbTenant => ({ id: nbTenant.id }))
)
tenantsToDelete.forEach(nbTenant => delete nbTenants[nbTenant.custom_fields.uuid])
Object.assign(
nbTenants,
keyBy(await this.#request('/tenancy/tenants/', 'POST', tenantsToCreate), 'custom_fields.uuid')
)
}

// Cluster type ------------------------------------------------------------

// Create a single cluster type called "XCP-ng Pool" to identify clusters
Expand Down Expand Up @@ -331,7 +445,7 @@ class Netbox {

log.info('Synchronizing VMs')

const createNbVm = async (xoVm, { nbCluster, nbPlatforms, nbTags }) => {
const createNbVm = async (xoVm, { nbCluster, nbPlatforms, nbTags, nbTenants }) => {
const nbVm = {
custom_fields: { uuid: xoVm.uuid },
name: xoVm.name_label.slice(0, NAME_MAX_LENGTH).trim(),
Expand Down Expand Up @@ -375,6 +489,7 @@ class Netbox {
nbVm.platform = nbPlatform.id
}

// Tags
const nbVmTags = []
for (const tag of xoVm.tags) {
const slug = slugify(tag)
Expand All @@ -401,6 +516,12 @@ class Netbox {
// Sort them so that they can be compared by diff()
nbVm.tags = nbVmTags.sort(({ id: id1 }, { id: id2 }) => (id1 < id2 ? -1 : 1))

// Tenant = VM creator
if (this.#syncUsers) {
const nbTenant = nbTenants[xoVm.creation?.user]
nbVm.tenant = nbTenant === undefined ? null : nbTenant.id
}

// https://netbox.readthedocs.io/en/stable/release-notes/version-2.7/#api-choice-fields-now-use-string-values-3569
if (this.#netboxVersion === undefined || !semver.satisfies(this.#netboxVersion, '>=2.7.0')) {
nbVm.status = xoVm.power_state === 'Running' ? 1 : 0
Expand All @@ -413,13 +534,14 @@ class Netbox {
const flattenNested = nbVm => ({
...nbVm,
cluster: nbVm.cluster?.id ?? null,
status: nbVm.status?.value ?? null,
platform: nbVm.platform?.id ?? null,
// If site is not supported by Netbox, its value is undefined
// If site is supported by Netbox but empty, its value is null
site: nbVm.site == null ? nbVm.site : nbVm.site.id,
status: nbVm.status?.value ?? null,
platform: nbVm.platform?.id ?? null,
// Sort them so that they can be compared by diff()
tags: nbVm.tags.map(nbTag => ({ id: nbTag.id })).sort(({ id: id1 }, { id: id2 }) => (id1 < id2 ? -1 : 1)),
tenant: nbVm.tenant?.id ?? null,
})

const nbPlatforms = keyBy(await this.#request('/dcim/platforms/'), 'id')
Expand Down Expand Up @@ -461,7 +583,7 @@ class Netbox {
const nbVm = allNbVms[xoVm.uuid]
delete xoPoolNbVms[xoVm.uuid]

const updatedVm = await createNbVm(xoVm, { nbCluster, nbPlatforms, nbTags })
const updatedVm = await createNbVm(xoVm, { nbCluster, nbPlatforms, nbTags, nbTenants })

if (nbVm !== undefined) {
// VM found in Netbox: update VM (I.1)
Expand All @@ -487,7 +609,7 @@ class Netbox {
const nbCluster = allNbClusters[xoPool?.uuid]
if (nbCluster !== undefined) {
// If the VM is found in XO: update it if necessary (II.1)
const updatedVm = await createNbVm(xoVm, { nbCluster, nbPlatforms, nbTags })
const updatedVm = await createNbVm(xoVm, { nbCluster, nbPlatforms, nbTags, nbTenants })
const patch = diff(updatedVm, flattenNested(nbVm))

if (patch === undefined) {
Expand Down

0 comments on commit be4db78

Please sign in to comment.