Files
xen-orchestra/@xen-orchestra/xapi/docs/vm-sync-hook.md
2022-09-26 12:23:51 +02:00

3.8 KiB

VM Sync Hook

This feature is currently unstable and might change or be removed in the future.

Feedbacks are very welcome on the project bugtracker.

This feature is not currently supported for backups done with XO Proxy.

Before snapshotting (with or without memory, ie checkpoint), XO can notify the VM via an HTTP request.

A typical use case is to make sure the VM is in a consistent state during the snapshot process, for instance by making sure database writes are flushed to the disk.

This request will only be sent if the VM is in a running state.

Configuration

The feature is opt-in via a tag on the VM: xo:notify-on-snapshot.

By default, it will be an HTTPS request on the port 1727, on the first IP address reported by the VM.

If the VM Tools (i.e. management agent) are not installed on the VM or if you which to use another URL, you can specify this in the tag: xo:notify-on-snapshot=<URL>.

To guarantee the request comes from XO, a secret must be provided in the xo-server's (and xo-proxy if relevant) configuration:

[xapiOptions]
syncHookSecret = 'unique long string to ensure the request comes from XO'

Specification

XO will waits for the request to be answered before starting the snapshot, but will not wait longer than 1 minute.

If the request fails for any reason, XO will go ahead with snapshot immediately.

GET /sync HTTP/1.1
Authorization: Bearer dW5pcXVlIGxvbmcgc3RyaW5nIHRvIGVuc3VyZSB0aGUgcmVxdWVzdCBjb21lcyBmcm9tIFhP

When the snapshot is finished, another request will be sent:

GET /post-sync HTTP/1.1
Authorization: Bearer dW5pcXVlIGxvbmcgc3RyaW5nIHRvIGVuc3VyZSB0aGUgcmVxdWVzdCBjb21lcyBmcm9tIFhP

The created snapshot will have the special xo:synced tag set to make it identifiable.

Example server in Node

index.cjs:

const exec = require('node:util').promisify(require('node:child_process').execFile)

const SECRET = 'unique long string to ensure the request comes from XO'

const HANDLERS = {
  __proto__: null,

  async '/sync'() {
    // actions to do before the VM is snapshotted

    // in this example, the Linux command `sync` is called:
    await exec('sync')
  },

  async '/post-sync'() {
    // actions to do after the VM is snapshotted
  },
}

function checkAuthorization(req) {
  try {
    const { authorization } = req.headers
    if (authorization !== undefined) {
      const parts = authorization.split('  ')
      if (parts.length >= 1 && parts[0].toLowerCase() === 'bearer') {
        return Buffer.from(parts[1], 'base64').toString() === SECRET
      }
    }
  } catch (error) {
    console.warn('checkAuthorization', error)
  }
  return false
}

async function main() {
  // generate a self-signed certificate
  const [, key, cert] =
    /^(-----BEGIN PRIVATE KEY-----.+-----END PRIVATE KEY-----\n)(-----BEGIN CERTIFICATE-----.+-----END CERTIFICATE-----\n)$/s.exec(
      (await exec('openssl', ['req', '-batch', '-new', '-x509', '-nodes', '-newkey', 'rsa:2048', '-keyout', '-']))
        .stdout
    )

  const server = require('node:https').createServer({ cert, key }, async function onRequest(req, res) {
    if (!checkAuthorization(req)) {
      res.statusCode = 403
      return res.end('Forbidden')
    }

    const handler = HANDLERS[req.url.split('?')[0]]
    if (handler === undefined || req.method !== 'GET') {
      res.statusCode = 404
      return res.end('Not Found')
    }

    try {
      await handler()

      res.statusCode = 200
      res.end('Ok')
    } catch (error) {
      console.warn(error)

      if (!res.headersSent) {
        res.statusCode = 500
        res.write('Internal Error')
      }
      res.end()
    }
  })

  await new Promise((resolve, reject) => {
    server.on('close', resolve).on('error', reject).listen(1727)
  })
}

main().catch(console.warn)