mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-13 10:41:09 -06:00
Compare commits
3 Commits
test_bit_p
...
web-stack/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dba45f6cf | ||
|
|
b112a72f23 | ||
|
|
069ed9215f |
62
.eslintrc.js
62
.eslintrc.js
@@ -171,6 +171,7 @@ module.exports = {
|
||||
'vue/no-empty-component-block': 'error',
|
||||
'vue/no-multiple-objects-in-class': 'error',
|
||||
'vue/no-ref-object-reactivity-loss': 'error',
|
||||
'vue/no-required-prop-with-default': 'error',
|
||||
'vue/no-static-inline-styles': 'error',
|
||||
'vue/no-template-target-blank': 'error',
|
||||
'vue/no-undef-components': ['error', { ignorePatterns: ['RouterLink', 'RouterView', 'I18nT'] }],
|
||||
@@ -191,14 +192,18 @@ module.exports = {
|
||||
// Specific rules for XO i18n-related files
|
||||
{
|
||||
files: [
|
||||
'@xen-orchestra/lite/**/*.{vue,ts}',
|
||||
'@xen-orchestra/web/**/*.{vue,ts}',
|
||||
'@xen-orchestra/lite/**/*.{vue,ts,json}',
|
||||
'@xen-orchestra/web/**/*.{vue,ts,json}',
|
||||
'@xen-orchestra/web-core/**/*.{vue,ts,json}',
|
||||
],
|
||||
extends: ['plugin:@intlify/vue-i18n/base'],
|
||||
settings: {
|
||||
'vue-i18n': {
|
||||
localeDir: ['@xen-orchestra/web-core/lib/locales/*.json'],
|
||||
localeDir: [
|
||||
'@xen-orchestra/lite/src/locales/*.json',
|
||||
'@xen-orchestra/web/src/locales/*.json',
|
||||
'@xen-orchestra/web-core/lib/locales/*.json',
|
||||
],
|
||||
messageSyntaxVersion: '^9.9.0',
|
||||
},
|
||||
},
|
||||
@@ -216,47 +221,13 @@ module.exports = {
|
||||
'@intlify/vue-i18n/no-raw-text': [
|
||||
'error',
|
||||
{
|
||||
ignoreText: [
|
||||
'Xen Orchestra Lite',
|
||||
'Xen Orchestra',
|
||||
'Xen Orchestra Appliance',
|
||||
'XO Lite',
|
||||
'XCP-ng',
|
||||
'XO 5',
|
||||
'404',
|
||||
'⚠️',
|
||||
],
|
||||
ignoreText: ['Xen Orchestra Lite', 'Xen Orchestra', 'XO Lite', 'XCP-ng', 'XO 5', '404', '⚠️'],
|
||||
},
|
||||
],
|
||||
'@intlify/vue-i18n/no-v-html': 'error',
|
||||
'@intlify/vue-i18n/valid-message-syntax': 'error',
|
||||
'@intlify/vue-i18n/no-duplicate-keys-in-locale': 'error',
|
||||
'@intlify/vue-i18n/no-missing-keys-in-other-locales': [
|
||||
'error',
|
||||
{
|
||||
ignoreLocales: [
|
||||
'de',
|
||||
'fa',
|
||||
'cs',
|
||||
'es',
|
||||
'sv',
|
||||
'it',
|
||||
'ru',
|
||||
'uk',
|
||||
'nl',
|
||||
'pt-BR',
|
||||
'da',
|
||||
'ko',
|
||||
'nb-NO',
|
||||
'ja',
|
||||
'fi',
|
||||
'pl',
|
||||
'zh-Hans',
|
||||
'sk',
|
||||
'pt',
|
||||
],
|
||||
},
|
||||
],
|
||||
'@intlify/vue-i18n/no-missing-keys-in-other-locales': ['error', { ignoreLocales: ['de', 'fa', 'cs'] }],
|
||||
},
|
||||
},
|
||||
// Specific rules for XO dev-related files
|
||||
@@ -281,7 +252,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['@xen-orchestra/{web-core,lite,web}/src/route-map.d.ts'],
|
||||
files: ['@xen-orchestra/{web-core,lite,web}/typed-router.d.ts'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
},
|
||||
@@ -290,17 +261,6 @@ module.exports = {
|
||||
'eslint-comments/no-unlimited-disable': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.d.ts'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
'n/no-unsupported-features/es-syntax': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
ignorePatterns: ['@vates/fatfs/'],
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -40,8 +40,8 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
- Don't forget to [read this first](https://docs.xen-orchestra.com/community)
|
||||
- As well as follow [this guide](https://docs.xen-orchestra.com/community#report-a-bug)
|
||||
- Don't forget to [read this first](https://xen-orchestra.com/docs/community.html)
|
||||
- As well as follow [this guide](https://xen-orchestra.com/docs/community.html#report-a-bug)
|
||||
- type: input
|
||||
id: xo-sources-commit-number
|
||||
attributes:
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Continuous Integration
|
||||
name: Continous Integration
|
||||
on: push
|
||||
jobs:
|
||||
CI:
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Setup Node environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '>=22.12'
|
||||
node-version: '20'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install project dependencies
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,8 +7,6 @@
|
||||
/@vates/*/node_modules/
|
||||
/@xen-orchestra/*/dist/
|
||||
/@xen-orchestra/*/node_modules/
|
||||
/@xen-orchestra/rest-api/open-api/spec/swagger.json
|
||||
/@xen-orchestra/rest-api/src/open-api/routes/routes.ts
|
||||
/packages/*/dist/
|
||||
/packages/*/node_modules/
|
||||
|
||||
|
||||
@@ -3,5 +3,4 @@
|
||||
**/*.hbs
|
||||
|
||||
# Generated files
|
||||
/@xen-orchestra/web/src/route-map.d.ts
|
||||
/@xen-orchestra/lite/src/route-map.d.ts
|
||||
/@xen-orchestra/web/typed-router.d.ts
|
||||
|
||||
@@ -16,7 +16,7 @@ Returns a promise wich rejects as soon as a call to `iteratee` throws or a promi
|
||||
|
||||
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `10`. The value `0` means no concurrency limit.
|
||||
- `signal`: an abort signal to stop the iteration
|
||||
- `stopOnError`: whether to stop iteration of first error, or wait for all calls to finish and throw an `AggregateError`, defaults to `true`
|
||||
- `stopOnError`: wether to stop iteration of first error, or wait for all calls to finish and throw an `AggregateError`, defaults to `true`
|
||||
|
||||
```js
|
||||
import { asyncEach } from '@vates/async-each'
|
||||
|
||||
@@ -34,7 +34,7 @@ Returns a promise wich rejects as soon as a call to `iteratee` throws or a promi
|
||||
|
||||
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `10`. The value `0` means no concurrency limit.
|
||||
- `signal`: an abort signal to stop the iteration
|
||||
- `stopOnError`: whether to stop iteration of first error, or wait for all calls to finish and throw an `AggregateError`, defaults to `true`
|
||||
- `stopOnError`: wether to stop iteration of first error, or wait for all calls to finish and throw an `AggregateError`, defaults to `true`
|
||||
|
||||
```js
|
||||
import { asyncEach } from '@vates/async-each'
|
||||
|
||||
5
@vates/async-each/index.d.ts
vendored
5
@vates/async-each/index.d.ts
vendored
@@ -1,5 +0,0 @@
|
||||
export function asyncEach<Item>(
|
||||
iterable: Iterable<Item> | AsyncIterable<Item>,
|
||||
iteratee: (item: Item, index: number, iterable: Iterable<Item> | AsyncIterable<Item>) => Promise<void>,
|
||||
opts?: { concurrency?: number; signal?: AbortSignal; stopOnError?: boolean }
|
||||
): Promise<void>
|
||||
@@ -11,8 +11,8 @@ class AggregateError extends Error {
|
||||
|
||||
/**
|
||||
* @template Item
|
||||
* @param {Iterable<Item>|AsyncIterable<Item>} iterable
|
||||
* @param {(item: Item, index: number, iterable: Iterable<Item>|AsyncIterable<Item>) => Promise<void>} iteratee
|
||||
* @param {Iterable<Item>} iterable
|
||||
* @param {(item: Item, index: number, iterable: Iterable<Item>) => Promise<void>} iteratee
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
exports.asyncEach = function asyncEach(iterable, iteratee, { concurrency = 10, signal, stopOnError = true } = {}) {
|
||||
@@ -73,20 +73,20 @@ exports.asyncEach = function asyncEach(iterable, iteratee, { concurrency = 10, s
|
||||
}
|
||||
nextIsRunning = true
|
||||
if (running < concurrency) {
|
||||
try {
|
||||
const cursor = await it.next()
|
||||
if (cursor.done) {
|
||||
next = () => {
|
||||
if (running === 0) {
|
||||
if (errors.length === 0) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new AggregateError(errors))
|
||||
}
|
||||
const cursor = await it.next()
|
||||
if (cursor.done) {
|
||||
next = () => {
|
||||
if (running === 0) {
|
||||
if (errors.length === 0) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new AggregateError(errors))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
++running
|
||||
}
|
||||
} else {
|
||||
++running
|
||||
try {
|
||||
const result = iteratee.call(this, cursor.value, index++, iterable)
|
||||
let then
|
||||
if (result != null && typeof result === 'object' && typeof (then = result.then) === 'function') {
|
||||
@@ -94,9 +94,9 @@ exports.asyncEach = function asyncEach(iterable, iteratee, { concurrency = 10, s
|
||||
} else {
|
||||
onFulfilled(result)
|
||||
}
|
||||
} catch (error) {
|
||||
onRejected(error)
|
||||
}
|
||||
} catch (error) {
|
||||
onRejected(error)
|
||||
}
|
||||
nextIsRunning = false
|
||||
return next()
|
||||
|
||||
@@ -67,17 +67,20 @@ describe('asyncEach', () => {
|
||||
})
|
||||
|
||||
it('stops on first error when stopOnError is true', async () => {
|
||||
const tracker = new assert.CallTracker()
|
||||
|
||||
const error = new Error()
|
||||
const iteratee = spy((_, i) => {
|
||||
const iteratee = tracker.calls((_, i) => {
|
||||
if (i === 1) {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}, 2)
|
||||
assert.deepStrictEqual(
|
||||
await rejectionOf(asyncEach(iterable, iteratee, { concurrency: 1, stopOnError: true })),
|
||||
error
|
||||
)
|
||||
assert.deepStrictEqual(iteratee.callCount, 2)
|
||||
|
||||
tracker.verify()
|
||||
})
|
||||
|
||||
it('rejects AggregateError when stopOnError is false', async () => {
|
||||
@@ -97,30 +100,19 @@ describe('asyncEach', () => {
|
||||
})
|
||||
|
||||
it('can be interrupted with an AbortSignal', async () => {
|
||||
const tracker = new assert.CallTracker()
|
||||
|
||||
const ac = new AbortController()
|
||||
const iteratee = spy((_, i) => {
|
||||
const iteratee = tracker.calls((_, i) => {
|
||||
if (i === 1) {
|
||||
ac.abort()
|
||||
}
|
||||
})
|
||||
}, 2)
|
||||
await assert.rejects(asyncEach(iterable, iteratee, { concurrency: 1, signal: ac.signal }), {
|
||||
message: 'asyncEach aborted',
|
||||
})
|
||||
assert.deepStrictEqual(iteratee.callCount, 2)
|
||||
})
|
||||
|
||||
it('handle error on iterable', async () => {
|
||||
const iteratee = spy(async () => {})
|
||||
const iterable = async function* () {
|
||||
for (const value of [1, 2, 3]) {
|
||||
if (value === 2) {
|
||||
throw new Error('iterator error')
|
||||
}
|
||||
await randomDelay()
|
||||
yield value
|
||||
}
|
||||
}
|
||||
await assert.rejects(() => asyncEach(iterable(), iteratee, { concurrency: 1 }), { message: 'iterator error' })
|
||||
tracker.verify()
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
@@ -24,8 +24,7 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @param {Array|Buffer|string} data1 - The first data array or buffer to compare.
|
||||
* @param {T} data2 - The second data array or buffer to compare.
|
||||
* @param {(index: number, diff: T) => void} [cb] - The callback function to invoke for each difference. If not provided, an array of differences will be returned.
|
||||
* @returns {Array<number|T>|undefined} - An array of differences if no callback is provided; otherwise, undefined.
|
||||
* @returns {Array<number|T>|undefined} - An array of differences if no callback is provided, otherwise undefined.
|
||||
*/
|
||||
module.exports = function diff(data1, data2, cb) {
|
||||
let result
|
||||
|
||||
@@ -14,7 +14,7 @@ import { deduped } from '@vates/disposable/deduped'
|
||||
// the connection with the passed host will be established once at the first call, then, it will be shared with the next calls
|
||||
const getConnection = deduped(async function (host)) {
|
||||
const connection = new Connection(host)
|
||||
return new Disposable(connection, () => connection.close())
|
||||
return new Disposabe(connection, () => connection.close())
|
||||
}, host => [host])
|
||||
```
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ import { deduped } from '@vates/disposable/deduped'
|
||||
// the connection with the passed host will be established once at the first call, then, it will be shared with the next calls
|
||||
const getConnection = deduped(async function (host)) {
|
||||
const connection = new Connection(host)
|
||||
return new Disposable(connection, () => connection.close())
|
||||
return new Disposabe(connection, () => connection.close())
|
||||
}, host => [host])
|
||||
```
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vates/multi-key-map": "^0.2.0",
|
||||
"@xen-orchestra/async-map": "^0.1.3",
|
||||
"@xen-orchestra/log": "^0.7.2",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.7.1",
|
||||
"ensure-array": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
3
@vates/fatfs/.gitignore
vendored
Normal file
3
@vates/fatfs/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.DS_Store
|
||||
.vscode/
|
||||
node_modules/
|
||||
@@ -28,7 +28,7 @@ fs.stat('autoexec.bat', function (e, stats) {
|
||||
|
||||
## API
|
||||
|
||||
- `fs = fatfs.createFileSystem(vol, [opts], [cb])` — Simply pass in a block driver (see below) mapped to a FAT partition somewhere, and get back an instance of an [`fs`](http://nodejs.org/api/fs.html). An options dictionary can be provided, details are in the next section. You may also optionally provide a callback `cb(err)` which will be automatically registered for the on `'ready'` or `'error'` event.
|
||||
- `fs = fatfs.createFileSystem(vol, [opts], [cb])` — Simply pass in a block driver (see below) mapped to a FAT partition somewhere, and get back the API documented [here](http://nodejs.org/api/fs.html). An options dictionary can be provided, details are in the next section. You may also optionally provide a callback `cb(err)` which will be automatically registered for the on `'ready'` or `'error'` event.
|
||||
- `'ready'` event — fired on `fs` when initial volume information has been determined and the API is ready to use. It is safe to call other `fs` methods before this fires **only if** you are sure the first sector will be readable and represents a valid FAT volume.
|
||||
- `'error'` event — fired if initialization fails for whatever reason.
|
||||
|
||||
@@ -36,7 +36,7 @@ fs.stat('autoexec.bat', function (e, stats) {
|
||||
|
||||
The `opts` dictionary you pass to `fatfs.createFileSystem` can contain any of the following options:
|
||||
|
||||
- `ro` — Enables readonly mode if `true`. It defaults to `false`, but if your volume driver does not provide a `writeSectors` method it will be overridden to `true`.
|
||||
- `ro` — Enables readonly mode if `true`. It defaults to `false`, but if your volume driver does not provide a `writeSectors` method it will be overriden to `true`.
|
||||
- `noatime` — The FAT filesystem can track the last access time (just a date, actually) but this means every read operation would also incur some write overhead. Defaults to `true`, meaning by default access times will **not** be updated on reads. Set this to `false` to track access times.
|
||||
- `modmode` — chooses how `fs.chmod` (and the mode field from `fs.stat`–family calls) should map FAT attributes to POSIX permissions. Set to the number `0111` to map the readonly flag to the user's write bit being unset, and the archive/system/hidden flags to the user/group/other executable bits respectively. Set to the number `07000` to map the readonly flag to _all_ write bits being unset, and the archive/system/hidden flags to the sticky/setgid/setuid bits respectively. Set to `null` for readonly mapping. Defaults to `0111`.
|
||||
- `umask` — any bits _set_ in this octal number will be _unset_ in the 'mode' field from `fs.stat`–family calls. It does not affect anything else. Defaults to `process.umask()`, or `0022` if that is unavailable.
|
||||
@@ -53,7 +53,7 @@ Well, sort of…
|
||||
|
||||
### Temporary
|
||||
|
||||
- **BETA** **BETA** **BETA**. Seriously, this is a _brand new_, _from scratch_, _completely unproven_ filesystem implementation. It does not have full automated test coverage, and it has not been manually tested very much either. Please, please, please, **make sure you have a backup** of any important drive/image/card you unleash this upon.
|
||||
- **BETA** **BETA** **BETA**. Seriously, this is a _brand new_, _from scratch_, _completely unproven_ filesystem implementation. It does not have full automated test coverage, and it has not been manually tested very much either. Please please please **make sure you have a backup** of any important drive/image/card you unleash this upon.
|
||||
- A few methods are not quite implemented, either: `fs.rename`, `fs.unlink` and `fs.rmdir`, as well as `fs.watchFile`/`fs.unwatchFile` and `fs.watch`. These are Coming Soon™.
|
||||
- There are several internal housekeeping items (redundant FAT tables, extra FAT32 information, etc.) that are not done. These do not seem to affect interop, but you may see warnings when repairing a filesystem written by this module.
|
||||
- Oh, and not to scare you, but if an IO error happens while writing, the library usually just bails — bubbling an error up to your callback as if it were a hot potato. Although some attempt has been made to do separate writes in the safest order (e.g. allocating an additional file cluster, then appending data into it, and then finally updating the file's size), but this behavior has not been thoroughly audited for all operations. There's certainly no attempt to retry/cleanup/rollback if a multi-step change runs into trouble partway through.
|
||||
|
||||
@@ -62,7 +62,7 @@ exports.parseFlags = function (flags) {
|
||||
case 'ax': info = {read:false, write:true, create:true, append:true, exclusive:true}; break;
|
||||
case 'a+': info = {read:true, write:true, create:true, append:true}; break;
|
||||
case 'ax+': info = {read:true, write:true, create:true, append:true, exclusive:true}; break;
|
||||
default: throw Error("Unknown mode: "+flags); // TODO: throw as `S.err.INVAL`
|
||||
default: throw Error("Uknown mode: "+flags); // TODO: throw as `S.err.INVAL`
|
||||
}
|
||||
if (info.sync) throw Error("Mode not implemented."); // TODO: what would this require of us?
|
||||
if (_dir) info._openDir = true;
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@vates/fatfs",
|
||||
"version": "0.11.1",
|
||||
"version": "0.11.0",
|
||||
"description": "fs implementation on top of raw FAT16/FAT32 block source",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
},
|
||||
"repository": {
|
||||
"directory": "@vates/fatfs",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
"url": "https://github.com/vatesfr/xen-orchestra"
|
||||
},
|
||||
"keywords": [
|
||||
"filesystem",
|
||||
@@ -21,14 +20,13 @@
|
||||
],
|
||||
"author": "Vates SAS",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/fatfs",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vatesfr/xen-orchestra/issues"
|
||||
},
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra",
|
||||
"dependencies": {
|
||||
"fifolock": "^1.0.0",
|
||||
"struct-fu": "^1.2.1",
|
||||
"xok": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ function startTests(vol, waitTime) {
|
||||
assert(path === file, "Fixed fluffy path matches normal one.");
|
||||
});
|
||||
fs.realpath([BASE_DIR,"non","existent","path"].join('/'), function (e) {
|
||||
assert(e, "Expected error calling fs.realpath on nonexistent file.");
|
||||
assert(e, "Expected error calling fs.realpath on non-existent file.");
|
||||
});
|
||||
|
||||
fs.readdir(BASE_DIR, function (e, arr) {
|
||||
|
||||
@@ -13,7 +13,7 @@ await mount(handler, diskId, mountPoint)
|
||||
|
||||
### cli
|
||||
|
||||
From the install folder:
|
||||
From the install folder :
|
||||
|
||||
```
|
||||
cli.mjs <remoteUrl> <vhdPathInRemote> <mountPoint>
|
||||
@@ -25,25 +25,4 @@ After installing the package
|
||||
xo-fuse-vhd <remoteUrl> <vhdPathInRemote> <mountPoint>
|
||||
```
|
||||
|
||||
The `remoteUrl` can be found using the following command from the XOA CLI, and then picking the right `url` property:
|
||||
|
||||
`xo-server-db ls remote`
|
||||
|
||||
The `vhdPathInRemote` follows a file structure described in [VM backups](https://github.com/vatesfr/xen-orchestra/blob/master/%40xen-orchestra/backups/docs/VM%20backups/README.md).
|
||||
|
||||
## Restore a file from a VHD using `fuse-vhd` CLI
|
||||
|
||||
1. Mount a VHD to the Filesystem (see **Usage**).
|
||||
2. Verify the VHD is Correctly Mounted:
|
||||
|
||||
`mount | grep fuse`
|
||||
|
||||
3. Get the `START` and `SIZE` of the disk whose file you want to restore. Multiply `START` by 512 (block size) to get the offset value. (`mountedVhdPath` is the `mountPoint` value from earlier, followed by `/vhd0`, for example). Depending on the partition type, you may need to do some additional setup to discover the partitions.
|
||||
|
||||
`partx --bytes --output=NR,START,SIZE,NAME,UUID,TYPE --pairs <mountedVhdPath>`
|
||||
|
||||
4. Mount the disk. Depending on the partition type, you may need to add some additional options. The `norecovery` option is used for ext3/ext4/xfs file systems; otherwise, remove this option.
|
||||
|
||||
`mount --options=loop,ro,norecovery,sizelimit=<SIZE>,offset=<START*512> --source=<mountedVhdPath> --target=<diskMountPoint>`
|
||||
|
||||
5. Copy your files from the mounted partition, then unmount the partition.
|
||||
remoteUrl can be found by using cli in `@xen-orchestra/fs` , for example a local remote will have a url like `file:///path/to/remote/root`
|
||||
|
||||
@@ -29,7 +29,7 @@ await mount(handler, diskId, mountPoint)
|
||||
|
||||
### cli
|
||||
|
||||
From the install folder:
|
||||
From the install folder :
|
||||
|
||||
```
|
||||
cli.mjs <remoteUrl> <vhdPathInRemote> <mountPoint>
|
||||
@@ -41,28 +41,7 @@ After installing the package
|
||||
xo-fuse-vhd <remoteUrl> <vhdPathInRemote> <mountPoint>
|
||||
```
|
||||
|
||||
The `remoteUrl` can be found using the following command from the XOA CLI, and then picking the right `url` property:
|
||||
|
||||
`xo-server-db ls remote`
|
||||
|
||||
The `vhdPathInRemote` follows a file structure described in [VM backups](https://github.com/vatesfr/xen-orchestra/blob/master/%40xen-orchestra/backups/docs/VM%20backups/README.md).
|
||||
|
||||
## Restore a file from a VHD using `fuse-vhd` CLI
|
||||
|
||||
1. Mount a VHD to the Filesystem (see **Usage**).
|
||||
2. Verify the VHD is Correctly Mounted:
|
||||
|
||||
`mount | grep fuse`
|
||||
|
||||
3. Get the `START` and `SIZE` of the disk whose file you want to restore. Multiply `START` by 512 (block size) to get the offset value. (`mountedVhdPath` is the `mountPoint` value from earlier, followed by `/vhd0`, for example). Depending on the partition type, you may need to do some additional setup to discover the partitions.
|
||||
|
||||
`partx --bytes --output=NR,START,SIZE,NAME,UUID,TYPE --pairs <mountedVhdPath>`
|
||||
|
||||
4. Mount the disk. Depending on the partition type, you may need to add some additional options. The `norecovery` option is used for ext3/ext4/xfs file systems; otherwise, remove this option.
|
||||
|
||||
`mount --options=loop,ro,norecovery,sizelimit=<SIZE>,offset=<START*512> --source=<mountedVhdPath> --target=<diskMountPoint>`
|
||||
|
||||
5. Copy your files from the mounted partition, then unmount the partition.
|
||||
remoteUrl can be found by using cli in `@xen-orchestra/fs` , for example a local remote will have a url like `file:///path/to/remote/root`
|
||||
|
||||
## Contributions
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import Fuse from 'fuse-native'
|
||||
import LRU from 'lru-cache'
|
||||
import Fuse from 'fuse-native'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
import { VhdSynthetic } from 'vhd-lib'
|
||||
import { Disposable, fromCallback } from 'promise-toolbox'
|
||||
|
||||
const { warn } = createLogger('vates:fuse-vhd')
|
||||
|
||||
// build a stat object from https://github.com/fuse-friends/fuse-native/blob/master/test/fixtures/stat.js
|
||||
// build a s stat object from https://github.com/fuse-friends/fuse-native/blob/master/test/fixtures/stat.js
|
||||
const stat = st => ({
|
||||
mtime: st.mtime || new Date(),
|
||||
atime: st.atime || new Date(),
|
||||
@@ -20,109 +20,42 @@ const stat = st => ({
|
||||
export const mount = Disposable.factory(async function* mount(handler, diskPath, mountDir) {
|
||||
const vhd = yield VhdSynthetic.fromVhdChain(handler, diskPath)
|
||||
|
||||
const cache = new LRU({
|
||||
max: 16, // each cached block is 2MB in size
|
||||
})
|
||||
await vhd.readBlockAllocationTable()
|
||||
const { blockSize } = vhd.header
|
||||
|
||||
// A zeroed buffer returned for VHD blocks that are not present in the chain.
|
||||
// Never mutated — safe to share across all reads.
|
||||
const EMPTY_BLOCK = Buffer.alloc(blockSize, 0)
|
||||
|
||||
// --- coalescing block read queue ---
|
||||
//
|
||||
// FUSE read requests from NTFS-3g arrive in small chunks (typically 4 KiB)
|
||||
// while VHD blocks are 2 MiB. Reading blocks one at a time from the backing
|
||||
// store (CIFS/NFS/…) bounds libuv threadpool usage to 1 concurrent I/O call.
|
||||
//
|
||||
// Coalescing: all pending requests for the same blockId share a single
|
||||
// readBlock() call. When the block resolves, every waiter is fulfilled at once.
|
||||
//
|
||||
// Together these two properties prevent the FUSE-on-FUSE threadpool deadlock:
|
||||
// concurrent NTFS-3g reads no longer starve the VHD block reads they depend on.
|
||||
// In-memory block cache. Each VHD block is 2 MiB; 16 blocks = 32 MiB.
|
||||
// This is the primary tool against the 300× CIFS amplification: NTFS-3g
|
||||
// reads a 2 MiB block in ~512 sequential 4 KiB FUSE requests. Without a
|
||||
// cache, each request causes a full 2 MiB CIFS read. With the cache, only
|
||||
// the first request reads from CIFS; the rest are served from RAM.
|
||||
const blockCache = new LRU({ max: 64 })
|
||||
|
||||
const pendingBlocks = new Map() // blockId → Promise<Buffer>
|
||||
const readQueue = [] // { blockId, resolve, reject }[]
|
||||
let queueRunning = false
|
||||
|
||||
// log stats every 10 s while there is activity
|
||||
|
||||
function enqueueBlock(blockId) {
|
||||
// 1. serve from cache if available
|
||||
if (blockCache.has(blockId)) {
|
||||
return Promise.resolve(blockCache.get(blockId))
|
||||
}
|
||||
// 2. coalesce: join an existing in-flight read for this block
|
||||
if (pendingBlocks.has(blockId)) {
|
||||
return pendingBlocks.get(blockId)
|
||||
}
|
||||
const p = new Promise((resolve, reject) => {
|
||||
readQueue.push({ blockId, resolve, reject })
|
||||
})
|
||||
pendingBlocks.set(blockId, p)
|
||||
p.then(
|
||||
data => {
|
||||
pendingBlocks.delete(blockId)
|
||||
blockCache.set(blockId, data)
|
||||
},
|
||||
() => pendingBlocks.delete(blockId)
|
||||
)
|
||||
drainQueue()
|
||||
return p
|
||||
}
|
||||
|
||||
function drainQueue() {
|
||||
if (queueRunning) return
|
||||
queueRunning = true
|
||||
// async IIFE — errors are forwarded to individual reject() callbacks,
|
||||
// so the outer floating promise is always fulfilled
|
||||
;(async () => {
|
||||
while (readQueue.length > 0) {
|
||||
const { blockId, resolve, reject } = readQueue.shift()
|
||||
try {
|
||||
resolve((await vhd.readBlock(blockId)).data)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
}
|
||||
queueRunning = false
|
||||
})()
|
||||
}
|
||||
|
||||
async function readData(buf, len, pos) {
|
||||
if (len === 0) return 0
|
||||
const startBlockId = Math.floor(pos / blockSize)
|
||||
// use (pos + len - 1) so an exact block boundary doesn't include the next block
|
||||
const endBlockId = Math.floor((pos + len - 1) / blockSize)
|
||||
|
||||
let copied = 0
|
||||
for (let blockId = startBlockId; blockId <= endBlockId; blockId++) {
|
||||
const data = vhd.containsBlock(blockId) ? await enqueueBlock(blockId) : EMPTY_BLOCK
|
||||
const offsetStart = blockId === startBlockId ? pos % blockSize : 0
|
||||
const offsetEnd = blockId === endBlockId ? ((pos + len - 1) % blockSize) + 1 : blockSize
|
||||
data.copy(buf, copied, offsetStart, offsetEnd)
|
||||
copied += offsetEnd - offsetStart
|
||||
}
|
||||
return copied
|
||||
}
|
||||
|
||||
const fuse = new Fuse(mountDir, {
|
||||
async readdir(path, cb) {
|
||||
if (path === '/') return cb(null, ['vhd0'])
|
||||
if (path === '/') {
|
||||
return cb(null, ['vhd0'])
|
||||
}
|
||||
cb(Fuse.ENOENT)
|
||||
},
|
||||
async getattr(path, cb) {
|
||||
if (path === '/') return cb(null, stat({ mode: 'dir', size: 4096 }))
|
||||
if (path === '/vhd0') return cb(null, stat({ mode: 'file', size: vhd.footer.currentSize }))
|
||||
if (path === '/') {
|
||||
return cb(
|
||||
null,
|
||||
stat({
|
||||
mode: 'dir',
|
||||
size: 4096,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (path === '/vhd0') {
|
||||
return cb(
|
||||
null,
|
||||
stat({
|
||||
mode: 'file',
|
||||
size: vhd.footer.currentSize,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
cb(Fuse.ENOENT)
|
||||
},
|
||||
read(path, fd, buf, len, pos, cb) {
|
||||
if (path === '/vhd0') {
|
||||
return readData(buf, len, pos).then(cb, error => {
|
||||
return vhd.readRawData(pos, len, cache, buf).then(cb, error => {
|
||||
warn('read error', { path, len, pos, error })
|
||||
cb(Fuse.EIO)
|
||||
})
|
||||
@@ -131,9 +64,7 @@ export const mount = Disposable.factory(async function* mount(handler, diskPath,
|
||||
},
|
||||
})
|
||||
return new Disposable(
|
||||
() => {
|
||||
return fromCallback(cb => fuse.unmount(cb))
|
||||
},
|
||||
() => fromCallback(cb => fuse.unmount(cb)),
|
||||
fromCallback(cb => fuse.mount(cb))
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vates/fuse-vhd",
|
||||
"version": "2.1.3",
|
||||
"version": "2.1.2",
|
||||
"license": "ISC",
|
||||
"private": false,
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/fuse-vhd",
|
||||
@@ -19,12 +19,12 @@
|
||||
},
|
||||
"main": "./index.mjs",
|
||||
"dependencies": {
|
||||
"@xen-orchestra/fs": "^4.9.2",
|
||||
"@xen-orchestra/log": "^0.7.2",
|
||||
"@xen-orchestra/fs": "^4.3.0",
|
||||
"@xen-orchestra/log": "^0.7.1",
|
||||
"fuse-native": "^2.2.6",
|
||||
"lru-cache": "^7.14.0",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"vhd-lib": "^4.16.1"
|
||||
"vhd-lib": "^4.11.2"
|
||||
},
|
||||
"bin": {
|
||||
"xo-fuse-vhd": "./cli.mjs"
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
A toolbox to ease the use of generator
|
||||
|
||||
### Timeout
|
||||
|
||||
wrap a source async generator to have it throw an error when timeout is reached
|
||||
timeout is a positive number in milliseconds
|
||||
|
||||
```js
|
||||
import { Timeout } from '@vates/generator-toolbox'
|
||||
|
||||
const wrappedGenerator = new Timeout(sourceGenerator, timeout)
|
||||
```
|
||||
|
||||
### Throttle
|
||||
|
||||
wrap a source async generator to have it respect a max speed ( in bytes per seconds).
|
||||
speed is either a strictly positive number or a function returning a strictly positive number. A speed change will be used for the next emitted packet.
|
||||
|
||||
The source generator must yield object with a length property.
|
||||
|
||||
Optimized for small yields regarding to the speed, since it won't split incoming packet.
|
||||
|
||||
If the generator reached the max speed it will be paused, limiting memory consumption.
|
||||
|
||||
```js
|
||||
import { Throttle } from '@vates/generator-toolbox'
|
||||
|
||||
const wrappedGenerator = new Throttle(sourceGenerator, speed)
|
||||
```
|
||||
|
||||
### Synchronized
|
||||
|
||||
Fork a generator. The rules are:
|
||||
|
||||
- if the source returns, all the forks return
|
||||
- if the forks error, all the forks error with the same error
|
||||
- if a fork returns, it is stopped, but the generator continue with the other
|
||||
- if a fork errors, it is stopped, but the generator continue with the other
|
||||
- if all the forks return, the source is stopped
|
||||
- if all the forks error, the source is errored with the last error
|
||||
- the source start producing a packet when the fastest forked ask for it
|
||||
- the source forks get the packet only when all the forks asked for it, no buffer stores in memory
|
||||
|
||||
```ts
|
||||
import { Synchronized } from '@vates/generator-toolbox'
|
||||
|
||||
async function consume(generator: AsyncGenerator) {
|
||||
for await (const val of generator) {
|
||||
console.log({ val })
|
||||
}
|
||||
}
|
||||
const forker = new Synchronized(generator)
|
||||
const first = forker.fork('first')
|
||||
const second = forker.fork('second')
|
||||
await Promise.all([consume(first), consume(second)])
|
||||
```
|
||||
|
||||
Note: you can stop early a generator by calling `generator.return()`, and you can stop in in error by calling `generator.throw(error)`
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,89 +0,0 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/generator-toolbox
|
||||
|
||||
[](https://npmjs.org/package/@vates/generator-toolbox)  [](https://bundlephobia.com/result?p=@vates/generator-toolbox) [](https://npmjs.org/package/@vates/generator-toolbox)
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/generator-toolbox):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/generator-toolbox
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
A toolbox to ease the use of generator
|
||||
|
||||
### Timeout
|
||||
|
||||
wrap a source async generator to have it throw an error when timeout is reached
|
||||
timeout is a positive number in milliseconds
|
||||
|
||||
```js
|
||||
import { Timeout } from '@vates/generator-toolbox'
|
||||
|
||||
const wrappedGenerator = new Timeout(sourceGenerator, timeout)
|
||||
```
|
||||
|
||||
### Throttle
|
||||
|
||||
wrap a source async generator to have it respect a max speed ( in bytes per seconds).
|
||||
speed is either a strictly positive number or a function returning a strictly positive number. A speed change will be used for the next emitted packet.
|
||||
|
||||
The source generator must yield object with a length property.
|
||||
|
||||
Optimized for small yields regarding to the speed, since it won't split incoming packet.
|
||||
|
||||
If the generator reached the max speed it will be paused, limiting memory consumption.
|
||||
|
||||
```js
|
||||
import { Throttle } from '@vates/generator-toolbox'
|
||||
|
||||
const wrappedGenerator = new Throttle(sourceGenerator, speed)
|
||||
```
|
||||
|
||||
### Synchronized
|
||||
|
||||
Fork a generator. The rules are:
|
||||
|
||||
- if the source returns, all the forks return
|
||||
- if the forks error, all the forks error with the same error
|
||||
- if a fork returns, it is stopped, but the generator continue with the other
|
||||
- if a fork errors, it is stopped, but the generator continue with the other
|
||||
- if all the forks return, the source is stopped
|
||||
- if all the forks error, the source is errored with the last error
|
||||
- the source start producing a packet when the fastest forked ask for it
|
||||
- the source forks get the packet only when all the forks asked for it, no buffer stores in memory
|
||||
|
||||
```ts
|
||||
import { Synchronized } from '@vates/generator-toolbox'
|
||||
|
||||
async function consume(generator: AsyncGenerator) {
|
||||
for await (const val of generator) {
|
||||
console.log({ val })
|
||||
}
|
||||
}
|
||||
const forker = new Synchronized(generator)
|
||||
const first = forker.fork('first')
|
||||
const second = forker.fork('second')
|
||||
await Promise.all([consume(first), consume(second)])
|
||||
```
|
||||
|
||||
Note: you can stop early a generator by calling `generator.return()`, and you can stop in in error by calling `generator.throw(error)`
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://spdx.org/licenses/MIT) © [Vates SAS](https://vates.fr)
|
||||
@@ -1,6 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import eslint from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(eslint.configs.recommended, tseslint.configs.recommended)
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"name": "@vates/generator-toolbox",
|
||||
"version": "1.1.3",
|
||||
"main": "dist/index.mjs",
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"@types/node": "^20.6",
|
||||
"typescript": "~5.6",
|
||||
"typescript-eslint": "^8.61.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"postversion": "npm publish --access public",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "yarn run prebuild",
|
||||
"test": "tsc && node --test **/*.test.mjs",
|
||||
"clean": "rimraf dist/"
|
||||
},
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/generator-toolbox",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/generator-toolbox",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.18"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { Synchronized } from './synchronized.mjs'
|
||||
export { Throttle } from './throttle.mjs'
|
||||
export { Timeout } from './timeout.mjs'
|
||||
@@ -1,150 +0,0 @@
|
||||
import assert from 'node:assert'
|
||||
|
||||
export class Synchronized<T, TReturn, TNext> {
|
||||
#source: AsyncGenerator<T, TReturn, TNext>
|
||||
#forks = new Map<string, Forked<T, TReturn, TNext>>()
|
||||
#removedForks = new Set<string>()
|
||||
#waitingForks = new Set<string>()
|
||||
#started = false
|
||||
|
||||
#nextValueForksReady?: {
|
||||
promise: Promise<IteratorResult<T>>
|
||||
forksWaitingReject: (error: Error) => void
|
||||
forksWaitingResolve: () => void
|
||||
}
|
||||
|
||||
constructor(source: AsyncGenerator<T, TReturn, TNext>) {
|
||||
this.#source = source
|
||||
}
|
||||
|
||||
fork(uid: string): AsyncGenerator<T, TReturn, TNext> {
|
||||
assert.strictEqual(this.#started, false, `can't create a fork after consuming the data`)
|
||||
assert.strictEqual(this.#forks.has(uid), false, `a fork with id ${uid} already exists`)
|
||||
|
||||
const fork = new Forked<T, TReturn, TNext>(this, uid)
|
||||
this.#forks.set(uid, fork)
|
||||
return fork
|
||||
}
|
||||
|
||||
async #resolveWhenAllForksReady(): Promise<IteratorResult<T>> {
|
||||
if (!this.#nextValueForksReady) {
|
||||
throw new Error('Can t wait forks if there are noone waiting')
|
||||
}
|
||||
const { promise, forksWaitingResolve } = this.#nextValueForksReady
|
||||
if (this.#waitingForks.size === this.#forks.size) {
|
||||
// reset value
|
||||
this.#waitingForks.clear()
|
||||
this.#nextValueForksReady = undefined
|
||||
forksWaitingResolve() // for the other forks waiting
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
async next(uid: string): Promise<IteratorResult<T>> {
|
||||
if (this.#removedForks.has(uid)) {
|
||||
return { done: true, value: undefined }
|
||||
}
|
||||
if (!this.#forks.has(uid)) {
|
||||
throw new Error(`trying to advance fork ${uid} that is not a fork of this one`)
|
||||
}
|
||||
|
||||
if (this.#waitingForks.has(uid)) {
|
||||
throw new Error(`Fork ${uid} is already waiting`)
|
||||
}
|
||||
|
||||
this.#started = true
|
||||
if (this.#nextValueForksReady === undefined) {
|
||||
let forksWaitingResolve = () => {}
|
||||
let forksWaitingReject: (reason?: Error) => void = () => {}
|
||||
const next = this.#source.next().catch(async error => {
|
||||
const e = new Error(`Error in the source generator ${error.message}`, { cause: error })
|
||||
forksWaitingReject(e)
|
||||
// source has failed, kill everything, and stop the forks
|
||||
for (const uid of [...this.#forks.keys()]) {
|
||||
await this.remove(uid, error)
|
||||
}
|
||||
})
|
||||
const promise = Promise.all([
|
||||
next,
|
||||
new Promise((_resolve, _reject) => {
|
||||
forksWaitingResolve = () => _resolve(undefined)
|
||||
forksWaitingReject = _reject
|
||||
}),
|
||||
]).then(([_]) => _ as IteratorResult<T>)
|
||||
|
||||
this.#nextValueForksReady = { promise, forksWaitingResolve, forksWaitingReject }
|
||||
}
|
||||
this.#waitingForks.add(uid)
|
||||
return this.#resolveWhenAllForksReady()
|
||||
}
|
||||
|
||||
async remove(uid: string, error?: Error): Promise<IteratorResult<T>> {
|
||||
const fork = this.#forks.get(uid)
|
||||
if (fork === undefined) {
|
||||
if (this.#removedForks.has(uid)) {
|
||||
// already removed
|
||||
return { done: true, value: undefined }
|
||||
}
|
||||
throw new Error(`trying to remove fork with uid ${uid} that is not a fork of this one`)
|
||||
}
|
||||
this.#forks.delete(uid)
|
||||
this.#waitingForks.delete(uid)
|
||||
this.#removedForks.add(uid)
|
||||
try {
|
||||
if (error === undefined) {
|
||||
await fork.return()
|
||||
} else {
|
||||
await fork.throw(error)
|
||||
}
|
||||
} catch (cleaningError) {
|
||||
console.error('Error while cleaning the forked', {
|
||||
cleaningError,
|
||||
sourceError: error,
|
||||
})
|
||||
}
|
||||
|
||||
if (this.#forks.size === 0) {
|
||||
if (error === undefined) {
|
||||
await this.#source.return(undefined as TReturn)
|
||||
} else {
|
||||
await this.#source.throw(error)
|
||||
}
|
||||
// Reject any pending forks waiting for the next value
|
||||
if (this.#nextValueForksReady) {
|
||||
this.#nextValueForksReady.forksWaitingReject(new Error('Source generator terminated.', { cause: error }))
|
||||
this.#nextValueForksReady = undefined
|
||||
}
|
||||
// clear state
|
||||
this.#removedForks.clear()
|
||||
this.#waitingForks.clear()
|
||||
} else {
|
||||
// this fork was maybe blocking the others
|
||||
if (this.#nextValueForksReady) {
|
||||
await this.#resolveWhenAllForksReady()
|
||||
}
|
||||
}
|
||||
return { done: true, value: undefined }
|
||||
}
|
||||
}
|
||||
|
||||
class Forked<T, TReturn, TNext> implements AsyncGenerator<T, TReturn, TNext> {
|
||||
#parent: Synchronized<T, TReturn, TNext>
|
||||
#uid: string
|
||||
constructor(parent: Synchronized<T, TReturn, TNext>, uid: string) {
|
||||
this.#parent = parent
|
||||
this.#uid = uid
|
||||
}
|
||||
next(): Promise<IteratorResult<T>> {
|
||||
return this.#parent.next(this.#uid)
|
||||
}
|
||||
async return(): Promise<IteratorResult<T>> {
|
||||
return this.#parent.remove(this.#uid)
|
||||
}
|
||||
async throw(e: Error): Promise<IteratorResult<T>> {
|
||||
return this.#parent.remove(this.#uid, e)
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator](): AsyncGenerator<T, TReturn, TNext> {
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
import assert from 'node:assert'
|
||||
import { suite, test } from 'node:test'
|
||||
import { Synchronized } from './synchronized.mjs'
|
||||
|
||||
async function* makeRangeGenerator(end = Infinity, progress = { yielded: 0 }, onYielded = (_val: unknown) => {}) {
|
||||
for (let i = 0; i < end; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
yield i
|
||||
onYielded(i)
|
||||
progress.yielded = i
|
||||
}
|
||||
return progress
|
||||
}
|
||||
|
||||
async function consume(
|
||||
iterable: AsyncGenerator,
|
||||
delay = 2500,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
onConsumed = (_val: unknown, _iterable: AsyncGenerator) => Promise.resolve(false)
|
||||
) {
|
||||
for await (const val of iterable) {
|
||||
await new Promise(resolve => setTimeout(resolve, delay))
|
||||
if (await onConsumed(val, iterable)) {
|
||||
iterable.return(undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suite('success', () => {
|
||||
test('for-await-of break stops the source generator', async () => {
|
||||
let sourceClosed = false
|
||||
const source = (async function* () {
|
||||
try {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
yield i
|
||||
}
|
||||
} finally {
|
||||
sourceClosed = true
|
||||
}
|
||||
})()
|
||||
|
||||
const forker = new Synchronized(source)
|
||||
const fork = forker.fork('first')
|
||||
|
||||
// for-await-of must call fork.return() on break, which propagates to the source
|
||||
for await (const val of fork) {
|
||||
if (val === 2) break
|
||||
}
|
||||
|
||||
assert.strictEqual(sourceClosed, true)
|
||||
})
|
||||
|
||||
test('if works with multiple consumer', async () => {
|
||||
const progress = { yielded: 0 }
|
||||
const generator = makeRangeGenerator(3, progress)
|
||||
const forker = new Synchronized(generator)
|
||||
const first = forker.fork('first')
|
||||
const second = forker.fork('second')
|
||||
await Promise.all([consume(first, 50), consume(second, 500)])
|
||||
assert.strictEqual(progress.yielded, 2)
|
||||
})
|
||||
|
||||
test('if works with one consumer returning', async () => {
|
||||
const progress = { yielded: 0 }
|
||||
const generator = makeRangeGenerator(10, progress)
|
||||
const forker = new Synchronized(generator)
|
||||
const first = forker.fork('first')
|
||||
const second = forker.fork('second')
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
await Promise.all([
|
||||
consume(first, 1),
|
||||
consume(second, 1, async (val: unknown) => {
|
||||
return Promise.resolve(val === 2)
|
||||
}),
|
||||
])
|
||||
assert.strictEqual(progress.yielded, 9)
|
||||
})
|
||||
test('if works with all consumers returning', async () => {
|
||||
const progress = { yielded: 0 }
|
||||
const generator = makeRangeGenerator(10, progress)
|
||||
const forker = new Synchronized(generator)
|
||||
const first = forker.fork('first')
|
||||
const second = forker.fork('second')
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
await Promise.all([
|
||||
consume(first, 1, async (val: unknown) => {
|
||||
return Promise.resolve(val === 5)
|
||||
}),
|
||||
consume(second, 1, async (val: unknown) => {
|
||||
return Promise.resolve(val === 2)
|
||||
}),
|
||||
])
|
||||
assert.strictEqual(progress.yielded, 4)
|
||||
})
|
||||
})
|
||||
|
||||
suite('error', () => {
|
||||
test('it fails all if error on source', async () => {
|
||||
const progress = { yielded: 0 }
|
||||
const generator = makeRangeGenerator(5, progress, (val: unknown) => {
|
||||
if (val === 2) {
|
||||
throw new Error('source stop')
|
||||
}
|
||||
})
|
||||
const forker = new Synchronized(generator)
|
||||
const first = forker.fork('first')
|
||||
const second = forker.fork('second')
|
||||
await assert.rejects(Promise.all([consume(first, 1), consume(second, 1)]))
|
||||
|
||||
assert.strictEqual(progress.yielded, 1)
|
||||
})
|
||||
test('it flows until the end if one consumer throws', async () => {
|
||||
const progress = { yielded: 0 }
|
||||
const generator = makeRangeGenerator(10, progress)
|
||||
const forker = new Synchronized(generator)
|
||||
const first = forker.fork('first')
|
||||
const second = forker.fork('second')
|
||||
|
||||
const firstConsume = consume(first, 1)
|
||||
|
||||
const secondConsume = assert.rejects(() =>
|
||||
consume(second, 1, async (val: unknown, iterable) => {
|
||||
if (val === 1) {
|
||||
await iterable.throw(new Error('error on second'))
|
||||
}
|
||||
return false
|
||||
})
|
||||
)
|
||||
await Promise.allSettled([firstConsume, secondConsume])
|
||||
assert.strictEqual(progress.yielded, 9)
|
||||
})
|
||||
test('it throw if ALL consumer throws', async () => {
|
||||
const progress = { yielded: 0 }
|
||||
const generator = makeRangeGenerator(5, progress)
|
||||
const forker = new Synchronized(generator)
|
||||
const first = forker.fork('first')
|
||||
const second = forker.fork('second')
|
||||
|
||||
const firstConsume = consume(first, 1, async (val: unknown, iterable) => {
|
||||
if (val === 3) {
|
||||
await iterable.throw(new Error('error on first'))
|
||||
}
|
||||
return false
|
||||
})
|
||||
const secondConsume = consume(second, 1, async (val: unknown, iterable) => {
|
||||
if (val === 1) {
|
||||
await iterable.throw(new Error('error on second'))
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
await assert.rejects(Promise.all([firstConsume, secondConsume]))
|
||||
assert.strictEqual(progress.yielded, 2)
|
||||
})
|
||||
|
||||
test('It should not allow a new fork after the start', async () => {
|
||||
const progress = { yielded: 0 }
|
||||
const generator = makeRangeGenerator(5, progress)
|
||||
const forker = new Synchronized(generator)
|
||||
const first = forker.fork('first')
|
||||
await first.next()
|
||||
assert.throws(() => forker.fork('second'))
|
||||
})
|
||||
test('It should not allow fork with the same uid', async () => {
|
||||
const progress = { yielded: 0 }
|
||||
const generator = makeRangeGenerator(5, progress)
|
||||
const forker = new Synchronized(generator)
|
||||
forker.fork('first')
|
||||
forker.fork('second')
|
||||
assert.throws(() => forker.fork('first'))
|
||||
})
|
||||
})
|
||||
@@ -1,68 +0,0 @@
|
||||
import assert from 'node:assert'
|
||||
|
||||
/**
|
||||
*
|
||||
* This class will throttle the production of a group of generators
|
||||
*
|
||||
* limits: depends on the event loop, so don't expect it to be very precise
|
||||
* works better with more small blocks
|
||||
* The source generator must yield object with a length property
|
||||
* Changing the speed will only be takin into account fot the next packets asked
|
||||
*/
|
||||
export class Throttle {
|
||||
#previousSlot = 0
|
||||
#bytesPerSecond: number | (() => number)
|
||||
get speed(): number {
|
||||
let speed: number
|
||||
if (typeof this.#bytesPerSecond === 'function') {
|
||||
speed = this.#bytesPerSecond()
|
||||
} else {
|
||||
speed = this.#bytesPerSecond
|
||||
}
|
||||
assert.ok(speed >= 0, `speed must be positive, ${speed} computed`)
|
||||
return speed
|
||||
}
|
||||
constructor(speed: number | (() => number)) {
|
||||
this.#bytesPerSecond = speed
|
||||
}
|
||||
|
||||
getNextSlot(length: number): { timeout?: ReturnType<typeof setTimeout>; promise?: Promise<unknown> } {
|
||||
assert.notStrictEqual(length, undefined, `throttled stream need to expose a length property }`)
|
||||
assert.ok(length > 0, `throttled stream must expose a positive length property , ${length} given }`)
|
||||
if (this.speed === 0) {
|
||||
return {}
|
||||
}
|
||||
const previous = this.#previousSlot
|
||||
const nextSlot = Math.round(previous + (length * 1000) / this.speed)
|
||||
if (nextSlot < Date.now()) {
|
||||
// we're above the limit, go now
|
||||
this.#previousSlot = Date.now()
|
||||
return {}
|
||||
}
|
||||
this.#previousSlot = nextSlot
|
||||
// wait till the next slot
|
||||
// it won't be extremely precise since the event loop is not
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined = undefined
|
||||
const promise = new Promise(function (resolve) {
|
||||
timeout = setTimeout(resolve, nextSlot - Date.now())
|
||||
})
|
||||
return { promise, timeout }
|
||||
}
|
||||
|
||||
async *createThrottledGenerator(source: AsyncGenerator<{ length: number }>): AsyncGenerator<{ length: number }> {
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
for await (const value of source) {
|
||||
const res = this.getNextSlot(value.length)
|
||||
timeout = res.timeout
|
||||
// wait for the time slot before yielding the data
|
||||
if (res.promise !== undefined) {
|
||||
await res.promise
|
||||
}
|
||||
yield value
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Throttle } from './throttle.mjs'
|
||||
import { suite, test } from 'node:test'
|
||||
import assert from 'node:assert'
|
||||
|
||||
async function* makeGenerator(chunkSize = 1024 * 1024, nbChunks = 100) {
|
||||
for (let i = 0; i < nbChunks; i++) {
|
||||
yield Buffer.allocUnsafe(chunkSize)
|
||||
}
|
||||
}
|
||||
|
||||
async function consumes(generator: AsyncGenerator) {
|
||||
// eslint-disable-next-line no-empty,@typescript-eslint/no-unused-vars
|
||||
for await (const data of generator) {
|
||||
}
|
||||
}
|
||||
|
||||
suite('it throttle one', () => {
|
||||
test('base test', async () => {
|
||||
// 10MB/s
|
||||
const throttler = new Throttle(10 * 1024 * 1024)
|
||||
const throttled = throttler.createThrottledGenerator(makeGenerator(1024 * 1024, 20))
|
||||
|
||||
const start = Date.now()
|
||||
await consumes(throttled)
|
||||
const end = Date.now()
|
||||
assert.strictEqual(Math.round((end - start) / 1000), 2)
|
||||
})
|
||||
test('variable speed test', async () => {
|
||||
// 10MB/s
|
||||
const throttler = new Throttle(() => 10 * 1024 * 1024)
|
||||
const throttled = throttler.createThrottledGenerator(makeGenerator(1024 * 1024, 20))
|
||||
|
||||
const start = Date.now()
|
||||
await consumes(throttled)
|
||||
const end = Date.now()
|
||||
assert.strictEqual(Math.round((end - start) / 1000), 2)
|
||||
})
|
||||
test('multiple generator test', async () => {
|
||||
// 10MB/s
|
||||
const throttler = new Throttle(10 * 1024 * 1024)
|
||||
const first = throttler.createThrottledGenerator(makeGenerator(1024 * 1024, 10))
|
||||
const second = throttler.createThrottledGenerator(makeGenerator(1024 * 1024, 10))
|
||||
const start = Date.now()
|
||||
|
||||
await Promise.all([consumes(first), consumes(second)])
|
||||
const end = Date.now()
|
||||
assert.strictEqual(Math.round((end - start) / 1000), 2)
|
||||
})
|
||||
test(`it doesn't accept negative value`, async () => {
|
||||
// 10MB/s
|
||||
let throttler = new Throttle(-10 * 1024 * 1024)
|
||||
let generator = throttler.createThrottledGenerator(makeGenerator(1024 * 1024, 10))
|
||||
await assert.rejects(() => consumes(generator))
|
||||
|
||||
let index = 5
|
||||
throttler = new Throttle(() => {
|
||||
index--
|
||||
return index > 0 ? 10 * 1024 * 1024 : -10 * 1024 * 1024
|
||||
})
|
||||
generator = throttler.createThrottledGenerator(makeGenerator(1024 * 1024, 10))
|
||||
await assert.rejects(() => consumes(generator))
|
||||
})
|
||||
})
|
||||
@@ -1,49 +0,0 @@
|
||||
import assert from 'node:assert'
|
||||
|
||||
const TIMEOUT = Symbol('timeout')
|
||||
|
||||
export class Timeout<T> implements AsyncGenerator {
|
||||
#source: AsyncGenerator<T>
|
||||
#timeout: number
|
||||
constructor(source: AsyncGenerator<T>, timeout: number) {
|
||||
assert.ok(timeout > 0, `Timeout value must be positive, ${timeout} received`)
|
||||
this.#source = source
|
||||
this.#timeout = timeout
|
||||
}
|
||||
async next(): Promise<IteratorResult<T>> {
|
||||
let timeoutHandle: ReturnType<typeof setTimeout>
|
||||
|
||||
const sourceNext = this.#source.next()
|
||||
const result = await Promise.race([
|
||||
sourceNext.then(res => {
|
||||
clearTimeout(timeoutHandle)
|
||||
return res
|
||||
}),
|
||||
new Promise<typeof TIMEOUT>(resolve => {
|
||||
timeoutHandle = setTimeout(() => resolve(TIMEOUT), this.#timeout)
|
||||
}),
|
||||
])
|
||||
|
||||
if (result === TIMEOUT) {
|
||||
// stop the source once the in-flight next settles to avoid data loss on the next call
|
||||
// we don't want to await for the end of timeouted code
|
||||
sourceNext.then(
|
||||
() => this.#source.return(undefined).catch(() => {}),
|
||||
() => {} // source already errored, nothing to clean up
|
||||
)
|
||||
throw new Error('Timeout reached')
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
return(): Promise<IteratorResult<T>> {
|
||||
return this.#source.return(undefined)
|
||||
}
|
||||
throw(e: Error): Promise<IteratorResult<T>> {
|
||||
return this.#source.throw(e)
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator](): AsyncGenerator<T> {
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert'
|
||||
import { Timeout } from './timeout.mjs'
|
||||
|
||||
describe('Timeout class', () => {
|
||||
it('should reject a timeout with a negative or zero value ', async () => {
|
||||
// Create a mock async generator that never resolves
|
||||
const mockGenerator = (async function* () {
|
||||
yield 1
|
||||
})()
|
||||
|
||||
assert.throws(() => new Timeout(mockGenerator, 0))
|
||||
assert.throws(() => new Timeout(mockGenerator, -10))
|
||||
})
|
||||
it('should resolve with the value from the source generator if it completes before the timeout', async () => {
|
||||
// Create a mock async generator that resolves immediately
|
||||
const mockGenerator = (async function* () {
|
||||
yield 1
|
||||
})()
|
||||
|
||||
const timeout = new Timeout(mockGenerator, 100) // 100ms timeout
|
||||
|
||||
const result = await timeout.next()
|
||||
assert.strictEqual(result.value, 1)
|
||||
assert.strictEqual(result.done, false)
|
||||
})
|
||||
|
||||
it('should reject with a timeout error if the source generator takes too long', async () => {
|
||||
// Create a mock async generator that never resolves
|
||||
// eslint-disable-next-line require-yield
|
||||
const mockGenerator = (async function* () {
|
||||
await new Promise(() => {}) // Never resolves
|
||||
})()
|
||||
|
||||
const timeout = new Timeout(mockGenerator, 10) // 10ms timeout
|
||||
|
||||
await assert.rejects(timeout.next(), 'Expected timeout error was not thrown')
|
||||
})
|
||||
it('should allow iteration using for-await-of', async () => {
|
||||
// Create a mock async generator that yields values
|
||||
const mockGenerator: AsyncGenerator<number> = (async function* () {
|
||||
yield 1
|
||||
yield 2
|
||||
yield 3
|
||||
})()
|
||||
|
||||
const timeout = new Timeout(mockGenerator, 100) // 100ms timeout
|
||||
|
||||
const results: Array<number> = []
|
||||
for await (const value of timeout) {
|
||||
results.push(value as number)
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(results, [1, 2, 3])
|
||||
})
|
||||
|
||||
it('should handle the throw method correctly', async () => {
|
||||
// Create a mock async generator with a throw method
|
||||
const mockGenerator = (async function* () {
|
||||
yield 1
|
||||
})()
|
||||
|
||||
const timeout = new Timeout(mockGenerator, 100) // 100ms timeout
|
||||
|
||||
await assert.rejects(timeout.throw(new Error('Test error')), 'Expected error was not thrown')
|
||||
})
|
||||
|
||||
it('should close the source generator after a timeout', async () => {
|
||||
let sourceClosed = false
|
||||
const slowSource = (async function* () {
|
||||
try {
|
||||
// resolves after 100ms so the in-flight next() eventually settles
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
yield 1
|
||||
} finally {
|
||||
sourceClosed = true
|
||||
}
|
||||
})()
|
||||
|
||||
const timeout = new Timeout(slowSource, 10) // fires before source yields
|
||||
await assert.rejects(() => timeout.next(), /Timeout reached/)
|
||||
|
||||
// wait for the in-flight next() to settle so source.return() can be called
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
assert.strictEqual(sourceClosed, true)
|
||||
})
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2023",
|
||||
"module": "ESNext",
|
||||
"types": ["node"], // Only use types from Node.js and packages that are explicitly imported
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"declaration": true, // Generates `.d.ts` files for type safety
|
||||
"outDir": "dist" // Outputs compiled code to the `dist` directory
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
### `create([opts], [requestListener])`
|
||||
|
||||
Creates a new server instance. `opts` may contain `createServer` and `createSecureServer` factories (defaults to `http.createServer` and `https.createServer`). `requestListener` is registered as a `request` event listener.
|
||||
|
||||
```js
|
||||
import { create } from '@vates/http-server-plus'
|
||||
|
||||
const server = create(function onRequest(req, res) {
|
||||
res.end('Hello')
|
||||
})
|
||||
```
|
||||
|
||||
### `server.listen(opts)`
|
||||
|
||||
Starts listening. Returns a promise that resolves to a human-readable address string, or rejects with `error.niceAddress` set.
|
||||
|
||||
```js
|
||||
// HTTP on a specific port
|
||||
await server.listen({ port: 8080 })
|
||||
|
||||
// HTTPS — detected automatically when cert+key are provided
|
||||
await server.listen({
|
||||
port: 443,
|
||||
cert: fs.readFileSync('certificate.pem'),
|
||||
key: fs.readFileSync('private_key.pem'),
|
||||
})
|
||||
|
||||
// Unix socket
|
||||
await server.listen({ socket: '/var/run/http.sock' })
|
||||
|
||||
// File descriptor
|
||||
await server.listen({ fd: 3 })
|
||||
|
||||
// Systemd socket activation
|
||||
await server.listen({ systemdSocket: 0 })
|
||||
```
|
||||
|
||||
> As a convenience, if `hostname` is `'localhost'`, it will listen on both `127.0.0.1` and `::1`, similar to what Node does when no hostname is provided.
|
||||
|
||||
Multiple endpoints can be started on the same instance:
|
||||
|
||||
```js
|
||||
const [httpAddr, httpsAddr] = await Promise.all([server.listen({ port: 80 }), server.listen({ port: 443, cert, key })])
|
||||
console.log('listening on', httpAddr, httpsAddr)
|
||||
```
|
||||
|
||||
### HTTP/2
|
||||
|
||||
Pass a custom `createSecureServer` factory to enable HTTP/2:
|
||||
|
||||
```js
|
||||
import { createSecureServer } from 'node:http2'
|
||||
import { create } from '@vates/http-server-plus'
|
||||
|
||||
const server = create({ createSecureServer }, app)
|
||||
await server.listen({ port: 443, cert, key })
|
||||
```
|
||||
|
||||
### `server.close()`
|
||||
|
||||
Closes all underlying servers. Returns a promise that resolves once the `close` event is emitted.
|
||||
|
||||
```js
|
||||
await server.close()
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,98 +0,0 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/http-server-plus
|
||||
|
||||
[](https://npmjs.org/package/@vates/http-server-plus)  [](https://bundlephobia.com/result?p=@vates/http-server-plus) [](https://npmjs.org/package/@vates/http-server-plus)
|
||||
|
||||
> Augmented `http.Server`, HTTP/HTTPS/HTTP2 and multiple ports on the same instance
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/http-server-plus):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/http-server-plus
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### `create([opts], [requestListener])`
|
||||
|
||||
Creates a new server instance. `opts` may contain `createServer` and `createSecureServer` factories (defaults to `http.createServer` and `https.createServer`). `requestListener` is registered as a `request` event listener.
|
||||
|
||||
```js
|
||||
import { create } from '@vates/http-server-plus'
|
||||
|
||||
const server = create(function onRequest(req, res) {
|
||||
res.end('Hello')
|
||||
})
|
||||
```
|
||||
|
||||
### `server.listen(opts)`
|
||||
|
||||
Starts listening. Returns a promise that resolves to a human-readable address string, or rejects with `error.niceAddress` set.
|
||||
|
||||
```js
|
||||
// HTTP on a specific port
|
||||
await server.listen({ port: 8080 })
|
||||
|
||||
// HTTPS — detected automatically when cert+key are provided
|
||||
await server.listen({
|
||||
port: 443,
|
||||
cert: fs.readFileSync('certificate.pem'),
|
||||
key: fs.readFileSync('private_key.pem'),
|
||||
})
|
||||
|
||||
// Unix socket
|
||||
await server.listen({ socket: '/var/run/http.sock' })
|
||||
|
||||
// File descriptor
|
||||
await server.listen({ fd: 3 })
|
||||
|
||||
// Systemd socket activation
|
||||
await server.listen({ systemdSocket: 0 })
|
||||
```
|
||||
|
||||
> As a convenience, if `hostname` is `'localhost'`, it will listen on both `127.0.0.1` and `::1`, similar to what Node does when no hostname is provided.
|
||||
|
||||
Multiple endpoints can be started on the same instance:
|
||||
|
||||
```js
|
||||
const [httpAddr, httpsAddr] = await Promise.all([server.listen({ port: 80 }), server.listen({ port: 443, cert, key })])
|
||||
console.log('listening on', httpAddr, httpsAddr)
|
||||
```
|
||||
|
||||
### HTTP/2
|
||||
|
||||
Pass a custom `createSecureServer` factory to enable HTTP/2:
|
||||
|
||||
```js
|
||||
import { createSecureServer } from 'node:http2'
|
||||
import { create } from '@vates/http-server-plus'
|
||||
|
||||
const server = create({ createSecureServer }, app)
|
||||
await server.listen({ port: 443, cert, key })
|
||||
```
|
||||
|
||||
### `server.close()`
|
||||
|
||||
Closes all underlying servers. Returns a promise that resolves once the `close` event is emitted.
|
||||
|
||||
```js
|
||||
await server.close()
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
@@ -1,268 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const EventEmitter = require('events').EventEmitter
|
||||
const formatUrl = require('url').format
|
||||
const fromEvent = require('promise-toolbox/fromEvent')
|
||||
const inherits = require('util').inherits
|
||||
const resolvePath = require('path').resolve
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const forwardedEvents = [
|
||||
'checkContinue',
|
||||
'checkExpectation',
|
||||
'clientError',
|
||||
'connect',
|
||||
'connection',
|
||||
'listening',
|
||||
'request',
|
||||
'secureConnection',
|
||||
'upgrade',
|
||||
]
|
||||
|
||||
function extractProperty(obj, prop) {
|
||||
const value = obj[prop]
|
||||
if (value !== undefined) {
|
||||
delete obj[prop]
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
function forOwn(obj, cb) {
|
||||
const keys = Object.keys(obj)
|
||||
for (let i = 0, n = keys.length; i < n; ++i) {
|
||||
cb(obj[keys[i]])
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemdFd(index) {
|
||||
if (process.pid !== +process.env.LISTEN_PID) {
|
||||
throw new Error('systemd sockets are not meant for us (LISTEN_PID)')
|
||||
}
|
||||
|
||||
const listenFds = process.env.LISTEN_FDS
|
||||
if (listenFds == null) {
|
||||
throw new Error('no systemd sockets found (LISTEN_FDS)')
|
||||
}
|
||||
const max = listenFds - 1
|
||||
|
||||
if (index > max) {
|
||||
throw new Error('no such systemd socket found')
|
||||
}
|
||||
|
||||
return index + 3 // 3 = SD_LISTEN_FDS_START
|
||||
}
|
||||
|
||||
const isEmpty = obj => Object.keys(obj).length === 0
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function onNewListener(event, listener) {
|
||||
if (forwardedEvents.includes(event)) {
|
||||
forOwn(this._servers, server => {
|
||||
server.on(event, listener)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function onRemoveListener(event, listener) {
|
||||
if (forwardedEvents.includes(event)) {
|
||||
forOwn(this._servers, server => {
|
||||
server.off(event, listener)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function Server(opts) {
|
||||
EventEmitter.call(this)
|
||||
|
||||
this._createServer = (opts != null && opts.createServer) || require('http').createServer
|
||||
this._createSecureServer = (opts != null && opts.createSecureServer) || require('https').createServer
|
||||
this._servers = Object.create(null)
|
||||
this._niceAddresses = Object.create(null)
|
||||
|
||||
this.on('removeListener', onRemoveListener).on('newListener', onNewListener)
|
||||
}
|
||||
inherits(Server, EventEmitter)
|
||||
|
||||
const proto = Server.prototype
|
||||
|
||||
function getAddress(serverId) {
|
||||
return this[serverId].address()
|
||||
}
|
||||
proto.addresses = function Server$addresses() {
|
||||
const servers = this._servers
|
||||
return Object.keys(servers).map(getAddress, servers)
|
||||
}
|
||||
|
||||
function getNiceAddress(serverId) {
|
||||
return this[serverId]
|
||||
}
|
||||
proto.niceAddresses = function Server$niceAddresses() {
|
||||
const niceAddresses = this._niceAddresses
|
||||
return Object.keys(niceAddresses).map(getNiceAddress, niceAddresses)
|
||||
}
|
||||
|
||||
function close(server) {
|
||||
server.close()
|
||||
}
|
||||
|
||||
proto.close = function Server$close(callback) {
|
||||
if (callback) {
|
||||
this.once('close', callback)
|
||||
}
|
||||
|
||||
// Emit the close event even if there are no registered servers.
|
||||
if (isEmpty(this._servers)) {
|
||||
const self = this
|
||||
setImmediate(function () {
|
||||
self.emit('close')
|
||||
})
|
||||
return Promise.resolve()
|
||||
}
|
||||
// Closes each servers.
|
||||
forOwn(this._servers, close)
|
||||
|
||||
return fromEvent(this, 'close')
|
||||
}
|
||||
|
||||
let nextId = 0
|
||||
proto.listen = function Server$listen(opts) {
|
||||
opts = Object.assign({}, opts)
|
||||
|
||||
if (opts.hostname === 'localhost') {
|
||||
return Promise.all([
|
||||
this.listen(Object.assign(opts, { hostname: '::1' })),
|
||||
this.listen(Object.assign(opts, { hostname: '127.0.0.1' })),
|
||||
]).then(addresses => addresses[0])
|
||||
}
|
||||
|
||||
const hostname = extractProperty(opts, 'hostname')
|
||||
let port = extractProperty(opts, 'port')
|
||||
const systemdSocket = extractProperty(opts, 'systemdSocket')
|
||||
let fd = extractProperty(opts, 'fd')
|
||||
let socket = extractProperty(opts, 'socket')
|
||||
|
||||
const servers = this._servers
|
||||
|
||||
const niceAddresses = this._niceAddresses
|
||||
|
||||
let server, protocol
|
||||
if (opts.pfx || opts.SNICallback || (opts.cert && opts.key)) {
|
||||
server = this._createSecureServer(opts)
|
||||
protocol = 'https'
|
||||
} else {
|
||||
server = this._createServer(opts)
|
||||
protocol = 'http'
|
||||
}
|
||||
|
||||
const id = nextId++
|
||||
servers[id] = server
|
||||
|
||||
if (systemdSocket != null) {
|
||||
fd = getSystemdFd(systemdSocket)
|
||||
}
|
||||
|
||||
// Compute a temporary nice address to display in case of error.
|
||||
let niceAddress
|
||||
if (fd != null) {
|
||||
server.listen({ fd })
|
||||
niceAddress = protocol + '://<fd:' + fd + '>'
|
||||
} else if (socket != null) {
|
||||
socket = resolvePath(socket)
|
||||
server.listen(socket)
|
||||
niceAddress = protocol + '://' + socket
|
||||
} else {
|
||||
if (port == null) {
|
||||
port = protocol === 'https' ? 443 : 80
|
||||
}
|
||||
server.listen(port, hostname)
|
||||
niceAddress = formatUrl({
|
||||
protocol,
|
||||
|
||||
// Hostname default to localhost.
|
||||
hostname: hostname || 'localhost',
|
||||
|
||||
// No port means random, unknown for now.
|
||||
port: port || '<unknown>',
|
||||
})
|
||||
}
|
||||
|
||||
const emit = this.emit.bind(this)
|
||||
server.once('close', function onClose() {
|
||||
delete servers[id]
|
||||
delete niceAddresses[id]
|
||||
|
||||
if (isEmpty(servers)) {
|
||||
emit('close')
|
||||
}
|
||||
})
|
||||
|
||||
server.on('error', function onError() {
|
||||
delete servers[id]
|
||||
|
||||
// FIXME: Should it be forwarded and be fatal if there are no
|
||||
// listeners?
|
||||
})
|
||||
|
||||
forwardedEvents.forEach(event => {
|
||||
this.listeners(event).forEach(listener => {
|
||||
server.on(event, listener)
|
||||
})
|
||||
})
|
||||
|
||||
return fromEvent(server, 'listening').then(
|
||||
function () {
|
||||
const address = server.address()
|
||||
const niceAddress =
|
||||
typeof address === 'string'
|
||||
? protocol + '://' + address
|
||||
: formatUrl({
|
||||
protocol,
|
||||
hostname: address.address,
|
||||
port: address.port,
|
||||
})
|
||||
niceAddresses[id] = niceAddress
|
||||
return niceAddress
|
||||
},
|
||||
function (error) {
|
||||
error.niceAddress = niceAddress
|
||||
throw error
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function ref(server) {
|
||||
server.ref()
|
||||
}
|
||||
|
||||
proto.ref = function Server$ref() {
|
||||
forOwn(this._servers, ref)
|
||||
}
|
||||
|
||||
function unref(server) {
|
||||
server.unref()
|
||||
}
|
||||
|
||||
proto.unref = function Server$unref() {
|
||||
forOwn(this._servers, unref)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
module.exports = exports = Server
|
||||
|
||||
exports.create = function create(opts, requestListener) {
|
||||
if (typeof opts === 'function') {
|
||||
requestListener = opts
|
||||
opts = undefined
|
||||
}
|
||||
|
||||
const server = new Server(opts)
|
||||
if (requestListener !== undefined) {
|
||||
server.on('request', requestListener)
|
||||
}
|
||||
return server
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const assert = require('assert/strict')
|
||||
const { EventEmitter } = require('events')
|
||||
const resolvePath = require('path').resolve
|
||||
const { describe, it } = require('node:test')
|
||||
|
||||
const HttpServerPlus = require('./')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Creates a minimal mock server that can be used as a drop-in for http/https.Server.
|
||||
// `address` is the value returned by mock.address() — use an object for TCP, a string for sockets.
|
||||
function createMock(address) {
|
||||
const mock = new EventEmitter()
|
||||
mock.listen = function () {
|
||||
setImmediate(() => this.emit('listening'))
|
||||
}
|
||||
mock.address = () => address
|
||||
mock.close = function () {
|
||||
setImmediate(() => this.emit('close'))
|
||||
}
|
||||
return mock
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('HttpServerPlus', function () {
|
||||
describe('#create()', function () {
|
||||
it('creates a new instance', function () {
|
||||
assert(HttpServerPlus.create() instanceof HttpServerPlus)
|
||||
})
|
||||
|
||||
it('can register a `request` listener', function () {
|
||||
const listener = function () {}
|
||||
const server = HttpServerPlus.create(listener)
|
||||
|
||||
assert.deepEqual(server.listeners('request'), [listener])
|
||||
})
|
||||
})
|
||||
|
||||
describe('.addresses()', function () {
|
||||
it('returns the list of addresses the server is listening at', function () {
|
||||
const addr = { address: '127.0.0.1', port: 8080, family: 'IPv4' }
|
||||
const mock = createMock(addr)
|
||||
const server = HttpServerPlus.create({ createServer: () => mock })
|
||||
|
||||
return server.listen({ port: 8080 }).then(function () {
|
||||
assert.deepEqual(server.addresses(), [addr])
|
||||
return server.close()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.niceAddresses()', function () {
|
||||
it('returns the list of human readable addresses the server is listening at', function () {
|
||||
const mock = createMock({ address: '127.0.0.1', port: 8080 })
|
||||
const server = HttpServerPlus.create({ createServer: () => mock })
|
||||
|
||||
return server.listen({ hostname: '127.0.0.1', port: 8080 }).then(function (address) {
|
||||
assert.deepEqual(server.niceAddresses(), [address])
|
||||
return server.close()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('secureConnection event forwarding', function () {
|
||||
function createMockHttpsServer() {
|
||||
const server = new EventEmitter()
|
||||
server.listen = function () {
|
||||
setImmediate(() => this.emit('listening'))
|
||||
}
|
||||
server.address = () => ({ address: '127.0.0.1', port: 8443 })
|
||||
server.close = function () {
|
||||
setImmediate(() => this.emit('close'))
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
function createServerWithMock() {
|
||||
let mockServer
|
||||
const server = HttpServerPlus.create({
|
||||
createSecureServer: () => {
|
||||
mockServer = createMockHttpsServer()
|
||||
return mockServer
|
||||
},
|
||||
})
|
||||
return {
|
||||
server,
|
||||
getMock: () => mockServer,
|
||||
}
|
||||
}
|
||||
|
||||
it('forwards secureConnection to listeners registered before listen()', function () {
|
||||
const { server, getMock } = createServerWithMock()
|
||||
|
||||
const received = []
|
||||
server.on('secureConnection', socket => received.push(socket))
|
||||
|
||||
const fakeSocket = {}
|
||||
return server.listen({ cert: 'fake-cert', key: 'fake-key' }).then(function () {
|
||||
getMock().emit('secureConnection', fakeSocket)
|
||||
assert.deepEqual(received, [fakeSocket])
|
||||
return server.close()
|
||||
})
|
||||
})
|
||||
|
||||
it('forwards secureConnection to listeners registered after listen()', function () {
|
||||
const { server, getMock } = createServerWithMock()
|
||||
|
||||
return server.listen({ cert: 'fake-cert', key: 'fake-key' }).then(function () {
|
||||
const received = []
|
||||
server.on('secureConnection', socket => received.push(socket))
|
||||
|
||||
const fakeSocket = {}
|
||||
getMock().emit('secureConnection', fakeSocket)
|
||||
assert.deepEqual(received, [fakeSocket])
|
||||
return server.close()
|
||||
})
|
||||
})
|
||||
|
||||
it('stops forwarding secureConnection after listener is removed', function () {
|
||||
const { server, getMock } = createServerWithMock()
|
||||
|
||||
const received = []
|
||||
const listener = socket => received.push(socket)
|
||||
server.on('secureConnection', listener)
|
||||
|
||||
return server.listen({ cert: 'fake-cert', key: 'fake-key' }).then(function () {
|
||||
server.off('secureConnection', listener)
|
||||
|
||||
getMock().emit('secureConnection', {})
|
||||
assert.deepEqual(received, [])
|
||||
return server.close()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.listen()', function () {
|
||||
it('can use a host:port', function () {
|
||||
let listenArgs
|
||||
const mock = createMock({ address: '127.0.0.1', port: 8080 })
|
||||
mock.listen = function (...args) {
|
||||
listenArgs = args
|
||||
setImmediate(() => this.emit('listening'))
|
||||
}
|
||||
const server = HttpServerPlus.create({ createServer: () => mock })
|
||||
|
||||
return server.listen({ hostname: '127.0.0.1', port: 8080 }).then(function () {
|
||||
assert.deepEqual(listenArgs, [8080, '127.0.0.1'])
|
||||
return server.close()
|
||||
})
|
||||
})
|
||||
|
||||
it('can use a systemd socket', function () {
|
||||
const SD_LISTEN_FDS_START = 3
|
||||
const SOCKET_INDEX = 0
|
||||
|
||||
let listenArgs
|
||||
const mock = createMock('http://<fd:3>')
|
||||
mock.listen = function (...args) {
|
||||
listenArgs = args
|
||||
setImmediate(() => this.emit('listening'))
|
||||
}
|
||||
|
||||
const server = HttpServerPlus.create({ createServer: () => mock })
|
||||
|
||||
const env = process.env
|
||||
env.LISTEN_FDS = String(SOCKET_INDEX + 1)
|
||||
env.LISTEN_PID = String(process.pid)
|
||||
|
||||
return server.listen({ systemdSocket: SOCKET_INDEX }).then(function () {
|
||||
delete env.LISTEN_FDS
|
||||
delete env.LISTEN_PID
|
||||
|
||||
assert.deepEqual(listenArgs, [{ fd: SOCKET_INDEX + SD_LISTEN_FDS_START }])
|
||||
return server.close()
|
||||
})
|
||||
})
|
||||
|
||||
it('can use a socket', function () {
|
||||
const socketPath = '/tmp/http-server-plus-test.sock'
|
||||
let listenArgs
|
||||
const mock = createMock(socketPath)
|
||||
mock.listen = function (...args) {
|
||||
listenArgs = args
|
||||
setImmediate(() => this.emit('listening'))
|
||||
}
|
||||
const server = HttpServerPlus.create({ createServer: () => mock })
|
||||
|
||||
return server.listen({ socket: socketPath }).then(function () {
|
||||
assert.deepEqual(listenArgs, [resolvePath(socketPath)])
|
||||
return server.close()
|
||||
})
|
||||
})
|
||||
|
||||
it('can use a HTTPS certificate', function () {
|
||||
let capturedOpts
|
||||
const mock = createMock({ address: '127.0.0.1', port: 8443 })
|
||||
const server = HttpServerPlus.create({
|
||||
createSecureServer: opts => {
|
||||
capturedOpts = opts
|
||||
return mock
|
||||
},
|
||||
})
|
||||
|
||||
return server.listen({ cert: 'my-cert', key: 'my-key', port: 8443 }).then(function (address) {
|
||||
assert.equal(capturedOpts.cert, 'my-cert')
|
||||
assert.equal(capturedOpts.key, 'my-key')
|
||||
assert.ok(address.startsWith('https://'), `expected https URL, got: ${address}`)
|
||||
return server.close()
|
||||
})
|
||||
})
|
||||
|
||||
it('returns a promise which will resolve once listening', function () {
|
||||
const mock = createMock({ address: '127.0.0.1', port: 8080 })
|
||||
const server = HttpServerPlus.create({ createServer: () => mock })
|
||||
|
||||
return server.listen({ port: 8080 }).then(function (address) {
|
||||
assert.equal(typeof address, 'string')
|
||||
assert.ok(address.startsWith('http://'), `expected http URL, got: ${address}`)
|
||||
return server.close()
|
||||
})
|
||||
})
|
||||
|
||||
it('returns a promise which will reject if failure', function () {
|
||||
const listenError = new Error('EADDRINUSE')
|
||||
const mock = createMock(null)
|
||||
mock.listen = function () {
|
||||
setImmediate(() => this.emit('error', listenError))
|
||||
}
|
||||
const server = HttpServerPlus.create({ createServer: () => mock })
|
||||
|
||||
return server.listen({ port: 8080 }).then(
|
||||
function () {
|
||||
assert.fail('should have rejected')
|
||||
},
|
||||
function (err) {
|
||||
assert.equal(err, listenError)
|
||||
assert.equal(typeof err.niceAddress, 'string')
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.close()', function () {
|
||||
it('closes all servers', function () {
|
||||
const closed = []
|
||||
let callCount = 0
|
||||
|
||||
const server = HttpServerPlus.create({
|
||||
createServer: () => {
|
||||
const id = callCount++
|
||||
const mock = createMock({ address: '127.0.0.1', port: id })
|
||||
mock.close = function () {
|
||||
closed.push(id)
|
||||
setImmediate(() => this.emit('close'))
|
||||
}
|
||||
return mock
|
||||
},
|
||||
})
|
||||
|
||||
return Promise.all([server.listen({ port: 0 }), server.listen({ port: 0 })])
|
||||
.then(function () {
|
||||
return server.close()
|
||||
})
|
||||
.then(function () {
|
||||
assert.equal(closed.length, 2)
|
||||
})
|
||||
})
|
||||
|
||||
it('emit the `close` event when all servers are closed', function () {
|
||||
const mock = createMock({ address: '127.0.0.1', port: 8080 })
|
||||
const server = HttpServerPlus.create({ createServer: () => mock })
|
||||
|
||||
let closeEmitted = false
|
||||
server.on('close', () => {
|
||||
closeEmitted = true
|
||||
})
|
||||
|
||||
return server
|
||||
.listen({ port: 8080 })
|
||||
.then(function () {
|
||||
return server.close()
|
||||
})
|
||||
.then(function () {
|
||||
assert.ok(closeEmitted)
|
||||
})
|
||||
})
|
||||
|
||||
it('emit the `close` event even if there are no servers', function () {
|
||||
const server = HttpServerPlus.create()
|
||||
|
||||
// close() with no servers returns Promise.resolve() immediately (before emitting
|
||||
// 'close' via setImmediate), so we must wait for the event itself, not the promise.
|
||||
return new Promise(function (resolve) {
|
||||
server.close(resolve)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"name": "@vates/http-server-plus",
|
||||
"version": "2.0.2",
|
||||
"license": "ISC",
|
||||
"description": "Augmented `http.Server`, HTTP/HTTPS/HTTP2 and multiple ports on the same instance",
|
||||
"keywords": [
|
||||
"http",
|
||||
"https",
|
||||
"server",
|
||||
"ports"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/http-server-plus",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/http-server-plus",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"dependencies": {
|
||||
"promise-toolbox": "^0.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-plugin-n": "^15.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"pretest": "eslint .",
|
||||
"test": "node --test index.spec.js",
|
||||
"postversion": "npm publish --access public"
|
||||
},
|
||||
"private": false,
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
The use for this library is to create a composite key from a JSON value, ignoring objects identity and properties order.
|
||||
The use for this librariy is to create a composite key from a JSON value, ignoring objects identity and properties order.
|
||||
|
||||
The hash algorithm used is intended to be fast and with low collisions and is not guaranteed to be be secure.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ npm install --save @vates/json-hash
|
||||
|
||||
## Usage
|
||||
|
||||
The use for this library is to create a composite key from a JSON value, ignoring objects identity and properties order.
|
||||
The use for this librariy is to create a composite key from a JSON value, ignoring objects identity and properties order.
|
||||
|
||||
The hash algorithm used is intended to be fast and with low collisions and is not guaranteed to be be secure.
|
||||
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import { RandomAccessDisk } from '@xen-orchestra/disk-transform'
|
||||
import NbdClient from '@vates/nbd-client'
|
||||
|
||||
/**
|
||||
* @typedef {{ offset: number, length: number }} DataRange
|
||||
*/
|
||||
|
||||
/**
|
||||
* @extends {RandomAccessDisk}
|
||||
*/
|
||||
export class NbdDisk extends RandomAccessDisk {
|
||||
#nbdInfos
|
||||
/** @type {NbdClient|undefined} */
|
||||
#nbdClient
|
||||
|
||||
/** @type {Array<DataRange> | undefined} */
|
||||
#dataMap
|
||||
|
||||
/** @type {number} */
|
||||
#blockSize
|
||||
|
||||
constructor(nbdInfos, blockSize, { dataMap } = {}) {
|
||||
super()
|
||||
this.#blockSize = blockSize
|
||||
this.#nbdInfos = nbdInfos
|
||||
this.#dataMap = dataMap && this.#processDatamap(dataMap)
|
||||
}
|
||||
|
||||
#processDatamap(rawDataMap) {
|
||||
return rawDataMap
|
||||
.filter(({ type }) => type === 0)
|
||||
.map(({ offset, length }) => ({ offset, length }))
|
||||
.sort(({ offset: offset1 }, { offset: offset2 }) => offset1 - offset2)
|
||||
}
|
||||
/**
|
||||
* @param {number} index
|
||||
* @returns {Promise<DiskBlock>}
|
||||
*/
|
||||
async readBlock(index) {
|
||||
if (!this.#nbdClient) {
|
||||
throw new Error("can't readBlock before init")
|
||||
}
|
||||
let data = await this.#nbdClient.readBlock(index, this.getBlockSize())
|
||||
if (data.length !== this.getBlockSize()) {
|
||||
// this condition only work on non aligned disks
|
||||
// we won't accept truncated block from unaligned disks
|
||||
if (index === Math.ceil(this.getVirtualSize() / this.getBlockSize()) - 1) {
|
||||
// last block add zeros at the end
|
||||
data = Buffer.concat([data], this.getBlockSize())
|
||||
} else {
|
||||
throw new Error(`Block ${index} is not at the right size, expecting ${this.getBlockSize()}, got ${data.length}`)
|
||||
}
|
||||
}
|
||||
return {
|
||||
index,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
getVirtualSize() {
|
||||
if (!this.#nbdClient) {
|
||||
throw new Error("can't get size before init")
|
||||
}
|
||||
return Number(this.#nbdClient.exportSize)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
getBlockSize() {
|
||||
return this.#blockSize
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async init() {
|
||||
this.#nbdClient = new NbdClient(this.#nbdInfos)
|
||||
await this.#nbdClient.connect()
|
||||
if (this.#dataMap === undefined) {
|
||||
this.#dataMap = this.#processDatamap(await this.#nbdClient.getMap())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async close() {
|
||||
await this.#nbdClient?.disconnect()
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isDifferencing() {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array<number>}
|
||||
*/
|
||||
getBlockIndexes() {
|
||||
const indexes = new Set()
|
||||
this.#dataMap?.forEach(({ offset, length }) => {
|
||||
if (length === 0) return
|
||||
|
||||
const firstBlockIndex = Math.floor(offset / this.getBlockSize())
|
||||
const lastBlockIndex = Math.floor((offset + length - 1) / this.getBlockSize())
|
||||
|
||||
for (let blockIndex = firstBlockIndex; blockIndex <= lastBlockIndex; blockIndex++) {
|
||||
indexes.add(blockIndex)
|
||||
}
|
||||
})
|
||||
return [...indexes].sort((a, b) => a - b)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} index
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasBlock(index) {
|
||||
if (!this.#dataMap) {
|
||||
throw new Error("can't hasBlock before init")
|
||||
}
|
||||
const blockStart = index * this.getBlockSize()
|
||||
const blockEnd = (index + 1) * this.getBlockSize()
|
||||
return this.#dataMap.some(({ offset, length }) => offset + length > blockStart && offset < blockEnd)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export const INIT_PASSWD = Buffer.from('NBDMAGIC') // "NBDMAGIC" ensure we're connected to a nbd server
|
||||
export const OPTS_MAGIC = Buffer.from('IHAVEOPT') // "IHAVEOPT" start an option block
|
||||
export const NBD_OPT_REPLY_MAGIC = 1100100111001001n // magic received during negotiation
|
||||
export const NBD_OPT_REPLY_MAGIC = 1100100111001001n // magic received during negociation
|
||||
export const NBD_OPT_EXPORT_NAME = 1
|
||||
export const NBD_OPT_ABORT = 2
|
||||
export const NBD_OPT_LIST = 3
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import assert from 'node:assert'
|
||||
import { Socket } from 'node:net'
|
||||
import { connect } from 'node:tls'
|
||||
import { pRetry, pDelay, pTimeout, pFromCallback } from 'promise-toolbox'
|
||||
import { fromCallback, pRetry, pDelay, pTimeout, pFromCallback } from 'promise-toolbox'
|
||||
import { readChunkStrict } from '@vates/read-chunk'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
import {
|
||||
@@ -20,8 +20,6 @@ import {
|
||||
OPTS_MAGIC,
|
||||
NBD_CMD_DISC,
|
||||
} from './constants.mjs'
|
||||
import { spawn } from 'node:child_process'
|
||||
|
||||
const { warn } = createLogger('vates:nbd-client')
|
||||
|
||||
// documentation is here : https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md
|
||||
@@ -44,9 +42,9 @@ export default class NbdClient {
|
||||
// AFAIK, there is no guaranty the server answers in the same order as the queries
|
||||
// so we handle a backlog of command waiting for response and handle concurrency manually
|
||||
|
||||
#waitingForResponse // there is already a listener waiting for a response
|
||||
#waitingForResponse // there is already a listenner waiting for a response
|
||||
#nextCommandQueryId = BigInt(0)
|
||||
#commandQueryBacklog // map of command waiting for a response queryId => { size/*in byte*/, resolve, reject}
|
||||
#commandQueryBacklog // map of command waiting for an response queryId => { size/*in byte*/, resolve, reject}
|
||||
#connected = false
|
||||
|
||||
#reconnectingPromise
|
||||
@@ -197,7 +195,7 @@ export default class NbdClient {
|
||||
}
|
||||
|
||||
// send export name we want to access.
|
||||
// it's implicitly closing the negotiation phase.
|
||||
// it's implictly closing the negociation phase.
|
||||
await this.#write(OPTS_MAGIC)
|
||||
await this.#writeInt32(NBD_OPT_EXPORT_NAME)
|
||||
const exportNameBuffer = Buffer.from(this.#exportName)
|
||||
@@ -220,22 +218,8 @@ export default class NbdClient {
|
||||
}
|
||||
|
||||
#write(buffer) {
|
||||
let timeout
|
||||
const messageTimeout = this.#messageTimeout
|
||||
const socket = this.#serverSocket
|
||||
return Promise.race([
|
||||
new Promise((resolve, reject) => {
|
||||
timeout = setTimeout(() => {
|
||||
reject(new Error('timeout'))
|
||||
}, messageTimeout)
|
||||
}),
|
||||
new Promise(resolve =>
|
||||
socket.write(buffer, () => {
|
||||
clearTimeout(timeout)
|
||||
resolve()
|
||||
})
|
||||
),
|
||||
])
|
||||
const promise = fromCallback.call(this.#serverSocket, 'write', buffer)
|
||||
return pTimeout.call(promise, this.#messageTimeout)
|
||||
}
|
||||
|
||||
async #readInt32() {
|
||||
@@ -353,56 +337,10 @@ export default class NbdClient {
|
||||
async readBlock(index, size = NBD_DEFAULT_BLOCK_SIZE) {
|
||||
return pRetry(() => this.#readBlock(index, size), {
|
||||
tries: this.#readBlockRetries,
|
||||
when: error => error.code !== 'ERR_ABORTED',
|
||||
onRetry: async err => {
|
||||
warn('will retry reading block ', index, err)
|
||||
await this.reconnect()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the map of the file with holes, zeros and data, useful to handle efficiently sparse source
|
||||
* to implement this internally: use structure response if the server supports it, and then ask for BLOCK_STATUS *
|
||||
*
|
||||
* @returns {Promise<{ offset: number, length: number, type: number }[]>}
|
||||
* A promise that resolves to an array where each object represents a segment:
|
||||
* - `offset` — The byte offset from the start.
|
||||
* - `length` — The size of the segment in bytes.
|
||||
* - `type` — A numeric code indicating the segment type ( 0 means data).
|
||||
*/
|
||||
/* async */ getMap(signal) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const process = spawn('nbdinfo', [
|
||||
'--json',
|
||||
'--map',
|
||||
`nbd://${this.#serverAddress}:${this.#serverPort}/${encodeURIComponent(this.#exportName)}`,
|
||||
])
|
||||
let text = ''
|
||||
let errText = ''
|
||||
process.stdout.on('data', data => (text += data))
|
||||
process.stderr.on('data', data => (errText += data))
|
||||
const onAbort = () => {
|
||||
process.kill()
|
||||
reject(signal.reason)
|
||||
}
|
||||
signal?.addEventListener('abort', onAbort, { once: true })
|
||||
process.on('error', err => {
|
||||
signal?.removeEventListener('abort', onAbort)
|
||||
reject(err)
|
||||
})
|
||||
process.on('close', code => {
|
||||
signal?.removeEventListener('abort', onAbort)
|
||||
if (code !== 0) {
|
||||
return reject(new Error(`Error during getMap (code: ${code}): ${errText}`))
|
||||
}
|
||||
try {
|
||||
const json = JSON.parse(text)
|
||||
resolve(json)
|
||||
} catch (error) {
|
||||
reject(new Error(`${error} ${text}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@ export default class MultiNbdClient {
|
||||
* open nbdConcurrency connections to NBD servers
|
||||
* it must obtain at least one connection to succeed
|
||||
* it tries to spread connections on multiple host
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async connect() {
|
||||
const candidates = [...this.#settings]
|
||||
@@ -70,9 +68,7 @@ export default class MultiNbdClient {
|
||||
await _connect()
|
||||
}
|
||||
if (this.#clients.length === 0) {
|
||||
const error = new Error(`Fail to connect to any Nbd client`, { nbdInfos: this.#settings })
|
||||
error.code = 'NO_NBD_AVAILABLE'
|
||||
throw error
|
||||
throw new Error(`Fail to connect to any Nbd client`)
|
||||
}
|
||||
if (this.#clients.length < this.#nbdConcurrency) {
|
||||
warn(
|
||||
@@ -81,30 +77,17 @@ export default class MultiNbdClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async disconnect() {
|
||||
await asyncEach(this.#clients, client => client.disconnect(), {
|
||||
stopOnError: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} index
|
||||
* @param {number} size
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
async readBlock(index, size = NBD_DEFAULT_BLOCK_SIZE) {
|
||||
const clientId = index % this.#clients.length
|
||||
return this.#clients[clientId].readBlock(index, size)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AsyncGenerator<Buffer>} indexGenerator
|
||||
*/
|
||||
async *readBlocks(indexGenerator) {
|
||||
// default : read all blocks
|
||||
const readAhead = []
|
||||
@@ -129,17 +112,4 @@ export default class MultiNbdClient {
|
||||
yield readAhead.shift()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* returns the map of the file with holes, zeros and data, useful to handle efficiently sparse source *
|
||||
*
|
||||
* @returns {Promise<{ offset: number, length: number, type: number }[]>}
|
||||
* A promise that resolves to an array where each object represents a segment:
|
||||
* - `offset` — The byte offset from the start.
|
||||
* - `length` — The size of the segment in bytes.
|
||||
* - `type` — A numeric code indicating the segment type (0 means no data).
|
||||
*/
|
||||
async getMap(signal) {
|
||||
// ask the map from one of the connected client
|
||||
return this.#clients[Math.floor(this.#clients.length * Math.random())].getMap(signal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,19 +13,18 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "3.4.0",
|
||||
"version": "3.1.2",
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
},
|
||||
"main": "./index.mjs",
|
||||
"dependencies": {
|
||||
"@vates/async-each": "^1.0.3",
|
||||
"@vates/read-chunk": "^1.2.1",
|
||||
"@xen-orchestra/async-map": "^0.1.3",
|
||||
"@xen-orchestra/disk-transform": "^1.3.2",
|
||||
"@xen-orchestra/log": "^0.7.2",
|
||||
"@vates/async-each": "^1.0.0",
|
||||
"@vates/read-chunk": "^1.2.0",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.7.1",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"xen-api": "^4.8.0"
|
||||
"xen-api": "^4.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap": "^18.7.0",
|
||||
|
||||
@@ -24,7 +24,7 @@ $ npm install node-vsphere-soap --save
|
||||
|
||||
## Sample Code
|
||||
|
||||
### To connect to a vCenter server
|
||||
### To connect to a vCenter server:
|
||||
|
||||
var nvs = require('node-vsphere-soap');
|
||||
var vc = new nvs.Client(host, user, password, sslVerify);
|
||||
@@ -54,7 +54,7 @@ $ npm install node-vsphere-soap --save
|
||||
- userName - username of authenticated user
|
||||
- fullName - full name of authenticated user
|
||||
|
||||
### To run a command
|
||||
### To run a command:
|
||||
|
||||
var vcCmd = vc.runCommand( commandToRun, arguments );
|
||||
vcCmd.once('result', function( result, raw, soapHeader) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
This file creates the Client class
|
||||
|
||||
- when the class is instantiated, a connection will be made to the ESXi/vCenter server to verify that the creds are good
|
||||
- upon a bad login, the connection will be terminated
|
||||
- upon a bad login, the connnection will be terminated
|
||||
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vates/node-vsphere-soap",
|
||||
"version": "2.1.3",
|
||||
"version": "2.1.1",
|
||||
"description": "interface to vSphere SOAP/WSDL from node for interfacing with vCenter or ESXi, forked from node-vsphere-soap",
|
||||
"main": "lib/client.mjs",
|
||||
"author": "reedog117",
|
||||
@@ -11,8 +11,8 @@
|
||||
},
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.7.2",
|
||||
"axios": "^1.15.0",
|
||||
"@xen-orchestra/log": "^0.7.1",
|
||||
"axios": "^1.4.0",
|
||||
"soap": "^1.0.0",
|
||||
"soap-cookie": "^0.10.1"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
### Usual workflow
|
||||
|
||||
> This section presents how this library should be used to implement a classic two factor authentication.
|
||||
> This section presents how this library should be used to implement a classic two factor authentification.
|
||||
|
||||
#### Setup
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ npm install --save @vates/otp
|
||||
|
||||
### Usual workflow
|
||||
|
||||
> This section presents how this library should be used to implement a classic two factor authentication.
|
||||
> This section presents how this library should be used to implement a classic two factor authentification.
|
||||
|
||||
#### Setup
|
||||
|
||||
|
||||
@@ -71,17 +71,13 @@ exports.readChunk = readChunk
|
||||
* If `size` bytes are not available to be read, the returned promise is rejected.
|
||||
*
|
||||
* @param {Readable} stream - A readable stream to read from.
|
||||
* @param {number?} [size] - The number of bytes to read for binary streams (ignored for object streams).
|
||||
* @param {number} [size] - The number of bytes to read for binary streams (ignored for object streams).
|
||||
* @returns {Promise<Buffer|string|unknown>} - A Promise that resolves to the read chunk.
|
||||
*/
|
||||
exports.readChunkStrict = async function readChunkStrict(stream, size) {
|
||||
const chunk = await readChunk(stream, size)
|
||||
if (chunk === null) {
|
||||
if (size === undefined) {
|
||||
throw new Error(`stream has ended without data`)
|
||||
} else {
|
||||
throw new Error(`stream has ended without data, was looking for ${size} bytes`)
|
||||
}
|
||||
throw new Error('stream has ended without data')
|
||||
}
|
||||
|
||||
if (size !== undefined && chunk.length !== size) {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.0",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
|
||||
@@ -63,16 +63,16 @@ task.failure(error)
|
||||
Inside a task:
|
||||
|
||||
```js
|
||||
// the abort signal of the current task if any; otherwise, is `undefined`
|
||||
// the abort signal of the current task if any, otherwise is `undefined`
|
||||
Task.abortSignal
|
||||
|
||||
// sends an info on the current task if any; otherwise, does nothing
|
||||
// sends an info on the current task if any, otherwise does nothing
|
||||
Task.info(message, data)
|
||||
|
||||
// sends an info on the current task if any; otherwise, does nothing
|
||||
// sends an info on the current task if any, otherwise does nothing
|
||||
Task.warning(message, data)
|
||||
|
||||
// attaches a property to the current task if any; otherwise, does nothing
|
||||
// attaches a property to the current task if any, otherwise does nothing
|
||||
//
|
||||
// the latest value takes precedence
|
||||
//
|
||||
@@ -149,7 +149,7 @@ const onProgress = makeOnProgress({
|
||||
|
||||
// This function is called each time a root task or a subtask is updated.
|
||||
//
|
||||
// `taskLog.$root` can be used to unconditionally access the root task.
|
||||
// `taskLog.$root` can be used to uncondionally access the root task.
|
||||
onTaskUpdate(taskLog) {},
|
||||
})
|
||||
|
||||
|
||||
@@ -79,16 +79,16 @@ task.failure(error)
|
||||
Inside a task:
|
||||
|
||||
```js
|
||||
// the abort signal of the current task if any; otherwise, is `undefined`
|
||||
// the abort signal of the current task if any, otherwise is `undefined`
|
||||
Task.abortSignal
|
||||
|
||||
// sends an info on the current task if any; otherwise, does nothing
|
||||
// sends an info on the current task if any, otherwise does nothing
|
||||
Task.info(message, data)
|
||||
|
||||
// sends an info on the current task if any; otherwise, does nothing
|
||||
// sends an info on the current task if any, otherwise does nothing
|
||||
Task.warning(message, data)
|
||||
|
||||
// attaches a property to the current task if any; otherwise, does nothing
|
||||
// attaches a property to the current task if any, otherwise does nothing
|
||||
//
|
||||
// the latest value takes precedence
|
||||
//
|
||||
@@ -165,7 +165,7 @@ const onProgress = makeOnProgress({
|
||||
|
||||
// This function is called each time a root task or a subtask is updated.
|
||||
//
|
||||
// `taskLog.$root` can be used to unconditionally access the root task.
|
||||
// `taskLog.$root` can be used to uncondionally access the root task.
|
||||
onTaskUpdate(taskLog) {},
|
||||
})
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ exports.makeOnProgress = function ({ onRootTaskEnd = noop, onRootTaskStart = noo
|
||||
Object.defineProperty(taskLog, '$root', { value: taskLog })
|
||||
|
||||
// start of a root task
|
||||
onRootTaskStart(taskLog, event)
|
||||
onRootTaskStart(taskLog)
|
||||
} else {
|
||||
// start of a subtask
|
||||
const parent = taskLogs.get(parentId)
|
||||
@@ -54,10 +54,10 @@ exports.makeOnProgress = function ({ onRootTaskEnd = noop, onRootTaskStart = noo
|
||||
}
|
||||
|
||||
if (type === 'end' && taskLog.$root === taskLog) {
|
||||
onRootTaskEnd(taskLog, event)
|
||||
onRootTaskEnd(taskLog)
|
||||
}
|
||||
}
|
||||
|
||||
onTaskUpdate(taskLog, event)
|
||||
onTaskUpdate(taskLog)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,27 +7,6 @@ function alreadyEnded() {
|
||||
throw new Error('task has already ended')
|
||||
}
|
||||
|
||||
const serializeErrors = errors => (Array.isArray(errors) ? errors.map(serializeError) : errors)
|
||||
|
||||
// Create a serializable object from an error.
|
||||
//
|
||||
// Otherwise some fields might be non-enumerable and missing from logs.
|
||||
const serializeError = error => {
|
||||
return error instanceof Error
|
||||
? {
|
||||
...error, // Copy enumerable properties.
|
||||
cause: error.cause,
|
||||
code: error.code,
|
||||
errors: serializeErrors(error.errors), // supports AggregateError
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
stack: error.stack,
|
||||
}
|
||||
: error
|
||||
}
|
||||
|
||||
exports.serializeError = serializeError
|
||||
|
||||
// define a read-only, non-enumerable, non-configurable property
|
||||
function define(object, property, value) {
|
||||
Object.defineProperty(object, property, { value })
|
||||
@@ -50,7 +29,7 @@ class Task {
|
||||
static get abortSignal() {
|
||||
const task = getTask()
|
||||
if (task !== undefined) {
|
||||
return task._abortController.signal
|
||||
return task.#abortController.signal
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,18 +65,11 @@ class Task {
|
||||
}
|
||||
}
|
||||
|
||||
// These two properties should not be used outside the class
|
||||
//
|
||||
// To ensure compatibility between multiple versions of this lib, they are kept public for now
|
||||
_abortController = new AbortController()
|
||||
_onProgress
|
||||
#abortController = new AbortController()
|
||||
#onProgress
|
||||
|
||||
get id() {
|
||||
// Use a compact, sortable, string representation of the creation date,
|
||||
// and add a random part to differentiate tasks created at the same time
|
||||
//
|
||||
// Due to the padding, dates are sortable up to 5188-04-22T11:04:28.415Z
|
||||
return (this.id = Date.now().toString(36).padStart(9, '0') + '-' + Math.random().toString(36).slice(2))
|
||||
return (this.id = Math.random().toString(36).slice(2))
|
||||
}
|
||||
set id(value) {
|
||||
define(this, 'id', value)
|
||||
@@ -114,23 +86,23 @@ class Task {
|
||||
this.#startData = { properties }
|
||||
|
||||
if (onProgress !== undefined) {
|
||||
this._onProgress = onProgress
|
||||
this.#onProgress = onProgress
|
||||
} else {
|
||||
const parent = getTask()
|
||||
if (parent !== undefined) {
|
||||
const { signal } = parent._abortController
|
||||
const { signal } = parent.#abortController
|
||||
signal.addEventListener('abort', () => {
|
||||
this._abortController.abort(signal.reason)
|
||||
this.#abortController.abort(signal.reason)
|
||||
})
|
||||
|
||||
this._onProgress = parent._onProgress
|
||||
this.#onProgress = parent.#onProgress
|
||||
this.#startData.parentId = parent.id
|
||||
} else {
|
||||
this._onProgress = noop
|
||||
this.#onProgress = noop
|
||||
}
|
||||
}
|
||||
|
||||
const { signal } = this._abortController
|
||||
const { signal } = this.#abortController
|
||||
signal.addEventListener('abort', () => {
|
||||
if (this.status === PENDING) {
|
||||
this.#maybeStart()
|
||||
@@ -145,7 +117,7 @@ class Task {
|
||||
}
|
||||
|
||||
abort(reason) {
|
||||
this._abortController.abort(reason)
|
||||
this.#abortController.abort(reason)
|
||||
}
|
||||
|
||||
#emit(type, data) {
|
||||
@@ -154,19 +126,15 @@ class Task {
|
||||
data.id = this.id
|
||||
data.timestamp = Date.now()
|
||||
data.type = type
|
||||
this._onProgress(data)
|
||||
this.#onProgress(data)
|
||||
}
|
||||
|
||||
#end(status, result) {
|
||||
assert.equal(this.#status, PENDING)
|
||||
assert.equal(this.#running, false)
|
||||
|
||||
if (result instanceof Error && result.toJSON === undefined) {
|
||||
result = serializeError(result)
|
||||
}
|
||||
|
||||
this.#emit('end', { status, result })
|
||||
this._onProgress = alreadyEnded
|
||||
this.#onProgress = alreadyEnded
|
||||
this.#status = status
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ const assert = require('node:assert').strict
|
||||
const { describe, it } = require('node:test')
|
||||
|
||||
const { makeOnProgress } = require('./combineEvents.js')
|
||||
const { Task, serializeError } = require('./index.js')
|
||||
const { Task } = require('./index.js')
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
@@ -28,38 +28,8 @@ function createTask(opts) {
|
||||
return task
|
||||
}
|
||||
|
||||
describe('serializeError', function () {
|
||||
it('serializes simple error', function () {
|
||||
const err = new Error('test Error')
|
||||
err.code = 404
|
||||
const serialized = serializeError(err)
|
||||
assert.equal(serialized.message, 'test Error')
|
||||
assert.equal(serialized.name, 'Error')
|
||||
assert.equal(serialized.code, 404)
|
||||
assert.notEqual(serialized.stack, undefined)
|
||||
})
|
||||
|
||||
it('serializes aggregate error', function () {
|
||||
const err = new AggregateError([new Error('test error 1'), new Error('test error 2')], 'Error message', {
|
||||
cause: 'broken',
|
||||
})
|
||||
const serialized = serializeError(err)
|
||||
assert.equal(serialized.message, 'Error message')
|
||||
assert.equal(serialized.name, 'AggregateError')
|
||||
assert.equal(serialized.cause, 'broken')
|
||||
assert.notEqual(serialized.stack, undefined)
|
||||
assert.equal(serialized.errors.length, 2)
|
||||
assert.equal(serialized.errors[0].name, 'Error')
|
||||
assert.equal(serialized.errors[1].name, 'Error')
|
||||
assert.equal(serialized.errors[0].message, 'test error 1')
|
||||
assert.equal(serialized.errors[1].message, 'test error 2')
|
||||
assert.notEqual(serialized.errors[0].stack, undefined)
|
||||
assert.notEqual(serialized.errors[1].stack, undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Task', function () {
|
||||
describe('constructor', function () {
|
||||
describe('contructor', function () {
|
||||
it('data properties are passed to the start event', async function () {
|
||||
const properties = { foo: 0, bar: 1 }
|
||||
const task = createTask({ properties })
|
||||
@@ -136,7 +106,7 @@ describe('Task', function () {
|
||||
assert.equal(task.$events.length, 3)
|
||||
assertEvent(task, { type: 'start' }, 0)
|
||||
assertEvent(task, { type: 'abortionRequested', reason }, 1)
|
||||
assertEvent(task, { type: 'end', status: 'failure', result: serializeError(result) }, 2)
|
||||
assertEvent(task, { type: 'end', status: 'failure', result }, 2)
|
||||
})
|
||||
|
||||
it('does not abort if the task succeed', async function () {
|
||||
@@ -206,7 +176,7 @@ describe('Task', function () {
|
||||
|
||||
assertEvent(task, {
|
||||
status: 'failure',
|
||||
result: serializeError(error),
|
||||
result: error,
|
||||
type: 'end',
|
||||
})
|
||||
})
|
||||
@@ -295,7 +265,7 @@ describe('Task', function () {
|
||||
assert.throws(() => task.set(name, value), { message: /^task has not started yet\b/ })
|
||||
})
|
||||
|
||||
it(`emits a property message`, async function () {
|
||||
it(`emits an property message`, async function () {
|
||||
const task = createTask()
|
||||
await task.run(async () => {
|
||||
await Task.run(() => {
|
||||
@@ -369,7 +339,7 @@ describe('Task', function () {
|
||||
Task.warning('foo')
|
||||
})
|
||||
|
||||
it('emits a warning message when run inside a task', async function () {
|
||||
it('emits an warning message when run inside a task', async function () {
|
||||
const task = createTask()
|
||||
await task.run(() => {
|
||||
Task.warning('foo')
|
||||
@@ -505,7 +475,7 @@ describe('Task', function () {
|
||||
assert.equal(task.status, 'failure')
|
||||
assertEvent(task, {
|
||||
status: 'failure',
|
||||
result: serializeError(e),
|
||||
result: e,
|
||||
type: 'end',
|
||||
})
|
||||
})
|
||||
@@ -541,7 +511,7 @@ describe('Task', function () {
|
||||
assert.equal(task.status, 'failure')
|
||||
assertEvent(task, {
|
||||
status: 'failure',
|
||||
result: serializeError(e),
|
||||
result: e,
|
||||
type: 'end',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "0.7.0",
|
||||
"version": "0.6.0",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,21 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import eslint from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export default tseslint.config(eslint.configs.recommended, tseslint.configs.recommended, {
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
},
|
||||
ignores: ['dist'],
|
||||
rules: {
|
||||
// Allow empty interface for recursive generic typing (E.g. interface XenApiNetworkWrapped)
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
},
|
||||
})
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"name": "@vates/types",
|
||||
"private": false,
|
||||
"version": "1.29.0",
|
||||
"main": "./dist/index.mjs",
|
||||
"exports": {
|
||||
".": {
|
||||
"default": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.mts"
|
||||
},
|
||||
"./xo": "./dist/xo.d.mts",
|
||||
"./xo-app": "./dist/xo-app.d.mts",
|
||||
"./xen-api": "./dist/xen-api.d.mts",
|
||||
"./common": {
|
||||
"default": "./dist/common.mjs",
|
||||
"types": "./dist/common.d.mts"
|
||||
},
|
||||
"./lib/vates/*": {
|
||||
"default": "./dist/lib/vates-*.mjs",
|
||||
"type": "./dist/lib/vates-*.d.mts"
|
||||
},
|
||||
"./lib/xen-orchestra/*": {
|
||||
"default": "./dist/lib/xen-orchestra-*.mjs",
|
||||
"type": "./dist/lib/xen-orchestra-*.d.mts"
|
||||
},
|
||||
"./lib/complex-matcher": {
|
||||
"default": "./dist/lib/complex-matcher.mjs",
|
||||
"type": "./dist/lib/complex-matcher.d.mts"
|
||||
}
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"typescript": "~5.6.3",
|
||||
"typescript-eslint": "^8.61.0",
|
||||
"@types/express": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"postversion": "npm publish --access public"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/types",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/types",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
}
|
||||
}
|
||||
@@ -1,771 +0,0 @@
|
||||
import type { XoHost, XoNetwork, XoVif } from './xo.mjs'
|
||||
|
||||
declare const __brand: unique symbol
|
||||
|
||||
export type Branded<TBrand extends string, TType = string> = TType & { [__brand]: TBrand }
|
||||
|
||||
export const HOST_POWER_STATE = {
|
||||
RUNNING: 'Running',
|
||||
HALTED: 'Halted',
|
||||
UNKNOWN: 'Unknown',
|
||||
} as const
|
||||
|
||||
export type HOST_POWER_STATE = (typeof HOST_POWER_STATE)[keyof typeof HOST_POWER_STATE]
|
||||
|
||||
export const TASK_ALLOWED_OPERATIONS = {
|
||||
CANCEL: 'cancel',
|
||||
DESTROY: 'destroy',
|
||||
} as const
|
||||
|
||||
export type TASK_ALLOWED_OPERATIONS = (typeof TASK_ALLOWED_OPERATIONS)[keyof typeof TASK_ALLOWED_OPERATIONS]
|
||||
|
||||
export const TASK_STATUS_TYPE = {
|
||||
CANCELLED: 'cancelled',
|
||||
CANCELLING: 'cancelling',
|
||||
FAILURE: 'failure',
|
||||
PENDING: 'pending',
|
||||
SUCCESS: 'success',
|
||||
} as const
|
||||
|
||||
export type TASK_STATUS_TYPE = (typeof TASK_STATUS_TYPE)[keyof typeof TASK_STATUS_TYPE]
|
||||
|
||||
export const EVENT_OPERATION = {
|
||||
ADD: 'add',
|
||||
DEL: 'del',
|
||||
MOD: 'mod',
|
||||
} as const
|
||||
|
||||
export type EVENT_OPERATION = (typeof EVENT_OPERATION)[keyof typeof EVENT_OPERATION]
|
||||
|
||||
export const POOL_ALLOWED_OPERATIONS = {
|
||||
APPLY_UPDATES: 'apply_updates',
|
||||
CERT_REFRESH: 'cert_refresh',
|
||||
CLUSTER_CREATE: 'cluster_create',
|
||||
CONFIGURE_REPOSITORIES: 'configure_repositories',
|
||||
COPY_PRIMARY_HOST_CERTS: 'copy_primary_host_certs',
|
||||
DESIGNATE_NEW_MASTER: 'designate_new_master',
|
||||
EJECT: 'eject',
|
||||
EXCHANGE_CA_CERTIFICATES_ON_JOIN: 'exchange_ca_certificates_on_join',
|
||||
EXCHANGE_CERTIFICATES_ON_JOIN: 'exchange_certificates_on_join',
|
||||
GET_UPDATES: 'get_updates',
|
||||
HA_DISABLE: 'ha_disable',
|
||||
HA_ENABLE: 'ha_enable',
|
||||
SYNC_UPDATES: 'sync_updates',
|
||||
TLS_VERIFICATION_ENABLE: 'tls_verification_enable',
|
||||
} as const
|
||||
|
||||
export type POOL_ALLOWED_OPERATIONS = (typeof POOL_ALLOWED_OPERATIONS)[keyof typeof POOL_ALLOWED_OPERATIONS]
|
||||
|
||||
export const TELEMETRY_FREQUENCY = {
|
||||
DAILY: 'daily',
|
||||
MONTHLY: 'monthly',
|
||||
WEEKLY: 'weekly',
|
||||
} as const
|
||||
|
||||
export type TELEMETRY_FREQUENCY = (typeof TELEMETRY_FREQUENCY)[keyof typeof TELEMETRY_FREQUENCY]
|
||||
|
||||
export const UPDATE_SYNC_FREQUENCY = {
|
||||
DAILY: 'daily',
|
||||
WEEKLY: 'weekly',
|
||||
} as const
|
||||
|
||||
export type UPDATE_SYNC_FREQUENCY = (typeof UPDATE_SYNC_FREQUENCY)[keyof typeof UPDATE_SYNC_FREQUENCY]
|
||||
|
||||
export const AFTER_APPLY_GUIDANCE = {
|
||||
RESTART_HOST: 'restartHost',
|
||||
RESTART_HVM: 'restartHVM',
|
||||
RESTART_PV: 'restartPV',
|
||||
RESTART_XAPI: 'restartXAPI',
|
||||
} as const
|
||||
|
||||
export type AFTER_APPLY_GUIDANCE = (typeof AFTER_APPLY_GUIDANCE)[keyof typeof AFTER_APPLY_GUIDANCE]
|
||||
|
||||
export const UPDATE_AFTER_APPLY_GUIDANCE = {
|
||||
RESTART_HOST: 'restartHost',
|
||||
RESTART_HVM: 'restartHVM',
|
||||
RESTART_PV: 'restartPV',
|
||||
RESTART_XAPI: 'restartXAPI',
|
||||
} as const
|
||||
|
||||
export type UPDATE_AFTER_APPLY_GUIDANCE = (typeof UPDATE_AFTER_APPLY_GUIDANCE)[keyof typeof UPDATE_AFTER_APPLY_GUIDANCE]
|
||||
|
||||
export const LIVEPATCH_STATUS = {
|
||||
OK: 'ok',
|
||||
OK_LIVEPATCH_COMPLETE: 'ok_livepatch_complete',
|
||||
OK_LIVEPATCH_INCOMPLETE: 'ok_livepatch_incomplete',
|
||||
} as const
|
||||
|
||||
export type LIVEPATCH_STATUS = (typeof LIVEPATCH_STATUS)[keyof typeof LIVEPATCH_STATUS]
|
||||
|
||||
export const VM_POWER_STATE = {
|
||||
HALTED: 'Halted',
|
||||
PAUSED: 'Paused',
|
||||
RUNNING: 'Running',
|
||||
SUSPENDED: 'Suspended',
|
||||
} as const
|
||||
|
||||
export type VM_POWER_STATE = (typeof VM_POWER_STATE)[keyof typeof VM_POWER_STATE]
|
||||
|
||||
export const UPDATE_GUIDANCES = {
|
||||
REBOOT_HOST: 'reboot_host',
|
||||
REBOOT_HOST_ON_KERNEL_LIVEPATCH_FAILURE: 'reboot_host_on_kernel_livepatch_failure',
|
||||
REBOOT_HOST_ON_LIVEPATCH_FAILURE: 'reboot_host_on_livepatch_failure',
|
||||
REBOOT_HOST_ON_XEN_LIVEPATCH_FAILURE: 'reboot_host_on_xen_livepatch_failure',
|
||||
RESTART_DEVICE_MODEL: 'restart_device_model',
|
||||
RESTART_TOOLSTACK: 'restart_toolstack',
|
||||
RESTART_VM: 'restart_vm',
|
||||
} as const
|
||||
|
||||
export type UPDATE_GUIDANCES = (typeof UPDATE_GUIDANCES)[keyof typeof UPDATE_GUIDANCES]
|
||||
|
||||
export const ON_SOFTREBOOT_BEHAVIOR = {
|
||||
DESTROY: 'destroy',
|
||||
PRESERVE: 'preserve',
|
||||
RESTART: 'restart',
|
||||
SOFT_REBOOT: 'soft_reboot',
|
||||
} as const
|
||||
|
||||
export type ON_SOFTREBOOT_BEHAVIOR = (typeof ON_SOFTREBOOT_BEHAVIOR)[keyof typeof ON_SOFTREBOOT_BEHAVIOR]
|
||||
|
||||
export const ON_NORMAL_EXIT = {
|
||||
DESTROY: 'destroy',
|
||||
RESTART: 'restart',
|
||||
} as const
|
||||
|
||||
export type ON_NORMAL_EXIT = (typeof ON_NORMAL_EXIT)[keyof typeof ON_NORMAL_EXIT]
|
||||
|
||||
export const VM_OPERATIONS = {
|
||||
ASSERT_OPERATION_VALID: 'assert_operation_valid',
|
||||
AWAITING_MEMORY_LIVE: 'awaiting_memory_live',
|
||||
CALL_PLUGIN: 'call_plugin',
|
||||
CHANGING_DYNAMIC_RANGE: 'changing_dynamic_range',
|
||||
CHANGING_MEMORY_LIMITS: 'changing_memory_limits',
|
||||
CHANGING_MEMORY_LIVE: 'changing_memory_live',
|
||||
CHANGING_NVRAM: 'changing_NVRAM',
|
||||
CHANGING_SHADOW_MEMORY: 'changing_shadow_memory',
|
||||
CHANGING_SHADOW_MEMORY_LIVE: 'changing_shadow_memory_live',
|
||||
CHANGING_STATIC_RANGE: 'changing_static_range',
|
||||
CHANGING_VCPUS: 'changing_VCPUs',
|
||||
CHANGING_VCPUS_LIVE: 'changing_VCPUs_live',
|
||||
CHECKPOINT: 'checkpoint',
|
||||
CLEAN_REBOOT: 'clean_reboot',
|
||||
CLEAN_SHUTDOWN: 'clean_shutdown',
|
||||
CLONE: 'clone',
|
||||
COPY: 'copy',
|
||||
CREATE_TEMPLATE: 'create_template',
|
||||
CREATE_VTPM: 'create_vtpm',
|
||||
CSVM: 'csvm',
|
||||
DATA_SOURCE_OP: 'data_source_op',
|
||||
DESTROY: 'destroy',
|
||||
EXPORT: 'export',
|
||||
GET_BOOT_RECORD: 'get_boot_record',
|
||||
HARD_REBOOT: 'hard_reboot',
|
||||
HARD_SHUTDOWN: 'hard_shutdown',
|
||||
IMPORT: 'import',
|
||||
MAKE_INTO_TEMPLATE: 'make_into_template',
|
||||
METADATA_EXPORT: 'metadata_export',
|
||||
MIGRATE_SEND: 'migrate_send',
|
||||
PAUSE: 'pause',
|
||||
POOL_MIGRATE: 'pool_migrate',
|
||||
POWER_STATE_RESET: 'power_state_reset',
|
||||
PROVISION: 'provision',
|
||||
QUERY_SERVICES: 'query_services',
|
||||
RESUME: 'resume',
|
||||
RESUME_ON: 'resume_on',
|
||||
REVERT: 'revert',
|
||||
REVERTING: 'reverting',
|
||||
SEND_SYSRQ: 'send_sysrq',
|
||||
SEND_TRIGGER: 'send_trigger',
|
||||
SHUTDOWN: 'shutdown',
|
||||
SNAPSHOT: 'snapshot',
|
||||
SNAPSHOT_WITH_QUIESCE: 'snapshot_with_quiesce',
|
||||
START: 'start',
|
||||
START_ON: 'start_on',
|
||||
SUSPEND: 'suspend',
|
||||
UNPAUSE: 'unpause',
|
||||
UPDATE_ALLOWED_OPERATIONS: 'update_allowed_operations',
|
||||
} as const
|
||||
|
||||
export type VM_OPERATIONS = (typeof VM_OPERATIONS)[keyof typeof VM_OPERATIONS]
|
||||
|
||||
export const ON_CRASH_BEHAVIOUR = {
|
||||
COREDUMP_AND_DESTROY: 'coredump_and_destroy',
|
||||
COREDUMP_AND_RESTART: 'coredump_and_restart',
|
||||
DESTROY: 'destroy',
|
||||
PRESERVE: 'preserve',
|
||||
RENAME_RESTART: 'rename_restart',
|
||||
RESTART: 'restart',
|
||||
} as const
|
||||
|
||||
export type ON_CRASH_BEHAVIOUR = (typeof ON_CRASH_BEHAVIOUR)[keyof typeof ON_CRASH_BEHAVIOUR]
|
||||
|
||||
export const DOMAIN_TYPE = {
|
||||
HVM: 'hvm',
|
||||
PV: 'pv',
|
||||
PVH: 'pvh',
|
||||
PV_IN_PVH: 'pv_in_pvh',
|
||||
UNSPECIFIED: 'unspecified',
|
||||
} as const
|
||||
|
||||
export type DOMAIN_TYPE = (typeof DOMAIN_TYPE)[keyof typeof DOMAIN_TYPE]
|
||||
|
||||
export const TRISTATE_TYPE = {
|
||||
NO: 'no',
|
||||
UNSPECIFIED: 'unspecified',
|
||||
YES: 'yes',
|
||||
} as const
|
||||
|
||||
export type TRISTATE_TYPE = (typeof TRISTATE_TYPE)[keyof typeof TRISTATE_TYPE]
|
||||
|
||||
export const VMPP_BACKUP_TYPE = {
|
||||
CHECKPOINT: 'checkpoint',
|
||||
SNAPSHOT: 'snapshot',
|
||||
} as const
|
||||
|
||||
export type VMPP_BACKUP_TYPE = (typeof VMPP_BACKUP_TYPE)[keyof typeof VMPP_BACKUP_TYPE]
|
||||
|
||||
export const VMPP_BACKUP_FREQUENCY = {
|
||||
DAILY: 'daily',
|
||||
HOURLY: 'hourly',
|
||||
WEEKLY: 'weekly',
|
||||
} as const
|
||||
|
||||
export type VMPP_BACKUP_FREQUENCY = (typeof VMPP_BACKUP_FREQUENCY)[keyof typeof VMPP_BACKUP_FREQUENCY]
|
||||
|
||||
export const VMPP_ARCHIVE_FREQUENCY = {
|
||||
ALWAYS_AFTER_BACKUP: 'always_after_backup',
|
||||
DAILY: 'daily',
|
||||
NEVER: 'never',
|
||||
WEEKLY: 'weekly',
|
||||
} as const
|
||||
|
||||
export type VMPP_ARCHIVE_FREQUENCY = (typeof VMPP_ARCHIVE_FREQUENCY)[keyof typeof VMPP_ARCHIVE_FREQUENCY]
|
||||
|
||||
export const VMPP_ARCHIVE_TARGET_TYPE = {
|
||||
CIFS: 'cifs',
|
||||
NFS: 'nfs',
|
||||
NONE: 'none',
|
||||
} as const
|
||||
|
||||
export type VMPP_ARCHIVE_TARGET_TYPE = (typeof VMPP_ARCHIVE_TARGET_TYPE)[keyof typeof VMPP_ARCHIVE_TARGET_TYPE]
|
||||
|
||||
export const VMSS_FREQUENCY = {
|
||||
DAILY: 'daily',
|
||||
HOURLY: 'hourly',
|
||||
WEEKLY: 'weekly',
|
||||
} as const
|
||||
|
||||
export type VMSS_FREQUENCY = (typeof VMSS_FREQUENCY)[keyof typeof VMSS_FREQUENCY]
|
||||
|
||||
export const VMSS_TYPE = {
|
||||
CHECKPOINT: 'checkpoint',
|
||||
SNAPSHOT: 'snapshot',
|
||||
SNAPSHOT_WITH_QUIESCE: 'snapshot_with_quiesce',
|
||||
} as const
|
||||
|
||||
export type VMSS_TYPE = (typeof VMSS_TYPE)[keyof typeof VMSS_TYPE]
|
||||
|
||||
export const VM_APPLIANCE_OPERATION = {
|
||||
CLEAN_SHUTDOWN: 'clean_shutdown',
|
||||
HARD_SHUTDOWN: 'hard_shutdown',
|
||||
SHUTDOWN: 'shutdown',
|
||||
START: 'start',
|
||||
} as const
|
||||
|
||||
export type VM_APPLIANCE_OPERATION = (typeof VM_APPLIANCE_OPERATION)[keyof typeof VM_APPLIANCE_OPERATION]
|
||||
|
||||
export const HOST_ALLOWED_OPERATIONS = {
|
||||
APPLY_UPDATES: 'apply_updates',
|
||||
ENABLE: 'enable',
|
||||
EVACUATE: 'evacuate',
|
||||
POWER_ON: 'power_on',
|
||||
PROVISION: 'provision',
|
||||
REBOOT: 'reboot',
|
||||
SHUTDOWN: 'shutdown',
|
||||
VM_MIGRATE: 'vm_migrate',
|
||||
VM_RESUME: 'vm_resume',
|
||||
VM_START: 'vm_start',
|
||||
} as const
|
||||
|
||||
export type HOST_ALLOWED_OPERATIONS = (typeof HOST_ALLOWED_OPERATIONS)[keyof typeof HOST_ALLOWED_OPERATIONS]
|
||||
|
||||
export const LATEST_SYNCED_UPDATES_APPLIED_STATE = {
|
||||
NO: 'no',
|
||||
UNKNOWN: 'unknown',
|
||||
YES: 'yes',
|
||||
} as const
|
||||
|
||||
export type LATEST_SYNCED_UPDATES_APPLIED_STATE =
|
||||
(typeof LATEST_SYNCED_UPDATES_APPLIED_STATE)[keyof typeof LATEST_SYNCED_UPDATES_APPLIED_STATE]
|
||||
|
||||
export const HOST_DISPLAY = {
|
||||
DISABLED: 'disabled',
|
||||
DISABLE_ON_REBOOT: 'disable_on_reboot',
|
||||
ENABLED: 'enabled',
|
||||
ENABLE_ON_REBOOT: 'enable_on_reboot',
|
||||
} as const
|
||||
|
||||
export type HOST_DISPLAY = (typeof HOST_DISPLAY)[keyof typeof HOST_DISPLAY]
|
||||
|
||||
export const HOST_SCHED_GRAN = {
|
||||
CORE: 'core',
|
||||
CPU: 'cpu',
|
||||
SOCKET: 'socket',
|
||||
} as const
|
||||
|
||||
export type HOST_SCHED_GRAN = (typeof HOST_SCHED_GRAN)[keyof typeof HOST_SCHED_GRAN]
|
||||
|
||||
export const HOST_NUMA_AFFINITY_POLICY = {
|
||||
ANY: 'any',
|
||||
BEST_EFFORT: 'best_effort',
|
||||
DEFAULT_POLICY: 'default_policy',
|
||||
} as const
|
||||
|
||||
export type HOST_NUMA_AFFINITY_POLICY = (typeof HOST_NUMA_AFFINITY_POLICY)[keyof typeof HOST_NUMA_AFFINITY_POLICY]
|
||||
|
||||
export const NETWORK_OPERATIONS = {
|
||||
ATTACHING: 'attaching',
|
||||
} as const
|
||||
|
||||
export type NETWORK_OPERATIONS = (typeof NETWORK_OPERATIONS)[keyof typeof NETWORK_OPERATIONS]
|
||||
|
||||
export const NETWORK_DEFAULT_LOCKING_MODE = {
|
||||
DISABLED: 'disabled',
|
||||
UNLOCKED: 'unlocked',
|
||||
} as const
|
||||
|
||||
export type NETWORK_DEFAULT_LOCKING_MODE =
|
||||
(typeof NETWORK_DEFAULT_LOCKING_MODE)[keyof typeof NETWORK_DEFAULT_LOCKING_MODE]
|
||||
|
||||
export const NETWORK_PURPOSE = {
|
||||
INSECURE_NBD: 'insecure_nbd',
|
||||
NBD: 'nbd',
|
||||
} as const
|
||||
|
||||
export type NETWORK_PURPOSE = (typeof NETWORK_PURPOSE)[keyof typeof NETWORK_PURPOSE]
|
||||
|
||||
export const VIF_OPERATIONS = {
|
||||
ATTACH: 'attach',
|
||||
PLUG: 'plug',
|
||||
UNPLUG: 'unplug',
|
||||
} as const
|
||||
|
||||
export type VIF_OPERATIONS = (typeof VIF_OPERATIONS)[keyof typeof VIF_OPERATIONS]
|
||||
|
||||
export const VIF_LOCKING_MODE = {
|
||||
DISABLED: 'disabled',
|
||||
LOCKED: 'locked',
|
||||
NETWORK_DEFAULT: 'network_default',
|
||||
UNLOCKED: 'unlocked',
|
||||
} as const
|
||||
|
||||
export type VIF_LOCKING_MODE = (typeof VIF_LOCKING_MODE)[keyof typeof VIF_LOCKING_MODE]
|
||||
|
||||
export const VIF_IPV4_CONFIGURATION_MODE = {
|
||||
NONE: 'None',
|
||||
STATIC: 'Static',
|
||||
} as const
|
||||
|
||||
export type VIF_IPV4_CONFIGURATION_MODE = (typeof VIF_IPV4_CONFIGURATION_MODE)[keyof typeof VIF_IPV4_CONFIGURATION_MODE]
|
||||
|
||||
export const VIF_IPV6_CONFIGURATION_MODE = {
|
||||
NONE: 'None',
|
||||
STATIC: 'Static',
|
||||
} as const
|
||||
|
||||
export type VIF_IPV6_CONFIGURATION_MODE = (typeof VIF_IPV6_CONFIGURATION_MODE)[keyof typeof VIF_IPV6_CONFIGURATION_MODE]
|
||||
|
||||
export const PIF_IGMP_STATUS = {
|
||||
DISABLED: 'disabled',
|
||||
ENABLED: 'enabled',
|
||||
UNKNOWN: 'unknown',
|
||||
} as const
|
||||
|
||||
export type PIF_IGMP_STATUS = (typeof PIF_IGMP_STATUS)[keyof typeof PIF_IGMP_STATUS]
|
||||
|
||||
export const IP_CONFIGURATION_MODE = {
|
||||
DHCP: 'DHCP',
|
||||
NONE: 'None',
|
||||
STATIC: 'Static',
|
||||
} as const
|
||||
|
||||
export type IP_CONFIGURATION_MODE = (typeof IP_CONFIGURATION_MODE)[keyof typeof IP_CONFIGURATION_MODE]
|
||||
|
||||
export const IPV6_CONFIGURATION_MODE = {
|
||||
AUTOCONF: 'Autoconf',
|
||||
DHCP: 'DHCP',
|
||||
NONE: 'None',
|
||||
STATIC: 'Static',
|
||||
} as const
|
||||
|
||||
export type IPV6_CONFIGURATION_MODE = (typeof IPV6_CONFIGURATION_MODE)[keyof typeof IPV6_CONFIGURATION_MODE]
|
||||
|
||||
export const PRIMARY_ADDRESS_TYPE = {
|
||||
IPV4: 'IPv4',
|
||||
IPV6: 'IPv6',
|
||||
} as const
|
||||
|
||||
export type PRIMARY_ADDRESS_TYPE = (typeof PRIMARY_ADDRESS_TYPE)[keyof typeof PRIMARY_ADDRESS_TYPE]
|
||||
|
||||
export const BOND_MODE = {
|
||||
ACTIVE_BACKUP: 'active-backup',
|
||||
BALANCE_SLB: 'balance-slb',
|
||||
LACP: 'lacp',
|
||||
} as const
|
||||
|
||||
export type BOND_MODE = (typeof BOND_MODE)[keyof typeof BOND_MODE]
|
||||
|
||||
export const STORAGE_OPERATIONS = {
|
||||
DESTROY: 'destroy',
|
||||
FORGET: 'forget',
|
||||
PBD_CREATE: 'pbd_create',
|
||||
PBD_DESTROY: 'pbd_destroy',
|
||||
PLUG: 'plug',
|
||||
SCAN: 'scan',
|
||||
UNPLUG: 'unplug',
|
||||
UPDATE: 'update',
|
||||
VDI_CLONE: 'vdi_clone',
|
||||
VDI_CREATE: 'vdi_create',
|
||||
VDI_DATA_DESTROY: 'vdi_data_destroy',
|
||||
VDI_DESTROY: 'vdi_destroy',
|
||||
VDI_DISABLE_CBT: 'vdi_disable_cbt',
|
||||
VDI_ENABLE_CBT: 'vdi_enable_cbt',
|
||||
VDI_INTRODUCE: 'vdi_introduce',
|
||||
VDI_LIST_CHANGED_BLOCKS: 'vdi_list_changed_blocks',
|
||||
VDI_MIRROR: 'vdi_mirror',
|
||||
VDI_RESIZE: 'vdi_resize',
|
||||
VDI_SET_ON_BOOT: 'vdi_set_on_boot',
|
||||
VDI_SNAPSHOT: 'vdi_snapshot',
|
||||
} as const
|
||||
|
||||
export type STORAGE_OPERATIONS = (typeof STORAGE_OPERATIONS)[keyof typeof STORAGE_OPERATIONS]
|
||||
|
||||
export const SR_HEALTH = {
|
||||
HEALTHY: 'healthy',
|
||||
RECOVERING: 'recovering',
|
||||
} as const
|
||||
|
||||
export type SR_HEALTH = (typeof SR_HEALTH)[keyof typeof SR_HEALTH]
|
||||
|
||||
export const VDI_OPERATIONS = {
|
||||
BLOCKED: 'blocked',
|
||||
CLONE: 'clone',
|
||||
COPY: 'copy',
|
||||
DATA_DESTROY: 'data_destroy',
|
||||
DESTROY: 'destroy',
|
||||
DISABLE_CBT: 'disable_cbt',
|
||||
ENABLE_CBT: 'enable_cbt',
|
||||
FORCE_UNLOCK: 'force_unlock',
|
||||
FORGET: 'forget',
|
||||
GENERATE_CONFIG: 'generate_config',
|
||||
LIST_CHANGED_BLOCKS: 'list_changed_blocks',
|
||||
MIRROR: 'mirror',
|
||||
RESIZE: 'resize',
|
||||
RESIZE_ONLINE: 'resize_online',
|
||||
SET_ON_BOOT: 'set_on_boot',
|
||||
SNAPSHOT: 'snapshot',
|
||||
UPDATE: 'update',
|
||||
} as const
|
||||
|
||||
export type VDI_OPERATIONS = (typeof VDI_OPERATIONS)[keyof typeof VDI_OPERATIONS]
|
||||
|
||||
export const VDI_TYPE = {
|
||||
CBT_METADATA: 'cbt_metadata',
|
||||
CRASHDUMP: 'crashdump',
|
||||
EPHEMERAL: 'ephemeral',
|
||||
HA_STATEFILE: 'ha_statefile',
|
||||
METADATA: 'metadata',
|
||||
PVS_CACHE: 'pvs_cache',
|
||||
REDO_LOG: 'redo_log',
|
||||
RRD: 'rrd',
|
||||
SUSPEND: 'suspend',
|
||||
SYSTEM: 'system',
|
||||
USER: 'user',
|
||||
} as const
|
||||
|
||||
export type VDI_TYPE = (typeof VDI_TYPE)[keyof typeof VDI_TYPE]
|
||||
|
||||
export const ON_BOOT = {
|
||||
PERSIST: 'persist',
|
||||
RESET: 'reset',
|
||||
} as const
|
||||
|
||||
export type ON_BOOT = (typeof ON_BOOT)[keyof typeof ON_BOOT]
|
||||
|
||||
export const VBD_OPERATIONS = {
|
||||
ATTACH: 'attach',
|
||||
EJECT: 'eject',
|
||||
INSERT: 'insert',
|
||||
PAUSE: 'pause',
|
||||
PLUG: 'plug',
|
||||
UNPAUSE: 'unpause',
|
||||
UNPLUG: 'unplug',
|
||||
UNPLUG_FORCE: 'unplug_force',
|
||||
} as const
|
||||
|
||||
export type VBD_OPERATIONS = (typeof VBD_OPERATIONS)[keyof typeof VBD_OPERATIONS]
|
||||
|
||||
export const VBD_TYPE = {
|
||||
CD: 'CD',
|
||||
DISK: 'Disk',
|
||||
FLOPPY: 'Floppy',
|
||||
} as const
|
||||
|
||||
export type VBD_TYPE = (typeof VBD_TYPE)[keyof typeof VBD_TYPE]
|
||||
|
||||
export const VBD_MODE = {
|
||||
RO: 'RO',
|
||||
RW: 'RW',
|
||||
} as const
|
||||
|
||||
export type VBD_MODE = (typeof VBD_MODE)[keyof typeof VBD_MODE]
|
||||
|
||||
export const VTPM_OPERATIONS = {
|
||||
DESTROY: 'destroy',
|
||||
} as const
|
||||
|
||||
export type VTPM_OPERATIONS = (typeof VTPM_OPERATIONS)[keyof typeof VTPM_OPERATIONS]
|
||||
|
||||
export const PERSISTENCE_BACKEND = {
|
||||
XAPI: 'xapi',
|
||||
} as const
|
||||
|
||||
export type PERSISTENCE_BACKEND = (typeof PERSISTENCE_BACKEND)[keyof typeof PERSISTENCE_BACKEND]
|
||||
|
||||
export const CONSOLE_PROTOCOL = {
|
||||
RDP: 'rdp',
|
||||
RFB: 'rfb',
|
||||
VT100: 'vt100',
|
||||
} as const
|
||||
|
||||
export type CONSOLE_PROTOCOL = (typeof CONSOLE_PROTOCOL)[keyof typeof CONSOLE_PROTOCOL]
|
||||
|
||||
export const CLS = {
|
||||
CERTIFICATE: 'Certificate',
|
||||
HOST: 'Host',
|
||||
POOL: 'Pool',
|
||||
PVS_PROXY: 'PVS_proxy',
|
||||
SR: 'SR',
|
||||
VDI: 'VDI',
|
||||
VM: 'VM',
|
||||
VMPP: 'VMPP',
|
||||
VMSS: 'VMSS',
|
||||
} as const
|
||||
|
||||
export type CLS = (typeof CLS)[keyof typeof CLS]
|
||||
|
||||
export const TUNNEL_PROTOCOL = {
|
||||
GRE: 'gre',
|
||||
VXLAN: 'vxlan',
|
||||
} as const
|
||||
|
||||
export type TUNNEL_PROTOCOL = (typeof TUNNEL_PROTOCOL)[keyof typeof TUNNEL_PROTOCOL]
|
||||
|
||||
export const SRIOV_CONFIGURATION_MODE = {
|
||||
MANUAL: 'manual',
|
||||
MODPROBE: 'modprobe',
|
||||
SYSFS: 'sysfs',
|
||||
UNKNOWN: 'unknown',
|
||||
} as const
|
||||
|
||||
export type SRIOV_CONFIGURATION_MODE = (typeof SRIOV_CONFIGURATION_MODE)[keyof typeof SRIOV_CONFIGURATION_MODE]
|
||||
|
||||
export const PGPU_DOM0_ACCESS = {
|
||||
DISABLED: 'disabled',
|
||||
DISABLE_ON_REBOOT: 'disable_on_reboot',
|
||||
ENABLED: 'enabled',
|
||||
ENABLE_ON_REBOOT: 'enable_on_reboot',
|
||||
} as const
|
||||
|
||||
export type PGPU_DOM0_ACCESS = (typeof PGPU_DOM0_ACCESS)[keyof typeof PGPU_DOM0_ACCESS]
|
||||
|
||||
export const ALLOCATION_ALGORITHM = {
|
||||
BREADTH_FIRST: 'breadth_first',
|
||||
DEPTH_FIRST: 'depth_first',
|
||||
} as const
|
||||
|
||||
export type ALLOCATION_ALGORITHM = (typeof ALLOCATION_ALGORITHM)[keyof typeof ALLOCATION_ALGORITHM]
|
||||
|
||||
export const VGPU_TYPE_IMPLEMENTATION = {
|
||||
GVT_G: 'gvt_g',
|
||||
MXGPU: 'mxgpu',
|
||||
NVIDIA: 'nvidia',
|
||||
NVIDIA_SRIOV: 'nvidia_sriov',
|
||||
PASSTHROUGH: 'passthrough',
|
||||
} as const
|
||||
|
||||
export type VGPU_TYPE_IMPLEMENTATION = (typeof VGPU_TYPE_IMPLEMENTATION)[keyof typeof VGPU_TYPE_IMPLEMENTATION]
|
||||
|
||||
export const PVS_PROXY_STATUS = {
|
||||
CACHING: 'caching',
|
||||
INCOMPATIBLE_PROTOCOL_VERSION: 'incompatible_protocol_version',
|
||||
INCOMPATIBLE_WRITE_CACHE_MODE: 'incompatible_write_cache_mode',
|
||||
INITIALISED: 'initialised',
|
||||
STOPPED: 'stopped',
|
||||
} as const
|
||||
|
||||
export type PVS_PROXY_STATUS = (typeof PVS_PROXY_STATUS)[keyof typeof PVS_PROXY_STATUS]
|
||||
|
||||
export const SDN_CONTROLLER_PROTOCOL = {
|
||||
PSSL: 'pssl',
|
||||
SSL: 'ssl',
|
||||
} as const
|
||||
|
||||
export type SDN_CONTROLLER_PROTOCOL = (typeof SDN_CONTROLLER_PROTOCOL)[keyof typeof SDN_CONTROLLER_PROTOCOL]
|
||||
|
||||
export const VUSB_OPERATIONS = {
|
||||
ATTACH: 'attach',
|
||||
PLUG: 'plug',
|
||||
UNPLUG: 'unplug',
|
||||
} as const
|
||||
|
||||
export type VUSB_OPERATIONS = (typeof VUSB_OPERATIONS)[keyof typeof VUSB_OPERATIONS]
|
||||
|
||||
export const CLUSTER_OPERATION = {
|
||||
ADD: 'add',
|
||||
DESTROY: 'destroy',
|
||||
DISABLE: 'disable',
|
||||
ENABLE: 'enable',
|
||||
REMOVE: 'remove',
|
||||
} as const
|
||||
|
||||
export type CLUSTER_OPERATION = (typeof CLUSTER_OPERATION)[keyof typeof CLUSTER_OPERATION]
|
||||
|
||||
export const CLUSTER_HOST_OPERATION = {
|
||||
DESTROY: 'destroy',
|
||||
DISABLE: 'disable',
|
||||
ENABLE: 'enable',
|
||||
} as const
|
||||
|
||||
export type CLUSTER_HOST_OPERATION = (typeof CLUSTER_HOST_OPERATION)[keyof typeof CLUSTER_HOST_OPERATION]
|
||||
|
||||
export const CERTIFICATE_TYPE = {
|
||||
CA: 'ca',
|
||||
HOST: 'host',
|
||||
HOST_INTERNAL: 'host_internal',
|
||||
} as const
|
||||
|
||||
export type CERTIFICATE_TYPE = (typeof CERTIFICATE_TYPE)[keyof typeof CERTIFICATE_TYPE]
|
||||
|
||||
export const OPAQUE_REF = { EMPTY: 'OpaqueRef:NULL' } as const
|
||||
|
||||
export type OPAQUE_REF_NULL = (typeof OPAQUE_REF)['EMPTY']
|
||||
|
||||
export const BACKUP_TYPE = { backup: 'backup', metadata: 'metadataBackup', mirror: 'mirrorBackup' } as const
|
||||
|
||||
export type BACKUP_TYPE = (typeof BACKUP_TYPE)[keyof typeof BACKUP_TYPE]
|
||||
|
||||
export const SUPPORTED_VDI_FORMAT = {
|
||||
raw: 'raw',
|
||||
vhd: 'vhd',
|
||||
qcow2: 'qcow2',
|
||||
} as const
|
||||
|
||||
export type SUPPORTED_VDI_FORMAT = (typeof SUPPORTED_VDI_FORMAT)[keyof typeof SUPPORTED_VDI_FORMAT]
|
||||
|
||||
// ----- XAPI Stats
|
||||
|
||||
type XapiStatsResponse<T> = {
|
||||
endTimestamp: number
|
||||
interval: number
|
||||
stats: T
|
||||
}
|
||||
type StatValues = (number | null)[]
|
||||
type RecordStatValues = Record<string, StatValues>
|
||||
|
||||
export type XapiStatsGranularity = 'seconds' | 'minutes' | 'hours' | 'days'
|
||||
|
||||
export type XapiHostStatsRaw = {
|
||||
cpus?: RecordStatValues
|
||||
ioThroughput?: {
|
||||
r: RecordStatValues
|
||||
w: RecordStatValues
|
||||
}
|
||||
iops?: {
|
||||
r: RecordStatValues
|
||||
w: RecordStatValues
|
||||
}
|
||||
iowait?: RecordStatValues
|
||||
latency?: {
|
||||
r: RecordStatValues
|
||||
w: RecordStatValues
|
||||
}
|
||||
load?: StatValues
|
||||
memory?: StatValues
|
||||
memoryFree?: StatValues
|
||||
pifs?: {
|
||||
rx: RecordStatValues
|
||||
tx: RecordStatValues
|
||||
}
|
||||
}
|
||||
export type XapiHostStats = XapiStatsResponse<XapiHostStatsRaw>
|
||||
|
||||
export type XapiVmStatsRaw = {
|
||||
cpus?: RecordStatValues
|
||||
cpuUsage?: StatValues
|
||||
runstateFullrun?: StatValues
|
||||
runstateFullContention?: StatValues
|
||||
runstatePartialRun?: StatValues
|
||||
runstatePartialContention?: StatValues
|
||||
runstateConcurrencyHazard?: StatValues
|
||||
runstateBlocked?: StatValues
|
||||
iops?: {
|
||||
r: RecordStatValues
|
||||
w: RecordStatValues
|
||||
}
|
||||
memory?: StatValues
|
||||
memoryFree?: StatValues
|
||||
memoryTarget?: StatValues
|
||||
vifs?: {
|
||||
rx: RecordStatValues
|
||||
tx: RecordStatValues
|
||||
}
|
||||
vifErrors?: {
|
||||
rx: RecordStatValues
|
||||
tx: RecordStatValues
|
||||
}
|
||||
xvds?: {
|
||||
w?: RecordStatValues
|
||||
r?: RecordStatValues
|
||||
total?: RecordStatValues
|
||||
}
|
||||
vbdLatency?: {
|
||||
w: RecordStatValues
|
||||
r: RecordStatValues
|
||||
}
|
||||
vbdIowait?: RecordStatValues
|
||||
vbdInflight?: RecordStatValues
|
||||
vbdAvgquSz?: RecordStatValues
|
||||
}
|
||||
export type XapiVmStats = XapiStatsResponse<XapiVmStatsRaw>
|
||||
|
||||
export type XapiPoolStats = Record<XoHost['id'], XapiHostStats | { error: Record<string, unknown> }>
|
||||
|
||||
export type TrafficRuleProtocol = (typeof TRAFFIC_RULE_PROTOCOLS)[number]
|
||||
|
||||
export const SDN_CONTROLLER_OF_RULES_KEY = 'xo:sdn-controller:of-rules'
|
||||
export const SDN_CONTROLLER_OF_METHOD_KEY = 'xo:sdn-controller:of-method'
|
||||
export const SDN_CONTROLLER_OF_FORMAT_KEY = 'xo:sdn-controller:of-format'
|
||||
export const TRAFFIC_RULE_PROTOCOLS = ['ARP', 'ICMP', 'IP', 'TCP', 'UDP'] as const
|
||||
export const TRAFFIC_RULE_PROTOCOLS_WITH_PORT: readonly TrafficRuleProtocol[] = ['TCP', 'UDP']
|
||||
|
||||
export type TrafficRuleTargetType = XoNetwork['type'] | XoVif['type']
|
||||
|
||||
export type TrafficRuleDirection = 'from' | 'to' | 'from/to'
|
||||
|
||||
export type RawTrafficRule = {
|
||||
allow: boolean
|
||||
protocol: string
|
||||
ipRange: string
|
||||
direction: string
|
||||
port?: string
|
||||
}
|
||||
|
||||
type BaseTrafficRule = RawTrafficRule & {
|
||||
id: string
|
||||
networkId: XoNetwork['id']
|
||||
}
|
||||
|
||||
export type TrafficRule =
|
||||
| (BaseTrafficRule & { type: XoVif['type']; sourceId: XoVif['id'] })
|
||||
| (BaseTrafficRule & { type: XoNetwork['type']; sourceId: XoNetwork['id'] })
|
||||
@@ -1,8 +0,0 @@
|
||||
export * from './common.mjs'
|
||||
export * from './xen-api.mjs'
|
||||
export * from './xo.mjs'
|
||||
export * from './xo-app.mjs'
|
||||
export * from './lib/xen-orchestra-xapi.mjs'
|
||||
export * from './lib/xen-orchestra-acl.mjs'
|
||||
export * from './lib/complex-matcher.mjs'
|
||||
export * from './lib/rest-api.mjs'
|
||||
@@ -1,5 +0,0 @@
|
||||
export type Or<T> = Id<{ __or: T[] }>
|
||||
|
||||
export type Id<T> = { id: T }
|
||||
|
||||
export type IdOr<T> = Id<T> | Or<T>
|
||||
@@ -1,102 +0,0 @@
|
||||
import type { NextFunction, Request, Response } from 'express'
|
||||
import type { VatesTask } from './vates-task.mjs'
|
||||
import type { XoApp } from '../xo-app.mjs'
|
||||
|
||||
export type SecurityName = '*' | 'token' | 'basic' | 'none'
|
||||
|
||||
export type FieldDefinition =
|
||||
| {
|
||||
type: 'string'
|
||||
example?: string
|
||||
optional?: boolean
|
||||
}
|
||||
| {
|
||||
type: 'boolean'
|
||||
example?: boolean
|
||||
optional?: boolean
|
||||
}
|
||||
| {
|
||||
type: 'number'
|
||||
example?: number
|
||||
optional?: boolean
|
||||
}
|
||||
| {
|
||||
type: 'enum'
|
||||
enum: string[]
|
||||
example?: string
|
||||
optional?: boolean
|
||||
}
|
||||
| {
|
||||
type: 'object'
|
||||
fields: Record<string, FieldDefinition>
|
||||
optional?: boolean
|
||||
}
|
||||
| {
|
||||
type: 'array'
|
||||
items: FieldDefinition
|
||||
example?: unknown[]
|
||||
optional?: boolean
|
||||
}
|
||||
|
||||
export type ParamFieldDefinition = Exclude<
|
||||
FieldDefinition,
|
||||
{ type: 'boolean' } | { type: 'object' } | { type: 'array' }
|
||||
>
|
||||
|
||||
export type QueryFieldDefinition = Exclude<FieldDefinition, { type: 'object' } | { type: 'array' }>
|
||||
type RestApi = object & { xoApp: XoApp }
|
||||
export type CreateAction = <Result>(
|
||||
callback: (task: VatesTask) => MaybePromise<Result>,
|
||||
opts: {
|
||||
sync?: boolean
|
||||
statusCode?: number
|
||||
taskProperties: { name: string; [key: string]: unknown }
|
||||
}
|
||||
) => Promise<Result | undefined>
|
||||
|
||||
export interface BaseRouteDefinition<Middleware> {
|
||||
method: 'get' | 'post' | 'put' | 'delete' | 'patch'
|
||||
endpoint: string
|
||||
description?: string
|
||||
tags?: string[]
|
||||
params?: Record<string, ParamFieldDefinition>
|
||||
query?: Record<string, QueryFieldDefinition>
|
||||
body?: Record<string, FieldDefinition>
|
||||
responses?: Array<{
|
||||
status: number
|
||||
description: string
|
||||
schema?: Record<string, FieldDefinition>
|
||||
}>
|
||||
middlewares?: Middleware[]
|
||||
scope?: 'acl'
|
||||
callback: (params: {
|
||||
req: Request
|
||||
res: Response
|
||||
next: NextFunction
|
||||
restApi: RestApi
|
||||
createAction: CreateAction
|
||||
}) => MaybePromise<unknown>
|
||||
security?: SecurityName
|
||||
}
|
||||
|
||||
export type MiddlewareDescriptor =
|
||||
| { name: 'json' | 'urlencoded' | 'text' | 'raw'; options?: Record<string, unknown> }
|
||||
| { name: 'acl'; acls: LooseAclEntry | LooseAclEntry[] }
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>
|
||||
/**
|
||||
* Loosely-typed mirror of {@link https://github.com/vatesfr/xen-orchestra/blob/master/@xen-orchestra/rest-api/src/middlewares/acl.middleware.mts | AclEntry}
|
||||
* from `@xen-orchestra/rest-api/src/middlewares/acl.middleware.mts`.
|
||||
*/
|
||||
export type LooseAclEntry = {
|
||||
resource: string
|
||||
action?: string | ((opts: { req: object; restApi: RestApi }) => string | undefined)
|
||||
actions?: string[] | ((opts: { req: object; restApi: RestApi }) => string[])
|
||||
objectId?: string | ((opts: { req: object; restApi: RestApi }) => string)
|
||||
objectIds?: string[] | ((opts: { req: object; restApi: RestApi }) => string[])
|
||||
object?: object | ((opts: { req: object; restApi: RestApi }) => MaybePromise<object> | undefined)
|
||||
objects?: object[] | ((opts: { req: object; restApi: RestApi }) => MaybePromise<object[]> | undefined)
|
||||
getObject?: (opts: { restApi: RestApi }) => (id: string) => MaybePromise<object>
|
||||
}
|
||||
|
||||
export type PluginRestRouteDefinition = BaseRouteDefinition<MiddlewareDescriptor>
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Branded } from '../common.mjs'
|
||||
|
||||
export interface VatesTask {
|
||||
_abortController: AbortController
|
||||
_onProgress(data?: unknown): void
|
||||
|
||||
id: Branded<'vates-task'>
|
||||
status: 'failure' | 'pending' | 'success'
|
||||
|
||||
abort(reason?: unknown): void
|
||||
failure(error?: unknown): void
|
||||
info(message: string, data: unknown): void
|
||||
run<T>(fn: () => T | (() => Promise<T>)): Promise<T>
|
||||
runInside<T>(fn: () => T | (() => Promise<T>)): Promise<T>
|
||||
set(name: string, value: unknown): void
|
||||
start(): void
|
||||
success(result?: unknown): void
|
||||
warning(message: string, data: unknown): void
|
||||
wrap<T>(fn: () => T | (() => Promise<T>)): Promise<T>
|
||||
wrapInside<T>(fn: () => T | (() => Promise<T>)): Promise<T>
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Branded } from '../common.mjs'
|
||||
import type { XoGroup, XoUser } from '../xo.mjs'
|
||||
|
||||
type XoAclBaseRole = {
|
||||
id: Branded<'acl-v2-role'>
|
||||
name: string
|
||||
description?: string
|
||||
privilegeIds: XoAclBasePrivilege['id'][]
|
||||
}
|
||||
|
||||
type XoAclRegularRole = XoAclBaseRole & {
|
||||
groupIds: XoGroup['id'][]
|
||||
userIds: XoUser['id'][]
|
||||
}
|
||||
|
||||
type XoAclTemplateRole = XoAclBaseRole & {
|
||||
isTemplate: true
|
||||
roleTemplateId: number
|
||||
}
|
||||
|
||||
export type XoAclRole = XoAclRegularRole | XoAclTemplateRole
|
||||
|
||||
export type XoAclSupportedActionsByResource = {
|
||||
[resource: string]: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* E.g
|
||||
* vms {
|
||||
* shutdown: {
|
||||
* clean: true,
|
||||
* hard: true,
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* `shutdown | shutdown:clean | shutdown:hard`
|
||||
*/
|
||||
export type GetKeysRecursively<T, Prefix extends string = ''> = {
|
||||
[K in keyof T]: T[K] extends object
|
||||
? K extends string
|
||||
? `${Prefix}${K}` | GetKeysRecursively<T[K], `${Prefix}${K}:`>
|
||||
: never
|
||||
: K extends string
|
||||
? `${Prefix}${K}`
|
||||
: never
|
||||
}[keyof T]
|
||||
|
||||
export type XoAclSupportedResource<TActionsByResource extends XoAclSupportedActionsByResource> =
|
||||
keyof TActionsByResource
|
||||
|
||||
export type XoAclSupportedActions<
|
||||
TActionsByResource extends XoAclSupportedActionsByResource,
|
||||
TResource extends XoAclSupportedResource<TActionsByResource>,
|
||||
> = (GetKeysRecursively<TActionsByResource[TResource]> & string) | '*'
|
||||
|
||||
export type XoAclBasePrivilege = {
|
||||
id: Branded<'acl-v2-privilege'>
|
||||
resource: string
|
||||
action: string
|
||||
selector?: string
|
||||
effect: 'allow' | 'deny'
|
||||
roleId: XoAclRole['id']
|
||||
}
|
||||
|
||||
export type XoAclPrivilege<
|
||||
TActionsByResource extends XoAclSupportedActionsByResource,
|
||||
TResource extends XoAclSupportedResource<TActionsByResource>,
|
||||
> = XoAclBasePrivilege & {
|
||||
resource: TResource
|
||||
action: XoAclSupportedActions<TActionsByResource, TResource>
|
||||
}
|
||||
|
||||
export type XoUserRole = {
|
||||
id: string
|
||||
roleId: XoAclRole['id']
|
||||
userId: XoUser['id']
|
||||
}
|
||||
|
||||
export type XoGroupRole = {
|
||||
id: string
|
||||
roleId: XoAclRole['id']
|
||||
groupId: XoGroup['id']
|
||||
}
|
||||
@@ -1,478 +0,0 @@
|
||||
import {
|
||||
WrappedXenApiRecord,
|
||||
XenApiHost,
|
||||
XenApiHostWrapped,
|
||||
XenApiNetwork,
|
||||
XenApiNetworkWrapped,
|
||||
XenApiRecord,
|
||||
XenApiSr,
|
||||
XenApiTask,
|
||||
XenApiVbd,
|
||||
XenApiVdi,
|
||||
XenApiVif,
|
||||
XenApiVm,
|
||||
XenApiVmWrapped,
|
||||
XenApiVtpm,
|
||||
} from '../xen-api.mjs'
|
||||
import type {
|
||||
BOND_MODE,
|
||||
OPAQUE_REF_NULL,
|
||||
SUPPORTED_VDI_FORMAT,
|
||||
VBD_MODE,
|
||||
VBD_TYPE,
|
||||
VDI_TYPE,
|
||||
VIF_LOCKING_MODE,
|
||||
VM_OPERATIONS,
|
||||
} from '../common.mjs'
|
||||
import type { PassThrough, Readable } from 'node:stream'
|
||||
import type {
|
||||
XapiXoRecord,
|
||||
XoGpuGroup,
|
||||
XoHost,
|
||||
XoNetwork,
|
||||
XoPif,
|
||||
XoSr,
|
||||
XoUser,
|
||||
XoVdi,
|
||||
XoVgpuType,
|
||||
XoVm,
|
||||
XoVmTemplate,
|
||||
XoVif,
|
||||
XoVmSnapshot,
|
||||
} from '../xo.mjs'
|
||||
|
||||
/**
|
||||
* Properties accepted by {@link Xapi.editVm}.
|
||||
*
|
||||
* Field names match the canonical (camelCase) properties defined by the
|
||||
* underlying `_editVm` (via `makeEditObject`). `_editVm` still resolves the
|
||||
* snake_case and legacy aliases, but the canonical names are used here.
|
||||
*/
|
||||
export interface EditVmProps {
|
||||
affinityHost?: string | null
|
||||
autoPoweron?: boolean
|
||||
blockedOperations?: Partial<Record<VM_OPERATIONS, boolean | string | null>>
|
||||
coresPerSocket?: number | string | null
|
||||
cpuCap?: number | null
|
||||
cpuMask?: number[]
|
||||
cpuWeight?: number | null
|
||||
cpus?: number
|
||||
cpusStaticMax?: number | string
|
||||
/**
|
||||
* Update VM creation metadata stored under `other_config.xo:*`. The object is
|
||||
* merged with the existing data.
|
||||
*/
|
||||
creation?: { user?: string }
|
||||
expNestedHvm?: boolean
|
||||
hasVendorDevice?: boolean
|
||||
highAvailability?: 'best-effort' | 'restart' | ''
|
||||
hvmBootFirmware?: string | null
|
||||
memory?: number | string
|
||||
memoryMax?: number | string
|
||||
memoryMin?: number | string
|
||||
memoryStaticMax?: number | string
|
||||
nameDescription?: string
|
||||
nameLabel?: string
|
||||
nestedVirt?: boolean
|
||||
nicType?: string | null
|
||||
notes?: string | null
|
||||
PV_args?: string
|
||||
secureBoot?: boolean
|
||||
startDelay?: number
|
||||
suspendSr?: string | null
|
||||
tags?: string[]
|
||||
uefiMode?: 'setup' | 'user'
|
||||
vga?: 'std' | 'cirrus'
|
||||
videoram?: 1 | 2 | 4 | 8 | 16
|
||||
viridian?: boolean
|
||||
virtualizationMode?: 'pv' | 'hvm'
|
||||
xenStoreData?: Record<string, string | null>
|
||||
}
|
||||
|
||||
/** Properties accepted by {@link Xapi.editVif}. */
|
||||
export interface EditVifProps {
|
||||
ipv4Allowed?: string[]
|
||||
ipv6Allowed?: string[]
|
||||
lockingMode?: VIF_LOCKING_MODE
|
||||
// in kB/s, `null` removes the limit
|
||||
rateLimit?: number | null
|
||||
txChecksumming?: boolean
|
||||
}
|
||||
|
||||
export type XcpPatches = {
|
||||
changelog?: {
|
||||
author: string
|
||||
date: number
|
||||
description: string
|
||||
}
|
||||
description: string
|
||||
license: string
|
||||
name: string
|
||||
release: string
|
||||
size: number
|
||||
url: string
|
||||
version: string
|
||||
}
|
||||
export type XsPatches = {
|
||||
conflicts?: string[]
|
||||
date: string
|
||||
description: string
|
||||
documentationUrl?: string
|
||||
guidances: string
|
||||
name: string
|
||||
id?: string
|
||||
paid?: boolean
|
||||
requirements?: string[]
|
||||
upgrade?: boolean
|
||||
url?: string
|
||||
uuid?: string
|
||||
}
|
||||
|
||||
export interface Xapi {
|
||||
status: string
|
||||
pool?: { uuid: string }
|
||||
sessionId: string
|
||||
_auth: { user: string; password: string }
|
||||
_url: { protocol: string; hostname: string; hostnameRaw: string; port?: string }
|
||||
|
||||
call: <ReturnType>(...args: unknown[]) => Promise<ReturnType>
|
||||
callAsync: <ReturnType>(...args: unknown[]) => Promise<ReturnType>
|
||||
|
||||
barrier<T extends XenApiRecord>(ref: T['$ref']): Promise<Extract<WrappedXenApiRecord, { $ref: T['$ref'] }>>
|
||||
connectVif(vifId: XoVif['id']): Promise<void>
|
||||
getField<T extends XenApiRecord, K extends keyof T>(
|
||||
type: Extract<WrappedXenApiRecord, T>['$type'],
|
||||
ref: T['$ref'],
|
||||
field: K
|
||||
): Promise<T[K]>
|
||||
getObject: <
|
||||
XoRecord extends XapiXoRecord,
|
||||
WrappedRecord extends WrappedXenApiRecord = Extract<WrappedXenApiRecord, { $type: XoRecord['type'] }>,
|
||||
>(
|
||||
idOrUuidOrRef: XoRecord['id'] | WrappedRecord['$ref'] | WrappedRecord['uuid'],
|
||||
defaultValue?: WrappedRecord
|
||||
) => WrappedRecord
|
||||
createNetwork(
|
||||
params:
|
||||
| {
|
||||
name: string
|
||||
description?: string
|
||||
mtu?: number
|
||||
}
|
||||
| {
|
||||
name: string
|
||||
description?: string
|
||||
pifId: XoPif['id']
|
||||
mtu?: number
|
||||
/* between 0 and 4094 */
|
||||
vlan: number
|
||||
}
|
||||
): Promise<XenApiNetworkWrapped>
|
||||
createBondedNetwork(params: {
|
||||
bondMode: BOND_MODE
|
||||
pifIds: XoPif['id'][]
|
||||
name: string
|
||||
description?: string
|
||||
mtu?: number
|
||||
}): Promise<XenApiNetworkWrapped>
|
||||
deleteNetwork(id: XoNetwork['id']): Promise<void>
|
||||
deleteVif(vifId: XoVif['id']): Promise<void>
|
||||
disconnectVif(vifId: XoVif['id']): Promise<void>
|
||||
exportVmOva(vmRef: XenApiVm['$ref']): Promise<PassThrough>
|
||||
emergencyShutdownHost(hostId: XoHost['id']): Promise<void>
|
||||
ejectHostFromPool(hostId: XoHost['id']): Promise<void>
|
||||
host_smartReboot(
|
||||
hostRef: XenApiHost['$ref'],
|
||||
bypassBlockedSuspend?: boolean,
|
||||
bypassCurrentVmCheck?: boolean
|
||||
): Promise<void>
|
||||
migrateVm(
|
||||
vmId: XoVm['id'],
|
||||
hostXapi: Xapi,
|
||||
hostId: XoHost['id'],
|
||||
opts?: {
|
||||
force?: boolean
|
||||
mapVdisSrs?: Record<XoVdi['id'], XoSr['id']>
|
||||
mapVifsNetworks?: Record<XoVif['id'], XoNetwork['id']>
|
||||
migrationNetworkId?: XoNetwork['id']
|
||||
sr?: XoSr['id']
|
||||
bypassAssert?: boolean
|
||||
}
|
||||
): Promise<void>
|
||||
joinPool(masterAddress: string, masterUsername: string, masterPassword: string, force?: boolean): Promise<void>
|
||||
listMissingPatches(host: XoHost['id']): Promise<XcpPatches[] | XsPatches[]>
|
||||
pool_emergencyShutdown(): Promise<void>
|
||||
resumeVm(id: XoVm['id']): Promise<void>
|
||||
revertVm(snapshotId: XoVmSnapshot['id']): Promise<void>
|
||||
unpauseVm(id: XoVm['id']): Promise<void>
|
||||
cloneVm(
|
||||
vmId: XoVm['id'],
|
||||
opts?: {
|
||||
nameLabel?: string
|
||||
fast?: boolean
|
||||
}
|
||||
): Promise<XenApiVmWrapped>
|
||||
copyVm(
|
||||
vmId: XoVm['id'],
|
||||
opts?: {
|
||||
nameLabel?: string
|
||||
srOrSrId?: XoSr['id'] | XoSr
|
||||
}
|
||||
): Promise<XenApiVmWrapped>
|
||||
remoteCopyVm(
|
||||
vmId: XoVm['id'],
|
||||
targetXapi: Xapi,
|
||||
targetSrId: XoSr['id'],
|
||||
opts?: {
|
||||
compress?: 'zstd' | 'gzip' | boolean
|
||||
nameLabel?: string
|
||||
}
|
||||
): Promise<{ vm: XenApiVmWrapped }>
|
||||
forgetSr(id: XoSr['id']): Promise<void>
|
||||
forgetHost(hostId: XoHost['id']): Promise<void>
|
||||
powerOnHost(hostId: XoHost['id']): Promise<void>
|
||||
rebootHost(hostId: XoHost['id'], force?: boolean): Promise<void>
|
||||
shutdownHost(hostId: XoHost['id'], opts?: { force?: boolean; bypassEvacuate?: boolean }): Promise<void>
|
||||
SR_create(params: {
|
||||
content_type?: XoSr['content_type']
|
||||
// index signature, not `Record<string, string>`: tsoa emits an empty schema for `Record`
|
||||
// (https://github.com/lukeautry/tsoa/pull/1858). device_config keys are driver-specific (SR_type).
|
||||
device_config: {
|
||||
chapuser?: string
|
||||
chappassword?: string
|
||||
device?: string
|
||||
legacy_mode?: string
|
||||
location?: string
|
||||
nfsversion?: string
|
||||
options?: string
|
||||
password?: string
|
||||
SCSIid?: string
|
||||
server?: string
|
||||
serverpath?: string
|
||||
target?: string
|
||||
targetIQN?: string
|
||||
username?: string
|
||||
} & {
|
||||
[key: string]: string
|
||||
}
|
||||
host: XenApiHost['$ref']
|
||||
name_description?: XoSr['name_description']
|
||||
name_label: XoSr['name_label']
|
||||
physical_size?: number
|
||||
shared: boolean
|
||||
sm_config?: { [key: string]: string }
|
||||
type: XoSr['SR_type']
|
||||
}): Promise<XenApiSr['$ref']>
|
||||
SR_importVdi(
|
||||
ref: XenApiSr['$ref'],
|
||||
stream: Readable,
|
||||
opts?: {
|
||||
format?: SUPPORTED_VDI_FORMAT
|
||||
name_description?: XoVdi['name_description']
|
||||
name_label?: XoVdi['name_label']
|
||||
other_config?: XoVdi['other_config']
|
||||
read_only?: boolean
|
||||
sharable?: boolean
|
||||
SR?: XenApiSr['$ref']
|
||||
tags?: XoVdi['tags']
|
||||
type?: XoVdi['type']
|
||||
virtual_size?: XoVdi['size']
|
||||
xenstore_data?: Record<string, string>
|
||||
}
|
||||
): Promise<XenApiVdi['$ref']>
|
||||
SR_reclaimSpace(ref: XenApiSr['$ref']): Promise<void>
|
||||
editVm(
|
||||
id: XoVm['id'],
|
||||
props: EditVmProps,
|
||||
checkLimits?: (limits: Record<string, number>, vm: XenApiVmWrapped) => Promise<void>
|
||||
): Promise<void>
|
||||
editVif(id: XoVif['id'], props: EditVifProps): Promise<void>
|
||||
startVm(
|
||||
id: XoVm['id'],
|
||||
opts?: {
|
||||
bypassMacAddressesCheck?: boolean
|
||||
force?: boolean
|
||||
hostId?: XoHost['id']
|
||||
/**
|
||||
* if startOnly is true and the VM is not halted, throw VM_BAD_POWER_STATE
|
||||
* otherwise, unpause/resume the VM
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
startOnly?: boolean
|
||||
}
|
||||
): Promise<void>
|
||||
VM_export(
|
||||
vmRef: XenApiVm['$ref'],
|
||||
opts?: { cancelToken?: unknown; compress?: boolean | 'zstd' | 'gzip'; useSnapshot?: boolean }
|
||||
): ReturnType<Xapi['getResource']>
|
||||
VM_import(
|
||||
stream: Readable,
|
||||
srRef?: XenApiSr['$ref'],
|
||||
onVmCreation?: null | ((vm: XenApiVm) => unknown)
|
||||
): Promise<XenApiVm['$ref']>
|
||||
createVm(
|
||||
templateUuid: XoVmTemplate['uuid'],
|
||||
metadataVm: {
|
||||
affinityHost?: XoHost['id']
|
||||
autoPoweron?: boolean
|
||||
cpus?: number
|
||||
memory?: number
|
||||
name_description?: string
|
||||
name_label: string
|
||||
nameLabel?: string
|
||||
clone?: boolean
|
||||
installRepository?: XoVdi['id'] | '' | null
|
||||
vdis?: (
|
||||
| /** Create VDI */ {
|
||||
name_label: string
|
||||
size: number
|
||||
sr?: XoSr['id']
|
||||
name_description?: string
|
||||
}
|
||||
| /** Update VDI*/ {
|
||||
userdevice: string
|
||||
name_label?: string
|
||||
size?: number
|
||||
sr?: XoSr['id']
|
||||
name_description?: string
|
||||
}
|
||||
| /** Destroy VDI*/ { destroy: true; userdevice: string }
|
||||
)[]
|
||||
vifs?: (
|
||||
| /** Create/update VIF */ {
|
||||
device?: string
|
||||
ipv4_allowed?: string[]
|
||||
ipv6_allowed?: string[]
|
||||
mac?: string
|
||||
mtu?: number
|
||||
network: string
|
||||
}
|
||||
| /** Destroy VIF */ { destroy: true; device: string }
|
||||
)[]
|
||||
existingVdis?: {
|
||||
$SR: XoSr['id']
|
||||
size: number
|
||||
userdevice: string
|
||||
}[]
|
||||
vgpuType?: XoVgpuType['id']
|
||||
gpuGroup?: XoGpuGroup['id']
|
||||
copyHostBiosStrings?: boolean
|
||||
highAvailability?: XoVm['high_availability']
|
||||
hvmBootFirmware?: 'uefi' | 'bios'
|
||||
secureBoot?: boolean
|
||||
},
|
||||
checkLimits?: boolean,
|
||||
creatorId?: XoUser['id'],
|
||||
opts?: { destroyAllVifs: boolean }
|
||||
): Promise<XenApiVmWrapped>
|
||||
moveVdi(
|
||||
vdiId: XenApiVdi['$ref'] | XoVdi['id'],
|
||||
srId: XenApiSr['$ref'] | XoSr['id'],
|
||||
opts?: { _failOnCbtError?: boolean }
|
||||
): Promise<XenApiVdi>
|
||||
VBD_create(params: {
|
||||
bootable?: boolean
|
||||
empty?: boolean
|
||||
mode?: VBD_MODE
|
||||
other_config?: Record<string, string>
|
||||
qos_algorithm_params?: Record<string, string>
|
||||
qos_algorithm_type?: string
|
||||
type?: VBD_TYPE
|
||||
unpluggable?: boolean
|
||||
userdevice?: string
|
||||
VDI: XenApiVdi['$ref'] | OPAQUE_REF_NULL
|
||||
VM: XenApiVm['$ref']
|
||||
}): Promise<XenApiVbd['$ref']>
|
||||
VBD_destroy(vbdRef: XenApiVbd['$ref']): Promise<void>
|
||||
VBD_unplug(vbdRef: XenApiVbd['$ref']): Promise<void>
|
||||
VDI_create(
|
||||
options: {
|
||||
name_description?: XoVdi['name_description']
|
||||
name_label?: XoVdi['name_label']
|
||||
other_config?: XoVdi['other_config']
|
||||
read_only?: boolean
|
||||
sharable?: boolean
|
||||
SR?: XenApiSr['$ref']
|
||||
tags?: XoVdi['tags']
|
||||
type?: VDI_TYPE
|
||||
virtual_size: XoVdi['size']
|
||||
xenstore_data?: Record<string, string>
|
||||
},
|
||||
extraOptions?: {
|
||||
sm_config?: Record<string, string>
|
||||
}
|
||||
): Promise<XenApiVdi['$ref']>
|
||||
VDI_destroy(vdiRef: XenApiVdi['$ref']): Promise<void>
|
||||
VDI_destroyCloudInitConfig(vdiRef: XenApiVdi['$ref'], opts?: { timeLimit?: number }): Promise<void>
|
||||
VDI_exportContent(
|
||||
vdiRef: XenApiVdi['$ref'],
|
||||
opts: { baseRef?: string; cancelToken?: unknown; format: SUPPORTED_VDI_FORMAT }
|
||||
): Promise<Readable & { length?: number }>
|
||||
VDI_importContent(
|
||||
vdiRef: XenApiVdi['$ref'],
|
||||
stream: Readable,
|
||||
opts: { cancelToken?: unknown; format: SUPPORTED_VDI_FORMAT }
|
||||
): Promise<void>
|
||||
VIF_create(
|
||||
options: {
|
||||
currently_attached?: boolean
|
||||
device?: string
|
||||
ipv4_allowed?: string[]
|
||||
ipv6_allowed?: string[]
|
||||
locking_mode?: VIF_LOCKING_MODE
|
||||
MTU?: number
|
||||
network: XenApiNetwork['$ref']
|
||||
other_config?: Record<string, string>
|
||||
qos_algorithm_params?: Record<string, string>
|
||||
qos_algorithm_type?: string
|
||||
VM: XenApiVm['$ref']
|
||||
},
|
||||
extraOptions?: {
|
||||
MAC?: string
|
||||
}
|
||||
): Promise<XenApiVif['$ref']>
|
||||
VM_createCloudInitConfig(
|
||||
vmRef: XenApiVm['$ref'],
|
||||
cloudConfig: string,
|
||||
opts?: { networkConfig?: string }
|
||||
): Promise<XoVdi['uuid']>
|
||||
VM_destroy(
|
||||
vmRef: XenApiVm['$ref'],
|
||||
opts?: {
|
||||
deleteDisks?: boolean
|
||||
force?: boolean
|
||||
bypassBlockedOperation?: boolean
|
||||
forceDeleteDefaultTemplate?: boolean
|
||||
}
|
||||
): Promise<void>
|
||||
getResource(
|
||||
pathname: string,
|
||||
params?: { host?: XenApiHost; query?: Record<string, unknown>; task?: boolean | XenApiTask['$ref'] }
|
||||
): Promise<{ body: Readable }>
|
||||
clearHost(
|
||||
host: Pick<XenApiHostWrapped, '$ref' | '$pool'>,
|
||||
force?: boolean,
|
||||
opts?: { transient?: boolean }
|
||||
): Promise<void>
|
||||
disableHost(hostId: XoHost['id'], opts?: { transient?: boolean }): Promise<void>
|
||||
enableHost(hostId: XoHost['id']): Promise<void>
|
||||
getRecordByUuid<
|
||||
Type extends WrappedXenApiRecord['$type'],
|
||||
XenApiRecord extends WrappedXenApiRecord = Extract<WrappedXenApiRecord, { $type: Type }>,
|
||||
>(
|
||||
type: Type,
|
||||
uuid: XenApiRecord['uuid']
|
||||
): Promise<XenApiRecord>
|
||||
isHyperThreadingEnabled(hostId: XoHost['id']): Promise<boolean | null>
|
||||
VTPM_create(params: { VM: XenApiVm['$ref']; is_unique?: boolean; contents?: string }): Promise<XenApiVtpm['$ref']>
|
||||
destroySr(id: XoSr['id']): Promise<void>
|
||||
xostor_delete(ref: XenApiSr['$ref']): Promise<void>
|
||||
objects: {
|
||||
indexes: {
|
||||
type: {
|
||||
[XenApiRecord in WrappedXenApiRecord as XenApiRecord['$type']]: Record<XenApiRecord['uuid'], XenApiRecord>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,416 +0,0 @@
|
||||
import { EventEmitter } from 'node:stream'
|
||||
import type {
|
||||
AnyXoJob,
|
||||
AnyXoLog,
|
||||
XapiXoRecord,
|
||||
XoAuthenticationToken,
|
||||
XoBackupRepository,
|
||||
XoConfigBackupArchive,
|
||||
XoGroup,
|
||||
XoHost,
|
||||
XoPool,
|
||||
XoPoolBackupArchive,
|
||||
XoProxy,
|
||||
XoSchedule,
|
||||
XoServer,
|
||||
XoTask,
|
||||
XoUser,
|
||||
XoVif,
|
||||
XoVm,
|
||||
XoVmBackupArchive,
|
||||
} from './xo.mjs'
|
||||
import { VatesTask } from './lib/vates-task.mjs'
|
||||
import type { PluginRestRouteDefinition } from './lib/rest-api.mjs'
|
||||
import {
|
||||
Xapi,
|
||||
XapiHostStats,
|
||||
XapiPoolStats,
|
||||
XapiStatsGranularity,
|
||||
XapiVmStats,
|
||||
XenApiGpuGroupWrapped,
|
||||
XenApiHostWrapped,
|
||||
XenApiMessageWrapped,
|
||||
XenApiNetworkWrapped,
|
||||
XenApiPbdWrapped,
|
||||
XenApiPciWrapped,
|
||||
XenApiPgpuWrapped,
|
||||
XenApiPifWrapped,
|
||||
XenApiPoolWrapped,
|
||||
XenApiSmWrapped,
|
||||
XenApiSrWrapped,
|
||||
XenApiVbdWrapped,
|
||||
XenApiVdiWrapped,
|
||||
XenApiVgpuTypeWrapped,
|
||||
XenApiVgpuWrapped,
|
||||
XenApiVifWrapped,
|
||||
XenApiVmWrapped,
|
||||
XenApiVtpmWrapped,
|
||||
XoAclBasePrivilege,
|
||||
XoAclRole,
|
||||
XoGroupRole,
|
||||
XoUserRole,
|
||||
} from './index.mjs'
|
||||
|
||||
type FeatureCode =
|
||||
| 'BACKUP.DELTA'
|
||||
| 'BACKUP.DELTA_REPLICATION'
|
||||
| 'BACKUP.FULL'
|
||||
| 'BACKUP.HEALTHCHECK'
|
||||
| 'BACKUP.METADATA'
|
||||
| 'BACKUP.MIRROR'
|
||||
| 'BACKUP.WITH_RAM'
|
||||
| 'BACKUP.SMART_BACKUP'
|
||||
| 'BACKUP.S3'
|
||||
| 'DOCKER'
|
||||
| 'EXPORT.XVA'
|
||||
| 'LIST_MISSING_PATCHES'
|
||||
| 'POOL_EMERGENCY_SHUTDOWN'
|
||||
| 'RBAC'
|
||||
| 'ROLLING_POOL_UPDATE'
|
||||
| 'ROLLING_POOL_REBOOT'
|
||||
| 'SMART_REBOOT'
|
||||
| 'WARM_MIGRATION'
|
||||
| 'PLUGIN.OPENMETRICS'
|
||||
|
||||
type XapiRecordByXapiXoRecord = {
|
||||
gpuGroup: XenApiGpuGroupWrapped
|
||||
host: XenApiHostWrapped
|
||||
message: XenApiMessageWrapped
|
||||
network: XenApiNetworkWrapped
|
||||
PBD: XenApiPbdWrapped
|
||||
PCI: XenApiPciWrapped
|
||||
PGPU: XenApiPgpuWrapped
|
||||
PIF: XenApiPifWrapped
|
||||
pool: XenApiPoolWrapped
|
||||
SR: XenApiSrWrapped
|
||||
SM: XenApiSmWrapped
|
||||
VBD: XenApiVbdWrapped
|
||||
VDI: XenApiVdiWrapped
|
||||
'VDI-snapshot': XenApiVdiWrapped
|
||||
'VDI-unmanaged': XenApiVdiWrapped
|
||||
vgpu: XenApiVgpuWrapped
|
||||
vgpuType: XenApiVgpuTypeWrapped
|
||||
VIF: XenApiVifWrapped
|
||||
VM: XenApiVmWrapped
|
||||
'VM-controller': XenApiVmWrapped
|
||||
'VM-snapshot': XenApiVmWrapped
|
||||
'VM-template': XenApiVmWrapped
|
||||
VTPM: XenApiVtpmWrapped
|
||||
}
|
||||
|
||||
type LicenseProductId =
|
||||
| 'premium'
|
||||
| 'xcpng-enterprise'
|
||||
| 'xcpng-standard'
|
||||
| 'xo-proxy'
|
||||
| 'xosan.trial'
|
||||
| 'xostor'
|
||||
| 'xostor.trial'
|
||||
type LicenseProductType = 'xo' | 'xoproxy' | 'xcpng' | 'xosan' | 'xostor'
|
||||
|
||||
type License = {
|
||||
id: string
|
||||
type: 'license'
|
||||
created: number
|
||||
licenseType: 'unit'
|
||||
productId: LicenseProductId
|
||||
tags: string[]
|
||||
expires?: number
|
||||
boundObjectId?: string
|
||||
bindDate?: number
|
||||
buyer?: { token: string; email: string }
|
||||
history?: { date: number; boundObjectId: string }[]
|
||||
rebound?: number
|
||||
productTypes?: LicenseProductType[]
|
||||
bundleInfo?: { name: string; id: string }
|
||||
}
|
||||
|
||||
export type XoApp = {
|
||||
hooks: EventEmitter
|
||||
_redis: {
|
||||
get(key: string): Promise<string | null>
|
||||
mSet(args: string[]): Promise<unknown>
|
||||
sMembers(key: string): Promise<string[]>
|
||||
sIsMember(key: string, member: string): Promise<boolean>
|
||||
}
|
||||
config: {
|
||||
get<T = string>(path: string): T
|
||||
getOptional<T = unknown>(path: string): T | undefined
|
||||
getOptionalDuration(path: string): number | undefined
|
||||
getGuiRoutes(): Promise<{
|
||||
default: {
|
||||
url: string
|
||||
path: string
|
||||
}
|
||||
v5?: {
|
||||
url: string
|
||||
path: string
|
||||
}
|
||||
v6?: {
|
||||
url: string
|
||||
path: string
|
||||
}
|
||||
[key: string]:
|
||||
| undefined
|
||||
| {
|
||||
url: string
|
||||
path: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
objects: EventEmitter & {
|
||||
allIndexes: {
|
||||
type: {
|
||||
getEventEmitterByType(type: XapiXoRecord['type']): EventEmitter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks: EventEmitter & {
|
||||
abort(id: XoTask['id'], reason?: string): Promise<void>
|
||||
clearLogs(): Promise<void>
|
||||
create: (params: { name: string; objectId?: string; type?: string }) => VatesTask
|
||||
deleteLog(id: XoTask['id']): Promise<void>
|
||||
get(id: XoTask['id']): Promise<XoTask>
|
||||
list(opts?: { filter?: string | ((obj: XoTask) => boolean); limit?: number }): AsyncGenerator<XoTask>
|
||||
watch(id: XoTask['id'], cb: (task: XoTask) => void): Promise<() => void>
|
||||
}
|
||||
apiContext: {
|
||||
user?: XoUser
|
||||
permission?: XoUser['permission'] | 'none' | null
|
||||
}
|
||||
|
||||
// methods ------------
|
||||
allocIpAddresses(vifId: XoVif['id'], addAddresses?: string[], removeAddresses?: string[]): Promise<void>
|
||||
addAclV2GroupRole(groupId: XoGroup['id'], roleId: XoAclRole['id']): Promise<XoGroupRole>
|
||||
addAclV2UserRole(userId: XoUser['id'], roleId: XoAclRole['id']): Promise<XoUserRole>
|
||||
addUserToGroup: (userId: XoUser['id'], groupId: XoGroup['id']) => Promise<void>
|
||||
addApiMethod: <A extends unknown[], R>(
|
||||
name: string,
|
||||
method: (...args: A) => Promise<R>,
|
||||
//we use any since it is a legacy call, might be better typed in another pr
|
||||
info: {
|
||||
resolve?: any
|
||||
params?: any
|
||||
}
|
||||
) => () => void // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
registerRestRoutes: (routes: PluginRestRouteDefinition[], base?: string) => () => void
|
||||
authenticateUser: (
|
||||
credentials: { token?: string; username?: string; password?: string },
|
||||
userData?: { ip?: string },
|
||||
opts?: { bypassOtp?: boolean; bypassTaskCreation?: boolean }
|
||||
) => Promise<{ bypassOtp: boolean; expiration: number; user: XoUser }>
|
||||
backupGuard(poolId: XoPool['id']): Promise<void>
|
||||
/* Throw if no authorization */
|
||||
checkFeatureAuthorization(featureCode: FeatureCode): Promise<void>
|
||||
/* connect a server (XCP-ng/XenServer) */
|
||||
connectXenServer(id: XoServer['id']): Promise<void>
|
||||
// TODO: replace all XoAclBasePrivilege with a more strict type. (discriminate union)
|
||||
createAclV2Privilege(
|
||||
privilege: Omit<XoAclBasePrivilege, 'id'>,
|
||||
options?: {
|
||||
force?: boolean
|
||||
}
|
||||
): Promise<XoAclBasePrivilege>
|
||||
copyAclV2Role(
|
||||
id: XoAclRole['id'],
|
||||
params?: { name?: XoAclRole['name']; description?: XoAclRole['description'] }
|
||||
): Promise<XoAclRole['id']>
|
||||
createAclV2Role(role: { name: XoAclRole['name']; description?: XoAclRole['description'] }): Promise<XoAclRole>
|
||||
createAuthenticationToken(opts: {
|
||||
client?: {
|
||||
id?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
description?: string
|
||||
expiresIn?: string | number
|
||||
userId: XoUser['id']
|
||||
}): Promise<XoAuthenticationToken>
|
||||
createRemote(params: {
|
||||
name: string
|
||||
options?: string
|
||||
proxy?: XoProxy['id']
|
||||
url: string
|
||||
}): Promise<XoBackupRepository>
|
||||
createUser(params: { name?: string; password?: string; [key: string]: unknown }): Promise<XoUser>
|
||||
deleteAclV2GroupRole(
|
||||
groupId: XoGroup['id'],
|
||||
roleId: XoAclRole['id'],
|
||||
opts?: { bypassAuthorization?: boolean }
|
||||
): Promise<boolean>
|
||||
deleteAclV2Privilege(privilegeId: XoAclBasePrivilege['id'], options?: { force?: boolean }): Promise<boolean>
|
||||
deleteAclV2Role(roleId: XoAclRole['id'], options?: { force?: boolean }): Promise<boolean>
|
||||
deleteGroup(id: XoGroup['id']): Promise<void>
|
||||
deleteUser(id: XoUser['id']): Promise<void>
|
||||
detachHostFromPool(hostId: XoHost['id']): Promise<void>
|
||||
createGroup(params: { name: string; provider?: string; providerGroup?: string }): Promise<XoGroup>
|
||||
/* disconnect a server (XCP-ng/XenServer) */
|
||||
disconnectXenServer(id: XoServer['id']): Promise<void>
|
||||
findEnabledScheduleSequenceFromSchedule(id: XoSchedule['id']): Promise<XoSchedule | undefined>
|
||||
getAclV2Privilege(id: XoAclBasePrivilege['id']): Promise<XoAclBasePrivilege>
|
||||
getAclV2Privileges(): Promise<XoAclBasePrivilege[]>
|
||||
getAclV2RolePrivileges(roleId: XoAclRole['id']): Promise<XoAclBasePrivilege[]>
|
||||
getAclV2Role(id: XoAclRole['id'], opts?: { bypassAuthorization?: boolean }): Promise<XoAclRole>
|
||||
deleteAclV2UserRole(
|
||||
userId: XoUser['id'],
|
||||
roleId: XoAclRole['id'],
|
||||
opts?: { bypassAuthorization?: boolean }
|
||||
): Promise<boolean>
|
||||
getAclV2GroupRoles(
|
||||
groupId: XoGroup['id'],
|
||||
opts?: { bypassAuthorization?: boolean }
|
||||
): Promise<Exclude<XoAclRole, { isTemplate: true }>[]>
|
||||
getAclV2Roles(): Promise<XoAclRole[]>
|
||||
getAclV2UserPrivileges(userId: XoUser['id']): Promise<XoAclBasePrivilege[]>
|
||||
getAclV2UserRoles(
|
||||
userId: XoUser['id'],
|
||||
opts?: { bypassAuthorization?: boolean; fromGroup?: boolean; fromUser?: boolean }
|
||||
): Promise<XoAclRole[]>
|
||||
getAllGroups(): Promise<XoGroup[]>
|
||||
getAllProxies(): Promise<XoProxy[]>
|
||||
getAllJobs<T extends AnyXoJob['type']>(type: T): Promise<Extract<AnyXoJob, { type: T }>[]>
|
||||
getAllJobs(type?: string): Promise<AnyXoJob[]>
|
||||
getProxy(id: XoProxy['id']): Promise<XoProxy>
|
||||
getRemote(id: XoBackupRepository['id']): Promise<XoBackupRepository>
|
||||
getAllRemotes(): Promise<XoBackupRepository[]>
|
||||
removeRemote(id: XoBackupRepository['id']): Promise<void>
|
||||
getAllRemotesInfo(): Promise<
|
||||
Record<
|
||||
XoBackupRepository['id'],
|
||||
{
|
||||
size?: number
|
||||
used?: number
|
||||
available?: number
|
||||
encryption?: {
|
||||
algorithm: string
|
||||
isLegacy: boolean
|
||||
recommanded: string
|
||||
}
|
||||
}
|
||||
>
|
||||
>
|
||||
getAllSchedules(): Promise<XoSchedule[]>
|
||||
getAllUsers(): Promise<XoUser[]>
|
||||
getAllXenServers(): Promise<XoServer[]>
|
||||
getAuthenticationTokensForUser(userId: XoUser['id']): Promise<XoAuthenticationToken[]>
|
||||
getBackupNgLogs(): Promise<Record<string, AnyXoLog>>
|
||||
getBackupNgLogs(id: AnyXoLog['id']): Promise<AnyXoLog>
|
||||
getBackupNgLogsSorted(opts: {
|
||||
after?: number
|
||||
before?: number
|
||||
filter: (log: AnyXoLog) => boolean
|
||||
limit?: number
|
||||
}): Promise<AnyXoLog[]>
|
||||
getGroup(id: XoGroup['id']): Promise<XoGroup>
|
||||
getHVSupportedVersions: undefined | (() => Promise<{ [key: XoHost['productBrand']]: string }>)
|
||||
getJob<T extends AnyXoJob>(id: T['id']): Promise<T>
|
||||
getObject: <T extends XapiXoRecord>(id: T['id'], type?: T['type'] | T['type'][]) => T
|
||||
getObjectsByType: <T extends XapiXoRecord>(
|
||||
type: T['type'],
|
||||
opts?: { filter?: string | ((obj: T) => boolean); limit?: number }
|
||||
) => Record<T['id'], T> | undefined
|
||||
getTotalBackupSizeOnRemote(id: XoBackupRepository['id']): Promise<{ onDisk: number }>
|
||||
getSchedule(id: XoSchedule['id']): Promise<XoSchedule>
|
||||
getUser: (id: XoUser['id']) => Promise<XoUser>
|
||||
getXapi(maybeId: XapiXoRecord['id'] | XapiXoRecord): Xapi
|
||||
getXapiHostStats: (hostId: XoHost['id'], granularity?: XapiStatsGranularity) => Promise<XapiHostStats>
|
||||
getXapiObject: <T extends XapiXoRecord>(
|
||||
maybeId: T['id'] | T,
|
||||
type: T['type'] | T['type'][]
|
||||
) => XapiRecordByXapiXoRecord[T['type']]
|
||||
getXapiPoolStats(poolId: XoPool['id'], granularity?: XapiStatsGranularity): Promise<XapiPoolStats>
|
||||
getXapiVmStats: (vmId: XoVm['id'], granularity?: XapiStatsGranularity) => Promise<XapiVmStats>
|
||||
getXenServer(id: XoServer['id']): Promise<XoServer>
|
||||
hasFeatureAuthorization(featureCode: string): Promise<boolean>
|
||||
hasObject<T extends XapiXoRecord>(id: T['id'], type: T['type']): boolean
|
||||
listMetadataBackups(backupRepositoryIds: XoBackupRepository['id'][]): Promise<{
|
||||
xo: Record<XoBackupRepository['id'], XoConfigBackupArchive[]>
|
||||
pool: Record<XoBackupRepository['id'], Record<XoPool['id'], XoPoolBackupArchive[]>>
|
||||
}>
|
||||
listVmBackupsNg(
|
||||
backupRepositoryIds: XoBackupRepository['id'][],
|
||||
opts?: { _forceRefresh?: boolean; vmId: XoVm['id'] }
|
||||
): Promise<Record<XoBackupRepository['id'], Record<XoVm['id'], XoVmBackupArchive[]>>>
|
||||
pingRemote(id: XoBackupRepository['id']): Promise<{ success: true }>
|
||||
/** Allow to add a new server in the DB (XCP-ng/XenServer) */
|
||||
registerXenServer(
|
||||
body: Pick<XoServer, 'host' | 'httpProxy' | 'label' | 'username'> & {
|
||||
allowUnauthorized?: XoServer['allowUnauthorized']
|
||||
password: string
|
||||
readOnly?: XoServer['readOnly']
|
||||
}
|
||||
): Promise<XoServer>
|
||||
rollingPoolReboot(pool: XoPool, opts?: { parentTask?: VatesTask; shutdownPinnedVms?: boolean }): Promise<void>
|
||||
rollingPoolUpdate(
|
||||
pool: XoPool,
|
||||
opts?: { rebootVm?: boolean; parentTask?: VatesTask; shutdownPinnedVms?: boolean }
|
||||
): Promise<void>
|
||||
setVmResourceSet(vmId: XoVm['id'], resourceSetId: string | null, force?: boolean): Promise<void>
|
||||
shareVmResourceSet(vmId: XoVm['id']): Promise<void>
|
||||
removeUserFromGroup(userId: XoUser['id'], id: XoGroup['id']): Promise<void>
|
||||
runJob(job: AnyXoJob, schedule: XoSchedule): void
|
||||
runWithApiContext: (user: XoUser | undefined, fn: () => void) => Promise<unknown>
|
||||
testRemote(
|
||||
id: XoBackupRepository['id']
|
||||
): Promise<
|
||||
| { success: true; readRate: number; writeRate: number }
|
||||
| { success: false; step: string; file: string; error: unknown }
|
||||
>
|
||||
/** Remove a server from the DB (XCP-ng/XenServer) */
|
||||
unregisterXenServer(id: XoServer['id']): Promise<void>
|
||||
updateUser(
|
||||
id: XoUser['id'],
|
||||
updates: {
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
email?: string
|
||||
authProviders?: Record<string, string>
|
||||
name?: string
|
||||
password?: string
|
||||
permission?: string
|
||||
preferences?: Record<string, string>
|
||||
}
|
||||
): Promise<void>
|
||||
updateAclV2Privilege(
|
||||
privilegeId: XoAclBasePrivilege['id'],
|
||||
privilege: XoAclBasePrivilege
|
||||
): Promise<XoAclBasePrivilege>
|
||||
updateAclV2Role(
|
||||
roleId: XoAclRole['id'],
|
||||
role: {
|
||||
name?: XoAclRole['name']
|
||||
description?: XoAclRole['description'] | null
|
||||
},
|
||||
options?: {
|
||||
force?: boolean
|
||||
}
|
||||
): Promise<XoAclRole>
|
||||
updateGroup(
|
||||
id: XoGroup['id'],
|
||||
updates: {
|
||||
name?: string
|
||||
}
|
||||
): void
|
||||
updateRemote(
|
||||
id: XoBackupRepository['id'],
|
||||
params: {
|
||||
enabled?: boolean
|
||||
name?: string
|
||||
options?: string | null
|
||||
proxy?: XoProxy['id'] | null
|
||||
url?: string
|
||||
}
|
||||
): Promise<XoBackupRepository>
|
||||
getAllXapis(): Record<string, Xapi>
|
||||
getObjects(opts?: { filter?: Record<string, unknown>; limit?: number }): Record<string, XapiXoRecord>
|
||||
getLicenses(params?: { productType?: LicenseProductType }): Promise<License[]>
|
||||
bindLicense(params: { licenseId: string; boundObjectId: string }): Promise<License>
|
||||
unbindLicense(params: {
|
||||
productId: LicenseProductId
|
||||
boundObjectId: string
|
||||
licenseId: string
|
||||
data?: Record<string, string>
|
||||
}): Promise<License>
|
||||
}
|
||||
@@ -1,925 +0,0 @@
|
||||
// Types based on xapi-object-to-xo
|
||||
|
||||
import type {
|
||||
Branded,
|
||||
DOMAIN_TYPE,
|
||||
HOST_ALLOWED_OPERATIONS,
|
||||
HOST_POWER_STATE,
|
||||
IP_CONFIGURATION_MODE,
|
||||
IPV6_CONFIGURATION_MODE,
|
||||
NETWORK_OPERATIONS,
|
||||
PGPU_DOM0_ACCESS,
|
||||
POOL_ALLOWED_OPERATIONS,
|
||||
PRIMARY_ADDRESS_TYPE,
|
||||
STORAGE_OPERATIONS,
|
||||
VDI_OPERATIONS,
|
||||
VDI_TYPE,
|
||||
VIF_LOCKING_MODE,
|
||||
VM_OPERATIONS,
|
||||
VM_POWER_STATE,
|
||||
} from './common.mjs'
|
||||
import type * as CMType from './lib/complex-matcher.mjs'
|
||||
import { XoAclPrivilege, XoAclRole, XoAclSupportedActionsByResource } from './lib/xen-orchestra-acl.mjs'
|
||||
import type { XenApiHost, XenApiPool } from './xen-api.mjs'
|
||||
|
||||
type BaseXapiXo = {
|
||||
$pool: XoPool['id']
|
||||
/**
|
||||
* @deprecated use $pool instead
|
||||
*/
|
||||
$poolId: XoPool['id']
|
||||
|
||||
_xapiRef: string
|
||||
|
||||
uuid: string
|
||||
}
|
||||
|
||||
type BaseXoVm = BaseXapiXo & {
|
||||
$VBDs: XoVbd['id'][]
|
||||
$VGPUs: XoVgpu['id'][]
|
||||
|
||||
$container: XoPool['id'] | XoHost['id']
|
||||
|
||||
CPUs: {
|
||||
max: number
|
||||
number: number
|
||||
}
|
||||
PV_args?: string
|
||||
VGPUs: XoVgpu['id'][]
|
||||
VIFs: XoVif['id'][]
|
||||
VTPMs: XoVtpm['id'][]
|
||||
|
||||
addresses: Record<string, string>
|
||||
affinityHost?: XoHost['id']
|
||||
attachedPcis?: string[]
|
||||
auto_poweron: boolean
|
||||
bios_strings: Record<string, string>
|
||||
blockedOperations: Record<VM_OPERATIONS, string>
|
||||
boot: Record<string, string>
|
||||
coresPerSocket?: number
|
||||
cpuCap?: number
|
||||
cpuMask?: number[]
|
||||
cpuWeight?: number
|
||||
creation: Record<string, string>
|
||||
current_operations: Record<string, VM_OPERATIONS>
|
||||
docker?: {
|
||||
containers?: string[]
|
||||
enabled: boolean
|
||||
info?: string
|
||||
/** @deprecated */
|
||||
process?: string
|
||||
version?: string
|
||||
}
|
||||
/**
|
||||
* @deprecated use isNestedVirtEnabled instead
|
||||
*/
|
||||
expNestedHvm: boolean
|
||||
isNestedVirtEnabled: boolean
|
||||
hasVendorDevice: boolean
|
||||
high_availability: 'best-effort' | 'restart' | ''
|
||||
installTime?: number | null
|
||||
isFirmwareSupported: boolean
|
||||
memory: {
|
||||
dynamic: number[]
|
||||
size: number
|
||||
static: number[]
|
||||
usage?: number
|
||||
}
|
||||
mainIpAddress?: string
|
||||
managementAgentDetected?: boolean
|
||||
name_description: string
|
||||
name_label: string
|
||||
needsVtpm: boolean
|
||||
nicType?: string
|
||||
notes?: string
|
||||
os_version: Record<string, string> | null
|
||||
other: Record<string, string>
|
||||
parent?: XoVmSnapshot['id']
|
||||
power_state: VM_POWER_STATE
|
||||
pvDriversDetected?: boolean
|
||||
pvDriversUpToDate?: boolean
|
||||
pvDriversVersion?: string
|
||||
resourceSet?: string
|
||||
secureBoot: boolean
|
||||
snapshots: XoVmSnapshot['id'][]
|
||||
startDelay: number
|
||||
startTime?: number | null
|
||||
suspendSr?: XoSr['id']
|
||||
tags: string[]
|
||||
vga?: string
|
||||
/**
|
||||
* Value in MiB.
|
||||
* See: https://wiki.xenproject.org/wiki/XCP_PV_templates_start
|
||||
*/
|
||||
videoram?: number
|
||||
viridian: boolean
|
||||
virtualizationMode: DOMAIN_TYPE
|
||||
xenStoreData: Record<string, string>
|
||||
/**
|
||||
* @deprecated use pvDriversVersion instead
|
||||
*/
|
||||
xentools?:
|
||||
| false
|
||||
| {
|
||||
major: null | number
|
||||
minor: null | number
|
||||
version: null | string
|
||||
}
|
||||
}
|
||||
|
||||
export type XoAlarm = Omit<XoMessage, '$object' | 'body'> & {
|
||||
body: {
|
||||
value?: string
|
||||
name: string
|
||||
}
|
||||
object: {
|
||||
type: XapiXoRecord['type'] | 'unknown'
|
||||
uuid: XapiXoRecord['uuid']
|
||||
href?: string
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: to be typed when Bastien.N has finished working on the XO task
|
||||
type BaseXoLog = {
|
||||
id: Branded<'xo-log'>
|
||||
infos?: { data: unknown; message: string }[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
export type XoBackupLog = BaseXoLog & {
|
||||
status: 'success' | 'skipped' | 'interrupted' | 'failure' | 'pending'
|
||||
message: 'backup'
|
||||
start: number
|
||||
end?: number
|
||||
tasks?: XoTask[]
|
||||
jobId?: AnyXoBackupJob['id']
|
||||
}
|
||||
export type XoRestoreLog = BaseXoLog & {
|
||||
message: 'restore'
|
||||
}
|
||||
export type XoVmBackupArchive = {
|
||||
id: Branded<'vm-backup-archive'>
|
||||
type: 'xo-vm-backup'
|
||||
backupRepository: XoBackupRepository['id']
|
||||
disks: { id: string; name: string; uuid: XoVdiSnapshot['uuid'] }[]
|
||||
isImmutable?: boolean
|
||||
jobId: XoVmBackupJob['id']
|
||||
mode: XoVmBackupJob['mode']
|
||||
scheduleId: XoSchedule['id']
|
||||
size: number
|
||||
timestamp: number
|
||||
vm: {
|
||||
uuid: XoVm['uuid']
|
||||
name_description: string
|
||||
name_label: string
|
||||
tags: XoVm['tags']
|
||||
}
|
||||
differencingVhds?: number
|
||||
dynamicVhds?: number
|
||||
withMemory: boolean
|
||||
}
|
||||
|
||||
type XoMetadataBackupArchive = {
|
||||
id: Branded<'metadata-backup-archive'>
|
||||
backupRepository: XoBackupRepository['id']
|
||||
jobId: XoVmBackupJob['id']
|
||||
jobName: XoVmBackupJob['name']
|
||||
scheduleId: XoSchedule['id']
|
||||
scheduleName: XoSchedule['name']
|
||||
timestamp: number
|
||||
}
|
||||
export type XoConfigBackupArchive = XoMetadataBackupArchive & {
|
||||
id: Branded<'config-backup-archive'>
|
||||
data: string
|
||||
type: 'xo-config-backup'
|
||||
}
|
||||
|
||||
export type XoPoolBackupArchive = XoMetadataBackupArchive & {
|
||||
id: Branded<'pool-backup-archive'>
|
||||
pool: XenApiPool
|
||||
poolMaster: XenApiHost
|
||||
type: 'xo-pool-metadata-backup'
|
||||
}
|
||||
|
||||
export type XoBackupRepository = {
|
||||
benchmarks?: { readRate: number; timestamp: number; writeRate: number }[]
|
||||
enabled: boolean
|
||||
error?: Record<string, unknown>
|
||||
id: Branded<'backup-repository'>
|
||||
name: string
|
||||
options?: string
|
||||
proxy?: XoProxy['id']
|
||||
url: string
|
||||
}
|
||||
|
||||
export type XoGpuGroup = BaseXapiXo & {
|
||||
id: Branded<'gpu-group'>
|
||||
type: 'gpuGroup'
|
||||
}
|
||||
|
||||
export type XoGroup = {
|
||||
id: Branded<'group'>
|
||||
name: string
|
||||
provider?: string
|
||||
providerGroupId?: string
|
||||
users: XoUser['id'][]
|
||||
aclRoleIds: XoAclRole['id'][]
|
||||
}
|
||||
|
||||
export type XoHost = BaseXapiXo & {
|
||||
$PBDs: XoPbd['id'][]
|
||||
$PCIs: XoPci['id'][]
|
||||
$PGPUs: XoPgpu['id'][]
|
||||
$PIFs: XoPif['id'][]
|
||||
|
||||
PCIs: XoPci['id'][]
|
||||
PGPUs: XoPgpu['id'][]
|
||||
PIFs: XoPif['id'][]
|
||||
/**
|
||||
* @deprecated use cpus instead
|
||||
*/
|
||||
CPUs: Record<string, string>
|
||||
|
||||
address: string
|
||||
agentStartTime: null | number
|
||||
bios_strings: Record<string, string>
|
||||
build: string
|
||||
certificates?: {
|
||||
fingerprint: string
|
||||
notAfter: number
|
||||
}[]
|
||||
chipset_info: {
|
||||
iommu?: boolean
|
||||
}
|
||||
controlDomain?: XoVmController['id']
|
||||
cpus: {
|
||||
cores?: number
|
||||
sockets?: number
|
||||
}
|
||||
current_operations: Record<string, HOST_ALLOWED_OPERATIONS>
|
||||
enabled: boolean
|
||||
hostname: string
|
||||
hvmCapable: boolean
|
||||
id: Branded<'host'>
|
||||
iscsiIqn: string
|
||||
license_expiry: null | number
|
||||
license_params: Record<string, string>
|
||||
license_server: Record<string, string>
|
||||
logging: Record<string, string>
|
||||
memory: {
|
||||
size: number
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
total?: number
|
||||
usage: number
|
||||
}
|
||||
multipathing: boolean
|
||||
name_description: string
|
||||
name_label: string
|
||||
otherConfig: Record<string, string>
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
patches: XoHostPatch['id'][]
|
||||
power_state: HOST_POWER_STATE
|
||||
powerOnMode: string
|
||||
productBrand: string
|
||||
rebootRequired: boolean
|
||||
residentVms: XoVm['id'][]
|
||||
startTime: null | number
|
||||
supplementalPacks:
|
||||
| {
|
||||
author: string
|
||||
description: string
|
||||
guidance: string
|
||||
hosts: XoHost['id'][]
|
||||
name: string
|
||||
size: number
|
||||
version: string
|
||||
vdi: XoVdi['id']
|
||||
}[]
|
||||
| {
|
||||
author: string
|
||||
description: string
|
||||
name: string
|
||||
version: string
|
||||
}[]
|
||||
tags: string[]
|
||||
type: 'host'
|
||||
version: string
|
||||
zstdSupported: boolean
|
||||
}
|
||||
|
||||
export type XoHostPatch = BaseXapiXo & {
|
||||
id: Branded<'host_patch'>
|
||||
type: 'host_patch'
|
||||
}
|
||||
|
||||
export type XoMessage = BaseXapiXo & {
|
||||
$object: XapiXoRecord['id']
|
||||
|
||||
body: string
|
||||
id: Branded<'message'>
|
||||
name: string
|
||||
time: number
|
||||
type: 'message'
|
||||
}
|
||||
|
||||
export type XoNetwork = BaseXapiXo & {
|
||||
MTU: number
|
||||
PIFs: XoPif['id'][]
|
||||
VIFs: XoVif['id'][]
|
||||
|
||||
automatic: boolean
|
||||
bridge: string
|
||||
current_operations: Record<string, NETWORK_OPERATIONS>
|
||||
defaultIsLocked: boolean
|
||||
id: Branded<'network'>
|
||||
insecureNbd?: boolean
|
||||
isBonded: boolean
|
||||
name_description: string
|
||||
name_label: string
|
||||
nbd?: boolean
|
||||
other_config: Record<string, string>
|
||||
tags: string[]
|
||||
type: 'network'
|
||||
}
|
||||
|
||||
export type XoPbd = BaseXapiXo & {
|
||||
attached: boolean
|
||||
device_config:
|
||||
| { device: string }
|
||||
| { location: string }
|
||||
| { path: string; location: string; legacy_mode: string }
|
||||
| { provisioning: string; redundancy: string; 'group-name': string }
|
||||
| { server: string; serverpath: string }
|
||||
| { type: string; location: string }
|
||||
| Record<string, string>
|
||||
host: XoHost['id']
|
||||
id: Branded<'PBD'>
|
||||
otherConfig: Record<string, string>
|
||||
SR: XoSr['id']
|
||||
type: 'PBD'
|
||||
}
|
||||
|
||||
export type XoPci = BaseXapiXo & {
|
||||
$host?: XoHost['id']
|
||||
|
||||
class_name: string
|
||||
device_name: string
|
||||
id: Branded<'PCI'>
|
||||
pci_id: string
|
||||
type: 'PCI'
|
||||
}
|
||||
|
||||
export type XoPgpu = BaseXapiXo & {
|
||||
$host: XoHost['id']
|
||||
$vgpus: XoVgpu['id'][]
|
||||
|
||||
dom0Access: PGPU_DOM0_ACCESS
|
||||
enabledVgpuTypes: XoVgpuType['id'][]
|
||||
gpuGroup?: XoGpuGroup['id']
|
||||
host: XoHost['id']
|
||||
id: Branded<'PGPU'>
|
||||
isSystemDisplayDevice: boolean
|
||||
pci?: XoPci['id']
|
||||
// it seems that supportedVgpuMaxCapcities always return undefined (See: xapi-objet-to-xo)
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
supportedVgpuMaxCapcities?: never
|
||||
supportedVgpuTypes: XoVgpuType['id'][]
|
||||
type: 'PGPU'
|
||||
vgpus: XoVgpu['id'][]
|
||||
}
|
||||
|
||||
export type XoPif = BaseXapiXo & {
|
||||
$host: XoHost['id']
|
||||
$network: XoNetwork['id']
|
||||
|
||||
attached: boolean
|
||||
bondMaster?: XoPif['id']
|
||||
bondSlaves?: XoPif['id'][]
|
||||
carrier: boolean
|
||||
device: string
|
||||
deviceName?: string
|
||||
disallowUnplug: boolean
|
||||
dns: string
|
||||
gateway: string
|
||||
id: Branded<'PIF'>
|
||||
ip: string
|
||||
ipv6: string[]
|
||||
ipv6Mode: IPV6_CONFIGURATION_MODE
|
||||
isBondMaster: boolean
|
||||
isBondSlave: boolean
|
||||
mac: string
|
||||
management: boolean
|
||||
mode: IP_CONFIGURATION_MODE
|
||||
mtu: number
|
||||
netmask: string
|
||||
physical: boolean
|
||||
primaryAddressType: PRIMARY_ADDRESS_TYPE
|
||||
speed?: number
|
||||
type: 'PIF'
|
||||
vlan: number
|
||||
}
|
||||
|
||||
export type XoPool = BaseXapiXo & {
|
||||
auto_poweron: boolean
|
||||
cpus: {
|
||||
cores?: number
|
||||
sockets?: number
|
||||
}
|
||||
crashDumpSr?: XoSr['id']
|
||||
current_operations: Record<string, POOL_ALLOWED_OPERATIONS>
|
||||
default_SR?: XoSr['id']
|
||||
HA_enabled: boolean
|
||||
haRebootVmOnInternalShutdown: boolean
|
||||
haSrs: XoSr['id'][]
|
||||
id: Branded<'pool'>
|
||||
master: XoHost['id']
|
||||
migrationCompression?: boolean
|
||||
name_description: string
|
||||
name_label: string
|
||||
otherConfig: Record<string, string>
|
||||
platform_version: string
|
||||
suspendSr?: XoSr['id']
|
||||
tags: string[]
|
||||
type: 'pool'
|
||||
vtpmSupported: boolean
|
||||
zstdSupported: boolean
|
||||
}
|
||||
|
||||
export type XoProxy = {
|
||||
id: Branded<'proxy'>
|
||||
url: string
|
||||
version?: string
|
||||
name: string
|
||||
} & (
|
||||
| { address?: undefined; vmUuid: XoVm['id'] }
|
||||
| { address: string; vmUuid?: undefined }
|
||||
| { address: string; vmUuid: XoVm['id'] }
|
||||
)
|
||||
|
||||
type BaseXoJob = {
|
||||
id: Branded<'job'>
|
||||
name?: string
|
||||
}
|
||||
|
||||
type XoBackupJobGeneralSettings = {
|
||||
backupReportTpl?: 'compactMjml' | 'mjml'
|
||||
reportWhen?: 'always' | 'error' | 'failure' | 'never'
|
||||
reportRecipients?: string[]
|
||||
hideSuccessfulItems?: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type XoVmBackupJobGeneralSettings = XoBackupJobGeneralSettings & {
|
||||
cbtDestroySnapshotData?: boolean
|
||||
concurrency?: number
|
||||
longTermRetention?: {
|
||||
daily?: { retention: number; settings: Record<string, unknown> }
|
||||
weekly?: { retention: number; settings: Record<string, unknown> }
|
||||
monthly?: { retention: number; settings: Record<string, unknown> }
|
||||
yearly?: { retention: number; settings: Record<string, unknown> }
|
||||
}
|
||||
checkpointSnapshot?: boolean
|
||||
offlineSnapshot?: boolean
|
||||
maxExportRate?: number
|
||||
nbdConcurrency?: number
|
||||
nRetriesVmBackupFailures?: number
|
||||
preferNbd?: boolean
|
||||
timezone?: string
|
||||
mergeBackupsSynchronously?: boolean
|
||||
offlineBackup?: boolean
|
||||
timeout?: number
|
||||
}
|
||||
export type XoVmBackupJobScheduleSettings = {
|
||||
exportRetention?: number
|
||||
healthCheckVmsWithTags?: XoVm['tags']
|
||||
fullInterval?: number
|
||||
copyRetention?: number
|
||||
snapshotRetention?: number
|
||||
cbtDestroySnapshotData?: boolean
|
||||
healthCheckSr?: XoSr['id']
|
||||
[key: string]: unknown
|
||||
}
|
||||
export type XoVmBackupJob = BaseXoJob & {
|
||||
compression?: 'native' | 'zstd' | ''
|
||||
proxy?: XoProxy['id']
|
||||
mode: 'full' | 'delta'
|
||||
remotes?: CMType.IdOr<XoBackupRepository['id']>
|
||||
vms: CMType.IdOr<XoVm['id']> | Record<string, unknown>
|
||||
srs?: CMType.IdOr<XoSr['id']>
|
||||
type: 'backup'
|
||||
settings: {
|
||||
'': XoVmBackupJobGeneralSettings
|
||||
[key: XoSchedule['id']]: XoVmBackupJobScheduleSettings | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export type XoMetadataBackupJobGeneralSettings = XoBackupJobGeneralSettings
|
||||
export type XoMetadataBackupJobScheduleSettings = {
|
||||
retentionPoolMetadata?: number
|
||||
retentionXoMetadata?: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
export type XoMetadataBackupJob = BaseXoJob & {
|
||||
type: 'metadataBackup'
|
||||
pools?: CMType.IdOr<XoPool['id']>
|
||||
remotes: CMType.IdOr<XoBackupRepository['id']>
|
||||
settings: {
|
||||
''?: XoMetadataBackupJobGeneralSettings
|
||||
[scheduleId: XoSchedule['id']]: XoMetadataBackupJobScheduleSettings | undefined
|
||||
}
|
||||
xoMetadata?: boolean
|
||||
userId: XoUser['id']
|
||||
proxy?: XoProxy['id']
|
||||
}
|
||||
|
||||
export type XoMirrorBackupGeneralSettings = XoBackupJobGeneralSettings & {
|
||||
concurrency?: number
|
||||
nRetriesVmBackupFailures?: number
|
||||
mergeBackupsSynchronously?: boolean
|
||||
timeout?: number
|
||||
maxExportRate?: number
|
||||
backupReportTpl?: 'compactMjml'
|
||||
reportWhen: 'failure'
|
||||
}
|
||||
export type XoMirrorBackupScheduleSettings = {
|
||||
exportRetention?: number
|
||||
healthCheckVmsWithTags?: XoVm['tags']
|
||||
healthCheckSr?: XoSr['id']
|
||||
[key: string]: unknown
|
||||
}
|
||||
export type XoMirrorBackupJob = BaseXoJob & {
|
||||
type: 'mirrorBackup'
|
||||
mode: 'full' | 'delta'
|
||||
sourceRemote: XoBackupRepository['id']
|
||||
remotes: CMType.IdOr<XoBackupRepository['id']>
|
||||
proxy?: XoProxy['id']
|
||||
settings: {
|
||||
'': XoMirrorBackupGeneralSettings
|
||||
[scheduleId: XoSchedule['id']]: XoMirrorBackupScheduleSettings | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export type XoJob = BaseXoJob & {
|
||||
type: 'call'
|
||||
method: string
|
||||
paramsVector?: {
|
||||
type: string
|
||||
items: {
|
||||
type?: string
|
||||
values?: {
|
||||
schedules?: XoSchedule['id'][]
|
||||
[key: string]: unknown
|
||||
}[]
|
||||
[key: string]: unknown
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
export type XoSchedule = {
|
||||
cron: string
|
||||
enabled: boolean
|
||||
id: Branded<'schedule'>
|
||||
jobId: AnyXoJob['id']
|
||||
name?: string
|
||||
timezone?: string
|
||||
}
|
||||
|
||||
export type XoServer = {
|
||||
allowUnauthorized: boolean
|
||||
enabled: boolean
|
||||
error?: Record<string, unknown>
|
||||
host: string
|
||||
httpProxy?: string
|
||||
id: Branded<'server'>
|
||||
label?: string
|
||||
master?: XoHost['id']
|
||||
poolId?: XoPool['id']
|
||||
poolNameDescription?: string
|
||||
poolNameLabel?: string
|
||||
readOnly: boolean
|
||||
status: 'connected' | 'disconnected' | 'connecting'
|
||||
username: string
|
||||
}
|
||||
|
||||
export type XoSr = BaseXapiXo & {
|
||||
$PBDs: XoPbd['id'][]
|
||||
|
||||
$container: XoPool['id'] | XoHost['id']
|
||||
|
||||
VDIs: AnyXoVdi['id'][]
|
||||
|
||||
allocationStrategy?: 'thin' | 'thick' | 'unknown'
|
||||
content_type: 'user' | 'iso' | 'disk'
|
||||
current_operations: Record<string, STORAGE_OPERATIONS>
|
||||
id: Branded<'SR'>
|
||||
inMaintenanceMode: boolean
|
||||
name_description: string
|
||||
name_label: string
|
||||
other_config: Record<string, string>
|
||||
physical_usage: number
|
||||
shared: boolean
|
||||
size: number
|
||||
sm_config: Record<string, string>
|
||||
// SM drivers shipped with XCP-ng/XenServer
|
||||
SR_type:
|
||||
| 'cephfs'
|
||||
| 'dummy'
|
||||
| 'ext'
|
||||
| 'file'
|
||||
| 'glusterfs'
|
||||
| 'hba'
|
||||
| 'iso'
|
||||
| 'largeblock'
|
||||
| 'linstor'
|
||||
| 'lvm'
|
||||
| 'lvmofcoe'
|
||||
| 'lvmohba'
|
||||
| 'lvmoiscsi'
|
||||
| 'moosefs'
|
||||
| 'nfs'
|
||||
| 'shm'
|
||||
| 'smb'
|
||||
| 'udev'
|
||||
| 'xfs'
|
||||
| 'zfs'
|
||||
tags: string[]
|
||||
type: 'SR'
|
||||
usage: number
|
||||
}
|
||||
|
||||
export type XoSm = BaseXapiXo & {
|
||||
id: Branded<'SM'>
|
||||
|
||||
SM_type: string
|
||||
vendor: string
|
||||
name_label: string
|
||||
name_description: string
|
||||
configuration: Record<string, string>
|
||||
features: Record<string, number>
|
||||
driver_filename: string
|
||||
required_cluster_stack: string[]
|
||||
supported_image_formats: string[]
|
||||
type: 'SM'
|
||||
}
|
||||
|
||||
export type XoTask = {
|
||||
abortionRequestedAt?: number
|
||||
data?: Record<string, string>
|
||||
end?: number
|
||||
id: Branded<'task'>
|
||||
infos?: { data: unknown; message: string }[]
|
||||
progress?: number
|
||||
properties: {
|
||||
method?: string
|
||||
name?: string
|
||||
objectId?: string
|
||||
objectType?:
|
||||
| XapiXoRecord['type']
|
||||
| 'backup'
|
||||
| 'backup-archive'
|
||||
| 'backup-job'
|
||||
| 'backup-log'
|
||||
| 'backup-repository'
|
||||
| 'group'
|
||||
| 'proxy'
|
||||
| 'restore'
|
||||
| 'restore-log'
|
||||
| 'schedule'
|
||||
| 'server'
|
||||
| 'task'
|
||||
| 'user'
|
||||
| 'acl-privilege'
|
||||
| 'acl-role'
|
||||
params?: Record<string, unknown>
|
||||
progress?: number
|
||||
type?: string
|
||||
userId?: string
|
||||
[key: string]: unknown | undefined
|
||||
}
|
||||
result?: Record<string, unknown>
|
||||
start: number
|
||||
status: 'failure' | 'interrupted' | 'pending' | 'success'
|
||||
tasks?: XoTask[]
|
||||
updatedAt?: number
|
||||
warnings?: { data: unknown; message: string }[]
|
||||
}
|
||||
|
||||
export type XoUser = {
|
||||
authProviders?: Record<string, string>
|
||||
email: string
|
||||
groups: XoGroup['id'][]
|
||||
id: Branded<'user'>
|
||||
name?: string
|
||||
permission: 'none' | 'admin'
|
||||
pw_hash?: string
|
||||
preferences: Record<string, string>
|
||||
}
|
||||
|
||||
export type XoAuthenticationToken = {
|
||||
client?: {
|
||||
id: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
created_at?: number
|
||||
description?: string
|
||||
user_id: XoUser['id']
|
||||
expiration: number
|
||||
last_uses?: Record<string, { timestamp: number }>
|
||||
id: Branded<'authentication-token'>
|
||||
}
|
||||
|
||||
export type XoVbd = BaseXapiXo & {
|
||||
attached: boolean
|
||||
bootable: boolean
|
||||
device: string | null
|
||||
id: Branded<'VBD'>
|
||||
is_cd_drive: boolean
|
||||
position: string
|
||||
read_only: boolean
|
||||
type: 'VBD'
|
||||
VDI?: XoVdi['id'] | XoVdiSnapshot['id']
|
||||
VM: AnyXoVm['id']
|
||||
}
|
||||
|
||||
type BaseXoVdi = BaseXapiXo & {
|
||||
$SR: XoSr['id']
|
||||
$VBDs: XoVbd['id'][]
|
||||
|
||||
VDI_type: VDI_TYPE
|
||||
|
||||
cbt_enabled?: boolean
|
||||
current_operations: Record<string, VDI_OPERATIONS>
|
||||
missing: boolean
|
||||
name_description: string
|
||||
name_label: string
|
||||
other_config: Record<string, string>
|
||||
parent?: XoVdiUnmanaged['id']
|
||||
image_format?: string
|
||||
size: number
|
||||
snapshots: XoVdiSnapshot['id'][]
|
||||
tags: string[]
|
||||
usage: number
|
||||
}
|
||||
|
||||
export type XoVdi = BaseXoVdi & {
|
||||
id: Branded<'VDI'>
|
||||
chainPhysicalUsage: number
|
||||
type: 'VDI'
|
||||
}
|
||||
|
||||
export type XoVdiSnapshot = BaseXoVdi & {
|
||||
id: Branded<'VDI-snapshot'>
|
||||
snapshot_time: number
|
||||
$snapshot_of?: XoVdi['id']
|
||||
type: 'VDI-snapshot'
|
||||
}
|
||||
|
||||
export type XoVdiUnmanaged = Omit<BaseXoVdi, '$VBDs'> & {
|
||||
id: Branded<'VDI-unmanaged'>
|
||||
type: 'VDI-unmanaged'
|
||||
|
||||
/**
|
||||
* Unmanaged VDI have no VBDs
|
||||
*/
|
||||
$VBDs: never[]
|
||||
}
|
||||
|
||||
export type XoVgpu = BaseXapiXo & {
|
||||
id: Branded<'VGPU'>
|
||||
type: 'vgpu'
|
||||
}
|
||||
|
||||
export type XoVgpuType = BaseXapiXo & {
|
||||
id: Branded<'vgpu-type'>
|
||||
type: 'vgpuType'
|
||||
}
|
||||
|
||||
export type XoVif = BaseXapiXo & {
|
||||
$VM: XoVm['id']
|
||||
|
||||
$network: XoNetwork['id']
|
||||
|
||||
allowedIpv4Addresses: string[]
|
||||
allowedIpv6Addresses: string[]
|
||||
attached: boolean
|
||||
device: string
|
||||
id: Branded<'VIF'>
|
||||
lockingMode: VIF_LOCKING_MODE
|
||||
MAC: string
|
||||
MTU: number
|
||||
other_config: Record<string, string>
|
||||
/**
|
||||
* In kB/s
|
||||
*/
|
||||
rateLimit?: number
|
||||
txChecksumming: boolean
|
||||
type: 'VIF'
|
||||
}
|
||||
|
||||
export type XoVm = BaseXoVm & {
|
||||
id: Branded<'VM'>
|
||||
type: 'VM'
|
||||
vulnerabilities: { xsa468: boolean | { reason: string; driver?: string; version?: string } }
|
||||
}
|
||||
|
||||
export type XoVmController = BaseXoVm & {
|
||||
id: Branded<'VM-controller'>
|
||||
type: 'VM-controller'
|
||||
}
|
||||
|
||||
export type XoVmSnapshot = BaseXoVm & {
|
||||
$snapshot_of: XoVm['id']
|
||||
|
||||
id: Branded<'VM-snapshot'>
|
||||
snapshot_time: number
|
||||
suspendVdi?: XoVdi['id']
|
||||
type: 'VM-snapshot'
|
||||
}
|
||||
|
||||
export type XoVmTemplate = BaseXoVm & {
|
||||
id: Branded<'VM-template'>
|
||||
isDefaultTemplate: boolean
|
||||
template_info: {
|
||||
arch?: string
|
||||
disks: {
|
||||
bootable: true
|
||||
size: number
|
||||
SR: string
|
||||
type: string
|
||||
}[]
|
||||
install_methods: string[]
|
||||
install_repository?: string
|
||||
}
|
||||
type: 'VM-template'
|
||||
}
|
||||
|
||||
export type XoVtpm = BaseXapiXo & {
|
||||
id: Branded<'VTPM'>
|
||||
type: 'VTPM'
|
||||
}
|
||||
|
||||
export type XapiXoRecord =
|
||||
| XoAlarm
|
||||
| XoGpuGroup
|
||||
| XoHost
|
||||
| XoMessage
|
||||
| XoNetwork
|
||||
| XoPbd
|
||||
| XoPci
|
||||
| XoPgpu
|
||||
| XoPif
|
||||
| XoPool
|
||||
| XoSr
|
||||
| XoVbd
|
||||
| XoVdi
|
||||
| XoVdiSnapshot
|
||||
| XoVdiUnmanaged
|
||||
| XoVgpu
|
||||
| XoVgpuType
|
||||
| XoVif
|
||||
| XoVm
|
||||
| XoVmController
|
||||
| XoVmSnapshot
|
||||
| XoVmTemplate
|
||||
| XoVtpm
|
||||
| XoSm
|
||||
|
||||
export type NonXapiXoRecord<
|
||||
TActionsByResource extends XoAclSupportedActionsByResource = never,
|
||||
TResource extends string = never,
|
||||
> =
|
||||
| AnyXoBackupArchive
|
||||
| AnyXoJob
|
||||
| AnyXoLog
|
||||
| XoGroup
|
||||
| XoProxy
|
||||
| XoJob
|
||||
| XoBackupRepository
|
||||
| XoSchedule
|
||||
| XoServer
|
||||
| XoTask
|
||||
| XoUser
|
||||
| XoAclRole
|
||||
| XoAclPrivilege<TActionsByResource, TResource>
|
||||
|
||||
export type XoRecord<
|
||||
TActionsByResource extends XoAclSupportedActionsByResource = never,
|
||||
TResource extends string = never,
|
||||
> = XapiXoRecord | NonXapiXoRecord<TActionsByResource, TResource>
|
||||
|
||||
export type AnyXoVm = XoVm | XoVmSnapshot | XoVmTemplate | XoVmController
|
||||
|
||||
export type AnyXoVdi = XoVdi | XoVdiSnapshot | XoVdiUnmanaged
|
||||
|
||||
export type AnyXoJob = XoJob | AnyXoBackupJob
|
||||
|
||||
export type AnyXoBackupJob = XoVmBackupJob | XoMetadataBackupJob | XoMirrorBackupJob
|
||||
|
||||
export type AnyXoLog = XoBackupLog | XoRestoreLog
|
||||
|
||||
export type AnyXoBackupArchive = XoVmBackupArchive | XoConfigBackupArchive | XoPoolBackupArchive
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES2022",
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"] // Only use types from Node.js and packages that are explicitly imported
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -20,7 +20,7 @@ function main([inputPath = 0]) {
|
||||
try {
|
||||
data = JSON.parse(input)
|
||||
} catch (error) {
|
||||
// fall back to XML
|
||||
// fallback to XML
|
||||
log(parseXml(input))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,16 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
import eslint from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export default tseslint.config(eslint.configs.recommended, tseslint.configs.recommended, {
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"main": "dist/index.mjs",
|
||||
"name": "@xen-orchestra/acl",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/acl",
|
||||
"version": "1.5.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">= 20.18"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && node scripts/generate-types.mjs",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "tsc --watch",
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "npm run build && node --test ./dist/**/*.test.mjs",
|
||||
"prebuild": "npm run clean",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@vates/types": "^1.29.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "~5.6",
|
||||
"typescript-eslint": "^8.61.0"
|
||||
},
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@xen-orchestra/acl",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"complex-matcher": "^1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// AUTO-GENERATES dist/generated/privilege-types.d.mts
|
||||
// Run via `npm run build` (after tsc).
|
||||
|
||||
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { SUPPORTED_ACTIONS_BY_RESOURCE } from '../dist/actions/index.mjs'
|
||||
import { getActionStrings } from '../dist/index.mjs'
|
||||
|
||||
function toPascalCase(str) {
|
||||
return str
|
||||
.split(/[-_]/)
|
||||
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
||||
.join('')
|
||||
}
|
||||
|
||||
const resources = Object.keys(SUPPORTED_ACTIONS_BY_RESOURCE)
|
||||
|
||||
const typeBlocks = resources.map(resource => {
|
||||
const actionUnion = getActionStrings(resource)
|
||||
.map(action => `'${action}'`)
|
||||
.join(' | ')
|
||||
return `export type ${toPascalCase(resource)}Privilege = {
|
||||
id: XoAclBasePrivilege['id']
|
||||
resource: '${resource}'
|
||||
action: ${actionUnion}
|
||||
selector?: XoAclBasePrivilege['selector']
|
||||
effect: XoAclBasePrivilege['effect']
|
||||
roleId: XoAclBasePrivilege['roleId']
|
||||
}`
|
||||
})
|
||||
|
||||
const anyUnion = resources.map(resource => ` | ${toPascalCase(resource)}Privilege`).join('\n')
|
||||
|
||||
const types = `\
|
||||
// -----------------------------------------------------------------------
|
||||
// AUTO-GENERATED — do not edit manually.
|
||||
// Regenerated on every \`npm run build\`.
|
||||
// -----------------------------------------------------------------------
|
||||
import type { XoAclBasePrivilege } from '@vates/types'
|
||||
|
||||
|
||||
${typeBlocks.join('\n\n')}
|
||||
|
||||
export type AnyPrivilege =
|
||||
${anyUnion}
|
||||
`
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const outDir = join(root, 'dist/generated')
|
||||
|
||||
mkdirSync(outDir, { recursive: true })
|
||||
writeFileSync(join(outDir, 'privilege-types.d.mts'), types, 'utf-8')
|
||||
@@ -1,11 +0,0 @@
|
||||
export default {
|
||||
create: true,
|
||||
delete: true,
|
||||
read: true,
|
||||
update: {
|
||||
action: true,
|
||||
effect: true,
|
||||
resource: true,
|
||||
selector: true,
|
||||
},
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export default {
|
||||
create: true,
|
||||
delete: true,
|
||||
read: true,
|
||||
update: {
|
||||
description: true,
|
||||
groups: true,
|
||||
name: true,
|
||||
users: true,
|
||||
},
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
read: true,
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
read: true,
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
read: true,
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
read: true,
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export default {
|
||||
benchmark: true,
|
||||
create: true,
|
||||
read: true,
|
||||
forget: true,
|
||||
update: {
|
||||
enabled: true,
|
||||
name: true,
|
||||
options: true,
|
||||
proxy: true,
|
||||
url: true,
|
||||
},
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
read: true,
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export default {
|
||||
create: true,
|
||||
delete: true,
|
||||
read: true,
|
||||
update: {
|
||||
name: true,
|
||||
users: true,
|
||||
},
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
export default {
|
||||
'allow-vm': true,
|
||||
detach: true,
|
||||
disable: true,
|
||||
enable: true,
|
||||
evacuate: true,
|
||||
export: {
|
||||
logs: true,
|
||||
},
|
||||
forget: true,
|
||||
'join-pool': true,
|
||||
'migrate-receive': true,
|
||||
read: true,
|
||||
reboot: {
|
||||
clean: true,
|
||||
smart: true,
|
||||
},
|
||||
'restart-toolstack': true,
|
||||
shutdown: {
|
||||
clean: true,
|
||||
emergency: true,
|
||||
},
|
||||
start: true,
|
||||
update: {
|
||||
tags: true,
|
||||
},
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { default as aclPrivilege } from './acl-privilege.mjs'
|
||||
import { default as aclRole } from './acl-role.mjs'
|
||||
import { default as alarm } from './alarm.mjs'
|
||||
import { default as backupArchive } from './backup-archive.mjs'
|
||||
import { default as backupJob } from './backup-job.mjs'
|
||||
import { default as backupLog } from './backup-log.mjs'
|
||||
import { default as backupRepository } from './backup-repository.mjs'
|
||||
import { default as gpuGroup } from './gpuGroup.mjs'
|
||||
import { default as group } from './group.mjs'
|
||||
import { default as host } from './host.mjs'
|
||||
import { default as message } from './message.mjs'
|
||||
import { default as network } from './network.mjs'
|
||||
import { default as pbd } from './pbd.mjs'
|
||||
import { default as pci } from './pci.mjs'
|
||||
import { default as pgpu } from './pgpu.mjs'
|
||||
import { default as pif } from './pif.mjs'
|
||||
import { default as pool } from './pool.mjs'
|
||||
import { default as proxy } from './proxy.mjs'
|
||||
import { default as restoreLog } from './restore-log.mjs'
|
||||
import { default as schedule } from './schedule.mjs'
|
||||
import { default as server } from './server.mjs'
|
||||
import { default as sm } from './sm.mjs'
|
||||
import { default as sr } from './sr.mjs'
|
||||
import { default as task } from './task.mjs'
|
||||
import { default as user } from './user.mjs'
|
||||
import { default as vbd } from './vbd.mjs'
|
||||
import { default as vdiSnapshot } from './vdi-snapshot.mjs'
|
||||
import { default as vdiUnmanaged } from './vdi-unmanaged.mjs'
|
||||
import { default as vdi } from './vdi.mjs'
|
||||
import { default as vgpu } from './vgpu.mjs'
|
||||
import { default as vgpuType } from './vgpuType.mjs'
|
||||
import { default as vif } from './vif.mjs'
|
||||
import { default as vmController } from './vm-controller.mjs'
|
||||
import { default as vmSnapshot } from './vm-snapshot.mjs'
|
||||
import { default as vmTemplate } from './vm-template.mjs'
|
||||
import { default as vm } from './vm.mjs'
|
||||
import { default as vtpm } from './vtpm.mjs'
|
||||
|
||||
/**
|
||||
* value without sub action like 'shutdown' mean: 'shutdown:*' (so shutdown mean you can shutdown:clean and shutdown:hard)
|
||||
*/
|
||||
export const SUPPORTED_ACTIONS_BY_RESOURCE = {
|
||||
'acl-privilege': aclPrivilege,
|
||||
'acl-role': aclRole,
|
||||
alarm,
|
||||
'backup-archive': backupArchive,
|
||||
'backup-job': backupJob,
|
||||
'backup-log': backupLog,
|
||||
'backup-repository': backupRepository,
|
||||
group,
|
||||
gpuGroup,
|
||||
host,
|
||||
message,
|
||||
network,
|
||||
pbd,
|
||||
pci,
|
||||
pgpu,
|
||||
pif,
|
||||
pool,
|
||||
proxy,
|
||||
'restore-log': restoreLog,
|
||||
schedule,
|
||||
server,
|
||||
sm,
|
||||
sr,
|
||||
task,
|
||||
user,
|
||||
vbd,
|
||||
'vdi-snapshot': vdiSnapshot,
|
||||
'vdi-unmanaged': vdiUnmanaged,
|
||||
vdi,
|
||||
vgpu,
|
||||
vgpuType,
|
||||
vif,
|
||||
'vm-controller': vmController,
|
||||
'vm-snapshot': vmSnapshot,
|
||||
'vm-template': vmTemplate,
|
||||
vm,
|
||||
vtpm,
|
||||
} as const
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
read: true,
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export default {
|
||||
create: true,
|
||||
delete: true,
|
||||
read: true,
|
||||
update: {
|
||||
tags: true,
|
||||
other_config: true,
|
||||
},
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export default {
|
||||
read: true,
|
||||
plug: true,
|
||||
unplug: true,
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
read: true,
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
read: true,
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
read: true,
|
||||
update: {
|
||||
management: true,
|
||||
},
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
export default {
|
||||
'add-host': true,
|
||||
create: {
|
||||
network: true,
|
||||
vm: true,
|
||||
},
|
||||
'emergency-shutdown': true,
|
||||
read: true,
|
||||
'rolling-reboot': true,
|
||||
'rolling-update': true,
|
||||
update: {
|
||||
tags: true,
|
||||
},
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user