Compare commits

...

123 Commits

Author SHA1 Message Date
Julien Fontanet
4bd7a76d47 feat(xen-api/Record#resolve_): resolve props named as types 2021-04-07 16:00:35 +02:00
Julien Fontanet
3c5d73224a feat(normalize-packages): delete empty dependencies, description and keywords fields 2021-04-07 15:14:48 +02:00
Julien Fontanet
05f9c07836 chore: add descriptions to all pkgs (but mixin) 2021-04-07 15:13:18 +02:00
Julien Fontanet
a7ba6add39 feat(read-chunk): ensure function is properly named 2021-04-07 14:02:50 +02:00
Julien Fontanet
479973bf06 fix(xapi): add missing cli.js
Introduced by be9b5332d9
2021-04-07 14:00:40 +02:00
Julien Fontanet
854c9fe794 fix(.gitignore): handle @vates/* pkgs 2021-04-07 13:59:07 +02:00
Julien Fontanet
5a17c75fe4 feat: unified .npmignore for all packages
Ensure sources, tests and USAGE.Md files will not be published.
2021-04-07 13:58:14 +02:00
Julien Fontanet
4dc5eff252 chore(normalize-packages): remove unused require 2021-04-07 13:39:12 +02:00
Julien Fontanet
7fe0d78154 fix(compose/USAGE): typo 2021-04-07 13:36:24 +02:00
Julien Fontanet
2c709dc205 fix(xo-server/proxies): readChunk import
Introduced by 538253cdc
2021-04-07 13:09:29 +02:00
Julien Fontanet
9353349a39 chore(vhd-lib): use @vates/read-chunk 2021-04-07 13:08:44 +02:00
Julien Fontanet
d3049b2bfa feat(@vates/read-chunk): 0.1.2 2021-04-07 13:07:57 +02:00
Julien Fontanet
61cb2529bd feat(read-chunk/package.json): only publish index.js 2021-04-07 13:05:49 +02:00
Julien Fontanet
e6c6e4395f chore(read-chunk): add tests 2021-04-07 13:03:56 +02:00
Julien Fontanet
959c955616 feat(read-chunk): handle 0 size 2021-04-07 12:51:27 +02:00
Julien Fontanet
538253cdc1 chore(xo-server/proxies): use @vates/read-chunk 2021-04-07 12:31:16 +02:00
Julien Fontanet
b4c6594333 chore(xo-server/utils): remove unused wrap 2021-04-07 12:25:47 +02:00
Julien Fontanet
a7f5f8889c chore(xo-server/http): use @xen-orchestra/defined 2021-04-07 12:15:33 +02:00
Julien Fontanet
1c9b4cf552 feat(xo-server/Xo): pass appDir, AppName and httpServer
As done in xo-proxy.

This may allow sharing mixins in the future.
2021-04-07 10:41:50 +02:00
Julien Fontanet
ce09f487bd feat(xo-server/XenServers): dont auto connect on safe mode 2021-04-07 10:41:50 +02:00
Julien Fontanet
a5d1decf40 feat(xo-server/Xo): safeMode param 2021-04-07 10:41:50 +02:00
Julien Fontanet
7024c7d598 chore(xo-server/Xo): pass config as named param
As done in xo-proxy.
2021-04-07 10:41:50 +02:00
Jon Sands
8109253eeb fix(xo-web/en): grammar fixes (#5713) 2021-04-06 10:31:52 +02:00
Julien Fontanet
b61f1e3803 fix(xapi/VM_snapshot): VM#refresh_snapshots does not exist
Fixes xoa-support#3587
2021-04-04 22:41:46 +02:00
Julien Fontanet
db40f80be7 chore(CHANGELOG.unreleased): formatting 2021-04-04 22:36:35 +02:00
Mathieu
26eaf97032 fix(xo-web/restore): generateNewMACAddresses disabled by default (#5707) 2021-04-04 18:41:37 +02:00
Ronan Abhamon
da349374bf feat(load-balancer): add option to disable migration (#5706) 2021-04-02 17:32:26 +02:00
Julien Fontanet
0ffa925fee chore(xo-server-load-balancer): format with Prettier 2021-04-02 15:34:38 +02:00
Julien Fontanet
082787c4cf chore: update dependencies 2021-04-02 15:34:19 +02:00
Julien Fontanet
be9b5332d9 feat(xapi): add xo-xapi CLI
Allows to easily test @xen-orchestra/xapi features.
2021-04-01 14:47:04 +02:00
Julien Fontanet
97ae3ba7d3 feat(@xen-orchestra/proxy): 0.12.0 2021-04-01 14:09:09 +02:00
Julien Fontanet
d047f401c2 fix(CHANGELOG): move Highlights title under badge
Introduced by 1e9e78223
2021-04-01 14:08:12 +02:00
Julien Fontanet
1e9e78223b fix(CHANGELOG): missing Highlights title 2021-04-01 13:44:21 +02:00
Julien Fontanet
6d5baebd08 feat: release 5.57.0 2021-04-01 13:29:58 +02:00
Pierre Donias
4e758dbb85 feat: technical release (#5705) 2021-04-01 11:28:09 +02:00
Julien Fontanet
40d943c620 feat(backups/CR writer): delete interrupted copies in prepare() 2021-04-01 10:52:06 +02:00
Julien Fontanet
e69b6c4dc8 fix(backups/delta writers): compute old entries before run 2021-04-01 10:48:39 +02:00
Julien Fontanet
23444f7083 fix(xo-server): Backup/getConnectedRecord receive XAPI not XO type 2021-03-31 16:48:20 +02:00
Julien Fontanet
8c077b96df chore(xo-server/getXapiObject): clarify that second param is an XO type 2021-03-31 16:31:56 +02:00
Pierre Donias
4b1a055a88 feat: technical release (#5704) 2021-03-31 11:15:41 +02:00
Rajaa.BARHTAOUI
b4ddcc1dec feat(xo-server,xo-web/startVm): avoid booting VM if there's an identical MAC address elsewhere (#5655)
Fixes #5601
2021-03-31 10:50:32 +02:00
badrAZ
271d2e3abc feat(fs): expose highWaterMark stream option (#5676) 2021-03-31 10:34:42 +02:00
Julien Fontanet
37b6399398 fix(backups/importDeltaVm): reverse newMacAddresses condition for VIF creation
Introduced by 4df8c9610
2021-03-30 21:26:08 +02:00
Julien Fontanet
ebf19b1506 fix(proxy/backup.importVmBackup): settings param is optional
Introduced by b475b265a
2021-03-30 21:26:08 +02:00
Julien Fontanet
e4dd773644 fix(proxy/api/ndJsonStream): dont fail if one message cannot be JSONified 2021-03-30 21:26:08 +02:00
Mathieu
f9b3a1f293 feat(xo-web/restore): support new mac addresses (#5697) 2021-03-30 17:48:45 +02:00
Rajaa.BARHTAOUI
7c9850ada8 feat(xo-server-perf-alert): ability to choose all hosts, VMs and SRs (#5692)
Fixes #2987
2021-03-30 17:26:48 +02:00
Ronan Abhamon
9ef05b8afe feat(load-balancer): add new anti-affinity mode (#5652)
Fixes #5600
2021-03-30 17:25:41 +02:00
Mathieu
efdd196441 feat(xo-web/proxies): move proxy actions to dropdown (#5688) 2021-03-30 16:56:10 +02:00
Julien Fontanet
6e780a3876 fix(xapi/call{,Async}): fix call to retry
Introduced by 3bb7d2c29
2021-03-30 15:42:05 +02:00
Julien Fontanet
b475b265ae feat(import VM backup): newMacAddresses setting
Related to #5697
2021-03-30 14:31:08 +02:00
Julien Fontanet
3bb7d2c294 feat(xapi/call{,Async}): retry if too many pending tasks
Logic from xo-server/xapi/call
2021-03-30 14:21:19 +02:00
Julien Fontanet
594a148a39 feat(xo-server): VDI_destroy instead of deleteVdi 2021-03-30 14:08:25 +02:00
Julien Fontanet
779591db36 feat(xapi/VDI_destroy): dont fail if VDI not found
Aligned with xo-server/xapi/deleteVdi.
2021-03-30 13:59:10 +02:00
Julien Fontanet
c002eeffb7 chore(proxy): remove unused Node test
The proxy now requires Node >=12
2021-03-30 09:23:15 +02:00
Julien Fontanet
1dac973d70 feat(backups/Task.wrapFn): compatibility with @decorateWith 2021-03-30 09:20:30 +02:00
Julien Fontanet
f5024f0e75 feat(backups/delta writers): split run method in prepare/transfer/cleanup
Fixes xoa-support#3523

This avoids starting the transfer before the writers are ready, which caused it to failed with `deleteFirst` when deletion was so long that the transfer stalled.
2021-03-30 09:20:30 +02:00
Pierre Donias
cf320c08c5 feat: technical release (#5702) 2021-03-29 16:45:00 +02:00
Rajaa.BARHTAOUI
8973c9550c fix(xo-web/vm/advanced): remove noop (#5700)
See https://github.com/vatesfr/xen-orchestra/pull/3774/files#r602860699
2021-03-29 15:10:48 +02:00
Julien Fontanet
bb671f0e93 feat(xapi): update to xo-common@0.7.0 2021-03-29 15:01:12 +02:00
Julien Fontanet
a8774b5011 chore(log/transport/console): remove unused code 2021-03-29 14:59:14 +02:00
Julien Fontanet
f092cd41bc chore: named import from @xen-orchestra/log 2021-03-29 14:06:29 +02:00
Julien Fontanet
b17ec9731a fix(xapi/task_create): dont use super
Introduced by 021810201b

It cannot be used due to our mixin architecture.
2021-03-28 23:29:17 +02:00
Julien Fontanet
021810201b fix(xapi/task_create): remove duplicate [XO] prefix
Found while investigating xoa-support#3553
2021-03-28 14:30:36 +02:00
Julien Fontanet
6038dc9c8a fix(xo-web/vm/advanced): RTL819 → RTL8139
Fixes #5698
2021-03-28 12:23:52 +02:00
Julien Fontanet
4df8c9610a fix(backups/importDeltaVm): new mac_seed when using newMacAddresses 2021-03-26 17:14:37 +01:00
Julien Fontanet
6c12dd4f16 feat(xapi/VM_create): generateMacSeed option 2021-03-26 17:14:37 +01:00
badrAZ
ad3b8fa59f feat(xo-server-usage-report): add VM IP addresses to the report (#5696) 2021-03-26 14:39:35 +01:00
Mathieu
cb52a8b51b feat(xo-server): VM_destroy instead of deleteVm (#5693)
Continuation of 5f1c127
2021-03-26 14:23:04 +01:00
badrAZ
22ba1302d2 fix(xo-server-backup-reports): support failed targets (#5694)
Introduced by d282d8dd5
2021-03-26 10:27:08 +01:00
Mathieu
7d04559921 fix(xo-web/disk import): an error has occurred (#5683)
Fixes #5663

undefined files in collection due to unhandled disk formats
2021-03-26 09:37:31 +01:00
Julien Fontanet
e40e35d30c fix(xo-server): dont provide mac_send when importing OVA VM
Fixes xoa-support#3544
2021-03-25 15:51:02 +01:00
Julien Fontanet
d1af9f236c feat(backup/importDeltaVm): restoreMacAddresses option 2021-03-24 16:39:08 +01:00
Julien Fontanet
45a0ff26c5 feat(xapi/VIF_create): MAC must be passed explicitely 2021-03-24 16:38:19 +01:00
Julien Fontanet
1fd330d7a4 fix(CHANGELOG.unreleased): @vates/disposable 2021-03-24 16:03:49 +01:00
Julien Fontanet
09833f31cf fix(xapi/VM_export): correctly destroy snapshot after export 2021-03-24 16:00:45 +01:00
Julien Fontanet
20e7a036cf feat(xapi): no longer use promise-toolbox/cancelable 2021-03-24 15:34:47 +01:00
Julien Fontanet
e6667c1782 fix(xapi/VM_destroy): only update blocked operations when bypassBlockedOperation 2021-03-24 15:13:09 +01:00
Julien Fontanet
657935eba5 feat(xapi/VM_destroy): split bypassBlockedOperation out of force 2021-03-24 15:12:25 +01:00
Julien Fontanet
67b905a757 feat(xapi/VM_destroy): forceDeleteDefaultTemplate default to force 2021-03-24 15:11:49 +01:00
Julien Fontanet
55cede0434 fix(xapi/VM_destroy): ignore missing VM.set_is_default_template
Introduced by 5f1c1278e
2021-03-24 15:09:09 +01:00
Julien Fontanet
c7677d6d1e fix(xapi/VM_destroy): only update default template when forceDeleteDefaultTemplate 2021-03-24 15:07:30 +01:00
Mathieu
d191ca54ad feat(xo-server/xapi-object-to-xo): display full driver version (#5691) 2021-03-24 13:38:49 +01:00
badrAZ
20f4c952fe feat(@xen-orchestra/backups#RemoteAdapter): ability to clean broken backups (#5684) 2021-03-24 10:00:47 +01:00
Julien Fontanet
0bd09896f3 feat(docs/xoa): network config for other interfaces 2021-03-24 09:49:59 +01:00
badrAZ
60ecfbfb8e feat(xo-server, proxy, @xen-orchestra/backups): execute backup jobs on different processes (#5660) 2021-03-24 09:25:52 +01:00
Julien Fontanet
8921d78610 fix(disposable/deduped): call dispose with disposable context 2021-03-24 00:27:25 +01:00
Julien Fontanet
b243ff94e9 feat(fs/lock): returns a disposable 2021-03-23 23:04:49 +01:00
Mathieu
5f1c1278e3 fix(xapi/VM_destroy): handle is_default_template (#5644) 2021-03-23 17:28:18 +01:00
badrAZ
fa56e594b1 feat(xo-server-transport-email): ability to customize transport settings (#5681)
See xoa-support#3327

This functionnality can help users to get more info about their SMTP issues
2021-03-23 15:47:31 +01:00
badrAZ
c9b64927be feat(@xen-orchestra/fs): ability to lock a path (#5689)
Related to #5684
2021-03-23 13:00:15 +01:00
Julien Fontanet
3689cb2a99 chore(fs/outputStream): dont normalize path twice 2021-03-23 12:53:17 +01:00
Julien Fontanet
3bb7541361 fix(fs/local): opts is optional 2021-03-23 12:53:17 +01:00
Julien Fontanet
7b15aa5f83 fix(package.json/jest.moduleNameMapper): dont map sub-modules
For instance `@xen-orchestra/async-map/legacy`.
2021-03-23 12:53:17 +01:00
Julien Fontanet
690d3036db chore(xo-server): use native Promise#finally() (#5687) 2021-03-22 17:25:40 +01:00
Julien Fontanet
416e8d02a1 chore(xo-server): usingDisposable.use (#5686)
`using` is deprecated.
2021-03-22 17:25:28 +01:00
Julien Fontanet
a968c2d2b7 chore(xo-server): remove promise-toolbox/all uses (#5685) 2021-03-22 15:58:13 +01:00
Julien Fontanet
b4787bf444 fix(xo-server/(re)deployProxy): works whith missing bound VM 2021-03-22 14:44:39 +01:00
Julien Fontanet
a4d90e8aff fix(xo-web/proxies): ignore running jobs on force upgrade
See #3527
2021-03-22 14:37:34 +01:00
Julien Fontanet
32d0606ee4 feat(@xen-orchestra/proxy): 0.11.6 2021-03-22 13:41:28 +01:00
Julien Fontanet
4541f7c758 chore(xo-server): remove pSettle (#5682) 2021-03-22 10:37:07 +01:00
Julien Fontanet
65428d629c feat: release 5.56.2 2021-03-22 10:36:24 +01:00
Julien Fontanet
bdfd9cc617 feat: technical release 2021-03-22 09:56:31 +01:00
Julien Fontanet
6d324921a0 fix(CHANGELOG.unreleased): add xapi to packages to release
Introduced by dcf0f5c5a
2021-03-19 14:53:29 +01:00
Julien Fontanet
dcf0f5c5a3 fix(xapi/VM_create): ignore missing VM.set_bios_strings
Fixes xoa-support#3516
2021-03-19 14:51:51 +01:00
Julien Fontanet
d98f851a2c fix(xo-web/restore): dont break if log is missing result
Found when investigating xoa-support#3516
2021-03-19 13:46:22 +01:00
Julien Fontanet
a95b102396 fix(backups/ImportVmBackup#run): dont swallow errors
Found when investigating xoa-support#3516
2021-03-19 13:38:17 +01:00
Julien Fontanet
7e2fbbaae6 fix(xo-server/createAuthenticationToken): parse expiresIn param
Introduced by 92cf6bb887
2021-03-19 13:02:19 +01:00
Julien Fontanet
070e8b0b54 fix(docs): restore full_backups.md
Introduced by 078f40281
2021-03-19 11:04:42 +01:00
Yannick Achy
7b49a1296c feat(doc): various changes (#5679) 2021-03-19 11:02:24 +01:00
Julien Fontanet
1e278bde92 feat(xo-common/Error.is): apply predicate only on data
See #5644
2021-03-19 10:30:35 +01:00
Yannick Achy
078f402819 feat(doc): check dead links and duplicate (#5664) 2021-03-18 09:35:07 +01:00
Mathieu
52af565f77 fix(fs/localHandler): resource temporarily unavailable (#5612)
Fixes xoa-support#3414
2021-03-17 15:28:04 +01:00
Julien Fontanet
853905e52f feat(xen-api): automatically retry ro calls on ECONNRESET (#5674)
See xoa-support#3266
2021-03-16 17:31:18 +01:00
Julien Fontanet
2e0e1d2aac fix(xo-server/xen-servers): fix autoconnection
Fixes #5675

Introduced by 2fbfc97cca
2021-03-16 13:59:01 +01:00
Julien Fontanet
7f33a62bb5 fix(async-map/asyncMap): fix typing of first param 2021-03-16 11:32:39 +01:00
Julien Fontanet
bdb59ea429 fix(Travis CI): use Node 14 2021-03-15 18:26:22 +01:00
Julien Fontanet
1c0ffe39f7 fix(xapi): merge vdiDestroyRetryWhenInUse after defaults 2021-03-15 13:22:41 +01:00
Julien Fontanet
2fbfc97cca fix(xo-server): connect servers after config import (#5672)
Fixes #5670
2021-03-15 11:49:41 +01:00
Pierre Donias
482299e765 fix(xo-web/pool): disconnectServer → disableServer (#5671)
Fixes #5669
Introduced c7aaeca530
2021-03-15 09:53:34 +01:00
Julien Fontanet
54f4734847 chore: remove babel-plugin-lodash where unnecessary 2021-03-13 20:47:20 +01:00
Julien Fontanet
0fb6cef577 chore(defined): remove Flow 2021-03-13 20:47:20 +01:00
badrAZ
7eec264961 chore(@xen-orchestra/backups): fix logger names (#5666) 2021-03-12 13:53:43 +01:00
241 changed files with 4030 additions and 3804 deletions

View File

@@ -1,13 +1,5 @@
module.exports = {
extends: [
'plugin:eslint-comments/recommended',
'standard',
'standard-jsx',
'prettier',
'prettier/standard',
'prettier/react',
],
extends: ['plugin:eslint-comments/recommended', 'standard', 'standard-jsx', 'prettier'],
globals: {
__DEV__: true,
$Dict: true,

2
.gitignore vendored
View File

@@ -4,6 +4,8 @@
/lerna-debug.log
/lerna-debug.log.*
/@vates/*/dist/
/@vates/*/node_modules/
/@xen-orchestra/*/dist/
/@xen-orchestra/*/node_modules/
/packages/*/dist/

View File

@@ -1,6 +1,6 @@
language: node_js
node_js:
- 12
- 14
# Use containers.
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/

View File

@@ -0,0 +1 @@
../../scripts/npmignore

1
@vates/compose/.npmignore Symbolic link
View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -50,7 +50,7 @@ Functions may also be passed in an array:
const f = compose([add2, mul3])
```
Options can be passed as first parameters:
Options can be passed as first parameter:
```js
const f = compose(

View File

@@ -32,7 +32,7 @@ Functions may also be passed in an array:
const f = compose([add2, mul3])
```
Options can be passed as first parameters:
Options can be passed as first parameter:
```js
const f = compose(

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -17,15 +17,15 @@ exports.deduped = (factory, keyFn = (...args) => args) =>
if (state === undefined) {
const result = factory.apply(this, arguments)
const createFactory = ({ value, dispose }) => {
const createFactory = disposable => {
const wrapper = {
dispose() {
if (--state.users === 0) {
states.delete(keys)
return dispose()
return disposable.dispose()
}
},
value,
value: disposable.value,
}
return () => {

View File

@@ -14,7 +14,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.1.0",
"version": "0.1.1",
"engines": {
"node": ">=8.10"
},

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -1,27 +1,30 @@
exports.readChunk = (stream, size) =>
new Promise((resolve, reject) => {
function onEnd() {
resolve(null)
removeListeners()
}
function onError(error) {
reject(error)
removeListeners()
}
function onReadable() {
const data = stream.read(size)
if (data !== null) {
resolve(data)
removeListeners()
}
}
function removeListeners() {
stream.removeListener('end', onEnd)
stream.removeListener('error', onError)
stream.removeListener('readable', onReadable)
}
stream.on('end', onEnd)
stream.on('error', onError)
stream.on('readable', onReadable)
onReadable()
})
const readChunk = (stream, size) =>
size === 0
? Promise.resolve(Buffer.alloc(0))
: new Promise((resolve, reject) => {
function onEnd() {
resolve(null)
removeListeners()
}
function onError(error) {
reject(error)
removeListeners()
}
function onReadable() {
const data = stream.read(size)
if (data !== null) {
resolve(data)
removeListeners()
}
}
function removeListeners() {
stream.removeListener('end', onEnd)
stream.removeListener('error', onError)
stream.removeListener('readable', onReadable)
}
stream.on('end', onEnd)
stream.on('error', onError)
stream.on('readable', onReadable)
onReadable()
})
exports.readChunk = readChunk

View File

@@ -0,0 +1,43 @@
/* eslint-env jest */
const { Readable } = require('stream')
const { readChunk } = require('./')
const makeStream = it => Readable.from(it, { objectMode: false })
makeStream.obj = Readable.from
describe('readChunk', () => {
it('returns null if stream is empty', async () => {
expect(await readChunk(makeStream([]))).toBe(null)
})
describe('with binary stream', () => {
it('returns the first chunk of data', async () => {
expect(await readChunk(makeStream(['foo', 'bar']))).toEqual(Buffer.from('foo'))
})
it('returns a chunk of the specified size (smaller than first)', async () => {
expect(await readChunk(makeStream(['foo', 'bar']), 2)).toEqual(Buffer.from('fo'))
})
it('returns a chunk of the specified size (larger than first)', async () => {
expect(await readChunk(makeStream(['foo', 'bar']), 4)).toEqual(Buffer.from('foob'))
})
it('returns less data if stream ends', async () => {
expect(await readChunk(makeStream(['foo', 'bar']), 10)).toEqual(Buffer.from('foobar'))
})
it('returns an empty buffer if the specified size is 0', async () => {
expect(await readChunk(makeStream(['foo', 'bar']), 0)).toEqual(Buffer.alloc(0))
})
})
describe('with object stream', () => {
it('returns the first chunk of data verbatim', async () => {
const chunks = [{}, {}]
expect(await readChunk(makeStream.obj(chunks))).toBe(chunks[0])
})
})
})

View File

@@ -19,10 +19,13 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.1.1",
"version": "0.1.2",
"engines": {
"node": ">=8.10"
},
"files": [
"index.js"
],
"scripts": {
"postversion": "npm publish --access public"
},

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -1,24 +0,0 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -12,7 +12,7 @@ const wrapCall = (fn, arg, thisArg) => {
* WARNING: Does not handle plain objects
*
* @template Item,This
* @param {Iterable<Item>} arrayLike
* @param {Iterable<Item>} iterable
* @param {(this: This, item: Item) => (Item | PromiseLike<Item>)} mapFn
* @param {This} [thisArg]
* @returns {Promise<Item[]>}

View File

@@ -1,24 +0,0 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -2,9 +2,9 @@
import 'core-js/features/symbol/async-iterator'
import assert from 'assert'
import createLogger from '@xen-orchestra/log'
import defer from 'golike-defer'
import hash from 'object-hash'
import { createLogger } from '@xen-orchestra/log'
const log = createLogger('xo:audit-core')

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -1,312 +1,16 @@
#!/usr/bin/env node
// assigned when options are parsed by the main function
let merge, remove
// -----------------------------------------------------------------------------
const assert = require('assert')
const asyncMap = require('lodash/curryRight')(require('@xen-orchestra/async-map').asyncMap)
const flatten = require('lodash/flatten')
const getopts = require('getopts')
const limitConcurrency = require('limit-concurrency-decorator').default
const lockfile = require('proper-lockfile')
const pipe = require('promise-toolbox/pipe')
const { default: Vhd, mergeVhd } = require('vhd-lib')
const { dirname, resolve } = require('path')
const { DISK_TYPE_DIFFERENCING } = require('vhd-lib/dist/_constants')
const { isValidXva } = require('@xen-orchestra/backups/isValidXva')
const { RemoteAdapter } = require('@xen-orchestra/backups/RemoteAdapter')
const { resolve } = require('path')
const fs = require('../_fs')
const handler = require('@xen-orchestra/fs').getHandler({ url: 'file://' })
// -----------------------------------------------------------------------------
// chain is an array of VHDs from child to parent
//
// the whole chain will be merged into parent, parent will be renamed to child
// and all the others will deleted
const mergeVhdChain = limitConcurrency(1)(async function mergeVhdChain(chain) {
assert(chain.length >= 2)
let child = chain[0]
const parent = chain[chain.length - 1]
const children = chain.slice(0, -1).reverse()
console.warn('Unused parents of VHD', child)
chain
.slice(1)
.reverse()
.forEach(parent => {
console.warn(' ', parent)
})
merge && console.warn(' merging…')
console.warn('')
if (merge) {
// `mergeVhd` does not work with a stream, either
// - make it accept a stream
// - or create synthetic VHD which is not a stream
if (children.length !== 1) {
console.warn('TODO: implement merging multiple children')
children.length = 1
child = children[0]
}
let done, total
const handle = setInterval(() => {
if (done !== undefined) {
console.log('merging %s: %s/%s', child, done, total)
}
}, 10e3)
await mergeVhd(
handler,
parent,
handler,
child,
// children.length === 1
// ? child
// : await createSyntheticStream(handler, children),
{
onProgress({ done: d, total: t }) {
done = d
total = t
},
}
)
clearInterval(handle)
}
await Promise.all([
remove && fs.rename(parent, child),
asyncMap(children.slice(0, -1), child => {
console.warn('Unused VHD', child)
remove && console.warn(' deleting…')
console.warn('')
return remove && handler.unlink(child)
}),
])
})
const listVhds = pipe([
vmDir => vmDir + '/vdis',
fs.readdir2,
asyncMap(fs.readdir2),
flatten,
asyncMap(fs.readdir2),
flatten,
_ => _.filter(_ => _.endsWith('.vhd')),
])
async function handleVm(vmDir) {
const vhds = new Set()
const vhdParents = { __proto__: null }
const vhdChildren = { __proto__: null }
// remove broken VHDs
await asyncMap(await listVhds(vmDir), async path => {
try {
const vhd = new Vhd(handler, path)
await vhd.readHeaderAndFooter()
vhds.add(path)
if (vhd.footer.diskType === DISK_TYPE_DIFFERENCING) {
const parent = resolve(dirname(path), vhd.header.parentUnicodeName)
vhdParents[path] = parent
if (parent in vhdChildren) {
const error = new Error('this script does not support multiple VHD children')
error.parent = parent
error.child1 = vhdChildren[parent]
error.child2 = path
throw error // should we throw?
}
vhdChildren[parent] = path
}
} catch (error) {
console.warn('Error while checking VHD', path)
console.warn(' ', error)
if (error != null && error.code === 'ERR_ASSERTION') {
remove && console.warn(' deleting…')
console.warn('')
remove && (await handler.unlink(path))
}
}
})
// remove VHDs with missing ancestors
{
const deletions = []
// return true if the VHD has been deleted or is missing
const deleteIfOrphan = vhd => {
const parent = vhdParents[vhd]
if (parent === undefined) {
return
}
// no longer needs to be checked
delete vhdParents[vhd]
deleteIfOrphan(parent)
if (!vhds.has(parent)) {
vhds.delete(vhd)
console.warn('Error while checking VHD', vhd)
console.warn(' missing parent', parent)
remove && console.warn(' deleting…')
console.warn('')
remove && deletions.push(handler.unlink(vhd))
}
}
// > A property that is deleted before it has been visited will not be
// > visited later.
// >
// > -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#Deleted_added_or_modified_properties
for (const child in vhdParents) {
deleteIfOrphan(child)
}
await Promise.all(deletions)
}
const [jsons, xvas, xvaSums] = await fs
.readdir2(vmDir)
.then(entries => [
entries.filter(_ => _.endsWith('.json')),
new Set(entries.filter(_ => _.endsWith('.xva'))),
entries.filter(_ => _.endsWith('.xva.cheksum')),
])
await asyncMap(xvas, async path => {
// check is not good enough to delete the file, the best we can do is report
// it
if (!(await isValidXva(path))) {
console.warn('Potential broken XVA', path)
console.warn('')
}
})
const unusedVhds = new Set(vhds)
const unusedXvas = new Set(xvas)
// compile the list of unused XVAs and VHDs, and remove backup metadata which
// reference a missing XVA/VHD
await asyncMap(jsons, async json => {
const metadata = JSON.parse(await fs.readFile(json))
const { mode } = metadata
if (mode === 'full') {
const linkedXva = resolve(vmDir, metadata.xva)
if (xvas.has(linkedXva)) {
unusedXvas.delete(linkedXva)
} else {
console.warn('Error while checking backup', json)
console.warn(' missing file', linkedXva)
remove && console.warn(' deleting…')
console.warn('')
remove && (await handler.unlink(json))
}
} else if (mode === 'delta') {
const linkedVhds = (() => {
const { vhds } = metadata
return Object.keys(vhds).map(key => resolve(vmDir, vhds[key]))
})()
// FIXME: find better approach by keeping as much of the backup as
// possible (existing disks) even if one disk is missing
if (linkedVhds.every(_ => vhds.has(_))) {
linkedVhds.forEach(_ => unusedVhds.delete(_))
} else {
console.warn('Error while checking backup', json)
const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
console.warn(' %i/%i missing VHDs', missingVhds.length, linkedVhds.length)
missingVhds.forEach(vhd => {
console.warn(' ', vhd)
})
remove && console.warn(' deleting…')
console.warn('')
remove && (await handler.unlink(json))
}
}
})
// TODO: parallelize by vm/job/vdi
const unusedVhdsDeletion = []
{
// VHD chains (as list from child to ancestor) to merge indexed by last
// ancestor
const vhdChainsToMerge = { __proto__: null }
const toCheck = new Set(unusedVhds)
const getUsedChildChainOrDelete = vhd => {
if (vhd in vhdChainsToMerge) {
const chain = vhdChainsToMerge[vhd]
delete vhdChainsToMerge[vhd]
return chain
}
if (!unusedVhds.has(vhd)) {
return [vhd]
}
// no longer needs to be checked
toCheck.delete(vhd)
const child = vhdChildren[vhd]
if (child !== undefined) {
const chain = getUsedChildChainOrDelete(child)
if (chain !== undefined) {
chain.push(vhd)
return chain
}
}
console.warn('Unused VHD', vhd)
remove && console.warn(' deleting…')
console.warn('')
remove && unusedVhdsDeletion.push(handler.unlink(vhd))
}
toCheck.forEach(vhd => {
vhdChainsToMerge[vhd] = getUsedChildChainOrDelete(vhd)
})
Object.keys(vhdChainsToMerge).forEach(key => {
const chain = vhdChainsToMerge[key]
if (chain !== undefined) {
unusedVhdsDeletion.push(mergeVhdChain(chain))
}
})
}
await Promise.all([
unusedVhdsDeletion,
asyncMap(unusedXvas, path => {
console.warn('Unused XVA', path)
remove && console.warn(' deleting…')
console.warn('')
return remove && handler.unlink(path)
}),
asyncMap(xvaSums, path => {
// no need to handle checksums for XVAs deleted by the script, they will be handled by `unlink()`
if (!xvas.has(path.slice(0, -'.checksum'.length))) {
console.warn('Unused XVA checksum', path)
remove && console.warn(' deleting…')
console.warn('')
return remove && handler.unlink(path)
}
}),
])
}
// -----------------------------------------------------------------------------
const adapter = new RemoteAdapter(require('@xen-orchestra/fs').getHandler({ url: 'file://' }))
module.exports = async function main(args) {
const opts = getopts(args, {
const { _, remove, merge } = getopts(args, {
alias: {
remove: 'r',
merge: 'm',
@@ -318,19 +22,12 @@ module.exports = async function main(args) {
},
})
;({ remove, merge } = opts)
await asyncMap(opts._, async vmDir => {
await asyncMap(_, async vmDir => {
vmDir = resolve(vmDir)
// TODO: implement this in `xo-server`, not easy because not compatible with
// `@xen-orchestra/fs`.
const release = await lockfile.lock(vmDir)
try {
await handleVm(vmDir)
await adapter.cleanVm(vmDir, { remove, merge, onLog: log => console.warn(log) })
} catch (error) {
console.error('handleVm', vmDir, error)
} finally {
await release()
console.error('adapter.cleanVm', vmDir, error)
}
})
}

View File

@@ -7,14 +7,12 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.7.0",
"@xen-orchestra/fs": "^0.13.0",
"@xen-orchestra/backups": "^0.9.1",
"@xen-orchestra/fs": "^0.14.0",
"filenamify": "^4.1.0",
"getopts": "^2.2.5",
"limit-concurrency-decorator": "^0.4.0",
"lodash": "^4.17.15",
"promise-toolbox": "^0.17.0",
"proper-lockfile": "^4.1.1",
"promise-toolbox": "^0.18.0",
"vhd-lib": "^1.0.0"
},
"engines": {
@@ -34,7 +32,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "0.4.0",
"version": "0.5.0",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -2,7 +2,6 @@ const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
const Disposable = require('promise-toolbox/Disposable')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const limitConcurrency = require('limit-concurrency-decorator').default
const using = require('promise-toolbox/using')
const { compileTemplate } = require('@xen-orchestra/template')
const { extractIdsFromSimplePattern } = require('./_extractIdsFromSimplePattern')
@@ -87,7 +86,7 @@ exports.Backup = class Backup {
throw new Error('no retentions corresponding to the metadata modes found')
}
await using(
await Disposable.use(
Disposable.all(
poolIds.map(id =>
this._getRecord('pool', id).catch(error => {
@@ -196,7 +195,7 @@ exports.Backup = class Backup {
...settings[schedule.id],
}
await using(
await Disposable.use(
Disposable.all(
extractIdsFromSimplePattern(job.srs).map(id =>
this._getRecord('SR', id).catch(error => {
@@ -242,7 +241,7 @@ exports.Backup = class Backup {
const handleVm = vmUuid =>
runTask({ name: 'backup VM', data: { type: 'VM', id: vmUuid } }, () =>
using(this._getRecord('VM', vmUuid), vm =>
Disposable.use(this._getRecord('VM', vmUuid), vm =>
new VmBackup({
config,
getSnapshotNameLabel,

View File

@@ -5,8 +5,9 @@ const { importDeltaVm } = require('./_deltaVm')
const { Task } = require('./Task')
exports.ImportVmBackup = class ImportVmBackup {
constructor({ adapter, metadata, srUuid, xapi }) {
constructor({ adapter, metadata, srUuid, xapi, settings: { newMacAddresses } = {} }) {
this._adapter = adapter
this._importDeltaVmSettings = { newMacAddresses }
this._metadata = metadata
this._srUuid = srUuid
this._xapi = xapi
@@ -37,6 +38,7 @@ exports.ImportVmBackup = class ImportVmBackup {
const vmRef = isFull
? await xapi.VM_import(backup, srRef)
: await importDeltaVm(backup, await xapi.getRecord('SR', srRef), {
...this._importDeltaVmSettings,
detectBase: false,
})
@@ -54,6 +56,6 @@ exports.ImportVmBackup = class ImportVmBackup {
id: await xapi.getField('VM', vmRef, 'uuid'),
}
}
).catch(() => {}) // errors are handled by logs
)
}
}

View File

@@ -4,7 +4,6 @@ const fromCallback = require('promise-toolbox/fromCallback')
const fromEvent = require('promise-toolbox/fromEvent')
const pDefer = require('promise-toolbox/defer')
const pump = require('pump')
const using = require('promise-toolbox/using')
const { basename, dirname, join, normalize, resolve } = require('path')
const { createLogger } = require('@xen-orchestra/log')
const { createSyntheticStream, mergeVhd, default: Vhd } = require('vhd-lib')
@@ -14,7 +13,9 @@ const { readdir, stat } = require('fs-extra')
const { ZipFile } = require('yazl')
const { BACKUP_DIR } = require('./_getVmBackupDir')
const { cleanVm } = require('./_cleanVm')
const { getTmpDir } = require('./_getTmpDir')
const { isMetadataFile, isVhdFile } = require('./_backupType')
const { listPartitions, LVM_PARTITION_TYPE } = require('./_listPartitions')
const { lvs, pvs } = require('./_lvm')
@@ -24,13 +25,10 @@ exports.DIR_XO_CONFIG_BACKUPS = DIR_XO_CONFIG_BACKUPS
const DIR_XO_POOL_METADATA_BACKUPS = 'xo-pool-metadata-backups'
exports.DIR_XO_POOL_METADATA_BACKUPS = DIR_XO_POOL_METADATA_BACKUPS
const { warn } = createLogger('xo:proxy:backups:RemoteAdapter')
const { warn } = createLogger('xo:backups:RemoteAdapter')
const compareTimestamp = (a, b) => a.timestamp - b.timestamp
const isMetadataFile = filename => filename.endsWith('.json')
const isVhdFile = filename => filename.endsWith('.vhd')
const noop = Function.prototype
const resolveRelativeFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)
@@ -67,8 +65,8 @@ const debounceResourceFactory = factory =>
return this._debounceResource(factory.apply(this, arguments))
}
exports.RemoteAdapter = class RemoteAdapter {
constructor(handler, { debounceResource, dirMode }) {
class RemoteAdapter {
constructor(handler, { debounceResource = res => res, dirMode } = {}) {
this._debounceResource = debounceResource
this._dirMode = dirMode
this._handler = handler
@@ -204,7 +202,7 @@ exports.RemoteAdapter = class RemoteAdapter {
}
_listLvmLogicalVolumes(devicePath, partition, results = []) {
return using(this._getLvmPhysicalVolume(devicePath, partition), async path => {
return Disposable.use(this._getLvmPhysicalVolume(devicePath, partition), async path => {
const lvs = await pvs(['lv_name', 'lv_path', 'lv_size', 'vg_name'], path)
const partitionId = partition !== undefined ? partition.id : ''
lvs.forEach((lv, i) => {
@@ -235,7 +233,7 @@ exports.RemoteAdapter = class RemoteAdapter {
fetchPartitionFiles(diskId, partitionId, paths) {
const { promise, reject, resolve } = pDefer()
using(
Disposable.use(
async function* () {
const files = yield this._usePartitionFiles(diskId, partitionId, paths)
const zip = new ZipFile()
@@ -375,7 +373,7 @@ exports.RemoteAdapter = class RemoteAdapter {
}
listPartitionFiles(diskId, partitionId, path) {
return using(this.getPartition(diskId, partitionId), async rootPath => {
return Disposable.use(this.getPartition(diskId, partitionId), async rootPath => {
path = resolveSubpath(rootPath, path)
const entriesMap = {}
@@ -395,7 +393,7 @@ exports.RemoteAdapter = class RemoteAdapter {
}
listPartitions(diskId) {
return using(this.getDisk(diskId), async devicePath => {
return Disposable.use(this.getDisk(diskId), async devicePath => {
const partitions = await listPartitions(devicePath)
if (partitions.length === 0) {
@@ -552,3 +550,9 @@ exports.RemoteAdapter = class RemoteAdapter {
return Object.defineProperty(JSON.parse(await this._handler.readFile(path)), '_filename', { value: path })
}
}
RemoteAdapter.prototype.cleanVm = function (vmDir) {
return Disposable.use(this._handler.lock(vmDir), () => cleanVm.apply(this, arguments))
}
exports.RemoteAdapter = RemoteAdapter

View File

@@ -15,7 +15,7 @@ exports.RestoreMetadataBackup = class RestoreMetadataBackup {
if (backupId.split('/')[0] === DIR_XO_POOL_METADATA_BACKUPS) {
return xapi.putResource(await handler.createReadStream(`${backupId}/data`), PATH_DB_DUMP, {
task: xapi.createTask('Import pool metadata'),
task: xapi.task_create('Import pool metadata'),
})
} else {
return String(await handler.readFile(`${backupId}/data.json`))

View File

@@ -164,7 +164,14 @@ const Task = {
}
},
wrapFn({ name, data, onLog }, fn) {
wrapFn(opts, fn) {
// compatibility with @decorateWith
if (typeof fn !== 'function') {
;[fn, opts] = [opts, fn]
}
const { name, data, onLog } = opts
return function () {
const evaluate = v => (typeof v === 'function' ? v.apply(this, arguments) : v)
return Task.run({ name: evaluate(name), data: evaluate(data), onLog }, () => fn.apply(this, arguments))

View File

@@ -14,7 +14,7 @@ exports.ContinuousReplicationWriter = class ContinuousReplicationWriter {
this._settings = settings
this._sr = sr
this.run = Task.wrapFn(
this.transfer = Task.wrapFn(
{
name: 'export',
data: ({ deltaExport }) => ({
@@ -23,7 +23,7 @@ exports.ContinuousReplicationWriter = class ContinuousReplicationWriter {
type: 'SR',
}),
},
this.run
this.transfer
)
}
@@ -51,26 +51,33 @@ exports.ContinuousReplicationWriter = class ContinuousReplicationWriter {
}
}
async run({ timestamp, deltaExport, sizeContainers }) {
const sr = this._sr
async prepare() {
const settings = this._settings
const { uuid: srUuid, $xapi: xapi } = this._sr
const { scheduleId, vm } = this._backup
// delete previous interrupted copies
ignoreErrors.call(asyncMapSettled(listReplicatedVms(xapi, scheduleId, undefined, vm.uuid), vm => vm.$destroy))
this._oldEntries = getOldEntries(settings.copyRetention - 1, listReplicatedVms(xapi, scheduleId, srUuid, vm.uuid))
if (settings.deleteFirst) {
await this._deleteOldEntries()
} else {
this.cleanup = this._deleteOldEntries
}
}
async _deleteOldEntries() {
return asyncMapSettled(this._oldEntries, vm => vm.$destroy())
}
async transfer({ timestamp, deltaExport, sizeContainers }) {
const sr = this._sr
const { job, scheduleId, vm } = this._backup
const { uuid: srUuid, $xapi: xapi } = sr
// delete previous interrupted copies
ignoreErrors.call(
asyncMapSettled(listReplicatedVms(xapi, scheduleId, undefined, vm.uuid), vm => xapi.VM_destroy(vm.$ref))
)
const oldVms = getOldEntries(settings.copyRetention - 1, listReplicatedVms(xapi, scheduleId, srUuid, vm.uuid))
const deleteOldBackups = () => asyncMapSettled(oldVms, vm => xapi.VM_destroy(vm.$ref))
const { deleteFirst } = settings
if (deleteFirst) {
await deleteOldBackups()
}
let targetVmRef
await Task.run({ name: 'transfer' }, async () => {
targetVmRef = await importDeltaVm(
@@ -108,9 +115,5 @@ exports.ContinuousReplicationWriter = class ContinuousReplicationWriter {
'xo:backup:vm': vm.uuid,
}),
])
if (!deleteFirst) {
await deleteOldBackups()
}
}
}

View File

@@ -14,7 +14,7 @@ const { getVmBackupDir } = require('./_getVmBackupDir')
const { packUuid } = require('./_packUuid')
const { Task } = require('./Task')
const { warn } = createLogger('xo:proxy:backups:DeltaBackupWriter')
const { warn } = createLogger('xo:backups:DeltaBackupWriter')
exports.DeltaBackupWriter = class DeltaBackupWriter {
constructor(backup, remoteId, settings) {
@@ -22,7 +22,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter {
this._backup = backup
this._settings = settings
this.run = Task.wrapFn(
this.transfer = Task.wrapFn(
{
name: 'export',
data: ({ deltaExport }) => ({
@@ -31,8 +31,10 @@ exports.DeltaBackupWriter = class DeltaBackupWriter {
type: 'remote',
}),
},
this.run
this.transfer
)
this[settings.deleteFirst ? 'prepare' : 'cleanup'] = this._deleteOldEntries
}
async checkBaseVdis(baseUuidToSrcVdi) {
@@ -70,23 +72,16 @@ exports.DeltaBackupWriter = class DeltaBackupWriter {
})
}
async run({ timestamp, deltaExport, sizeContainers }) {
async prepare() {
const adapter = this._adapter
const backup = this._backup
const settings = this._settings
const { scheduleId, vm } = this._backup
const { job, scheduleId, vm } = backup
const jobId = job.id
const handler = adapter.handler
const backupDir = getVmBackupDir(vm.uuid)
// TODO: clean VM backup directory
const oldBackups = getOldEntries(
const oldEntries = getOldEntries(
settings.exportRetention - 1,
await adapter.listVmBackups(vm.uuid, _ => _.mode === 'delta' && _.scheduleId === scheduleId)
)
this._oldEntries = oldEntries
// FIXME: implement optimized multiple VHDs merging with synthetic
// delta
@@ -98,21 +93,44 @@ exports.DeltaBackupWriter = class DeltaBackupWriter {
// The old backups will be eventually merged in future runs of the
// job.
const { maxMergedDeltasPerRun } = this._settings
if (oldBackups.length > maxMergedDeltasPerRun) {
oldBackups.length = maxMergedDeltasPerRun
if (oldEntries.length > maxMergedDeltasPerRun) {
oldEntries.length = maxMergedDeltasPerRun
}
const deleteOldBackups = () =>
Task.run({ name: 'merge' }, async () => {
let size = 0
// delete sequentially from newest to oldest to avoid unnecessary merges
for (let i = oldBackups.length; i-- > 0; ) {
size += await adapter.deleteDeltaVmBackups([oldBackups[i]])
}
return {
size,
}
})
if (settings.deleteFirst) {
await this._deleteOldEntries()
} else {
this.cleanup = this._deleteOldEntries
}
}
async _deleteOldEntries() {
return Task.run({ name: 'merge' }, async () => {
const adapter = this._adapter
const oldEntries = this._oldEntries
let size = 0
// delete sequentially from newest to oldest to avoid unnecessary merges
for (let i = oldEntries.length; i-- > 0; ) {
size += await adapter.deleteDeltaVmBackups([oldEntries[i]])
}
return {
size,
}
})
}
async transfer({ timestamp, deltaExport, sizeContainers }) {
const adapter = this._adapter
const backup = this._backup
const { job, scheduleId, vm } = backup
const jobId = job.id
const handler = adapter.handler
const backupDir = getVmBackupDir(vm.uuid)
// TODO: clean VM backup directory
const basename = formatFilenameDate(timestamp)
const vhds = mapValues(
@@ -142,11 +160,6 @@ exports.DeltaBackupWriter = class DeltaBackupWriter {
vmSnapshot: this._backup.exportedVm,
}
const { deleteFirst } = settings
if (deleteFirst) {
await deleteOldBackups()
}
const { size } = await Task.run({ name: 'transfer' }, async () => {
await Promise.all(
map(deltaExport.vdis, async (vdi, id) => {
@@ -201,10 +214,6 @@ exports.DeltaBackupWriter = class DeltaBackupWriter {
dirMode: backup.config.dirMode,
})
if (!deleteFirst) {
await deleteOldBackups()
}
// TODO: run cleanup?
}
}

View File

@@ -21,7 +21,7 @@ exports.PoolMetadataBackup = class PoolMetadataBackup {
_exportPoolMetadata() {
const xapi = this._pool.$xapi
return xapi.getResource(PATH_DB_DUMP, {
task: xapi.createTask('Export pool metadata'),
task: xapi.task_create('Export pool metadata'),
})
}

View File

@@ -16,7 +16,7 @@ const { getOldEntries } = require('./_getOldEntries')
const { Task } = require('./Task')
const { watchStreamSize } = require('./_watchStreamSize')
const { debug, warn } = createLogger('xo:proxy:backups:VmBackup')
const { debug, warn } = createLogger('xo:backups:VmBackup')
const forkDeltaExport = deltaExport =>
Object.create(deltaExport, {
@@ -121,9 +121,9 @@ exports.VmBackup = class VmBackup {
await vm.$assertHealthyVdiChains()
}
const snapshotRef = await vm[settings.checkpointSnapshot ? '$checkpoint' : '$snapshot'](
this._getSnapshotNameLabel(vm)
)
const snapshotRef = await vm[settings.checkpointSnapshot ? '$checkpoint' : '$snapshot']({
name_label: this._getSnapshotNameLabel(vm),
})
this.timestamp = Date.now()
await xapi.setFieldEntries('VM', snapshotRef, 'other_config', {
@@ -147,6 +147,8 @@ exports.VmBackup = class VmBackup {
const { exportedVm } = this
const baseVm = this._baseVm
await asyncMap(this._writers, writer => writer.prepare && writer.prepare())
const deltaExport = await exportDeltaVm(exportedVm, baseVm, {
fullVdisRequired: this._fullVdisRequired,
})
@@ -156,7 +158,7 @@ exports.VmBackup = class VmBackup {
await asyncMap(this._writers, async writer => {
try {
await writer.run({
await writer.transfer({
deltaExport: forkDeltaExport(deltaExport),
sizeContainers,
timestamp,
@@ -192,6 +194,8 @@ exports.VmBackup = class VmBackup {
speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,
size,
})
await asyncMap(this._writers, writer => writer.cleanup && writer.cleanup())
}
async _copyFull() {

View File

@@ -0,0 +1,4 @@
exports.isMetadataFile = filename => filename.endsWith('.json')
exports.isVhdFile = filename => filename.endsWith('.vhd')
exports.isXvaFile = filename => filename.endsWith('.xva')
exports.isXvaSumFile = filename => filename.endsWith('.xva.cheksum')

View File

@@ -0,0 +1,151 @@
const Disposable = require('promise-toolbox/Disposable')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const { compose } = require('@vates/compose')
const { createDebounceResource } = require('@vates/disposable/debounceResource')
const { deduped } = require('@vates/disposable/deduped')
const { getHandler } = require('@xen-orchestra/fs')
const { parseDuration } = require('@vates/parse-duration')
const { Xapi } = require('@xen-orchestra/xapi')
const { Backup } = require('./Backup')
const { RemoteAdapter } = require('./RemoteAdapter')
const { Task } = require('./Task')
class BackupWorker {
#config
#job
#recordToXapi
#remoteOptions
#remotes
#schedule
#xapiOptions
#xapis
constructor({ config, job, recordToXapi, remoteOptions, remotes, resourceCacheDelay, schedule, xapiOptions, xapis }) {
this.#config = config
this.#job = job
this.#recordToXapi = recordToXapi
this.#remoteOptions = remoteOptions
this.#remotes = remotes
this.#schedule = schedule
this.#xapiOptions = xapiOptions
this.#xapis = xapis
const debounceResource = createDebounceResource()
debounceResource.defaultDelay = parseDuration(resourceCacheDelay)
this.debounceResource = debounceResource
}
run() {
return new Backup({
config: this.#config,
getAdapter: remoteId => this.getAdapter(this.#remotes[remoteId]),
getConnectedRecord: Disposable.factory(async function* getConnectedRecord(type, uuid) {
const xapiId = this.#recordToXapi[uuid]
if (xapiId === undefined) {
throw new Error('no XAPI associated to ' + uuid)
}
const xapi = yield this.getXapi(this.#xapis[xapiId])
return xapi.getRecordByUuid(type, uuid)
}).bind(this),
job: this.#job,
schedule: this.#schedule,
}).run()
}
getAdapter = Disposable.factory(this.getAdapter)
getAdapter = deduped(this.getAdapter, remote => [remote.url])
getAdapter = compose(this.getAdapter, function (resource) {
return this.debounceResource(resource)
})
async *getAdapter(remote) {
const handler = getHandler(remote, this.#remoteOptions)
await handler.sync()
try {
yield new RemoteAdapter(handler, {
debounceResource: this.debounceResource,
dirMode: this.#config.dirMode,
})
} finally {
await handler.forget()
}
}
getXapi = Disposable.factory(this.getXapi)
getXapi = deduped(this.getXapi, ({ url }) => [url])
getXapi = compose(this.getXapi, function (resource) {
return this.debounceResource(resource)
})
async *getXapi({ credentials: { username: user, password }, ...opts }) {
const xapi = new Xapi({
...this.#xapiOptions,
...opts,
auth: {
user,
password,
},
})
await xapi.connect()
try {
await xapi.objectsFetched
yield xapi
} finally {
await xapi.disconnect()
}
}
}
// Received message:
//
// Message {
// action: 'run'
// data: object
// runWithLogs: boolean
// }
//
// Sent message:
//
// Message {
// type: 'log' | 'result'
// data?: object
// status?: 'success' | 'failure'
// result?: any
// }
process.on('message', async message => {
if (message.action === 'run') {
const backupWorker = new BackupWorker(message.data)
try {
const result = message.runWithLogs
? await Task.run(
{
name: 'backup run',
onLog: data =>
process.send({
data,
type: 'log',
}),
},
() => backupWorker.run()
)
: await backupWorker.run()
process.send({
type: 'result',
result,
status: 'success',
})
} catch (error) {
process.send({
type: 'result',
result: error,
status: 'failure',
})
} finally {
await ignoreErrors.call(backupWorker.debounceResource.flushAll())
process.disconnect()
}
}
})

View File

@@ -0,0 +1,277 @@
const assert = require('assert')
const limitConcurrency = require('limit-concurrency-decorator').default
const { asyncMap } = require('@xen-orchestra/async-map')
const { default: Vhd, mergeVhd } = require('vhd-lib')
const { dirname, resolve } = require('path')
const { DISK_TYPE_DIFFERENCING } = require('vhd-lib/dist/_constants')
const { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } = require('./_backupType')
const { isValidXva } = require('./isValidXva')
// chain is an array of VHDs from child to parent
//
// the whole chain will be merged into parent, parent will be renamed to child
// and all the others will deleted
const mergeVhdChain = limitConcurrency(1)(async function mergeVhdChain(chain, { handler, onLog, remove, merge }) {
assert(chain.length >= 2)
let child = chain[0]
const parent = chain[chain.length - 1]
const children = chain.slice(0, -1).reverse()
chain
.slice(1)
.reverse()
.forEach(parent => {
onLog(`the parent ${parent} of the child ${child} is unused`)
})
if (merge) {
// `mergeVhd` does not work with a stream, either
// - make it accept a stream
// - or create synthetic VHD which is not a stream
if (children.length !== 1) {
// TODO: implement merging multiple children
children.length = 1
child = children[0]
}
let done, total
const handle = setInterval(() => {
if (done !== undefined) {
onLog(`merging ${child}: ${done}/${total}`)
}
}, 10e3)
await mergeVhd(
handler,
parent,
handler,
child,
// children.length === 1
// ? child
// : await createSyntheticStream(handler, children),
{
onProgress({ done: d, total: t }) {
done = d
total = t
},
}
)
clearInterval(handle)
}
await Promise.all([
remove && handler.rename(parent, child),
asyncMap(children.slice(0, -1), child => {
onLog(`the VHD ${child} is unused`)
return remove && handler.unlink(child)
}),
])
})
const noop = Function.prototype
exports.cleanVm = async function cleanVm(vmDir, { remove, merge, onLog = noop }) {
const handler = this._handler
const vhds = new Set()
const vhdParents = { __proto__: null }
const vhdChildren = { __proto__: null }
// remove broken VHDs
await asyncMap(
await handler.list(`${vmDir}/vdis`, {
filter: isVhdFile,
prependDir: true,
}),
async path => {
try {
const vhd = new Vhd(handler, path)
await vhd.readHeaderAndFooter()
vhds.add(path)
if (vhd.footer.diskType === DISK_TYPE_DIFFERENCING) {
const parent = resolve(dirname(path), vhd.header.parentUnicodeName)
vhdParents[path] = parent
if (parent in vhdChildren) {
const error = new Error('this script does not support multiple VHD children')
error.parent = parent
error.child1 = vhdChildren[parent]
error.child2 = path
throw error // should we throw?
}
vhdChildren[parent] = path
}
} catch (error) {
onLog(`error while checking the VHD with path ${path}`)
if (error?.code === 'ERR_ASSERTION' && remove) {
await handler.unlink(path)
}
}
}
)
// remove VHDs with missing ancestors
{
const deletions = []
// return true if the VHD has been deleted or is missing
const deleteIfOrphan = vhd => {
const parent = vhdParents[vhd]
if (parent === undefined) {
return
}
// no longer needs to be checked
delete vhdParents[vhd]
deleteIfOrphan(parent)
if (!vhds.has(parent)) {
vhds.delete(vhd)
onLog(`the parent ${parent} of the VHD ${vhd} is missing`)
if (remove) {
deletions.push(handler.unlink(vhd))
}
}
}
// > A property that is deleted before it has been visited will not be
// > visited later.
// >
// > -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#Deleted_added_or_modified_properties
for (const child in vhdParents) {
deleteIfOrphan(child)
}
await Promise.all(deletions)
}
const jsons = []
const xvas = new Set()
const xvaSums = []
const entries = await handler.list(vmDir, {
prependDir: true,
})
entries.forEach(path => {
if (isMetadataFile(path)) {
jsons.push(path)
} else if (isXvaFile(path)) {
xvas.add(path)
} else if (isXvaSumFile(path)) {
xvaSums.push(path)
}
})
await asyncMap(xvas, async path => {
// check is not good enough to delete the file, the best we can do is report
// it
if (!(await isValidXva(path))) {
onLog(`the XVA with path ${path} is potentially broken`)
}
})
const unusedVhds = new Set(vhds)
const unusedXvas = new Set(xvas)
// compile the list of unused XVAs and VHDs, and remove backup metadata which
// reference a missing XVA/VHD
await asyncMap(jsons, async json => {
const metadata = JSON.parse(await handler.readFile(json))
const { mode } = metadata
if (mode === 'full') {
const linkedXva = resolve(vmDir, metadata.xva)
if (xvas.has(linkedXva)) {
unusedXvas.delete(linkedXva)
} else {
onLog(`the XVA linked to the metadata ${json} is missing`)
if (remove) {
await handler.unlink(json)
}
}
} else if (mode === 'delta') {
const linkedVhds = (() => {
const { vhds } = metadata
return Object.keys(vhds).map(key => resolve(vmDir, vhds[key]))
})()
// FIXME: find better approach by keeping as much of the backup as
// possible (existing disks) even if one disk is missing
if (linkedVhds.every(_ => vhds.has(_))) {
linkedVhds.forEach(_ => unusedVhds.delete(_))
} else {
onLog(`Some VHDs linked to the metadata ${json} are missing`)
if (remove) {
await handler.unlink(json)
}
}
}
})
// TODO: parallelize by vm/job/vdi
const unusedVhdsDeletion = []
{
// VHD chains (as list from child to ancestor) to merge indexed by last
// ancestor
const vhdChainsToMerge = { __proto__: null }
const toCheck = new Set(unusedVhds)
const getUsedChildChainOrDelete = vhd => {
if (vhd in vhdChainsToMerge) {
const chain = vhdChainsToMerge[vhd]
delete vhdChainsToMerge[vhd]
return chain
}
if (!unusedVhds.has(vhd)) {
return [vhd]
}
// no longer needs to be checked
toCheck.delete(vhd)
const child = vhdChildren[vhd]
if (child !== undefined) {
const chain = getUsedChildChainOrDelete(child)
if (chain !== undefined) {
chain.push(vhd)
return chain
}
}
onLog(`the VHD ${vhd} is unused`)
if (remove) {
unusedVhdsDeletion.push(handler.unlink(vhd))
}
}
toCheck.forEach(vhd => {
vhdChainsToMerge[vhd] = getUsedChildChainOrDelete(vhd)
})
Object.keys(vhdChainsToMerge).forEach(key => {
const chain = vhdChainsToMerge[key]
if (chain !== undefined) {
unusedVhdsDeletion.push(mergeVhdChain(chain, { onLog, remove, merge }))
}
})
}
await Promise.all([
unusedVhdsDeletion,
asyncMap(unusedXvas, path => {
onLog(`the XVA ${path} is unused`)
return remove && handler.unlink(path)
}),
asyncMap(xvaSums, path => {
// no need to handle checksums for XVAs deleted by the script, they will be handled by `unlink()`
if (!xvas.has(path.slice(0, -'.checksum'.length))) {
onLog(`the XVA checksum ${path} is unused`)
return remove && handler.unlink(path)
}
}),
])
}

View File

@@ -143,7 +143,7 @@ exports.importDeltaVm = defer(async function importDeltaVm(
$defer,
deltaVm,
sr,
{ cancelToken = CancelToken.none, detectBase = true, mapVdisSrs = {} } = {}
{ cancelToken = CancelToken.none, detectBase = true, mapVdisSrs = {}, newMacAddresses = false } = {}
) {
const { version } = deltaVm
if (compareVersions(version, '1.0.0') < 0) {
@@ -213,6 +213,7 @@ exports.importDeltaVm = defer(async function importDeltaVm(
},
{
bios_strings: vmRecord.bios_strings,
generateMacSeed: newMacAddresses,
suspend_VDI: suspendVdi?.$ref,
}
)
@@ -325,11 +326,16 @@ exports.importDeltaVm = defer(async function importDeltaVm(
}
if (network) {
return xapi.VIF_create({
...vif,
network: network.$ref,
VM: vmRef,
})
return xapi.VIF_create(
{
...vif,
network: network.$ref,
VM: vmRef,
},
{
MAC: newMacAddresses ? undefined : vif.MAC,
}
)
}
}),
])

View File

@@ -3,7 +3,7 @@ const { createLogger } = require('@xen-orchestra/log')
const { createParser } = require('parse-pairs')
const { execFile } = require('child_process')
const { debug } = createLogger('xo:proxy:api')
const { debug } = createLogger('xo:backups:listPartitions')
const IGNORED_PARTITION_TYPES = {
// https://github.com/jhermsmeier/node-mbr/blob/master/lib/partition.js#L38

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.7.0",
"version": "0.9.1",
"engines": {
"node": ">=14.5"
},
@@ -16,9 +16,12 @@
"postversion": "npm publish --access public"
},
"dependencies": {
"@vates/disposable": "^0.1.0",
"@vates/compose": "^2.0.0",
"@vates/disposable": "^0.1.1",
"@vates/multi-key-map": "^0.1.0",
"@vates/parse-duration": "^0.1.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^0.14.0",
"@xen-orchestra/log": "^0.2.0",
"@xen-orchestra/template": "^0.1.0",
"compare-versions": "^3.6.0",
@@ -31,12 +34,12 @@
"lodash": "^4.17.20",
"node-zone": "^0.4.0",
"parse-pairs": "^1.1.0",
"promise-toolbox": "^0.17.0",
"promise-toolbox": "^0.18.0",
"vhd-lib": "^1.0.0",
"yazl": "^2.5.1"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^0.4.4"
"@xen-orchestra/xapi": "^0.6.0"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -0,0 +1,38 @@
const path = require('path')
const { createLogger } = require('@xen-orchestra/log')
const { fork } = require('child_process')
const { warn } = createLogger('xo:backups:backupWorker')
const PATH = path.resolve(__dirname, '_backupWorker.js')
exports.runBackupWorker = function runBackupWorker(params, onLog) {
return new Promise((resolve, reject) => {
const worker = fork(PATH)
worker.on('exit', code => reject(new Error(`worker exited with code ${code}`)))
worker.on('error', reject)
worker.on('message', message => {
try {
if (message.type === 'result') {
if (message.status === 'success') {
resolve(message.result)
} else {
reject(message.result)
}
} else if (message.type === 'log') {
onLog(message.data)
}
} catch (error) {
warn(error)
}
})
worker.send({
action: 'run',
data: params,
runWithLogs: onLog !== undefined,
})
})
}

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -18,7 +18,7 @@
"preferGlobal": true,
"dependencies": {
"golike-defer": "^0.5.1",
"xen-api": "^0.30.0"
"xen-api": "^0.31.0"
},
"scripts": {
"postversion": "npm publish"

View File

@@ -1,24 +0,0 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -1,24 +0,0 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -4,6 +4,8 @@
[![Package Version](https://badgen.net/npm/v/@xen-orchestra/defined)](https://npmjs.org/package/@xen-orchestra/defined) ![License](https://badgen.net/npm/license/@xen-orchestra/defined) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@xen-orchestra/defined)](https://bundlephobia.com/result?p=@xen-orchestra/defined) [![Node compatibility](https://badgen.net/npm/node/@xen-orchestra/defined)](https://npmjs.org/package/@xen-orchestra/defined)
> Utilities to help handling (possibly) undefined values
## Install
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/defined):

View File

@@ -3,8 +3,7 @@
"name": "@xen-orchestra/defined",
"version": "0.0.0",
"license": "ISC",
"description": "",
"keywords": [],
"description": "Utilities to help handling (possibly) undefined values",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/defined",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
@@ -27,13 +26,10 @@
"engines": {
"node": ">=6"
},
"dependencies": {},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^7.0.2",
"rimraf": "^3.0.0"
},

View File

@@ -1,5 +1,3 @@
// @flow
// Usage:
//
// ```js
@@ -41,7 +39,7 @@ export default function defined() {
// const getFriendName = _ => _.friends[0].name
// const friendName = get(getFriendName, props.user)
// ```
export const get = (accessor: (input: ?any) => any, arg: ?any) => {
export const get = (accessor, arg) => {
try {
return accessor(arg)
} catch (error) {
@@ -60,4 +58,4 @@ export const get = (accessor: (input: ?any) => any, arg: ?any) => {
// _ => new ProxyAgent(_)
// )
// ```
export const ifDef = (value: ?any, thenFn: (value: any) => any) => (value !== undefined ? thenFn(value) : value)
export const ifDef = (value, thenFn) => (value !== undefined ? thenFn(value) : value)

View File

@@ -1,24 +0,0 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -4,6 +4,8 @@
[![Package Version](https://badgen.net/npm/v/@xen-orchestra/emit-async)](https://npmjs.org/package/@xen-orchestra/emit-async) ![License](https://badgen.net/npm/license/@xen-orchestra/emit-async) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@xen-orchestra/emit-async)](https://bundlephobia.com/result?p=@xen-orchestra/emit-async) [![Node compatibility](https://badgen.net/npm/node/@xen-orchestra/emit-async)](https://npmjs.org/package/@xen-orchestra/emit-async)
> Emit an event for async listeners to settle
## Install
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/emit-async):

View File

@@ -3,8 +3,7 @@
"name": "@xen-orchestra/emit-async",
"version": "0.0.0",
"license": "ISC",
"description": "",
"keywords": [],
"description": "Emit an event for async listeners to settle",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/emit-async",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
@@ -27,12 +26,10 @@
"engines": {
"node": ">=6"
},
"dependencies": {},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^7.0.2",
"rimraf": "^3.0.0"
},

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -1,10 +1,9 @@
{
"private": false,
"name": "@xen-orchestra/fs",
"version": "0.13.0",
"version": "0.14.0",
"license": "AGPL-3.0-or-later",
"description": "The File System for Xen Orchestra backups.",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
@@ -32,7 +31,8 @@
"get-stream": "^6.0.0",
"limit-concurrency-decorator": "^0.4.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.17.0",
"promise-toolbox": "^0.18.0",
"proper-lockfile": "^4.1.2",
"readable-stream": "^3.0.6",
"through2": "^4.0.2",
"tmp": "^0.2.1",

View File

@@ -18,6 +18,7 @@ import { createChecksumStream, validChecksumOfReadStream } from './checksum'
const { dirname } = path.posix
type Data = Buffer | Readable | string
type Disposable<T> = {| dispose: () => void | Promise<void>, value?: T |}
type FileDescriptor = {| fd: mixed, path: string |}
type LaxReadable = Readable & Object
type LaxWritable = Writable & Object
@@ -72,6 +73,7 @@ class PrefixWrapper {
}
export default class RemoteHandlerAbstract {
_highWaterMark: number
_remote: Object
_timeout: number
@@ -84,7 +86,7 @@ export default class RemoteHandlerAbstract {
throw new Error('Incorrect remote type')
}
}
;({ timeout: this._timeout = DEFAULT_TIMEOUT } = options)
;({ highWaterMark: this._highWaterMark, timeout: this._timeout = DEFAULT_TIMEOUT } = options)
const sharedLimit = limit(options.maxParallelOperations ?? DEFAULT_MAX_PARALLEL_OPERATIONS)
this.closeFile = sharedLimit(this.closeFile)
@@ -163,24 +165,26 @@ export default class RemoteHandlerAbstract {
file = normalizePath(file)
}
const path = typeof file === 'string' ? file : file.path
const streamP = timeout.call(this._createReadStream(file, options), this._timeout).then(stream => {
// detect early errors
let promise = fromEvent(stream, 'readable')
const streamP = timeout
.call(this._createReadStream(file, { ...options, highWaterMark: this._highWaterMark }), this._timeout)
.then(stream => {
// detect early errors
let promise = fromEvent(stream, 'readable')
// try to add the length prop if missing and not a range stream
if (stream.length === undefined && options.end === undefined && options.start === undefined) {
promise = Promise.all([
promise,
ignoreErrors.call(
this._getSize(file).then(size => {
stream.length = size
})
),
])
}
// try to add the length prop if missing and not a range stream
if (stream.length === undefined && options.end === undefined && options.start === undefined) {
promise = Promise.all([
promise,
ignoreErrors.call(
this._getSize(file).then(size => {
stream.length = size
})
),
])
}
return promise.then(() => stream)
})
return promise.then(() => stream)
})
if (!checksum) {
return streamP
@@ -213,7 +217,6 @@ export default class RemoteHandlerAbstract {
input: Readable | Promise<Readable>,
{ checksum = true, dirMode }: { checksum?: boolean, dirMode?: number } = {}
): Promise<void> {
path = normalizePath(path)
return this._outputStream(normalizePath(path), await input, {
checksum,
dirMode,
@@ -260,6 +263,11 @@ export default class RemoteHandlerAbstract {
return entries
}
async lock(path: string): Promise<Disposable> {
path = normalizePath(path)
return { dispose: await this._lock(path) }
}
async mkdir(dir: string, { mode }: { mode?: number } = {}): Promise<void> {
await this.__mkdir(normalizePath(dir), { mode })
}
@@ -409,7 +417,7 @@ export default class RemoteHandlerAbstract {
async _createOutputStream(file: File, { dirMode, ...options }: Object = {}): Promise<LaxWritable> {
try {
return await this._createWriteStream(file, options)
return await this._createWriteStream(file, { ...options, highWaterMark: this._highWaterMark })
} catch (error) {
if (typeof file !== 'string' || error.code !== 'ENOENT') {
throw error
@@ -424,6 +432,8 @@ export default class RemoteHandlerAbstract {
throw new Error('Not implemented')
}
// createWriteStream takes highWaterMark as option even if it's not documented.
// Source: https://stackoverflow.com/questions/55026306/how-to-set-writeable-highwatermark
async _createWriteStream(file: File, options: Object): Promise<LaxWritable> {
throw new Error('Not implemented')
}
@@ -435,6 +445,10 @@ export default class RemoteHandlerAbstract {
return {}
}
async _lock(path: string): Promise<Function> {
return () => Promise.resolve()
}
async _getSize(file: File): Promise<number> {
throw new Error('Not implemented')
}
@@ -501,7 +515,7 @@ export default class RemoteHandlerAbstract {
}
_readFile(file: string, options?: Object): Promise<Buffer> {
return this._createReadStream(file, options).then(getStream.buffer)
return this._createReadStream(file, { ...options, highWaterMark: this._highWaterMark }).then(getStream.buffer)
}
async _rename(oldPath: string, newPath: string) {

View File

@@ -1,10 +1,22 @@
import df from '@sindresorhus/df'
import fs from 'fs-extra'
import { fromEvent } from 'promise-toolbox'
import lockfile from 'proper-lockfile'
import { fromEvent, retry } from 'promise-toolbox'
import RemoteHandlerAbstract from './abstract'
export default class LocalHandler extends RemoteHandlerAbstract {
constructor(remote, opts = {}) {
super(remote)
this._retriesOnEagain = {
delay: 1e3,
retries: 9,
...opts.retriesOnEagain,
when: {
code: 'EAGAIN',
},
}
}
get type() {
return 'file'
}
@@ -71,6 +83,10 @@ export default class LocalHandler extends RemoteHandlerAbstract {
return fs.readdir(this._getFilePath(dir))
}
_lock(path) {
return lockfile.lock(path)
}
_mkdir(dir, { mode }) {
return fs.mkdir(this._getFilePath(dir), { mode })
}
@@ -92,7 +108,8 @@ export default class LocalHandler extends RemoteHandlerAbstract {
}
async _readFile(file, options) {
return fs.readFile(this._getFilePath(file), options)
const filePath = this._getFilePath(file)
return await retry(() => fs.readFile(filePath, options), this._retriesOnEagain)
}
async _rename(oldPath, newPath) {
@@ -114,7 +131,8 @@ export default class LocalHandler extends RemoteHandlerAbstract {
}
async _unlink(file) {
return fs.unlink(this._getFilePath(file))
const filePath = this._getFilePath(file)
return await retry(() => fs.unlink(filePath), this._retriesOnEagain)
}
_writeFd(file, buffer, position) {

View File

@@ -1,24 +0,0 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -4,6 +4,8 @@
[![Package Version](https://badgen.net/npm/v/@xen-orchestra/log)](https://npmjs.org/package/@xen-orchestra/log) ![License](https://badgen.net/npm/license/@xen-orchestra/log) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@xen-orchestra/log)](https://bundlephobia.com/result?p=@xen-orchestra/log) [![Node compatibility](https://badgen.net/npm/node/@xen-orchestra/log)](https://npmjs.org/package/@xen-orchestra/log)
> Logging system with decoupled producers/consumer
## Install
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/log):

View File

@@ -3,8 +3,7 @@
"name": "@xen-orchestra/log",
"version": "0.2.0",
"license": "ISC",
"description": "",
"keywords": [],
"description": "Logging system with decoupled producers/consumer",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/log",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
@@ -31,7 +30,7 @@
},
"dependencies": {
"lodash": "^4.17.4",
"promise-toolbox": "^0.17.0"
"promise-toolbox": "^0.18.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -57,15 +57,6 @@ if (process.stdout !== undefined && process.stdout.isTTY && process.stderr !== u
for (let i = 0, n = namespace.length; i < n; ++i) {
hash = ((hash << 5) - hash + namespace.charCodeAt(i)) | 0
}
// // select a hue (HSV)
// const h = (Math.abs(hash) % 20) * 18
// // convert to RGB
// const f = (n, k = (n + h / 60) % 6) =>
// Math.round(255 * (1 - Math.max(Math.min(k, 4 - k, 1), 0)))
// const r = f(5)
// const g = f(3)
// const b = f(1)
// return ansi(`38;2;${r};${g};${b}`, namespace)
return ansi(`1;38;5;${NAMESPACE_COLORS[Math.abs(hash) % NAMESPACE_COLORS.length]}`, namespace)
}
} else {

View File

@@ -1,24 +0,0 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -3,8 +3,6 @@
"name": "@xen-orchestra/mixin",
"version": "0.0.0",
"license": "ISC",
"description": "",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/mixin",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
@@ -35,7 +33,6 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-dev": "^1.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^7.0.2",
"rimraf": "^3.0.0"
},

View File

@@ -1,24 +0,0 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -30,7 +30,7 @@
"rimraf": "^3.0.0"
},
"dependencies": {
"@vates/read-chunk": "^0.1.0"
"@vates/read-chunk": "^0.1.2"
},
"author": {
"name": "Vates SAS",

View File

@@ -1,24 +0,0 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -4,6 +4,8 @@
[![Package Version](https://badgen.net/npm/v/@xen-orchestra/proxy-cli)](https://npmjs.org/package/@xen-orchestra/proxy-cli) ![License](https://badgen.net/npm/license/@xen-orchestra/proxy-cli) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@xen-orchestra/proxy-cli)](https://bundlephobia.com/result?p=@xen-orchestra/proxy-cli) [![Node compatibility](https://badgen.net/npm/node/@xen-orchestra/proxy-cli)](https://npmjs.org/package/@xen-orchestra/proxy-cli)
> CLI for @xen-orchestra/proxy
## Install
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/proxy-cli):

View File

@@ -3,7 +3,7 @@
"name": "@xen-orchestra/proxy-cli",
"version": "0.2.0",
"license": "AGPL-3.0-or-later",
"description": "",
"description": "CLI for @xen-orchestra/proxy",
"keywords": [
"backup",
"proxy",
@@ -30,7 +30,7 @@
},
"dependencies": {
"@iarna/toml": "^2.2.0",
"@vates/read-chunk": "^0.1.1",
"@vates/read-chunk": "^0.1.2",
"ansi-colors": "^4.1.1",
"app-conf": "^0.9.0",
"content-type": "^1.0.4",
@@ -38,7 +38,7 @@
"getopts": "^2.2.3",
"http-request-plus": "^0.9.1",
"json-rpc-protocol": "^0.13.1",
"promise-toolbox": "^0.15.1",
"promise-toolbox": "^0.18.1",
"pump": "^3.0.0",
"pumpify": "^2.0.1",
"split2": "^3.1.1"
@@ -49,7 +49,6 @@
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.7.4",
"@babel/plugin-proposal-optional-chaining": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^7.0.2",
"rimraf": "^3.0.0"
},

View File

@@ -1,24 +0,0 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -2,6 +2,8 @@
# @xen-orchestra/proxy
> XO Proxy used to remotely execute backup jobs
## Contributions
Contributions are _very_ welcomed, either on the documentation or on

View File

@@ -140,6 +140,7 @@ declare namespace backup {
function listPoolMetadataBackups(_: {
remotes: { [id: string]: Remote }
}): { [remoteId: string]: { [poolUuid: string]: object[] } }
function listVmBackups(_: {
remotes: { [remoteId: string]: Remote }
}): { [remoteId: string]: { [vmUuid: string]: object[] } }

View File

@@ -1,9 +1,9 @@
{
"private": true,
"name": "@xen-orchestra/proxy",
"version": "0.11.5",
"version": "0.12.0",
"license": "AGPL-3.0-or-later",
"description": "",
"description": "XO Proxy used to remotely execute backup jobs",
"keywords": [
"backup",
"proxy",
@@ -32,41 +32,40 @@
},
"dependencies": {
"@iarna/toml": "^2.2.0",
"@koa/router": "^10.0.0",
"@vates/compose": "^2.0.0",
"@vates/decorate-with": "^0.0.1",
"@vates/disposable": "^0.1.0",
"@vates/disposable": "^0.1.1",
"@vates/parse-duration": "^0.1.0",
"@xen-orchestra/backups": "^0.7.0",
"@xen-orchestra/backups": "^0.9.1",
"@xen-orchestra/emit-async": "^0.0.0",
"@xen-orchestra/fs": "^0.13.0",
"@xen-orchestra/fs": "^0.14.0",
"@xen-orchestra/log": "^0.2.0",
"@xen-orchestra/self-signed": "^0.1.0",
"@xen-orchestra/xapi": "^0.4.4",
"ajv": "^6.10.0",
"@xen-orchestra/xapi": "^0.6.0",
"ajv": "^8.0.3",
"app-conf": "^0.9.0",
"async-iterator-to-stream": "^1.1.0",
"compare-versions": "^3.4.0",
"fs-extra": "^8.1.0",
"get-stream": "^5.1.0",
"fs-extra": "^9.1.0",
"get-stream": "^6.0.0",
"getopts": "^2.2.3",
"golike-defer": "^0.5.1",
"http-server-plus": "^0.11.0",
"json-rpc-protocol": "^0.13.1",
"jsonrpc-websocket-client": "^0.5.0",
"koa": "^2.5.1",
"koa-compress": "^3.0.0",
"koa-compress": "^4.0.0",
"koa-helmet": "^5.1.0",
"koa-router": "^7.4.0",
"lodash": "^4.17.10",
"ms": "^2.1.2",
"node-zone": "^0.4.0",
"parse-pairs": "^1.0.0",
"promise-toolbox": "^0.17.0",
"promise-toolbox": "^0.18.0",
"source-map-support": "^0.5.16",
"stoppable": "^1.0.6",
"xdg-basedir": "^4.0.0",
"xen-api": "^0.30.0",
"xo-common": "^0.6.0"
"xen-api": "^0.31.0",
"xo-common": "^0.7.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -8,7 +8,7 @@ import getStream from 'get-stream'
import helmet from 'koa-helmet'
import Koa from 'koa'
import once from 'lodash/once'
import Router from 'koa-router'
import Router from '@koa/router'
import Zone from 'node-zone'
import { createLogger } from '@xen-orchestra/log'
@@ -19,7 +19,11 @@ const { debug, warn } = createLogger('xo:proxy:api')
const ndJsonStream = asyncIteratorToStream(async function* (responseId, iterable) {
yield format.response(responseId, { $responseType: 'ndjson' }) + '\n'
for await (const data of iterable) {
yield JSON.stringify(data) + '\n'
try {
yield JSON.stringify(data) + '\n'
} catch (error) {
warn('ndJsonStream', { error })
}
}
})

View File

@@ -3,7 +3,6 @@ import fromCallback from 'promise-toolbox/fromCallback'
import fromEvent from 'promise-toolbox/fromEvent'
import JsonRpcWebsocketClient from 'jsonrpc-websocket-client'
import parsePairs from 'parse-pairs'
import using from 'promise-toolbox/using'
import { createLogger } from '@xen-orchestra/log/dist'
import { deduped } from '@vates/disposable/deduped'
import { execFile, spawn } from 'child_process'
@@ -20,7 +19,7 @@ const getUpdater = deduped(async function () {
})
const callUpdate = params =>
using(
Disposable.use(
getUpdater(),
updater =>
new Promise((resolve, reject) => {
@@ -144,7 +143,7 @@ export default class Appliance {
],
},
updater: {
getLocalManifest: () => using(getUpdater(), _ => _.call('getLocalManifest')),
getLocalManifest: () => Disposable.use(getUpdater(), _ => _.call('getLocalManifest')),
getState: () => callUpdate(),
upgrade: () => callUpdate({ upgrade: true }),
},
@@ -154,6 +153,6 @@ export default class Appliance {
// A proxy can be bound to a unique license
getSelfLicense() {
return using(getUpdater(), _ => _.call('getSelfLicenses').then(licenses => licenses[0]))
return Disposable.use(getUpdater(), _ => _.call('getSelfLicenses').then(licenses => licenses[0]))
}
}

View File

@@ -1,7 +1,6 @@
import defer from 'golike-defer'
import Disposable from 'promise-toolbox/Disposable'
import fromCallback from 'promise-toolbox/fromCallback'
import using from 'promise-toolbox/using'
import { asyncMap } from '@xen-orchestra/async-map'
import { Backup } from '@xen-orchestra/backups/Backup'
import { compose } from '@vates/compose'
@@ -15,6 +14,7 @@ import { ImportVmBackup } from '@xen-orchestra/backups/ImportVmBackup'
import { Readable } from 'stream'
import { RemoteAdapter } from '@xen-orchestra/backups/RemoteAdapter'
import { RestoreMetadataBackup } from '@xen-orchestra/backups/RestoreMetadataBackup'
import { runBackupWorker } from '@xen-orchestra/backups/runBackupWorker'
import { Task } from '@xen-orchestra/backups/Task'
import { Xapi } from '@xen-orchestra/xapi'
@@ -46,26 +46,42 @@ export default class Backups {
await fromCallback(execFile, 'pvscan', ['--cache'])
})
let run = ({ recordToXapi, remotes, xapis, ...rest }) =>
new Backup({
...rest,
let run = (params, onLog) => {
// don't change config during backup execution
const config = app.config.get('backups')
if (config.disableWorkers) {
const { recordToXapi, remotes, xapis, ...rest } = params
return new Backup({
...rest,
// don't change config during backup execution
config: app.config.get('backups'),
config,
// pass getAdapter in order to mutualize the adapter resources usage
getAdapter: remoteId => this.getAdapter(remotes[remoteId]),
// pass getAdapter in order to mutualize the adapter resources usage
getAdapter: remoteId => this.getAdapter(remotes[remoteId]),
getConnectedRecord: Disposable.factory(async function* getConnectedRecord(type, uuid) {
const xapiId = recordToXapi[uuid]
if (xapiId === undefined) {
throw new Error('no XAPI associated to ' + uuid)
}
getConnectedRecord: Disposable.factory(async function* getConnectedRecord(type, uuid) {
const xapiId = recordToXapi[uuid]
if (xapiId === undefined) {
throw new Error('no XAPI associated to ' + uuid)
}
const xapi = yield this.getXapi(xapis[xapiId])
return xapi.getRecordByUuid(type, uuid)
}).bind(this),
}).run()
const xapi = yield this.getXapi(xapis[xapiId])
return xapi.getRecordByUuid(type, uuid)
}).bind(this),
}).run()
} else {
return runBackupWorker(
{
config,
remoteOptions: app.config.get('remoteOptions'),
resourceCacheDelay: app.config.getDuration('resourceCacheDelay'),
xapiOptions: app.config.get('xapiOptions'),
...params,
},
onLog
)
}
}
const runningJobs = { __proto__: null }
run = (run => {
@@ -98,8 +114,8 @@ export default class Backups {
return run.apply(this, arguments)
})(run)
run = (run => async (params, onLog) => {
if (onLog === undefined) {
return run(params)
if (onLog === undefined || !app.config.get('backups').disableWorkers) {
return run(params, onLog)
}
const { job, schedule } = params
@@ -126,7 +142,8 @@ export default class Backups {
app.api.addMethods({
backup: {
deleteMetadataBackup: [
({ backupId, remote }) => using(this.getAdapter(remote), adapter => adapter.deleteMetadataBackup(backupId)),
({ backupId, remote }) =>
Disposable.use(this.getAdapter(remote), adapter => adapter.deleteMetadataBackup(backupId)),
{
description: 'delete Metadata backup',
params: {
@@ -136,7 +153,8 @@ export default class Backups {
},
],
deleteVmBackup: [
({ filename, remote }) => using(this.getAdapter(remote), adapter => adapter.deleteVmBackup(filename)),
({ filename, remote }) =>
Disposable.use(this.getAdapter(remote), adapter => adapter.deleteVmBackup(filename)),
{
description: 'delete VM backup',
params: {
@@ -147,7 +165,7 @@ export default class Backups {
],
fetchPartitionFiles: [
({ disk: diskId, remote, partition: partitionId, paths }) =>
using(this.getAdapter(remote), adapter => adapter.fetchPartitionFiles(diskId, partitionId, paths)),
Disposable.use(this.getAdapter(remote), adapter => adapter.fetchPartitionFiles(diskId, partitionId, paths)),
{
description: 'fetch files from partition',
params: {
@@ -159,10 +177,10 @@ export default class Backups {
},
],
importVmBackup: [
defer(($defer, { backupId, remote, srUuid, streamLogs = false, xapi: xapiOpts }) =>
using(this.getAdapter(remote), this.getXapi(xapiOpts), async (adapter, xapi) => {
defer(($defer, { backupId, remote, srUuid, settings, streamLogs = false, xapi: xapiOpts }) =>
Disposable.use(this.getAdapter(remote), this.getXapi(xapiOpts), async (adapter, xapi) => {
const metadata = await adapter.readVmBackupMetadata(backupId)
const run = () => new ImportVmBackup({ adapter, metadata, srUuid, xapi }).run()
const run = () => new ImportVmBackup({ adapter, metadata, settings, srUuid, xapi }).run()
return streamLogs
? runWithLogs(
async (args, onLog) =>
@@ -187,6 +205,7 @@ export default class Backups {
params: {
backupId: { type: 'string' },
remote: { type: 'object' },
settings: { type: 'object', optional: true },
srUuid: { type: 'string' },
streamLogs: { type: 'boolean', optional: true },
xapi: { type: 'object' },
@@ -194,7 +213,8 @@ export default class Backups {
},
],
listDiskPartitions: [
({ disk: diskId, remote }) => using(this.getAdapter(remote), adapter => adapter.listPartitions(diskId)),
({ disk: diskId, remote }) =>
Disposable.use(this.getAdapter(remote), adapter => adapter.listPartitions(diskId)),
{
description: 'list disk partitions',
params: {
@@ -205,7 +225,7 @@ export default class Backups {
],
listPartitionFiles: [
({ disk: diskId, remote, partition: partitionId, path }) =>
using(this.getAdapter(remote), adapter => adapter.listPartitionFiles(diskId, partitionId, path)),
Disposable.use(this.getAdapter(remote), adapter => adapter.listPartitionFiles(diskId, partitionId, path)),
{
description: 'list partition files',
params: {
@@ -221,7 +241,7 @@ export default class Backups {
const backupsByRemote = {}
await asyncMap(Object.entries(remotes), async ([remoteId, remote]) => {
try {
await using(this.getAdapter(remote), async adapter => {
await Disposable.use(this.getAdapter(remote), async adapter => {
backupsByRemote[remoteId] = await adapter.listPoolMetadataBackups()
})
} catch (error) {
@@ -245,7 +265,7 @@ export default class Backups {
const backups = {}
await asyncMap(Object.keys(remotes), async remoteId => {
try {
await using(this.getAdapter(remotes[remoteId]), async adapter => {
await Disposable.use(this.getAdapter(remotes[remoteId]), async adapter => {
backups[remoteId] = formatVmBackups(await adapter.listAllVmBackups())
})
} catch (error) {
@@ -275,7 +295,7 @@ export default class Backups {
const backupsByRemote = {}
await asyncMap(Object.entries(remotes), async ([remoteId, remote]) => {
try {
await using(this.getAdapter(remote), async adapter => {
await Disposable.use(this.getAdapter(remote), async adapter => {
backupsByRemote[remoteId] = await adapter.listXoMetadataBackups()
})
} catch (error) {
@@ -296,7 +316,7 @@ export default class Backups {
],
restoreMetadataBackup: [
({ backupId, remote, xapi: xapiOptions }) =>
using(app.remotes.getHandler(remote), xapiOptions && this.getXapi(xapiOptions), (handler, xapi) =>
Disposable.use(app.remotes.getHandler(remote), xapiOptions && this.getXapi(xapiOptions), (handler, xapi) =>
runWithLogs(
async (args, onLog) =>
Task.run(
@@ -347,7 +367,7 @@ export default class Backups {
backup: {
mountPartition: [
async ({ disk, partition, remote }) =>
using(this.getAdapter(remote), adapter => durablePartition.mount(adapter, disk, partition)),
Disposable.use(this.getAdapter(remote), adapter => durablePartition.mount(adapter, disk, partition)),
{
description: 'mount a partition',
params: {

View File

@@ -1,5 +1,4 @@
import Disposable from 'promise-toolbox/Disposable'
import using from 'promise-toolbox/using'
import { compose } from '@vates/compose'
import { decorateWith } from '@vates/decorate-with'
import { deduped } from '@vates/disposable/deduped'
@@ -12,7 +11,7 @@ export default class Remotes {
app.api.addMethods({
remote: {
getInfo: [
({ remote }) => using(this.getHandler(remote), handler => handler.getInfo()),
({ remote }) => Disposable.use(this.getHandler(remote), handler => handler.getInfo()),
{
params: {
remote: { type: 'object' },
@@ -22,7 +21,7 @@ export default class Remotes {
test: [
({ remote }) =>
using(this.getHandler(remote), handler => handler.test()).catch(error => ({
Disposable.use(this.getHandler(remote), handler => handler.test()).catch(error => ({
success: false,
error: error.message ?? String(error),
})),

View File

@@ -43,10 +43,9 @@ ${name} v${version}
})
let httpServer = new (require('http-server-plus'))({
createSecureServer:
require('compare-versions')(process.version, '10.10.0') >= 0
? (({ createSecureServer }) => opts => createSecureServer({ ...opts, allowHTTP1: true }))(require('http2'))
: undefined,
createSecureServer: (({ createSecureServer }) => opts => createSecureServer({ ...opts, allowHTTP1: true }))(
require('http2')
),
})
const { readFileSync, outputFileSync, unlinkSync } = require('fs-extra')

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -1,24 +0,0 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -35,7 +35,7 @@
"dependencies": {
"chalk": "^4.1.0",
"exec-promise": "^0.7.0",
"form-data": "^3.0.0",
"form-data": "^4.0.0",
"fs-extra": "^9.0.0",
"fs-promise": "^2.0.3",
"get-stream": "^6.0.0",

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/xapi",
"version": "0.4.4",
"version": "0.6.0",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/xapi",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
@@ -12,6 +12,9 @@
"files": [
"dist/"
],
"bin": {
"xo-xapi": "./dist/cli.js"
},
"engines": {
"node": ">=8.10"
},
@@ -21,10 +24,11 @@
"@babel/plugin-proposal-decorators": "^7.3.0",
"@babel/preset-env": "^7.3.1",
"cross-env": "^7.0.2",
"rimraf": "^3.0.0"
"rimraf": "^3.0.0",
"xo-common": "^0.7.0"
},
"peerDependencies": {
"xen-api": "^0.30.0"
"xen-api": "^0.31.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
@@ -39,11 +43,11 @@
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.2.0",
"d3-time-format": "^2.2.3",
"d3-time-format": "^3.0.0",
"golike-defer": "^0.5.1",
"lodash": "^4.17.15",
"make-error": "^1.3.5",
"promise-toolbox": "^0.17.0"
"promise-toolbox": "^0.18.0"
},
"private": false,
"license": "AGPL-3.0-or-later",

6
@xen-orchestra/xapi/src/cli.js Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env node
const { Xapi } = require('./')
require('xen-api/dist/cli')
.default(opts => new Xapi({ ignoreNobakVdis: true, ...opts }))
.catch(console.error.bind(console, 'FATAL'))

View File

@@ -1,8 +1,11 @@
const assert = require('assert')
const defer = require('promise-toolbox/defer')
const pRetry = require('promise-toolbox/retry')
const { utcFormat, utcParse } = require('d3-time-format')
const { Xapi: Base } = require('xen-api')
exports.isDefaultTemplate = require('./isDefaultTemplate')
// VDI formats. (Raw is not available for delta vdi.)
exports.VDI_FORMAT_RAW = 'raw'
exports.VDI_FORMAT_VHD = 'vhd'
@@ -32,16 +35,28 @@ const hasProps = o => {
}
class Xapi extends Base {
constructor({ ignoreNobakVdis, maxUncoalescedVdis, vdiDestroyRetryWhenInUse, ...opts }) {
constructor({
callRetryWhenTooManyPendingTasks,
ignoreNobakVdis,
maxUncoalescedVdis,
vdiDestroyRetryWhenInUse,
...opts
}) {
assert.notStrictEqual(ignoreNobakVdis, undefined)
super(opts)
this._callRetryWhenTooManyPendingTasks = {
delay: 5e3,
tries: 10,
...callRetryWhenTooManyPendingTasks,
when: { code: 'TOO_MANY_PENDING_TASKS' },
}
this._ignoreNobakVdis = ignoreNobakVdis
this._maxUncoalescedVdis = maxUncoalescedVdis
this._vdiDestroyRetryWhenInUse = {
...vdiDestroyRetryWhenInUse,
delay: 5e3,
retries: 10,
...vdiDestroyRetryWhenInUse,
}
const genericWatchers = (this._genericWatchers = new Set())
@@ -122,3 +137,17 @@ mixin({
VM: require('./vm'),
})
exports.Xapi = Xapi
// TODO: remove once using next promise-toolbox
function pRetryWrap(fn, options) {
const getOptions = typeof options !== 'function' ? () => options : options
return function () {
return pRetry(() => fn.apply(this, arguments), getOptions.apply(this, arguments))
}
}
function getCallRetryOpts() {
return this._callRetryWhenTooManyPendingTasks
}
Xapi.prototype.call = pRetryWrap(Xapi.prototype.call, getCallRetryOpts)
Xapi.prototype.callAsync = pRetryWrap(Xapi.prototype.callAsync, getCallRetryOpts)

View File

@@ -0,0 +1 @@
module.exports = vmTpl => vmTpl.is_default_template || vmTpl.other_config.default_template === 'true'

View File

@@ -1,19 +1,27 @@
const CancelToken = require('promise-toolbox/CancelToken')
const pCatch = require('promise-toolbox/catch')
const pRetry = require('promise-toolbox/retry')
const extractOpaqueRef = require('./_extractOpaqueRef')
const noop = Function.prototype
module.exports = class Vdi {
async clone(vdiRef) {
return extractOpaqueRef(await this.callAsync('VDI.clone', vdiRef))
}
async destroy(vdiRef) {
// work around a race condition in XCP-ng/XenServer where the disk is not fully unmounted yet
await pRetry(() => this.callAsync('VDI.destroy', vdiRef), {
...this._vdiDestroyRetry,
when: { code: 'VDI_IN_USE' },
})
await pCatch.call(
// work around a race condition in XCP-ng/XenServer where the disk is not fully unmounted yet
pRetry(() => this.callAsync('VDI.destroy', vdiRef), {
...this._vdiDestroyRetry,
when: { code: 'VDI_IN_USE' },
}),
// if this VDI is not found, consider it destroyed
{ code: 'HANDLE_INVALID' },
noop
)
}
async create(

View File

@@ -1,20 +1,26 @@
const isVmRunning = require('./_isVmRunning')
module.exports = class Vif {
async create({
currently_attached = true,
device,
ipv4_allowed,
ipv6_allowed,
locking_mode,
MAC,
MTU,
network,
other_config = {},
qos_algorithm_params = {},
qos_algorithm_type = '',
VM,
}) {
async create(
{
currently_attached = true,
device,
ipv4_allowed,
ipv6_allowed,
locking_mode,
MTU,
network,
other_config = {},
qos_algorithm_params = {},
qos_algorithm_type = '',
VM,
},
{
// duplicated MAC addresses can lead to issues,
// therefore it should be passed explicitely
MAC = '',
} = {}
) {
const [powerState, ...rest] = await Promise.all([
this.getField('VM', VM, 'power_state'),
device ?? (await this.call('VM.get_allowed_VIF_devices', VM))[0],

View File

@@ -1,15 +1,18 @@
const cancelable = require('promise-toolbox/cancelable')
const CancelToken = require('promise-toolbox/CancelToken')
const defer = require('golike-defer').default
const groupBy = require('lodash/groupBy')
const pickBy = require('lodash/pickBy')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const omit = require('lodash/omit')
const pCatch = require('promise-toolbox/catch')
const pRetry = require('promise-toolbox/retry')
const { asyncMap } = require('@xen-orchestra/async-map')
const { createLogger } = require('@xen-orchestra/log')
const { incorrectState } = require('xo-common/api-errors')
const { Ref } = require('xen-api')
const extractOpaqueRef = require('./_extractOpaqueRef')
const isDefaultTemplate = require('./isDefaultTemplate')
const isVmRunning = require('./_isVmRunning')
const { warn } = createLogger('xo:xapi:vm')
@@ -122,16 +125,15 @@ module.exports = class Vm {
}
}
@cancelable
async checkpoint($cancelToken, vmRef, nameLabel) {
if (nameLabel === undefined) {
nameLabel = await this.getField('VM', vmRef, 'name_label')
async checkpoint(vmRef, { cancelToken = CancelToken.none, name_label } = {}) {
if (name_label === undefined) {
name_label = await this.getField('VM', vmRef, 'name_label')
}
try {
return await this.callAsync($cancelToken, 'VM.checkpoint', vmRef, nameLabel).then(extractOpaqueRef)
return await this.callAsync(cancelToken, 'VM.checkpoint', vmRef, name_label).then(extractOpaqueRef)
} catch (error) {
if (error.code === 'VM_BAD_POWER_STATE') {
return this.VM_snapshot($cancelToken, vmRef, nameLabel)
return this.VM_snapshot(vmRef, { cancelToken, name_label })
}
throw error
}
@@ -195,6 +197,17 @@ module.exports = class Vm {
// not supported by `VM.create`, therefore it should be passed explicitly
bios_strings,
// The field `other_config.mac_seed` is used (in conjunction with VIFs'
// devices) to generate MAC addresses of VIFs for this VM.
//
// It's automatically generated by VM.create if missing.
//
// If this is true, it will be filtered out by this method to ensure a
// new one is generated.
//
// See https://github.com/xapi-project/xen-api/blob/0a6d6de0704ca2cc439326c35af7cf45128a17d5/ocaml/xapi/xapi_vm.ml#L628
generateMacSeed = true,
// if set, will create the VM in Suspended power_state with this VDI
//
// it's a separate param because it's not supported for all versions of
@@ -214,7 +227,7 @@ module.exports = class Vm {
memory_dynamic_min,
memory_static_max,
memory_static_min,
other_config,
other_config: generateMacSeed ? omit(other_config, 'mac_seed') : other_config,
PCI_bus,
platform,
PV_args,
@@ -261,21 +274,30 @@ module.exports = class Vm {
bios_strings = cleanBiosStrings(bios_strings)
if (bios_strings !== undefined) {
await this.call('VM.set_bios_strings', ref, bios_strings)
// Only available on XS >= 7.3
await pCatch.call(this.call('VM.set_bios_strings', ref, bios_strings), { code: 'MESSAGE_METHOD_UNKNOWN' }, noop)
}
return ref
}
async destroy(vmRef, { deleteDisks = true, force = false, forceDeleteDefaultTemplate = false } = {}) {
async destroy(
vmRef,
{ deleteDisks = true, force = false, bypassBlockedOperation = force, forceDeleteDefaultTemplate = force } = {}
) {
const vm = await this.getRecord('VM', vmRef)
if (!force && 'destroy' in vm.blocked_operations) {
if (!bypassBlockedOperation && 'destroy' in vm.blocked_operations) {
throw new Error('destroy is blocked')
}
if (!forceDeleteDefaultTemplate && vm.other_config.default_template === 'true') {
throw new Error('VM is default template')
if (!forceDeleteDefaultTemplate && isDefaultTemplate(vm)) {
throw incorrectState({
actual: true,
expected: false,
object: vm.$id,
property: 'isDefaultTemplate',
})
}
// It is necessary for suspended VMs to be shut down
@@ -285,9 +307,12 @@ module.exports = class Vm {
}
await Promise.all([
forceDeleteDefaultTemplate &&
// Only available on XS >= 7.2
pCatch.call(vm.set_is_default_template(false), { code: 'MESSAGE_METHOD_UNKNOWN' }, noop),
forceDeleteDefaultTemplate && vm.update_other_config('default_template', null),
vm.set_is_a_template(false),
vm.update_blocked_operations('destroy', null),
vm.update_other_config('default_template', null),
bypassBlockedOperation && vm.update_blocked_operations('destroy', null),
])
// must be done before destroying the VM
@@ -316,26 +341,36 @@ module.exports = class Vm {
])
}
@cancelable
@defer
async export($defer, $cancelToken, vmRef, { compress = false, useSnapshot } = {}) {
async export($defer, vmRef, { cancelToken = CancelToken.none, compress = false, useSnapshot } = {}) {
const vm = await this.getRecord('VM', vmRef)
const taskRef = await this.task_create('VM export', vm.name_label)
$defer.onFailure.call(this, 'task_destroy', taskRef)
if (useSnapshot === undefined) {
useSnapshot = isVmRunning(vm)
}
const exportedVmRef = useSnapshot
? await this.VM_snapshot($cancelToken, vmRef, `[XO Export] ${vm.name_label}`)
: vmRef
let exportedVmRef, destroySnapshot
if (useSnapshot) {
exportedVmRef = await this.VM_snapshot(vmRef, { cancelToken, name_label: `[XO Export] ${vm.name_label}` })
destroySnapshot = () => ignoreErrors.call(this.VM_destroy(exportedVmRef))
$defer.onFailure(destroySnapshot)
} else {
exportedVmRef = vmRef
}
try {
return await this.getResource($cancelToken, '/export/', {
const stream = await this.getResource(cancelToken, '/export/', {
query: {
ref: exportedVmRef,
use_compression: compress === 'zstd' ? 'zstd' : compress === true || compress === 'gzip' ? 'true' : 'false',
},
task: taskRef,
})
if (useSnapshot) {
stream.once('end', destroySnapshot).once('error', destroySnapshot)
}
return stream
} catch (error) {
// augment the error with as much relevant info as possible
const [poolMaster, exportedVm] = await Promise.all([
@@ -345,14 +380,7 @@ module.exports = class Vm {
error.pool_master = poolMaster
error.VM = exportedVm
throw error
} finally {
}
// if (useSnapshot) {
// const destroySnapshot = () => this.deleteVm(exportedVm)::ignoreErrors()
// promise.then(_ => _.task::pFinally(destroySnapshot), destroySnapshot)
// }
//
// return promise
}
async getDisks(vmRef, vbdRefs) {
@@ -400,8 +428,7 @@ module.exports = class Vm {
}
@defer
@cancelable
async snapshot($cancelToken, $defer, vmRef, nameLabel) {
async snapshot($defer, vmRef, { cancelToken = CancelToken.none, name_label } = {}) {
const vm = await this.getRecord('VM', vmRef)
// cannot unplug VBDs on Running, Paused and Suspended VMs
if (vm.power_state === 'Halted' && this._ignoreNobakVdis) {
@@ -418,31 +445,32 @@ module.exports = class Vm {
})
}
if (nameLabel === undefined) {
nameLabel = vm.name_label
if (name_label === undefined) {
name_label = vm.name_label
}
let ref
do {
if (!vm.tags.includes('xo-disable-quiesce')) {
try {
let { snapshots } = vm
ref = await pRetry(
async bail => {
async () => {
try {
return await this.callAsync($cancelToken, 'VM.snapshot_with_quiesce', vmRef, nameLabel)
return await this.callAsync(cancelToken, 'VM.snapshot_with_quiesce', vmRef, name_label)
} catch (error) {
if (error == null || error.code !== 'VM_SNAPSHOT_WITH_QUIESCE_FAILED') {
throw bail(error)
throw pRetry.bail(error)
}
// detect and remove new broken snapshots
//
// see https://github.com/vatesfr/xen-orchestra/issues/3936
const prevSnapshotRefs = new Set(vm.snapshots)
const prevSnapshotRefs = new Set(snapshots)
const snapshotNameLabelPrefix = `Snapshot of ${vm.uuid} [`
await vm.refresh_snapshots()
snapshots = await this.getField('VM', vm.$ref, 'snapshots')
const createdSnapshots = (
await this.getRecords(
'VM',
vm.snapshots.filter(_ => !prevSnapshotRefs.has(_))
snapshots.filter(_ => !prevSnapshotRefs.has(_))
)
).filter(_ => _.name_label.startsWith(snapshotNameLabelPrefix))
// be safe: only delete if there was a single match
@@ -475,7 +503,7 @@ module.exports = class Vm {
}
}
}
ref = await this.callAsync($cancelToken, 'VM.snapshot', vmRef, nameLabel).then(extractOpaqueRef)
ref = await this.callAsync(cancelToken, 'VM.snapshot', vmRef, name_label).then(extractOpaqueRef)
} while (false)
// VM snapshots are marked as templates, unfortunately it does not play well with XVA export/import

Some files were not shown because too many files have changed in this diff Show More