|
|
|
|
@@ -17,6 +17,7 @@ import slugify from './slugify'
|
|
|
|
|
const log = createLogger('xo:netbox')
|
|
|
|
|
|
|
|
|
|
const CLUSTER_TYPE = 'XCP-ng Pool'
|
|
|
|
|
const XO_TAG_COLOR = '2598d9'
|
|
|
|
|
const TYPES_WITH_UUID = ['virtualization.cluster', 'virtualization.virtualmachine', 'virtualization.vminterface']
|
|
|
|
|
const CHUNK_SIZE = 100
|
|
|
|
|
export const NAME_MAX_LENGTH = 64
|
|
|
|
|
@@ -39,7 +40,7 @@ class Netbox {
|
|
|
|
|
#endpoint
|
|
|
|
|
#intervalToken
|
|
|
|
|
#loaded
|
|
|
|
|
#netboxApiVersion
|
|
|
|
|
#netboxVersion
|
|
|
|
|
#xoPools
|
|
|
|
|
#removeApiMethods
|
|
|
|
|
#syncInterval
|
|
|
|
|
@@ -103,6 +104,7 @@ class Netbox {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async test() {
|
|
|
|
|
await this.#fetchNetboxVersion()
|
|
|
|
|
await this.#checkCustomFields()
|
|
|
|
|
|
|
|
|
|
const randomSuffix = Math.random().toString(36).slice(2, 11)
|
|
|
|
|
@@ -144,9 +146,6 @@ class Netbox {
|
|
|
|
|
const httpRequest = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await this.#xo.httpRequest(url, options)
|
|
|
|
|
// API version only follows minor version, which is less precise and is not semver-valid
|
|
|
|
|
// See https://github.com/netbox-community/netbox/issues/12879#issuecomment-1589190236
|
|
|
|
|
this.#netboxApiVersion = semver.coerce(response.headers['api-version'])?.version ?? undefined
|
|
|
|
|
const body = await response.text()
|
|
|
|
|
if (body.length > 0) {
|
|
|
|
|
return JSON.parse(body)
|
|
|
|
|
@@ -185,7 +184,7 @@ class Netbox {
|
|
|
|
|
response = await httpRequest()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (method !== 'GET') {
|
|
|
|
|
if (method !== 'GET' || response.results === undefined) {
|
|
|
|
|
return response
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -213,9 +212,15 @@ class Netbox {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async #fetchNetboxVersion() {
|
|
|
|
|
// Endpoint supported since v2.10. If not supported, Netbox needs to be updated.
|
|
|
|
|
this.#netboxVersion = (await this.#request('/status/'))['netbox-version']
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
async #synchronize(xoPools = this.#xoPools) {
|
|
|
|
|
await this.#fetchNetboxVersion()
|
|
|
|
|
await this.#checkCustomFields()
|
|
|
|
|
|
|
|
|
|
log.info(`Synchronizing ${xoPools.length} pools with Netbox`, { pools: xoPools })
|
|
|
|
|
@@ -342,7 +347,7 @@ class Netbox {
|
|
|
|
|
// v3.3.0: "site" is REQUIRED and MUST be the same as cluster's site
|
|
|
|
|
// v3.3.5: "site" is OPTIONAL (auto-assigned in UI, not in API). `null` and cluster's site are accepted.
|
|
|
|
|
// v3.4.8: "site" is OPTIONAL and AUTO-ASSIGNED with cluster's site. If passed: ignored except if site is different from cluster's, then error.
|
|
|
|
|
if (this.#netboxApiVersion === undefined || semver.satisfies(this.#netboxApiVersion, '3.3.0 - 3.4.7')) {
|
|
|
|
|
if (this.#netboxVersion === undefined || semver.satisfies(this.#netboxVersion, '3.3.0 - 3.4.7')) {
|
|
|
|
|
nbVm.site = find(nbClusters, { id: nbCluster.id })?.site?.id ?? null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -362,7 +367,7 @@ class Netbox {
|
|
|
|
|
nbVm.platform = nbPlatform.id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nbVmTags = []
|
|
|
|
|
nbVm.tags = []
|
|
|
|
|
for (const tag of xoVm.tags) {
|
|
|
|
|
const slug = slugify(tag)
|
|
|
|
|
let nbTag = find(nbTags, { slug })
|
|
|
|
|
@@ -371,7 +376,7 @@ class Netbox {
|
|
|
|
|
nbTag = await this.#request('/extras/tags/', 'POST', {
|
|
|
|
|
name: tag,
|
|
|
|
|
slug,
|
|
|
|
|
color: '2598d9',
|
|
|
|
|
color: XO_TAG_COLOR,
|
|
|
|
|
description: 'XO tag',
|
|
|
|
|
})
|
|
|
|
|
nbTags[nbTag.id] = nbTag
|
|
|
|
|
@@ -380,16 +385,13 @@ class Netbox {
|
|
|
|
|
// Edge case: tags "foo" and "Foo" would have the same slug. It's
|
|
|
|
|
// allowed in XO but not in Netbox so in that case, we only add it once
|
|
|
|
|
// to Netbox.
|
|
|
|
|
if (!some(nbVmTags, { id: nbTag.id })) {
|
|
|
|
|
nbVmTags.push({ id: nbTag.id })
|
|
|
|
|
if (!some(nbVm.tags, { id: nbTag.id })) {
|
|
|
|
|
nbVm.tags.push({ id: nbTag.id })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort them so that they can be compared by diff()
|
|
|
|
|
nbVm.tags = nbVmTags.sort(({ id: id1 }, { id: id2 }) => (id1 < id2 ? -1 : 1))
|
|
|
|
|
|
|
|
|
|
// https://netbox.readthedocs.io/en/stable/release-notes/version-2.7/#api-choice-fields-now-use-string-values-3569
|
|
|
|
|
if (this.#netboxApiVersion !== undefined && !semver.satisfies(this.#netboxApiVersion, '>=2.7.0')) {
|
|
|
|
|
if (this.#netboxVersion !== undefined && !semver.satisfies(this.#netboxVersion, '>=2.7.0')) {
|
|
|
|
|
nbVm.status = xoVm.power_state === 'Running' ? 1 : 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -450,6 +452,16 @@ class Netbox {
|
|
|
|
|
|
|
|
|
|
if (nbVm !== undefined) {
|
|
|
|
|
// VM found in Netbox: update VM (I.1)
|
|
|
|
|
|
|
|
|
|
// Keep user defined tags by adding all existing tags that don't have XO tags color
|
|
|
|
|
updatedVm.tags = updatedVm.tags
|
|
|
|
|
.concat(
|
|
|
|
|
nbVm.tags
|
|
|
|
|
.filter(nbTag => nbTag.color !== XO_TAG_COLOR && !some(updatedVm.tags, { id: nbTag.id }))
|
|
|
|
|
.map(nbTag => ({ id: nbTag.id }))
|
|
|
|
|
)
|
|
|
|
|
.sort(({ id: id1 }, { id: id2 }) => (id1 < id2 ? -1 : 1))
|
|
|
|
|
|
|
|
|
|
const patch = diff(updatedVm, flattenNested(nbVm))
|
|
|
|
|
if (patch !== undefined) {
|
|
|
|
|
vmsToUpdate.push(patch)
|
|
|
|
|
|