Compare commits

..

584 Commits

Author SHA1 Message Date
Julien Fontanet
48c8d25774 WiP: feat(self-signed): genSignedCert 2021-12-14 12:09:31 +01:00
Julien Fontanet
cf9f0da6e5 fix(backups): ignore VMs created by current job
See xoa-support#4271
2021-12-14 12:07:51 +01:00
Pierre Donias
10ac23e265 feat(docs/Netbox): specify minimum required permissions (#6047)
See https://xcp-ng.org/forum/topic/5300/
2021-12-14 11:48:19 +01:00
Rajaa.BARHTAOUI
dc2e1cba1f feat(xo-web/pool,VM/advanced): ability to set suspend SR (#6044)
Fixes #4163
2021-12-14 10:13:11 +01:00
Julien Fontanet
7bfd190c22 fix(backups/_VmBackup): no base VM when no base VDIs found
Introduced by 5b188f35b
2021-12-13 17:55:54 +01:00
Manon Mercier
c3bafeb468 fix(docs/xoa): must reboot after changing password (#6056)
Co-authored-by: Jon Sands <fohdeesha@gmail.com>
2021-12-13 17:48:14 +01:00
Mathieu
7bacd781cf feat(xo-web/health): display non shared default SRs (#6033)
Fixes #5871
2021-12-13 10:00:24 +01:00
Julien Fontanet
ee005c3679 fix(xo-server-usage-report): csv-stringify usage
Fixes #6053

Introduced by b179dc1d5
2021-12-13 09:39:00 +01:00
Florent BEAUCHAMP
315f54497a fix(vhd-lib/resolveAlias): limit size (#6004)
Current resolution loads whole file in memory. It can lead to crash is the alias is malformed (for example a full VHD named `.alias.vhd`).
2021-12-12 14:19:40 +01:00
Julien Fontanet
e30233347b feat: release 5.65.2 2021-12-10 17:19:54 +01:00
Julien Fontanet
56d4a7f01e fix(xo-web/about): show commit iff available & always versions
Fixes #6052
2021-12-10 16:40:26 +01:00
Julien Fontanet
7c110eebd8 feat(CHANGELOG): integrate released changes 2021-12-10 12:06:26 +01:00
Julien Fontanet
39394f8c09 feat(@xen-orchestra/proxy): 0.15.5 2021-12-10 12:04:33 +01:00
Julien Fontanet
3283130dfc feat(xo-server): 5.84.3 2021-12-10 12:04:33 +01:00
Julien Fontanet
3146a591d0 feat(@xen-orchestra/backups): 0.16.2 2021-12-10 12:04:30 +01:00
Julien Fontanet
e478b1ec04 feat(vhd-lib): 2.0.3 2021-12-10 12:04:00 +01:00
Julien Fontanet
7bc4d14f46 feat(@xen-orchestra/fs): 0.19.2 2021-12-10 11:38:20 +01:00
Florent BEAUCHAMP
f3eeeef389 fix(fs/S3#list): should not look into all the file tree (#6048) 2021-12-09 14:06:21 +01:00
Florent BEAUCHAMP
8d69208197 fix(backups/MixinBackupWriter#_cleanVm): always returns an object (#6050) 2021-12-09 10:38:31 +01:00
Julien Fontanet
2c689af1a9 fix(backups): add random suffixes to task files to avoid collitions
See https://xcp-ng.org/forum/post/44661
2021-12-08 18:03:13 +01:00
Julien Fontanet
cb2a34c765 feat(backups/RemoteAdapter#cleanVm): show missing VHDs
Related to investigation on zammad#4156
2021-12-08 11:41:18 +01:00
Julien Fontanet
465c8f9009 feat(xo-web/about): show build commit in sources version (#6045) 2021-12-08 09:57:15 +01:00
Florent BEAUCHAMP
8ea4c1c1fd fix(vhd-lib): output parent locator in VhdAbstract#stream (#6035) 2021-12-07 14:14:39 +01:00
Julien Fontanet
ba0f7df9e8 feat(xapi-explore-sr): 0.4.1 2021-12-06 17:59:27 +01:00
Julien Fontanet
e47dd723b0 fix(xapi-explore-sr): add missing @xen-orchestra/defined dep
Introduced by 2412f8b1e
2021-12-06 17:43:47 +01:00
Julien Fontanet
fca6e2f6bf fix(fs/S3#_list): throw if result is truncated 2021-12-06 14:25:55 +01:00
Florent BEAUCHAMP
faa7ba6f24 fix(vhd-lib, fs): use rmtree and not rmTree (#6041) 2021-12-06 14:25:09 +01:00
Julien Fontanet
fc2dbbe3ee feat: release 5.65.1 2021-12-03 16:42:11 +01:00
Julien Fontanet
cc98b81825 fix(CHANGELOG): incorrect secion name Packages to release
Introduced in ae24b10da
2021-12-03 15:29:48 +01:00
Julien Fontanet
eb4a7069d4 feat(CHANGELOG): integrate released changes 2021-12-03 15:27:54 +01:00
Julien Fontanet
4f65d9214e feat(xo-server): 5.84.2 2021-12-03 15:23:59 +01:00
Julien Fontanet
4d3c8ee63c feat(@xen-orchestra/proxy): 0.15.4 2021-12-03 15:23:26 +01:00
Julien Fontanet
e41c1b826a feat(@xen-orchestra/backups): 0.16.1 2021-12-03 15:23:02 +01:00
Julien Fontanet
644bb48135 feat(xo-vmdk-to-vhd): 2.0.1 2021-12-03 15:22:23 +01:00
Julien Fontanet
c9809285f6 feat(vhd-lib): 2.0.2 2021-12-03 15:19:07 +01:00
Julien Fontanet
5704949f4d feat(@vates/compose): 2.1.0 2021-12-03 15:17:47 +01:00
Julien Fontanet
a19e00fbc0 fix(backups/_VmBackup#_selectBaseVm): cant read .uuid of undefined srcVdi (#6034)
See xoa-support#4263

The debug message is now clearer and has correct associated data.
2021-12-03 10:22:17 +01:00
Julien Fontanet
470a9b3e27 chore(decorate-with/README): document usage with @vates/compose 2021-12-02 21:37:25 +01:00
Julien Fontanet
ace31dc566 feat(compose): supports attaching extra params 2021-12-02 21:37:25 +01:00
Julien Fontanet
ed252276cb fix(compose): dont mutate passed functions array 2021-12-02 21:37:25 +01:00
Julien Fontanet
26d0ff3c9a fix(vhd-lib/VhdAbtract#stream): explicitely ignore differencing
Because parentLocator entries handling are broken.
2021-12-02 16:48:19 +01:00
Florent Beauchamp
ff806a3ff9 fix(vhd-lib): use parent locator of root disk in VhdSynthetic 2021-12-02 16:48:19 +01:00
Florent Beauchamp
949b17dee6 fix(vhd-lib): fix footer and header accessor in vhd hierarchy 2021-12-02 16:48:19 +01:00
Florent Beauchamp
b1fdc68623 fix(vhd-lib): platformDataSpace in sectors not bytes 2021-12-02 16:48:19 +01:00
Florent BEAUCHAMP
f502facfd1 fix(backup): createAlias to data instead of circular alias (#6029) 2021-12-02 13:56:20 +01:00
Mathieu
bf0a74d709 fix(xo-web/SortedTable): properly disable collapsed actions (#6023) 2021-12-02 13:48:22 +01:00
Julien Fontanet
7296d98313 fix(backups/RemoteAdapter#_createSyntheticStream): only dispose once
See https://xcp-ng.org/forum/topic/5257/problems-building-from-source/20
2021-12-01 13:24:46 +01:00
Julien Fontanet
30568ced49 fix(vhd-lib/VhdSynthetic): fix parent UUID assert
See https://xcp-ng.org/forum/topic/5257/problems-building-from-source
2021-12-01 12:54:00 +01:00
Julien Fontanet
5e1284a9e0 chore: refresh yarn.lock
Introduced by 03d6e3356 due to extra files in my repo…
2021-12-01 12:33:43 +01:00
Julien Fontanet
27d2de872a chore: update to lint-staged@^12.0.3
See https://xcp-ng.org/forum/topic/5257/problems-building-from-source

Fix missing peer dependency `inquirer`.
2021-12-01 12:19:19 +01:00
Julien Fontanet
03d6e3356b chore: refresh yarn.lock 2021-12-01 12:17:52 +01:00
Julien Fontanet
ca8baa62fb fix(xo-vmdk-to-vhd): remove duplicate promise-toolbox dep
See https://xcp-ng.org/forum/topic/5257/problems-building-from-source
2021-12-01 12:17:25 +01:00
Florent BEAUCHAMP
2f607357c6 feat: release 5.65 (#6028) 2021-11-30 17:45:31 +01:00
Julien Fontanet
2de80f7aff feat(xo-server): 5.84.1 2021-11-30 17:04:37 +01:00
Julien Fontanet
386058ed88 chore(CHANGELOG): update vhd-lib version
Introduced by 033fa9e067
2021-11-30 17:04:25 +01:00
Julien Fontanet
033fa9e067 feat(vhd-lib): 2.0.1 2021-11-30 17:00:49 +01:00
Julien Fontanet
e8104420b5 fix(vhd-lib): add missing @vates/async-each dep
Introduced by 56c3d70149
2021-11-30 16:59:01 +01:00
Florent BEAUCHAMP
ae24b10da0 feat: technical release (#6025) 2021-11-30 15:45:36 +01:00
Florent BEAUCHAMP
407b05b643 fix(backups): use the full VHD hierarchy for restore (#6027) 2021-11-30 15:27:54 +01:00
Julien Fontanet
79bf8bc9f6 fix(xo-server): add missing complex-matcher dep
Introduced by 65d6dca52
2021-11-30 09:35:10 +01:00
Julien Fontanet
65d6dca52c feat(xo-server/xo.getAllObjects): add complex-matcher support
See https://xcp-ng.org/forum/topic/5238/xo-cli-command-list-vms-which-ha-snapshots
2021-11-29 19:00:44 +01:00
Julien Fontanet
66eeefbd7b feat(xo-server/vm.set): suspendSr support
See #4163
2021-11-29 14:44:02 +01:00
Mathieu
c10bbcde00 feat(xo-web,xo-server/snapshot): ability to export snapshot memory (#6015)
See xoa-support#4113
2021-11-29 14:08:02 +01:00
Julien Fontanet
fe69928bcc feat(xo-server/pool.set): suspendSr support
See #4163
2021-11-29 10:53:49 +01:00
Florent BEAUCHAMP
3ad8508ea5 feat(vhd-lib/VhdDirectory#_writeChunk): use outputFile (#6019)
This is much faster than manually creating parent directories.
2021-11-29 09:52:47 +01:00
Florent BEAUCHAMP
1f1ae759e0 feat(fs): use keepalive for queries to s3 (#6018) 2021-11-27 10:10:19 +01:00
Mathieu
6e4bfe8f0f feat(xo-web,xo-server): ability to create a cloud config network template (#5979)
Fixes #5931
2021-11-26 10:28:22 +01:00
Rajaa.BARHTAOUI
6276c48768 fix(xo-server/proxies): remove state cache after the proxy update (#6013) 2021-11-26 10:02:30 +01:00
Julien Fontanet
f6005baf1a feat(vhd-cli info): human format some fields 2021-11-25 18:29:25 +01:00
Julien Fontanet
b62fdbc6a6 feat(vhd-lib/Constants): make disk types and platorms maps
BREAKING
2021-11-25 18:02:26 +01:00
Florent BEAUCHAMP
bbd3d31b6a fix(backups/writeVhd): await outputStream (#6017) 2021-11-25 16:34:21 +01:00
Julien Fontanet
481ac92bf8 fix(backups/RemoteAdapter): dont use .dir suffix (#6016)
An alias can point to any kind of VHD, file or directory.

Also, for now, aliases are only used for VHD directories.
2021-11-25 15:31:25 +01:00
Florent BEAUCHAMP
a2f2b50f57 feat(s3): allow self signed certificate (#5961) 2021-11-25 11:32:08 +01:00
Julien Fontanet
bbab9d0f36 fix(xapi/vm/_assertHealthyVdiChain): ignore unused unmanaged VDIs
Fixes xoa-support#4280
2021-11-25 11:28:50 +01:00
Florent BEAUCHAMP
7f8190056d fix(backups/RemoteAdapter): unused import and path in writeVhd (#6014) 2021-11-25 11:26:59 +01:00
Julien Fontanet
8f4737c5f1 chore: upgrade to jsonrpc-websocket-client@0.7.2 2021-11-25 10:33:39 +01:00
Julien Fontanet
c5adba3c97 fix(xo-lib): upgrade to jsonrpc-websocket-client@^0.7.2
Fix default value for `protocols` option.
2021-11-25 10:28:40 +01:00
Julien Fontanet
d91eb9e396 fix(CHANGELOG.unreleased): fix duplicate package
Introduced by d5f21bc27c
2021-11-25 10:27:01 +01:00
Julien Fontanet
1b47102d6c chore: refresh yarn.lock 2021-11-25 00:06:01 +01:00
Julien Fontanet
cd147f3fc5 feat(xo-cli): 0.12.0 2021-11-25 00:03:21 +01:00
Julien Fontanet
c3acdc8cbd feat(xo-cli register): --allowUnauthorized flag
See https://xcp-ng.org/forum/topic/5226/xo-cli-and-using-self-signed-certificates
2021-11-25 00:02:08 +01:00
Julien Fontanet
c3d755dc7b feat(xo-lib): 0.11.0 2021-11-24 23:59:05 +01:00
Julien Fontanet
6f49c48bd4 feat(xo-lib): upgrade to jsonrpc-websocket-client@0.7.1
Use secure protocol (`wss`) by default and contains a fix for `rejectUnauthorized` option.
2021-11-24 23:55:23 +01:00
Julien Fontanet
446f390b3d feat(xo-lib): allow passing opts to JsonRpcWebSocketClient 2021-11-24 23:53:33 +01:00
Julien Fontanet
966091593a chore(vhd-lib): rename build{Footer,Header} to unpack{Footer,Header}
To make it clearer that it unpacks a binary footer/header to a JS object.
2021-11-24 23:34:23 +01:00
Florent Beauchamp
d5f21bc27c feat(backups): handle the choice of the vhd type to use during backup 2021-11-24 21:08:15 +01:00
Florent Beauchamp
8c3b452c0d feat(backup): DeltaBackupWriter can handle any type of vhd 2021-11-24 21:08:15 +01:00
Florent Beauchamp
9cacb92c2c feat(backups): remoteadapter can delete any type of vhd 2021-11-24 21:08:15 +01:00
Florent Beauchamp
7a1b56db87 feat(backups): checkvhd can handle all vhd types 2021-11-24 21:08:15 +01:00
Florent Beauchamp
56c3d70149 feat(vhd-lib): generate a vhd directory from a vhd stream 2021-11-24 21:08:15 +01:00
Florent Beauchamp
1ec8fcc73f feat(vhd-lib): extract computeSectorsPerBlock, computeBlockBitmapSize and computeSectorOfBitmap to utils 2021-11-24 21:08:15 +01:00
Rajaa.BARHTAOUI
060b16c5ca feat(xo-web/backup/logs): identify XAPI errors (#6001)
See xoa-support#3977
2021-11-24 15:25:27 +01:00
Yannick Achy
0acc52e3e9 fix(docs): move NOBAK from Delta to general concepts (#6012)
Co-authored-by: yannick Achy <yannick.achy@vates.fr>
2021-11-24 09:10:35 +01:00
Florent Beauchamp
a9c2c9b6ba refator(vhd-lib): move createSyntheticStream to backup, move stream() tests to vhdabstracts 2021-11-23 15:56:25 +01:00
Florent Beauchamp
5b2a6bc56b chore(vhd-lib/createSyntheticStream): based on VhdSynthetic#stream() 2021-11-23 15:56:25 +01:00
Florent Beauchamp
19c8693b62 fix(vhd-lib/VhdSynthetic#readHeaderAndFooter()): root vhd can be a dynamic and check chaining 2021-11-23 15:56:25 +01:00
Florent Beauchamp
c4720e1215 fix(vhd-lib/VhdAbstract#stream()): stream.length should contain blocks 2021-11-23 15:56:25 +01:00
Florent BEAUCHAMP
b6d4c8044c feat(backups/cleanVm) : support VHD dirs and aliases (#6000) 2021-11-22 17:14:29 +01:00
Florent BEAUCHAMP
57dd6ebfba chore(vhd-lib): use openVhd for chain and checkChain (#5997) 2021-11-22 15:50:30 +01:00
Julien Fontanet
c75569f278 feat(proxy/authentication.setToken): API method to change auth token 2021-11-18 18:14:19 +01:00
Julien Fontanet
a8757f9074 chore(proxy/authentication): use private field for auth token
More idiomatic and potentially more secure.
2021-11-18 18:02:26 +01:00
Julien Fontanet
f5c3bf72e5 fix(mixins/Config): dont create multiple stop listeners 2021-11-18 16:41:46 +01:00
Florent BEAUCHAMP
d7ee13f98d feat(vhd-lib/merge): use Vhd* classes (#5950) 2021-11-18 11:30:04 +01:00
Julien Fontanet
1f47aa491d fix(xo-server/pool.mergeInto): dont export masterPassword on error
Fixes xoa-support#4265
2021-11-17 22:42:00 +01:00
Julien Fontanet
ffe430758e feat(async-each): run async fn for each item in (async) iterable 2021-11-17 22:27:43 +01:00
Florent BEAUCHAMP
a4bb453401 feat(vhd-lib): add VhdAbstract#{stream,rawContent}() methods (#5992) 2021-11-17 09:16:34 +01:00
Florent BEAUCHAMP
5c8ebce9eb feat(vhd-lib): add vhd synthetic class (#5990) 2021-11-17 09:15:13 +01:00
Julien Fontanet
8b0cee5e6f feat(@xen-orchestra/backups-cli): 0.6.1 2021-11-16 14:26:50 +01:00
Julien Fontanet
e5f4f825b6 fix(xapi): group retry options together
- it does not make sense to only set the delay or the number of tries without the other
- it allow using any options either as default or in config without worrying about incompatibilities (e.g. `tries` & `retries`)
2021-11-16 14:26:11 +01:00
Julien Fontanet
b179dc1d56 chore: update dev deps 2021-11-15 23:43:20 +01:00
Julien Fontanet
7281c9505d fix(CHANGELOG.unreleased): new release backups-cli
`vhd-cli@^1` compat was broken by 7ef89d504
2021-11-15 14:46:51 +01:00
Julien Fontanet
4db82f447d fix(xo-web/about): update link to create issue
Related to 71b8e625f

See #5977
2021-11-15 14:22:46 +01:00
Julien Fontanet
834da3d2b4 fix(vhd-lib/VhdAbstract): remove duplicate field declarations
Introduced in c6c3a33dc
2021-11-10 16:04:59 +01:00
Julien Fontanet
c6c3a33dcc feat(vhd-cli/VhdAbstract): make derived values getters
It makes them read-only, make sure they are always up-to-date with the header and avoid duplicating their logic.
2021-11-10 15:45:42 +01:00
Julien Fontanet
fb720d9b05 fix(docs/xoa): use wget instead of curl
The version of curl installed on XCP-ng 8.2.0, (curl 7.29.0) does not support any encryption algos available on https://xoa.io
2021-11-09 19:55:49 +01:00
Florent Beauchamp
547d318e55 fix(vhd-lib): write parent locator before the blocks 2021-11-08 18:03:46 +01:00
Florent Beauchamp
cb5a2c18f2 fix(vhd-lib): ensure block allocation table is written after modifying it in tests 2021-11-08 18:03:46 +01:00
Florent Beauchamp
e01ca3ad07 refactor(vhd-lib): use method from test/utils when possible 2021-11-08 18:03:46 +01:00
Florent Beauchamp
314d193f35 fix(vhd-lib): set platform code when setting unique parent locator 2021-11-08 18:03:46 +01:00
Florent Beauchamp
e0200bb730 refactor(vhd-lib): split tests 2021-11-08 18:03:46 +01:00
Florent BEAUCHAMP
2a3f4a6f97 feat(vhd-lib): handle file alias (#5962) 2021-11-08 14:46:00 +01:00
Nicolas Raynaud
88628bbdc0 chore(xo-vmdk-to-vhd): fix tests (#5981)
Introduced by fdf52a3d59

Follow-up of b00750bfa3
2021-11-07 15:38:45 +01:00
Olivier Lambert
cb7b695a72 feat(docs/netbox): add how to add a custom field in Netbox 3 (#5984) 2021-11-07 13:44:02 +01:00
Julien Fontanet
ae549e2a88 fix(jest): dont use fake timers by default
Introduced by 844efb88d

The upgrade to Jest 27 (15630aee5) revealed this issue.
2021-11-05 13:24:51 +01:00
Julien Fontanet
7f9a970714 fix(log/USAGE): document filter array
Introduced by d3cb31f1a
2021-11-04 10:45:58 +01:00
Julien Fontanet
7661d3372d fix(xen-api/USAGE): add httpProxy option
Introduced by 2412f8b1e
2021-11-04 10:38:22 +01:00
Julien Fontanet
dbb4f34015 chore(xapi/VDI_destroy): decorate with retry.wrap()
- more efficient than creating a function at each call
- better logging
2021-11-03 23:10:58 +01:00
Julien Fontanet
8f15a4c29d feat(ISSUE_TEMPLATE/bug_report): add hypervisor version 2021-11-03 16:55:17 +01:00
Florent BEAUCHAMP
1b0a885ac3 feat(vhd-cli): use any remote for copy and compare (#5927) 2021-11-03 15:45:52 +01:00
Nicolas Raynaud
f7195bad88 fix(xo-server): fix ova multipart upload (#5976)
Introduced by 0451aaeb5c
2021-11-02 17:43:45 +01:00
Julien Fontanet
15630aee5e chore: update dev deps 2021-11-02 13:43:49 +01:00
Florent BEAUCHAMP
a950a1fe24 refactor(vhd-lib): centralize test methods (#5968) 2021-11-02 09:53:30 +01:00
Julien Fontanet
71b8e625fe chore: update issue templates (#5974) 2021-10-30 15:06:51 +02:00
Julien Fontanet
e7391675fb feat(@xen-orchestra/proxy): 0.15.2 2021-10-29 17:41:02 +02:00
Julien Fontanet
84fdd3fe4b fix(proxy/api/ndJsonStream): send header for empty iterables
Introduced by ed987e161
2021-10-29 17:05:05 +02:00
Julien Fontanet
4dc4b635f2 feat(@xen-orchestra/proxy): 0.15.1 2021-10-29 15:50:42 +02:00
Julien Fontanet
ee0c6d7f8b feat(xen-api): 0.35.1 2021-10-29 15:50:05 +02:00
Julien Fontanet
a637af395d fix(xen-api): add missing dep proxy-agent
Introduced by 2412f8b1e
2021-10-29 15:40:25 +02:00
Julien Fontanet
59fb612315 feat(@xen-orchestra/proxy): 0.15.0 2021-10-29 15:20:09 +02:00
Mathieu
59b21c7a3e feat: release 5.64 (#5971) 2021-10-29 11:40:16 +02:00
Mathieu
40f881c2ac feat: technical release (#5970) 2021-10-28 16:30:00 +02:00
Rajaa.BARHTAOUI
1d069683ca feat(xo-web/host): manage evacuation failure during host shutdown (#5966) 2021-10-28 14:23:43 +02:00
Julien Fontanet
de1d942b90 fix(xo-server/listPoolsMatchingCriteria): check{Sr,Pool}Name is not a function
Fixes xoa-support#4193

Introduced by cd8c618f0
2021-10-28 13:29:32 +02:00
Rajaa.BARHTAOUI
fc73971d63 feat(xo-server,xo-web/menu): proxy upgrade notification (#5930)
See xoa-support#4105
2021-10-28 10:52:23 +02:00
Rajaa.BARHTAOUI
eb238bf107 feat(xo-web/pool/advanced, xen-api/{get,put}Resource): introduce backup network (#5957) 2021-10-28 10:21:48 +02:00
Florent BEAUCHAMP
2412f8b1e2 feat(xen-api): add HTTP proxy support (#5958)
See #5436

Using an IP address as HTTPS proxy show this warning: `DeprecationWarning: Setting the TLS ServerName to an IP address is not permitted by RFC 6066`

The corresponding issue is there : TooTallNate/node-https-proxy-agent#127
2021-10-27 17:30:41 +02:00
Pierre Donias
0c87dee31c fix(xo-web/xoa): handle string expiration dates (#5967)
See xoa-support#4114
See xoa-support#4192

www-xo may return a string instead of a number in some rare cases
2021-10-27 16:59:59 +02:00
Mathieu
215146f663 feat(xo-web/vm/export): allow to copy the export URL (#5948) 2021-10-27 16:58:09 +02:00
Mathieu
9fe1069df0 feat(xo-web/host): format logs (#5943)
See xoa-support#4100
2021-10-27 15:41:29 +02:00
Julien Fontanet
d2c5b52bf1 feat(backups): enable merge worker by default
Related to 47f9da216

It can still be disabled in case of problems:

```toml
[backups]
disableMergeWorker = true
```
2021-10-27 09:29:50 +02:00
Pierre Donias
12153a414d fix(xo-server/{clone,copy}Vm): force is_a_template to false on the new VM (#5955)
See xoa-support#4137
2021-10-26 16:53:09 +02:00
Pierre Donias
5ec1092a83 fix(xo-server-netbox/test): perform test with a 50-character name (#5963)
See https://xcp-ng.org/forum/topic/5111
See https://netbox.readthedocs.io/en/stable/release-notes/version-2.10/#other-changes > #5011

Versions of Netbox <2.10 only allow cluster type names of length <= 50.
2021-10-26 15:55:11 +02:00
Julien Fontanet
284169a2f2 chore(vhd-lib/VhdAbstract): format with Prettier
Introduced by 7ef89d504
2021-10-25 16:12:49 +02:00
Julien Fontanet
838bfbb75f fix(backups/cleanVm): wait for merge to finish
Introduced by 9c83e70a2
2021-10-25 09:14:38 +02:00
Julien Fontanet
a448da77c9 fix(backups/cleanVm): mergeLimiter support
Introduced by 9c83e70a2
2021-10-25 09:13:58 +02:00
Rajaa.BARHTAOUI
268fb22d5f feat(xo-web/host/advanced): add button to disable/enable host (#5952) 2021-10-20 16:39:54 +02:00
Julien Fontanet
07cc4c853d fix(vhd-lib): fix block table properties & accessors
Fixes #5956

Introduced by 7ef89d504
2021-10-18 23:13:55 +02:00
Florent BEAUCHAMP
c62d727cbe feat(vhd-cli compare): compare metadata and content of two VHDs (#5920) 2021-10-18 16:21:40 +02:00
Florent BEAUCHAMP
7ef89d5043 feat(vhd-{cli,lib}): implement chunking and copy command (#5919) 2021-10-18 14:56:58 +02:00
Mathieu
9ceba1d6e8 feat(xo-web/jobs): add button to copy jobs IDs (#5951)
Useful to create a `job.runSequence` job. Follow-up of #5944.
2021-10-15 14:25:02 +02:00
Pierre Donias
e2e453985f fix(xo-web/job): properly handle array arguments (#5944)
See https://xcp-ng.org/forum/topic/5010

When creating/editing a job, properties of type `array` must not go through the
cross product builder, they must be saved as arrays.
2021-10-15 10:42:33 +02:00
Florent BEAUCHAMP
84dccd800f feat(backups): clean up other schedules snapshots (#5949)
Fixes xoa-support#4129
2021-10-14 14:44:40 +02:00
Julien Fontanet
f9734d202b chore(backups/_VmBackup): remove unused import 2021-10-14 13:51:29 +02:00
Julien Fontanet
d3cb0f4672 feat(xo-server): 5.82.4 2021-10-14 09:47:39 +02:00
Julien Fontanet
c198bbb6fa feat(@xen-orchestra/backups): 0.14.0 2021-10-14 09:45:20 +02:00
Julien Fontanet
c965a89509 feat(xo-server-netbox): 0.3.2 2021-10-14 09:43:38 +02:00
Julien Fontanet
47f9da2160 feat(backups/MixinBackupWriter): use merge worker if not disabled 2021-10-13 16:26:12 +02:00
Julien Fontanet
348a75adb4 feat(backups): merge worker implementation
This CLI must be run directly in the directory where the remote is mounted.

It's only compatible with local remote at the moment.

To start the worker:

```js
const MergeWorker = require('@xen-orchestra/backups/merge-worker/index.js')

await MergeWorker.run(remotePath)
```

To register a VM backup dir to be clean (thus merging its unused VHD), create a file in the queue directory containing the VM UUID:

```
> echo cc700fe2-724e-44a5-8663-5f8f88e05e34 > .queue/clean-vm/20211013T142401Z
```

The queue directory is available as `MergeWorker.CLEAN_VM_QUEUE`.
2021-10-13 16:25:21 +02:00
Julien Fontanet
332218a7f7 feat(backups): move merge responsability to cleanVm 2021-10-13 16:10:19 +02:00
Julien Fontanet
6d7a26d2b9 chore(backups/MixinBackupWriter): use private fields 2021-10-13 10:02:57 +02:00
Pierre Donias
d19a748f0c fix(xo-server-netbox): support older versions of Netbox (#5946)
Fixes #5898
See https://netbox.readthedocs.io/en/stable/release-notes/version-2.7/#api-choice-fields-now-use-string-values-3569
2021-10-13 09:28:46 +02:00
Julien Fontanet
9c83e70a28 feat(backups/RemoteAdapter#cleanVm): configurable merge limiter 2021-10-12 09:17:42 +02:00
Rajaa.BARHTAOUI
abcabb736b feat(xo-web/tasks): filter out short tasks with a default filter (#5941)
See xoa-support#4096
2021-10-08 16:42:16 +02:00
Julien Fontanet
0451aaeb5c fix(xo-server/vm.import): restore non-multipart upload (#5936)
See xoa-support#4085

Introduced by fdf52a3d5

Required by `xo-cli`.
2021-10-08 15:24:21 +02:00
Julien Fontanet
880c45830c fix(xo-cli): http-request-plus@0.12 has no longer default export
Introduced by 62e5ab699
2021-10-07 17:11:54 +02:00
Julien Fontanet
5fa16d2344 chore: format with Prettier 2021-10-07 14:40:41 +02:00
Julien Fontanet
9e50b5dd83 feat(proxy): logging is now dynamically configurable
It was done for xo-server in f20d5cd8d
2021-10-06 16:54:57 +02:00
Julien Fontanet
29d8753574 chore(backups/VmBackup#_selectBaseVm): add debug logs 2021-10-06 16:48:42 +02:00
Pierre Donias
f93e1e1695 feat: release 5.63.0 (#5925) 2021-09-30 15:25:34 +02:00
Pierre Donias
0eaac8fd7a feat: technical release (#5924) 2021-09-30 11:17:45 +02:00
Julien Fontanet
06c71154b9 fix(xen-api/_setHostAddressInUrl): pass params in array
Introduced in fb21e4d58
2021-09-30 10:32:12 +02:00
Julien Fontanet
0e8f314dd6 fix(xo-web/new-vm): don't send default networkConfig (#5923)
Fixes #5918
2021-09-30 09:37:12 +02:00
Florent BEAUCHAMP
f53ec8968b feat(xo-web/SortedTable): move filter and pagination to top (#5914) 2021-09-29 17:35:46 +02:00
Mathieu
919d118f21 feat(xo-web/health): filter duplicated MAC addresses by running VMs (#5917)
See xoa-support#4054
2021-09-24 17:25:42 +02:00
Mathieu
216b759df1 feat(xo-web/health): hide CR VMs duplicated MAC addresses (#5916)
See xoa-support#4054
2021-09-24 15:52:34 +02:00
Julien Fontanet
01450db71e fix(proxy/backup.run): clear error on license issue
Fixes https://xcp-ng.org/forum/topic/4901/backups-silently-fail-with-invalid-xo-proxy-license
2021-09-24 13:15:32 +02:00
Julien Fontanet
ed987e1610 fix(proxy/api/ndJsonStream): send JSON-RPC error if whole iteration failed
See https://xcp-ng.org/forum/topic/4901/backups-silently-fail-with-invalid-xo-proxy-license
2021-09-24 13:15:24 +02:00
Florent BEAUCHAMP
2773591e1f feat(xo-web): add go back to ActionButton and use it when saving a backup (#5913)
See xoa-support#2149
2021-09-24 11:38:37 +02:00
Pierre Donias
a995276d1e fix(xo-server-netbox): better handle missing uuid custom field (#5909)
Fixes #5905
See #5806
See #5834
See xoa-support#3812

- Check if `uuid` custom field has correctly been configured before synchronizing
- Delete VMs that don't have a UUID before synchronizing VMs to avoid conflicts
2021-09-22 18:08:09 +02:00
Nicolas Raynaud
ffb6a8fa3f feat(VHD import): ensure uploaded file is a VHD (#5906) 2021-09-21 16:25:50 +02:00
Pierre Donias
0966efb7f2 fix(xo-server-netbox): handle nested prefixes (#5908)
See xoa-support#4018

When assigning prefixes to VMs, always pick the smallest prefix that the IP
matches
2021-09-21 09:55:47 +02:00
Julien Fontanet
4a0a708092 feat: release 5.62.1 2021-09-17 10:04:36 +02:00
Julien Fontanet
6bf3b6f3e0 feat(xo-server): 5.82.2 2021-09-17 09:24:32 +02:00
Julien Fontanet
8f197fe266 feat(@xen-orchestra/proxy): 0.14.6 2021-09-17 09:24:05 +02:00
Julien Fontanet
e1a3f680f2 feat(xen-api): 0.34.2 2021-09-17 09:23:28 +02:00
Julien Fontanet
e89cca7e90 feat: technical release 2021-09-17 09:19:26 +02:00
Nicolas Raynaud
5bb2767d62 fix(xo-server/{disk,vm}.import): fix import of very small VMDK files (#5903) 2021-09-17 09:17:34 +02:00
Julien Fontanet
95f029e0e7 fix(xen-api/putResource): fix non-stream use case
Introduced by ea10df8a92
2021-09-14 17:42:20 +02:00
Julien Fontanet
fb21e4d585 chore(xen-api/_setHostAddressInUrl): use _roCall to fetch network ref
Introduced by a84fac1b6
2021-09-14 17:42:20 +02:00
Julien Fontanet
633805cec9 fix(xen-api/_setHostAddressInUrl): correctly fetch network ref
Introduced by a84fac1b6
2021-09-14 17:42:20 +02:00
Marc Ungeschikts
b8801d7d2a "rentention" instead of "retention" (#5904) 2021-09-14 16:30:10 +02:00
Julien Fontanet
a84fac1b6a fix(xen-api/{get,put}Resource): use provided address when possible
Fixes #5896

Introduced by ea10df8a92

Don't use the address provided by XAPI when connecting to the pool master and without a default migration network as it will unnecessarily break NATted hosts.
2021-09-14 13:52:34 +02:00
Julien Fontanet
a9de4ceb30 chore(xo-server/config.toml): explicit auth delay is per user 2021-09-12 10:55:31 +02:00
Julien Fontanet
827b55d60c fix(xo-server/config.toml): typo 2021-09-12 10:54:49 +02:00
Julien Fontanet
0e1fe76b46 chore: update dev deps 2021-09-09 13:48:15 +02:00
Julien Fontanet
097c9e8e12 feat(@xen-orchestra/proxy): 0.14.5 2021-09-07 19:02:57 +02:00
Pierre Donias
266356cb20 fix(xo-server/xapi-objects-to-xo/VM/addresses): handle newline-delimited IPs (#5897)
See xoa-support#3812
See #5860

This is related to a505cd9 which handled space delimited IPs, but apparently,
IPs can also be newline delimited depending on which Xen tools version is used.
2021-09-03 12:30:47 +02:00
Julien Fontanet
6dba39a804 fix(xo-server/vm.set): fix converting to BIOS (#5895)
Fixes xoa-support#3991
2021-09-02 14:11:39 +02:00
Olivier Lambert
3ddafa7aca fix(docs/xoa): clarify first console connection (#5894) 2021-09-01 12:51:33 +02:00
Julien Fontanet
9d8e232684 chore(xen-api): dont import promise-toolbox/retry twice
Introduced by ea10df8a9
2021-08-31 12:28:23 +02:00
Anthony Stivers
bf83c269c4 fix(xo-web/user): SSH key formatting (#5892)
Fixes #5891

Allow SSH key to be broken anywhere to avoid breaking page formatting.
2021-08-31 11:42:25 +02:00
Pierre Donias
54e47c98cc feat: release 5.62.0 (#5893) 2021-08-31 10:59:07 +02:00
Pierre Donias
118f2594ea feat: technical release (#5889) 2021-08-30 15:40:26 +02:00
Julien Fontanet
ab4fcd6ac4 fix(xen-api/{get,put}Resource): correctly fetch host
Introduced by ea10df8a9
2021-08-30 15:23:42 +02:00
Pierre Donias
ca6f345429 feat: technical release (#5888) 2021-08-30 12:08:10 +02:00
Pierre Donias
79b8e1b4e4 fix(xo-server-auth-ldap): ensure-array dependency (#5887) 2021-08-30 12:01:06 +02:00
Pierre Donias
cafa1ffa14 feat: technical release (#5886) 2021-08-30 11:01:14 +02:00
Mathieu
ea10df8a92 feat(xen-api/{get,put}Resource): use default migration network if available (#5883) 2021-08-30 00:14:31 +02:00
Julien Fontanet
85abc42100 chore(xo-web): use sass instead of node-sass
Fixes build with Node 16
2021-08-27 14:22:00 +02:00
Mathieu
4747eb4386 feat(host): display warning for eol host version (#5847)
Fixes #5840
2021-08-24 14:43:01 +02:00
tisteagle
ad9cc900b8 feat(docs/updater): add nodejs.org to required domains (#5881) 2021-08-22 16:33:16 +02:00
Pierre Donias
6cd93a7bb0 feat(xo-server-netbox): add primary IPs to VMs (#5879)
See xoa-support#3812
See #5633
2021-08-20 12:47:29 +02:00
Julien Fontanet
3338a02afb feat(fs/getSyncedHandler): returns disposable to an already synced remote
Also, no need to forget it.
2021-08-20 10:14:39 +02:00
Julien Fontanet
31cfe82224 chore: update to index-modules@0.4.3
Fixes #5877

Introduced by 030477454

This new version fixes the `--auto` mode used by `xo-web`.
2021-08-18 10:08:10 +02:00
Pierre Donias
70a191336b fix(CHANGELOG): missing PR link (#5876) 2021-08-17 10:13:22 +02:00
Julien Fontanet
030477454c chore: update deps 2021-08-17 09:59:42 +02:00
Pierre Donias
2a078d1572 fix(xo-server/host): clearHost argument needs to have a $pool property (#5875)
See xoa-support#3118
Introduced by b2a56c047c
2021-08-17 09:51:36 +02:00
Julien Fontanet
3c1f96bc69 chore: update dev deps 2021-08-16 14:10:18 +02:00
Mathieu
7d30bdc148 fix(xo-web/TabButtonLink): should not be empty on small screens (#5874) 2021-08-16 09:45:44 +02:00
Mathieu
5d42961761 feat(xo-server/network.create): allow pool admins (#5873) 2021-08-13 14:22:58 +02:00
Julien Fontanet
f20d5cd8d3 feat(xo-server): logging is now dynamically configurable 2021-08-12 17:30:56 +02:00
Julien Fontanet
f5111c0f41 fix(mixins/Config#watch): use deep equality to check changes
Because objects (and arrays) will always be new ones and thus different.
2021-08-12 17:29:57 +02:00
Pierre Donias
f5473236d0 fix(xo-web): dont warn when restoring XO config (#5872) 2021-08-12 09:52:45 +02:00
Julien Fontanet
d3cb31f1a7 feat(log/configure): filter can be an array 2021-08-11 18:09:42 +02:00
Pierre Donias
d5f5cdd27a fix(xo-server-auth-ldap): create logger inside plugin (#5864)
The plugin was wrongly expecting a logger instance to be passed on instantiation
2021-08-11 11:21:22 +02:00
Pierre Donias
656dc8fefc fix(xo-server-ldap): handle groups with no members (#5862)
See xoa-support#3906
2021-08-10 14:12:39 +02:00
Pierre Donias
a505cd9567 fix(xo-server/xapi-objects-to-xo/VM/addresses): handle old tools alias properties (#5860)
See https://xcp-ng.org/forum/topic/4810
See #5805
2021-08-10 10:22:13 +02:00
Pierre Donias
f2a860b01a feat: release 5.61.0 (#5867) 2021-07-30 16:48:13 +02:00
Pierre Donias
1a5b93de9c feat: technical release (#5866) 2021-07-30 16:31:16 +02:00
Pierre Donias
0f165b33a6 feat: technical release (#5865) 2021-07-30 15:21:49 +02:00
Pierre Donias
4f53555f09 Revert "chore(backups/DeltaReplication): unify base VM detection" (#5861)
This reverts commit 9139c5e9d6.
See https://xcp-ng.org/forum/topic/4817
2021-07-30 14:55:00 +02:00
Pierre Donias
175be44823 feat(xo-web/VM/advanced): handle pv_in_pvh virtualization mode (#5857)
And handle unknown virtualization modes by showing the raw string
2021-07-28 18:41:22 +02:00
Julien Fontanet
20a6428290 fix(xo-server/xen-servers): fix lodash/pick import
Introduced by 4b4bea5f3

Fixes #5858
2021-07-28 08:48:17 +02:00
Julien Fontanet
4b4bea5f3b chore(xo-server): log ids on xapiObjectToXo errors 2021-07-27 15:05:00 +02:00
Pierre Donias
c82f860334 feat: technical release (#5856) 2021-07-27 11:08:53 +02:00
Pierre Donias
b2a56c047c feat(xo-server/clearHost): use pool's default migration network (#5851)
Fixes #5802
See xoa-support#3118
2021-07-27 10:44:30 +02:00
Julien Fontanet
bc6afc3933 fix(xo-server): don't fail on invalid pool pattern
Fixes #5849
2021-07-27 05:13:45 +02:00
Pierre Donias
280e4b65c3 feat(xo-web/VM/{shutdown,reboot}): ask user if they want to force when no tools (#5855)
Fixes #5838
2021-07-26 17:22:31 +02:00
Julien Fontanet
c6f22f4d75 fix(backups): block start_on operation on replicated VMs (#5852) 2021-07-26 15:01:11 +02:00
Pierre Donias
4bed8eb86f feat(xo-server-netbox): optionally allow self-signed certificates (#5850)
See https://xcp-ng.org/forum/topic/4786/netbox-plugin-does-not-allow-self-signed-certificate
2021-07-23 09:53:02 +02:00
Julien Fontanet
c482f18572 chore(xo-web/vm/tab-advanced): shutdown is a valid operation 2021-07-23 09:49:32 +02:00
Mathieu
d7668acd9b feat(xo-web/sr/tab-disks): display the active vdi of the basecopy (#5826)
See xoa-support#3446
2021-07-21 09:32:24 +02:00
Julien Fontanet
05b978c568 chore: update dev deps 2021-07-20 10:20:52 +02:00
Julien Fontanet
62e5ab6990 chore: update to http-request-plus@0.12.0 2021-07-20 10:03:16 +02:00
Mathieu
12216f1463 feat(xo-web/vm): rescan ISO SRs available in console view (#5841)
See xoa-support#3896
See xoa-support#3888
See xoa-support#3909
Continuity of d7940292d0
Introduced by f3501acb64
2021-07-16 17:02:10 +02:00
Pierre Donias
cbfa13a8b4 docs(netbox): make it clear that the uuid custom field needs to be lower case (#5843)
Fixes #5831
2021-07-15 09:45:05 +02:00
Pierre Donias
03ec0cab1e feat(xo-server-netbox): add data field to Netbox API errors (#5842)
Fixes #5834
2021-07-13 17:22:51 +02:00
mathieuRA
d7940292d0 feat(xo-web/vm): rescan ISO SRs available in console view 2021-07-12 11:55:02 +02:00
Julien Fontanet
9139c5e9d6 chore(backups/DeltaReplication): unify base VM detection
Might help avoiding the *unable to find base VM* error.
2021-07-09 15:14:37 +02:00
Julien Fontanet
65e62018e6 chore(backups/importDeltaVm): dont explicitly wait for export tasks
Might be related to stuck importation issues.
2021-07-08 09:56:06 +02:00
Julien Fontanet
138a3673ce fix(xo-server/importConfig): fix this._app.clean is not a function
Fixes #5836
2021-07-05 17:57:47 +02:00
Pierre Donias
096f443b56 feat: release 5.60.0 (#5833) 2021-06-30 15:49:52 +02:00
Pierre Donias
b37f30393d feat: technical release (#5832) 2021-06-30 11:07:14 +02:00
Ronan Abhamon
f095a05c42 feat(docs/load_balancing): add doc about VM anti-affinity mode (#5830)
* feat(docs/load_balancing): add doc about VM anti-affinity mode

Signed-off-by: Ronan Abhamon <ronan.abhamon@vates.fr>

* grammar edits for anti-affinity

Co-authored-by: Jon Sands <fohdeesha@gmail.com>
2021-06-30 10:37:25 +02:00
Pierre Donias
3d15a73f1b feat(xo-web/vm/new disk): generate random name (#5828) 2021-06-28 11:26:09 +02:00
Julien Fontanet
bbd571e311 chore(xo-web/vm/tab-disks.js): format with Prettier 2021-06-28 11:25:31 +02:00
Pierre Donias
a7c554f033 feat(xo-web/snapshots): identify VM's parent snapshot (#5824)
See xoa-support#3775
2021-06-25 12:07:50 +02:00
Pierre Donias
25b4532ce3 feat: technical release (#5825) 2021-06-25 11:13:23 +02:00
Pierre Donias
a304f50a6b fix(xo-server-netbox): compare compact notations of IPv6 (#5822)
XAPI doesn't use IPv6 compact notation while Netbox automatically compacts them
on creation. Comparing those 2 notations makes XO believe that the IPs in
Netbox should be deleted and new ones should be created, even though they're
actually the same IPs. This change compacts the IPs before comparing them.
2021-06-24 17:00:07 +02:00
Pierre Donias
e75f476965 fix(xo-server-netbox): filter out devices' interfaces (#5821)
See xoa-support#3812

In Netbox, a device interface and a VM interface can have the same ID `x`,
which means that listing IPs with `assigned_object_id=x` won't only get the
VM's interface's IPs but also the device's interface's IPs. This made XO
believe that those extra IPs shouldn't exist and delete them. This change
makes sure to only grab VM interface IPs.
2021-06-23 15:27:11 +02:00
Julien Fontanet
1c31460d27 fix(xo-server/disconnectXenServer): delete pool association
This should prevent the *server is already connected* issue after reinstalling host.
2021-06-23 10:11:12 +02:00
Julien Fontanet
19db468bf0 fix(CHANGELOG.unreleased): vhd-lib
Introduced by aa4f1b834
2021-06-23 09:26:23 +02:00
Julien Fontanet
5fe05578c4 fix(xo-server/backupNg.importVmBackup): returns id of imported VM
Fixes #5820

Introduced by d9ce1b3a9.
2021-06-22 18:26:01 +02:00
Julien Fontanet
956f5a56cf feat(backups/RemoteAdapter#cleanVm): fix backup size if necessary
Fixes #5810
Fixes #5815
2021-06-22 18:16:52 +02:00
Julien Fontanet
a3f589d740 feat(@xen-orchestra/proxy): 0.14.3 2021-06-21 14:36:55 +02:00
Julien Fontanet
beef09bb6d feat(@xen-orchestra/backups): 0.11.2 2021-06-21 14:30:32 +02:00
Julien Fontanet
ff0a246c28 feat(proxy/api/ndJsonStream): handle iterable error 2021-06-21 14:26:55 +02:00
Julien Fontanet
f1459a1a52 fix(backups/VmBackup#_callWriters): writers.delete
Introduced by 56e4847b6
2021-06-21 14:26:55 +02:00
Mathieu
f3501acb64 feat(xo-web/vm/tab-disks): rescan ISO SRs (#5814)
See https://xcp-ng.org/forum/topic/4588/add-rescan-iso-sr-from-vm-menu
2021-06-18 16:15:33 +02:00
Ronan Abhamon
2238c98e95 feat(load-balancer): log vm and host names when a VM is migrated + category (density, performance, ...) (#5808)
Co-authored-by: Julien Fontanet <julien.fontanet@isonoe.net>
2021-06-18 09:49:33 +02:00
Julien Fontanet
9658d43f1f feat(xo-server-load-balancer): use @xen-orchestra/log 2021-06-18 09:44:37 +02:00
Julien Fontanet
1748a0c3e5 chore(xen-api): remove unused inject-events 2021-06-17 16:41:04 +02:00
Julien Fontanet
4463d81758 feat(@xen-orchestra/proxy): 0.14.2 2021-06-17 15:58:00 +02:00
Julien Fontanet
74221a4ab5 feat(@xen-orchestra/backups): 0.11.1 2021-06-17 15:57:10 +02:00
Julien Fontanet
0d998ed342 feat(@xen-orchestra/xapi): 0.6.4 2021-06-17 15:56:21 +02:00
Julien Fontanet
7d5a01756e feat(xen-api): 0.33.1 2021-06-17 15:55:20 +02:00
Pierre Donias
d66313406b fix(xo-web/new-vm): show correct amount of memory in summary (#5817) 2021-06-17 14:36:44 +02:00
Pierre Donias
d96a267191 docs(web-hooks): add "wait for response" and backup related doc (#5819)
See #5420
See #5360
2021-06-17 14:34:03 +02:00
Julien Fontanet
5467583bb3 fix(backups/_VmBackup#_callWriters): dont run single writer twice
Introduced by 56e4847b6

See https://xcp-ng.org/forum/topic/4659/backup-failed
2021-06-17 14:14:48 +02:00
Rajaa.BARHTAOUI
9a8138d07b fix(xo-server-perf-alert): smart mode: select only running VMs and hosts (#5811) 2021-06-17 11:56:04 +02:00
Pierre Donias
36c290ffea feat(xo-web/jobs): add host.emergencyShutdownHost to the methods list (#5818) 2021-06-17 11:55:51 +02:00
Julien Fontanet
3413bf9f64 fix(xen-api/{get,put}Resource): distinguish cancelation and connection issue (2)
Follow up of 057a1cbab
2021-06-17 10:12:09 +02:00
Julien Fontanet
3c352a3545 fix(backups/_VmBackup#_callWriters): missing writer var
Fixes #5816
2021-06-17 08:53:38 +02:00
Julien Fontanet
56e4847b6b feat(backups/_VmBackup#_callWriters): dont use generic error when only one writer 2021-06-16 10:15:10 +02:00
Julien Fontanet
033b671d0b fix(xo-server): limit number of xapiObjectToXo logs
See xoa-support#3830
2021-06-16 09:59:07 +02:00
Julien Fontanet
51f013851d feat(xen-api): limit concurrent calls to 20
Fixes xoa-support#3767

Can be changed via `callConcurrency` option.
2021-06-14 18:37:58 +02:00
Yannick Achy
dafa4ced27 feat(docs/backups): new concurrency model (#5701) 2021-06-14 16:38:29 +02:00
Pierre Donias
05fe154749 fix(xo-server/xapi): don't silently swallow errors on _callInstallationPlugin (#5809)
See xoa-support#3738

Introduced by a73acedc4d

This was done to prevent triggering an error when the pack was already
installed but a XENAPI_PLUGIN_FAILURE error can happen for other reasons
2021-06-14 16:01:02 +02:00
Nick Zana
5ddceb4660 fix(docs/from sources): change GitHub URL to use TLS (#5813) 2021-06-14 00:34:42 +02:00
Julien Fontanet
341a1b195c fix(docs): filenames in how to update self-signed cert
See xoa-support#3821
2021-06-11 17:09:23 +02:00
Julien Fontanet
29c3d1f9a6 feat(xo-web/debug): add timing 2021-06-11 10:08:14 +02:00
Rajaa.BARHTAOUI
734d4fb92b fix(xo-server#listPoolsMatchingCriteria): fix "unknown error from the peer" error (#5807)
See xoa-support#3489

Introduced by cd8c618f08
2021-06-08 17:00:45 +02:00
Julien Fontanet
057a1cbab6 feat(xen-api/{get,put}Resource): distringuish cancelation and connection issue
See xoa-support#3643
2021-06-05 01:15:36 +02:00
Pierre Donias
d44509b2cd fix(xo-server/xapi-object-to-xo/vm): handle space-delimited IP addresses (#5805)
Fixes #5801
2021-06-04 10:01:08 +02:00
Julien Fontanet
58cf69795a fix(xo-server): remove broken API methods
Introduced bybdb0ca836

These methods were linked to the legacy backups which are no longer supported.
2021-06-03 14:49:18 +02:00
Julien Fontanet
6d39512576 chore: format with Prettier
Introduced by 059843f03
2021-06-03 14:49:14 +02:00
Julien Fontanet
ec4dde86f5 fix(CHANGELOG.unreleased): add missing entries
Introduced by 1c91fb9dd
2021-06-02 16:55:45 +02:00
Nicolas Raynaud
1c91fb9dd5 feat(xo-{server,web}): improve OVA import error reporting (#5797) 2021-06-02 16:23:08 +02:00
Yannick Achy
cbd650c5ef feat(docs/troubleshooting): set xoa SSH password (#5798) 2021-06-02 09:50:29 +02:00
Julien Fontanet
c5a769cb29 fix(xo-server/glob-matcher): fix micromatch import
Introduced by 254558e9d
2021-05-31 17:36:47 +02:00
Julien Fontanet
00a7277377 feat(xo-server-sdn-controller): 1.0.5 2021-05-31 14:33:21 +02:00
BenjiReis
b8c32d41f5 fix(sdn-controller): dont assume all tunnels in private networks use the same device/vlan (#5793)
Fixes xoa-support#3771
2021-05-31 14:30:58 +02:00
Rajaa.BARHTAOUI
49c9fc79c7 feat(@vates/decorate-with): 0.1.0 (#5795) 2021-05-31 14:29:23 +02:00
Rajaa.BARHTAOUI
1284a7708e feat: release 5.59 (#5796) 2021-05-31 12:07:46 +02:00
Julien Fontanet
0dd8d15a9a fix(xo-web): use terser instead of uglify-es
Fixes https://xcp-ng.org/forum/topic/4638/yarn-build-failure

Better maintenance and support of modern ES features.
2021-05-28 15:38:25 +02:00
Julien Fontanet
90f59e954a fix(docs/from sources): clarify that Node >=14.17 is required
Related to 00beb6170
2021-05-28 15:14:28 +02:00
Julien Fontanet
03d7ec55a7 feat(decorate-with): decorateMethodsWith() 2021-05-28 12:15:22 +02:00
Julien Fontanet
1929b69145 chore(decorate-with): improve doc 2021-05-28 12:06:45 +02:00
Julien Fontanet
fbf194e4be chore(decorate-with): named function 2021-05-28 12:06:00 +02:00
Julien Fontanet
a20927343a chore: remove now unnecessary core-js deps
BREAKING CHANGE: @xen-orchestra/audit-core now requires Node >=10
2021-05-28 09:44:44 +02:00
Julien Fontanet
3b465dc09e fix: dont use deprecated fs-extra
BREAKING CHANGE: vhd-lib and xo-vmdk-to-vhd now require Node >=10
2021-05-28 09:39:51 +02:00
Julien Fontanet
fb8ca00ad1 fix: dont use deprecated event-to-promise 2021-05-28 09:34:49 +02:00
Julien Fontanet
dd7dddaa2b chore(xo-import-servers-csv): remove unmaintained tslint conf 2021-05-28 09:28:40 +02:00
Julien Fontanet
f41903c2a1 fix(xo-cli,xo-upload-ova}: dont use deprecated nice-pipe
BREAKING CHANGE: they now require Node >=10
2021-05-28 09:25:19 +02:00
Julien Fontanet
9984b5882d feat(@xen-orchestra/proxy): 0.14.1 2021-05-27 15:15:34 +02:00
Julien Fontanet
9ff20bee5a fix(proxy/package.json): fix bin and start script
Introduced by df9689854
2021-05-27 15:15:13 +02:00
Julien Fontanet
53caa11bc4 chore(proxy/package.json): remove useless main entry
This package is not a library.
2021-05-27 15:13:26 +02:00
Julien Fontanet
f6ac08567c feat(@xen-orchestra/xapi): 0.6.3 2021-05-27 15:04:13 +02:00
Julien Fontanet
040c6375c0 chore(xo-server/config.toml): remove unnecessary quotes 2021-05-27 15:00:59 +02:00
Julien Fontanet
a03266aaad feat(@xen-orchestra/proxy): 0.14.0 2021-05-27 14:28:29 +02:00
Julien Fontanet
3479064348 feat(xo-server-netbox): 0.1.1 2021-05-27 10:37:35 +02:00
Julien Fontanet
b02d823b30 fix(xo-server-netbox): fix dependencies 2021-05-27 10:37:35 +02:00
Julien Fontanet
a204b6fb3f feat(xo-server): 5.79.5 2021-05-26 17:51:07 +02:00
Rajaa.BARHTAOUI
c2450843a5 feat: technical release (#5790) 2021-05-26 16:52:20 +02:00
Julien Fontanet
00beb6170e fix(xo-server): require Node >=14.17
Fixes #5789

Better import of CommonJS modules.
2021-05-26 16:07:34 +02:00
Julien Fontanet
9f1a300d2a fix(backups): properly close streams are destroyed in case of failure
Fixes xoa-support#3753
2021-05-26 14:39:56 +02:00
Julien Fontanet
05aefa1d5c chore: update to http-request-plus@0.10.0 2021-05-25 14:35:52 +02:00
Julien Fontanet
059843f030 chore: update dev deps 2021-05-25 14:22:58 +02:00
Julien Fontanet
e202dc9851 fix(docs): use correct bin with forever-service 2021-05-23 18:53:26 +02:00
Pierre Donias
18ae664ba7 feat(xo-server-netbox): new plugin to synchronize pools with Netbox (#5783)
Fixes #5633
2021-05-21 19:39:02 +02:00
Julien Fontanet
76b563fa88 feat(xo-web/vm/console): make multiline clipboard input monospaced 2021-05-21 14:21:33 +02:00
Julien Fontanet
2553f4c161 feat(xo-web/host/install-certificate): make inputs monospaced 2021-05-21 14:20:56 +02:00
Julien Fontanet
f35c865348 feat(xo-web): SSH key input monospaced 2021-05-21 14:19:50 +02:00
Julien Fontanet
b873ba3a75 feat(xo-web): make CloudConfig inputs monospaced
Fixed #5786
2021-05-21 14:15:12 +02:00
Julien Fontanet
d49e388ea3 feat(xo-server/registerPlugin): log plugin metadata errors 2021-05-21 14:00:25 +02:00
Julien Fontanet
b931699175 feat(xo-server/registerPlugin): don't fail on JSON parsing errors 2021-05-21 14:00:06 +02:00
Julien Fontanet
55fd58efd8 fix(xo-server): reading plugin metadata
Fixes #5782
2021-05-21 13:58:32 +02:00
Julien Fontanet
773847e139 feat(xo-server,xo-proxy): add backupId to restore tasks 2021-05-21 13:50:27 +02:00
Julien Fontanet
3a52944f21 fix(docs): use correct bin with forever 2021-05-20 18:49:36 +02:00
Julien Fontanet
cc9d741275 fix(xo-server): fix plugins import
Fixes #5782 part 2

Introduced by 254558e9d
2021-05-20 12:07:00 +02:00
Julien Fontanet
f0096cf0e2 chore(xo-server): remove useless imports
Introduced by 254558e9d
2021-05-20 10:21:26 +02:00
Julien Fontanet
1d673bf6ff chore(xo-server): remove useless entry point
Introduced by 254558e9d

Due to ESM, it's no longer easy to alter `DEBUG` before all instances of `debug` are loaded, which makes it useless.
2021-05-20 10:16:23 +02:00
Julien Fontanet
d986f00b6a chore(xo-server): remove rimraf dev dep 2021-05-19 17:43:12 +02:00
Julien Fontanet
01c3ca4f37 chore(proxy): remove unused dev dep rimraf
Introduced by df9689854
2021-05-19 17:36:26 +02:00
Julien Fontanet
497bd7dad5 fix(xo-server): fix executables
Fixes #5782

Remove `bin` dir in favor of explicit listing, this allows to use ESM with executables without extensions.
2021-05-19 17:33:30 +02:00
Julien Fontanet
1d6a0ae8f1 chore(lint): apply overrides to .cjs and .mjs files 2021-05-19 17:29:09 +02:00
Julien Fontanet
c5e6b5ec7a chore(xo-server/recover-account): remove unused import
Introduced by 254558e9d
2021-05-19 17:28:41 +02:00
Julien Fontanet
ca26b4b30d chore(xo-server): remove unused run-vhd-test 2021-05-19 17:17:42 +02:00
Julien Fontanet
254558e9de chore(xo-server): convert to ESM 2021-05-19 15:53:21 +02:00
Julien Fontanet
da0cd0b99c chore: update to limit-concurrency-decorator@0.5.0 2021-05-19 15:08:53 +02:00
Julien Fontanet
2e49c685cc chore(emit-async): remove build step
It also helps with compatibility with Native ESM for default exports.
2021-05-19 15:00:59 +02:00
Julien Fontanet
a64af4da7c chore(defined): remove build step
It also helps with compatibility with Native ESM for default exports.
2021-05-19 12:07:18 +02:00
Julien Fontanet
68bb2fa7f0 feat(xo-collection): named instead of default exports
Behave better with Babel and native ESM.
2021-05-19 10:58:22 +02:00
Julien Fontanet
8bc2710380 chore(xo-collection/view.example): fix lint error 2021-05-19 10:51:57 +02:00
Julien Fontanet
1691e7ad83 chore(xo-collection): event-to-promise → promise-toolbox/fromEvent 2021-05-19 10:51:08 +02:00
Julien Fontanet
6c2cb31923 fix(proxy/api): fix JsonRpcWebSocketClient import
Introduced by 84b3162bc
2021-05-18 22:11:13 +02:00
Julien Fontanet
0c6d920682 chore(log): remove build step
It also helps with compatibility with native ESM for default exports.
2021-05-18 21:24:42 +02:00
Pierre Donias
a126b5b61b feat(xo-server-auth-saml): use registerUser2 (#5781) 2021-05-18 11:28:49 +02:00
Pierre Donias
dadb16bb04 feat(xen-api): ability to connect using a session ID (#5763) 2021-05-18 11:21:39 +02:00
Pierre Donias
f29473ef4c fix(xo-server/isHostServerTimeConsistent): change host permission from administrate to view (#5780) 2021-05-18 10:22:24 +02:00
Julien Fontanet
84b3162bcd fix(proxy/api): fix JsonRpcWebSocketClient import
Introduced by df9689854
2021-05-17 16:45:27 +02:00
Julien Fontanet
c7f1469e1f fix(proxy/backup.run): handle multiple self licenses
See xoa-support#3730

Previous code would fail if the first license returned was already expired.
2021-05-16 16:51:32 +02:00
Julien Fontanet
d1dfd93e15 feat(xen-api): 0.32.0 2021-05-12 17:57:06 +02:00
Julien Fontanet
4ef55b8d1f feat(xen-api): reverseHostIpAddresses option
See xoa-support#3727

When enabled, will attempt to get a FQDN from the host address returned by XAPI when using `getResource()` or `putResource()`.
2021-05-12 12:18:05 +02:00
Yannick Achy
7da22094f3 feat(docs/proxy): SSH connection and second nic (#5777) 2021-05-12 09:03:44 +02:00
Julien Fontanet
cf45cb56ad feat(npmignore): ignore /coverage/ 2021-05-11 09:04:35 +02:00
Julien Fontanet
df96898543 chore(proxy): convert to ESM 2021-05-10 23:23:37 +02:00
Julien Fontanet
a58bf66dea feat(scripts/travis-tests): handle .[cm]js files 2021-05-10 23:21:10 +02:00
Julien Fontanet
0f1fc0cc79 chore(proxy): remove rimraf dev dep 2021-05-10 18:17:40 +02:00
Julien Fontanet
dc41f60f52 feat(scripts/lint-staged): handle .[cm]js files 2021-05-10 14:50:19 +02:00
Julien Fontanet
3d21afb640 feat(package.json/scripts/prettify): handle cjs files 2021-05-10 14:48:32 +02:00
Julien Fontanet
79c3667fd4 fix(xo-server/api): never log pool.listMissingPatches or hosts.stats 2021-05-10 11:35:17 +02:00
Julien Fontanet
ab1549f60e feat(@xen-orchestra/backups-cli): 0.6.0 2021-05-08 10:44:49 +02:00
Julien Fontanet
5d32fa36ff feat(backups/_VmBackup#_callWriters): clearer error message
See xoa-support#3709
2021-05-08 10:43:39 +02:00
badrAZ
8ac17ab6e3 fix(xo-server): log missing pools (#5768)
Fixes #2844
2021-05-07 16:35:48 +02:00
badrAZ
2076141f47 feat(xo-web): add warning on restoring metadata backup (#5769)
See xoa-support#3691
2021-05-07 13:47:47 +02:00
badrAZ
6d0f479f81 fix(xo-server-backup-reports): don't take into account ignored tasks (#5770) 2021-05-07 11:09:38 +02:00
Julien Fontanet
f56a5a3de1 fix(xo-server/xapiObjectToXo/link): don't fail on array with missing objects
Fixes xoa-support#3691
2021-05-07 09:33:12 +02:00
Julien Fontanet
d0c34fd760 fix(CHANGELOG): update latest badge
Introduced by 9e7afd67b
2021-05-06 18:52:06 +02:00
Julien Fontanet
9e7afd67bc feat: release 5.58.1 2021-05-06 16:17:41 +02:00
Julien Fontanet
964810858b fix(fs/fs.spec): remove .only modifiers
Introduced by 48af5c7ed
2021-05-06 16:17:05 +02:00
Julien Fontanet
7a51361099 fix(CHANGELOG): typo
Introduced in e6f8fd923
2021-05-06 16:16:58 +02:00
Julien Fontanet
ec2e71a22f feat(CHANGELOG.unreleased): add better handling of remotes' errors
Introduced in 5b188f35b
2021-05-06 16:09:45 +02:00
Julien Fontanet
5b188f35b5 fix(backups/_VmBackup): better handling of writers' failures
- always wait for writers to finish their action
- log all writers' failures
- only interrupt process if all writers have failed
2021-05-05 14:32:39 +02:00
Julien Fontanet
5683571577 fix(xo-server): revert to schema-inspector@1
Fixes https://xcp-ng.org/forum/topic/4556/can-t-edit-xo-metatata-backup-config

See  schema-inspector/schema-inspector#119
2021-05-05 10:10:03 +02:00
badrAZ
db75568905 feat(backups/writers#beforeBackup): continue interrupted merges 2021-05-05 09:57:05 +02:00
badrAZ
5517305973 feat(backups/RemoteAdapter#cleanVm): optional lock 2021-05-05 09:57:05 +02:00
badrAZ
57ef531be0 feat(backups/cleanVm): detection of interrupted merges 2021-05-05 09:57:05 +02:00
Julien Fontanet
b590e29608 feat(@vates/parse-duration): 0.1.1 2021-05-05 09:51:44 +02:00
Julien Fontanet
569d575a96 fix(parse-duration): ISC license
Unrelated to XO code.
2021-05-05 09:49:50 +02:00
Julien Fontanet
dd8bf3776e fix(parse-duration): show original value in error message 2021-05-05 09:48:30 +02:00
Julien Fontanet
d4ea9c8892 fix(backups/_VmBackup#_selectBaseVm): typo
Fixes #5766

Introduced in 1d1bf504d
2021-05-04 16:39:32 +02:00
Julien Fontanet
793c6b4a5a chore(backups/_VmBackup#_copyDelta): remove useless check
All delta writers now have a `prepare()` method since e0d6b501c
2021-05-04 11:56:12 +02:00
Julien Fontanet
917c9dabc7 chore(backups/_VmBackup#copy{Delta,Full}): don't log writer errors
These errors are already logged in tasks.
2021-05-04 11:47:54 +02:00
Julien Fontanet
1d1bf504de chore(backups/VmBackup): make _writers a set
It will be easier to remove some writers in case of error.
2021-05-04 11:43:58 +02:00
Julien Fontanet
d0c07e1e97 chore: update promise-toolbox to 0.19.2 2021-05-03 15:41:53 +02:00
Julien Fontanet
dfff520259 fix(proxy/api): backup.{importVm,restoreMetadata}Backup
Fixes xoa-support#3688

Issue with `Disposable.use()` when returning an iterator like `runWithLogs()`.

Fixes by `promise-toolbox@0.19.2`.
2021-05-03 15:27:40 +02:00
Julien Fontanet
bb928bbd73 fix(backups/RemoteAdapter#cleanVm): don't fail if no vdis dir
Detected in #5756

Necessary to handle VMs with only full backups.
2021-05-02 11:22:01 +02:00
Julien Fontanet
f86ec98e05 fix(fs/list): ignoreMissing option
Introduced by 48af5c7ed

I messed up while renaming the option.
2021-05-02 11:22:01 +02:00
badrAZ
48af5c7ed6 feat(fs/abstract#list): ignore ENOENT error 2021-05-02 10:22:16 +02:00
Julien Fontanet
cfaf336597 feat(@xen-orchestra/proxy): 0.13.0 2021-04-30 23:11:25 +02:00
Julien Fontanet
b52345236d chore(fs): remove unused deps 2021-04-30 23:10:19 +02:00
Julien Fontanet
87ebaf62c1 fix(openflow): fix incorrect dev dep 2021-04-30 23:10:11 +02:00
Julien Fontanet
c7721d6100 feat(xo-server): 5.79.2 2021-04-30 22:53:05 +02:00
Julien Fontanet
40a722a7ff feat(@xen-orchestra/fs): 0.16.1 2021-04-30 22:52:25 +02:00
Julien Fontanet
d41fbb9216 fix(fs/_outputStream): validator should receive tmp path 2021-04-30 22:51:21 +02:00
Julien Fontanet
8bee0925d0 chore(fs/outputStream): remove incorrect await 2021-04-30 22:45:53 +02:00
Julien Fontanet
b8edca53cb feat: release 5.58.0 2021-04-30 22:28:17 +02:00
Julien Fontanet
34a13dd293 feat(xo-server): 5.79.1 2021-04-30 22:23:51 +02:00
Julien Fontanet
f72e582a80 feat(@xen-orchestra/backups): 0.10.1 2021-04-30 22:23:22 +02:00
Julien Fontanet
6da2865781 feat(@xen-orchestra/fs): 0.16.0 2021-04-30 22:22:19 +02:00
Julien Fontanet
a0ea12cf6c feat(CHANGELOG.unreleased): add S3 fix 2021-04-30 22:18:45 +02:00
Julien Fontanet
317bfde574 fix(fs/S3#_mkdir): throw ENOTDIR if file exists 2021-04-30 22:16:51 +02:00
Julien Fontanet
5f53ebdf12 chore(fs/S3#_rmdir): use _isNotEmptyDir 2021-04-30 22:16:51 +02:00
Julien Fontanet
cb835b7b6a fix(fs/S3#_unlink): throw EISDIR if dir
This fix `rmtree()`.
2021-04-30 22:16:51 +02:00
Julien Fontanet
bf76787e49 fix(fs/S3#_createReadStream): throw ENOENT if file doesn't exist 2021-04-30 22:16:51 +02:00
Julien Fontanet
15a4f7e273 fix(fs/S3): basic rmdir implementation 2021-04-30 22:16:51 +02:00
Julien Fontanet
dc3e5ffa4b chore(backups/RemoteAdapter#outputStream): use fs/outputStream
`createOutputStream` is deprecated and does not work with S3 remote.
2021-04-30 22:16:51 +02:00
Julien Fontanet
b84c7cc2bb feat(fs/outputStream): validator support 2021-04-30 22:16:51 +02:00
Julien Fontanet
049717260d chore(fs/outputStream): JsDoc 2021-04-30 19:50:00 +02:00
Julien Fontanet
a50a96de82 feat(fs/outputStream): remove support for promise input 2021-04-30 19:42:10 +02:00
Julien Fontanet
8ff8c0d176 chore(fs/outputStream): remove input.task handling
This should be handled at a higher level, not in this lib.
2021-04-30 19:38:31 +02:00
Julien Fontanet
a29b63c7d1 chore(fs/outputStream): move checksum handling to public wrapper 2021-04-30 19:31:56 +02:00
Julien Fontanet
a8400c77fb feat(fs/Abstract#outputStream): use stream.pipeline()
BREAKING CHANGE: requires Node >=14

- properly detect both input and output errors
- properly destroy streams in case of errors
2021-04-30 18:42:45 +02:00
Julien Fontanet
e1c40bd218 fix(fs/S3#mkdir): noop implementation 2021-04-30 18:26:50 +02:00
Julien Fontanet
757224683f chore(xo-server-audit): remove unused dep 2021-04-30 12:30:49 +02:00
Julien Fontanet
95d982f3f3 chore(xo-server-transport-icinga2): remove unused dep 2021-04-30 12:30:39 +02:00
Julien Fontanet
7bfdfe5e41 chore(xapi): remove unused dep 2021-04-30 12:10:14 +02:00
Julien Fontanet
8888b1a89a fix(proxy): add missing dep 2021-04-30 12:10:04 +02:00
Julien Fontanet
c6ba48be10 chore(proxy): remove unused deps 2021-04-30 12:09:51 +02:00
Julien Fontanet
f132c4b5d1 chore(log): remove unused dev dep 2021-04-30 12:08:03 +02:00
Julien Fontanet
87f5a8f6f2 chore(backups-cli): remove unused dep 2021-04-30 12:06:51 +02:00
Julien Fontanet
de500af30d fix(backups): add missing dep 2021-04-30 12:06:40 +02:00
Julien Fontanet
8b5607ac89 chore(audit-core): remove unused dep 2021-04-30 12:04:42 +02:00
Julien Fontanet
22727f68c1 fix(audit-core): fix incorrect dev dep 2021-04-30 12:04:30 +02:00
Julien Fontanet
ba64f8e5b5 fix(disposable): add missing dep 2021-04-30 12:02:39 +02:00
Julien Fontanet
b3bde5857e chore(xo-vmdk-to-vhd): remove unused dev dep 2021-04-30 12:01:29 +02:00
Julien Fontanet
6e36a21d18 chore(xo-server-web-hooks): remove unused deps 2021-04-30 12:01:07 +02:00
Julien Fontanet
968ebeb5a3 chore(xo-import-servers-csv): remove unused deps 2021-04-30 11:59:45 +02:00
Julien Fontanet
47e11652fb chore(vhd-lib): remove unused dev dep 2021-04-30 11:53:26 +02:00
Julien Fontanet
84019ed4e7 chore(vhd-cli): remove unused dep 2021-04-30 11:53:14 +02:00
Julien Fontanet
37befd89e7 chore(xo-server): remove unused deps 2021-04-30 11:48:28 +02:00
badrAZ
aa4f1b834a feat(vhd-lib/mergeVhd): continuable (#5749) 2021-04-30 09:18:21 +02:00
Rajaa.BARHTAOUI
e6f8fd9234 feat: technical release (#5761) 2021-04-29 10:40:55 +02:00
Rajaa.BARHTAOUI
86904892f2 fix(xo-server-perf-alert): fix 'Invalid parameters' error (#5755)
Introduced by 7c9850ada
2021-04-29 09:48:05 +02:00
badrAZ
d176dd6533 fix(xo-server-test/backupNg): remove obsolete snapshots (#5760) 2021-04-29 09:38:51 +02:00
badrAZ
283efe0eac fix(backups/cleanVm): pass handler to mergeVhdChain (#5758)
Introduced by 20f4c952fe
2021-04-28 18:21:23 +02:00
badrAZ
0e361cb105 fix(backups/cleanVm): correctly wait VHD deletions (#5757)
Introduced by c955da9bc6
2021-04-28 18:20:01 +02:00
Julien Fontanet
53aeb085ac fix(backups/MixinBackupWriter): ensure dir exist before locking 2021-04-28 17:52:46 +02:00
Rajaa.BARHTAOUI
cd8c618f08 feat(xo-server/pool.listPoolsMatchingCriteria): new API method (#5715)
See xoa-support#3489
2021-04-28 15:48:22 +02:00
Julien Fontanet
18b74d9797 fix(backups/RemoteAdapter#cleanVm): correctly rename/remove VHDs after merge 2021-04-28 15:06:22 +02:00
Ronan Abhamon
4008934bbb feat(load-balancer): improve migration (perf mode) regarding memory and cpu usage
- ensure we optimize CPU first instead of free memory
- use low threshold now to forbid bad migration based on cpu usage
- add a tolerance on the VM CPU usage to migrate VM with the most memory used
- do not migrate if we create an unbalanced configuration (only if high tresholds are not reached)
- change factors to take into account the new algorithm
2021-04-28 14:22:30 +02:00
Ronan Abhamon
8ae432554e fix(load-balancer): memory free limit must be expressed in B instead of KiB (bad calculations otherwise) 2021-04-28 14:22:30 +02:00
Ronan Abhamon
337b26176a fix(load-balancer): ensure anti-affinity tag array is always defined 2021-04-28 14:22:30 +02:00
Julien Fontanet
2e643fce28 fix(backups/MixinBackupWriter): clean VM dir after backup
Otherwise, it might trigger a chain reaction which will force all VDIs to be fully exported:

1. a single VDI chain is corrupted
2. it gets removed
3. the linked backups are removed
4. all other VDIs are now unused and are removed as well
5. all VDIs must now be fully exported
2021-04-28 13:20:56 +02:00
Julien Fontanet
5edd271975 fix(backups/RemoteAdapter#cleanVms): restore action logs
Introduced by 20f4c952fe

They are necessary because `cleanVms` can run in diagnostic or cleaning mode and the difference must be visible in logs.
2021-04-28 13:07:29 +02:00
Nicolas Raynaud
c219ea06bf feat(backup/s3): add http and region parameters to S3 (#5658) 2021-04-28 11:30:23 +02:00
badrAZ
ffacc0d8d0 fix(xo-server-test/backupNg): follow the new backup implementation (#5732) 2021-04-28 11:23:12 +02:00
Julien Fontanet
70fff77a28 fix(backups/_MixinBackupWriter): warn issues detected in cleanVm
`debug` is not good enough because not shown by default.
2021-04-28 10:43:48 +02:00
Rajaa.BARHTAOUI
bcc52d586e fix(xo-server-perf-alert): fix "required property uuids is not defined" warning (#5752)
See https://github.com/vatesfr/xen-orchestra/pull/5692#discussion_r611984364
2021-04-27 22:46:27 +02:00
Mathieu
521ded5079 feat(xo-web/host/network): identify management network (#5743)
Fixes #5731
2021-04-27 14:50:05 +02:00
Pierre Donias
73b6b59ec9 fix(xen-api/_sessionOpen): prevent deadlock (#5751)
Dead lock loop:
- `_sessionOpen`
- `getAllRecords`
- `_roCall`
- `_sessionCall` → `onRetry: _sessionOpen`

This triggers a dead lock because `_sessionOpen`'s calls are coalesced. Without `coalesceCalls`, this would be an infinite loop instead.
2021-04-27 14:00:00 +02:00
badrAZ
157c81b0e9 fix(@xen-orchestra/xapi#VM_import): ensure onVmCreation is called (#5747)
It was not called if the import task was not received (for instance because the import was very fast).
2021-04-26 17:29:14 +02:00
Rajaa.BARHTAOUI
233096354c feat(xo-web/xoa): notify user when proxies need to be upgraded (#5717)
See xoa-support#3597
2021-04-26 16:38:59 +02:00
Julien Fontanet
01ac23162f fix(xapi/watchObject): dont break potential promise chain 2021-04-26 16:12:30 +02:00
Julien Fontanet
4e3628c6fb fix(xapi/watchObject): correctly register generic watcher 2021-04-26 16:12:00 +02:00
Julien Fontanet
d6bea8aed8 feat(xapi/waitObject): simpler API
Align the API of `watchObject`, take a callback as param and return a function to stop waiting.
2021-04-26 15:30:45 +02:00
Julien Fontanet
a254097092 feat(xapi/watchObject): split from waitObject 2021-04-26 15:26:06 +02:00
Julien Fontanet
b2a3d224a5 feat(xapi/waitObject): make public 2021-04-26 14:29:52 +02:00
Julien Fontanet
b495c2b60b fix(xo-server/Xapi#importDeltaVm): remove transferSize in result
Not necessary and broken since bdb0ca836
2021-04-26 14:27:11 +02:00
Julien Fontanet
452f76cbef fix(xo-server/xapi): remove _waitObject
It was shadowing the parent implementation.
2021-04-26 14:23:43 +02:00
Julien Fontanet
3a0690bfee chore(proxy/api): dont access to stream private state 2021-04-26 11:44:55 +02:00
Julien Fontanet
29fd2ff5e9 feat(backups): lock VM dir during backup (#5746)
May fix xoa-support#3387
2021-04-26 09:23:20 +02:00
Julien Fontanet
a344b3b76d feat(xapi/_waitObject): cancelation support
Related to #5747
2021-04-25 16:01:32 +02:00
Julien Fontanet
14cf955cb9 chore(xapi): use extensions for file imports
Will be necessary for ESM.
2021-04-25 14:40:06 +02:00
badrAZ
31193d5b40 fix(xo-server/backup-ng#deleteVmBackupNg): pass remote obj to deleteVmBackup (#5744) 2021-04-23 16:34:23 +02:00
Julien Fontanet
d6dc63c491 chore(CHANGELOG.unreleased): format with Prettier 2021-04-23 16:04:38 +02:00
Julien Fontanet
263f693542 chore(xen-api): remove unused memory test 2021-04-23 14:59:02 +02:00
Julien Fontanet
3f42199f8f feat(normalize-packages): dont use files field
A centralized npmignore is easier to use and maintain.
2021-04-23 14:47:34 +02:00
Julien Fontanet
251ccd2e38 chore(npmignore): dont publish docs directories 2021-04-23 14:47:34 +02:00
Julien Fontanet
82ccf5886e chore(npmignore): dont publish hidden files 2021-04-23 14:47:34 +02:00
Julien Fontanet
6acb1e3853 chore(eslint): only use @babel/eslint-parser for pkgs using Babel 2021-04-23 14:47:34 +02:00
Mathieu
8c0238e98f feat(xo-server/pif.reconfigureIp): reconfigure on host if management (#5745)
Fixes #5730
2021-04-23 14:07:18 +02:00
Mathieu
e7779c3d55 feat(xo-server/template): ability to create a template from snapshot (#5736)
Fixes #4891
2021-04-23 10:52:35 +02:00
Julien Fontanet
bdb0ca836c feat(xo-server): remove legacy backups (#5735)
BREAKING: all `backup.*` API methods removed
2021-04-23 09:40:46 +02:00
Rajaa.BARHTAOUI
53038a0372 feat(xo-web): remove legacy backups (#5718) 2021-04-23 09:39:12 +02:00
Julien Fontanet
1b0eb91d58 chore(backups/writers): remove unnecessary index 2021-04-22 14:22:03 +02:00
Julien Fontanet
5814ba38ac chore(ackups,proxy,xo-server): use extensions for file imports
Follow-up on 7f570c074, 5171378be and b2ec0d288

Will be necessary for ESM.
2021-04-22 13:43:57 +02:00
Julien Fontanet
b2ec0d288b chore(xo-server): use extensions for file imports
Will be necessary for ESM.
2021-04-22 13:24:06 +02:00
Julien Fontanet
5171378bea chore(proxy): use extensions for file imports
Will be necessary for ESM.
2021-04-22 13:16:47 +02:00
Julien Fontanet
7f570c074b chore(backups): use extensions for file imports
Will be necessary for ESM.
2021-04-22 13:12:14 +02:00
Julien Fontanet
dac675143f chore(proxy): backups/index.js → backups.js 2021-04-22 13:10:30 +02:00
Julien Fontanet
72a5f0e220 chore: use decorateWith instead of defer decorator syntax
`golike-defer` built-in decorator syntax will be removed in future versions.
2021-04-21 17:47:40 +02:00
Julien Fontanet
375aaa8430 chore: dont use default export from golike-defer
- will be removed in future version
- not compatible with ESM implementation in Node
2021-04-21 17:20:16 +02:00
Julien Fontanet
4c704a8a3a chore(proxy/appliance): dont import log from dist/ 2021-04-21 16:59:50 +02:00
Julien Fontanet
78c0f2c7e9 chore: remove Flow
It was not used nor maintained by XO devs, and was causing issues with editors.

JSDoc or TypeScript should be used instead.
2021-04-21 16:55:03 +02:00
badrAZ
c262dd06e6 fix(@xen-orchestra/backups/isValidXva): move as RemoteAdapter method (#5741) 2021-04-21 16:27:13 +02:00
badrAZ
e0d6b501c7 feat(@xen-orchestra/backups): clean VM backups on run (#5727) 2021-04-21 13:27:33 +02:00
Julien Fontanet
efc3f45ef6 feat(babel-config): use top level targets option
See https://babeljs.io/blog/2021/02/22/7.13.0#top-level-targets-option-12189httpsgithubcombabelbabelpull12189-rfchttpsgithubcombabelrfcspull2
2021-04-20 16:24:44 +02:00
Julien Fontanet
24d8ef25bb feat(backups/VmBackup#run): assert offlineBackup not with snapshotRetention
See #5740
2021-04-20 15:03:17 +02:00
Julien Fontanet
2aca775907 fix(backups/VmBackup#_snapshot): dont fail on !offlineBackup && !snapshotRetention
Introduced by 7aa10ef4be
2021-04-20 14:59:18 +02:00
badrAZ
7aa10ef4be fix(backups): don't snapshot in case of offline backup (#5739)
Introduced by 0811da9014
2021-04-20 11:01:39 +02:00
Julien Fontanet
17ad622ce3 chore: update dev deps 2021-04-20 10:58:25 +02:00
Julien Fontanet
cc7431a092 chore(xo-server-test): update jest to 24.6.3 2021-04-20 10:51:57 +02:00
Julien Fontanet
4199d02d98 chore(xo-server-{auth-saml,transport-nagios}): remove unused dep babel-preset-env 2021-04-20 10:44:02 +02:00
badrAZ
8c434760fb fix(@xen-orchestra/backups/_cleanVm): don't resolve paths relatively to cwd (#5738) 2021-04-20 10:35:28 +02:00
Julien Fontanet
5f63b99dc8 feat(backups/_backupWorker): log global errors 2021-04-19 20:13:57 +02:00
Julien Fontanet
edd0ae4c59 fix(xapi/VM_snapshot): correctly delete broken snapshot
Introduced by 6b1c30157
2021-04-19 18:35:05 +02:00
Pierre Donias
3944e6450d feat(fs/nfs): remove vers=3 default option (#5725) 2021-04-19 15:29:17 +02:00
Julien Fontanet
a8e5ad42ba chore(xo-web): migrate to babel 7
Fix linting.
2021-04-19 15:18:09 +02:00
Julien Fontanet
d3bfb0b87b fix(xapi/VM_destroy): ensure all VDIs deletion errors are caught/logged
Related to 6b1c30157
2021-04-19 10:45:46 +02:00
Pierre Donias
75e3e36aa8 feat(xo-web/new VM): only send memory param so that it doesn't enable DMC (#5729)
Fixes xoa-support#3591
See 70d1537ecc

- If only "RAM" field is filled: only send `memory` param
- If any of the advanced memory fields are filled:
  - only send those
  - if "Dynamic memory max" field is empty, use the "RAM" field value for
  `memoryDynamicMax` param
2021-04-19 10:16:56 +02:00
Julien Fontanet
9102b4aa1b fix(fs): coalesce calls to sync/forget (#4770)
Might help with xoa-support#3637

It does not makes sense to call them multiple times and can create issues.
2021-04-17 14:55:21 +02:00
badrAZ
e744d90dbb fix(xo-server/backups-ng): continue execution when VM/SR is missing (#5733)
Introduced by 60ecfbfb8e
2021-04-16 23:33:20 +02:00
badrAZ
c38b957d7c fix(xo-{server,proxy}/config): add copyRetention default value (#5737)
Introduced in xo-server by 0811da901
2021-04-16 14:56:43 +02:00
Julien Fontanet
282bb26da9 chore(xapi/VM_{destroy,snapshot}): delete → destroy
Introduced by 6b1c30157
2021-04-16 10:35:48 +02:00
Julien Fontanet
6b1c30157f feat(xapi/VM_{destroy,snapshot}): warn instead of ignoring errors 2021-04-16 10:32:04 +02:00
Julien Fontanet
e433251420 fix(xo-server/recover-account): pass config as named param
Introduced by 7024c7d59
2021-04-15 15:52:56 +02:00
Julien Fontanet
49ed9c7f7f fix(xo-server/api): fix config name entry verboseApiLogsOnErrors 2021-04-15 13:34:05 +02:00
Julien Fontanet
5a5c0326b7 fix(xapi/VM_destroy): correctly check *other* VM is not control domain 2021-04-15 11:52:56 +02:00
Julien Fontanet
a25708be2b fix(xapi/VM_create): default actions_after_{crash,reboot} is restart
See https://xapi-project.github.io/xen-api/classes/vm.html

`reboot` is not valid.
2021-04-15 11:48:40 +02:00
Julien Fontanet
e8f2934534 feat(xo-server/getBackupNgLogs): expose proxyId
Follow up on b454b4dff
2021-04-15 11:43:34 +02:00
badrAZ
37f8ac9da9 fix(fs/LocalHandler#_lock): correctly resolve path (#5726) 2021-04-14 15:59:11 +02:00
badrAZ
0ded95ce48 fix(xo-server/backup-ng): add slash between backup and remote ids (#5723)
This is symmetric to the parsing: 052aafd7cb/packages/xo-server/src/xo-mixins/backups-ng/index.js (L88-L94)
2021-04-14 14:43:28 +02:00
Julien Fontanet
108e769833 fix(CHANGELOG.unreleased): @xen-orchestra/xapi
Introduced by 864946477
2021-04-14 11:46:12 +02:00
Julien Fontanet
5b2313ee56 feat(xapi): warn on retry 2021-04-14 11:10:20 +02:00
Julien Fontanet
368b84b7ff chore(xapi/VDI_destroy): move retry condition in constructor 2021-04-14 10:30:11 +02:00
Julien Fontanet
864946477b fix(xapi/VDI_destroy): respect vdiDestroyRetryWhenInUse option 2021-04-14 10:23:53 +02:00
Julien Fontanet
da67298b43 chore: update promise-toolbox to 0.19.0 2021-04-14 00:12:34 +02:00
Julien Fontanet
db5cb8b3a9 chore(disposables): using → Disposable.use 2021-04-13 23:35:10 +02:00
Julien Fontanet
9643292be6 fix(babel): dont ignore test files when linting 2021-04-13 18:09:40 +02:00
Julien Fontanet
a651e34206 fix(xo-server/math): fix ESLint directive 2021-04-13 18:09:40 +02:00
Julien Fontanet
a4e7fd3209 feat(xo-server): use @xen-orchestra/mixins/Config 2021-04-13 18:09:40 +02:00
Julien Fontanet
d1113d40aa chore(mixins): use PascalCase as they are classes 2021-04-13 18:09:40 +02:00
Julien Fontanet
dcd834d3e4 chore(xo-server/xo-mixins): xo → app
- already used in some mixins
- used in xo-proxy
2021-04-13 18:09:40 +02:00
badrAZ
c0be8a2c04 fix(@xen-orchestra/backups/_cleanVm): VHDs not correctly listed (#5720)
Introduced by 20f4c95
2021-04-13 16:09:42 +02:00
Julien Fontanet
09182172cf chore(xo-server): use @xen-orchestra/mixins/hooks 2021-04-13 13:41:22 +02:00
Julien Fontanet
56e903e359 feat(mixins): mixins shared between xo-proxy and xo-server 2021-04-13 13:17:50 +02:00
Julien Fontanet
9922d60e5b feat(@xen-orchestra/mixin): 0.1.0 2021-04-13 13:01:24 +02:00
Julien Fontanet
09ea42439e chore(mixin): remove build step 2021-04-13 12:31:11 +02:00
Julien Fontanet
ce1acf1adc feat(@xen-orchestra/proxy): 0.12.1 2021-04-13 10:46:44 +02:00
Julien Fontanet
fe00badb0f feat: release 5.57.1 2021-04-13 10:27:38 +02:00
Julien Fontanet
2146d67dc2 fix(CHANGELOG{,.unreleased}): move backup dev notes
Introduced by e7b846155
2021-04-13 10:26:26 +02:00
Julien Fontanet
6728768b3e feat(xo-server): 5.78.4 2021-04-12 23:43:33 +02:00
Julien Fontanet
48db3de08c feat(@xen-orchestra/backups): 0.9.3 2021-04-12 23:43:16 +02:00
Julien Fontanet
b944364d1e fix(backups/_copyDelta): dont pass extra params to watchStreamSize
Introduced by 9b1fbf0fb
2021-04-12 23:42:29 +02:00
Julien Fontanet
39c2fbe8c3 feat(xo-web): 5.80.1 2021-04-12 22:56:48 +02:00
Julien Fontanet
c7ba640ecb feat(xo-server): 5.78.3 2021-04-12 22:56:29 +02:00
Julien Fontanet
f749f6be72 feat(xo-server-load-balancer): 0.5.0 2021-04-12 22:56:09 +02:00
Julien Fontanet
ccdd384c6e feat(@xen-orchestra/backups): 0.9.2 2021-04-12 22:55:36 +02:00
Julien Fontanet
4061e2c149 feat(@xen-orchestra/xapi): 0.6.1 2021-04-12 22:55:19 +02:00
Julien Fontanet
e7b8461555 chore(CHANGELOG): update next 2021-04-12 22:54:51 +02:00
Julien Fontanet
70d1537ecc feat(xo-server/vm.set): dont switch to DMC when changing memory
Fixes #4983
2021-04-12 21:15:55 +02:00
Julien Fontanet
cb37f85d8e fix(xo-web/proxies): fix force ugprade
Introduced by a4d90e8aff

See xoa-support#3613

Forward options in `upgradeAppliance` effect.
2021-04-12 12:16:18 +02:00
Julien Fontanet
9becf565a4 fix(CHANGELOG.unreleased): add missing entriy
Introduced by 4bbe8488f
2021-04-12 11:12:54 +02:00
Julien Fontanet
b1a4e5467d feat(xo-server/xapi/startVm): move hostId into options 2021-04-12 11:01:42 +02:00
Julien Fontanet
4bbe8488fc fix(xo-server/xapi/startVm): dont destructure options without default value
See xoa-support#3613
2021-04-12 10:52:41 +02:00
Jon Sands
54a0d126b5 fix(xo-web/en): more grammar fixes (#5714) 2021-04-10 10:30:39 +02:00
Julien Fontanet
9b1fbf0fbf fix(backups/ImportVmBackup): use transfered size instead of backup size
Backup size is smaller in case of delta VHDs.
2021-04-09 15:33:50 +02:00
Julien Fontanet
6f626974ac chore(backups/readDeltaVmBackup): remove unused value 2021-04-09 15:02:26 +02:00
Julien Fontanet
5c47beb1c4 fix(CHANGELOG.unreleased): add missing entry
Related to 3cc9fd278
2021-04-09 11:35:49 +02:00
Julien Fontanet
b4fbe8df07 feat(xo-server/api): explicitely allow $type and enumNames in schemas 2021-04-09 11:16:17 +02:00
Julien Fontanet
3cc9fd2782 fix(xo-server/api): log instead of rejecting non-strict schemas
Fixes https://xcp-ng.org/forum/topic/4439/plugin-transport-email-v0-6-0-broken
2021-04-09 11:03:13 +02:00
Julien Fontanet
eaecba7ec8 fix(xo-server/api): dont log pool.listMissingPatches & host.stats errors
Introduced by 9226c6cac
2021-04-09 10:47:01 +02:00
Julien Fontanet
42a43be092 feat(backups/Task.wrapFn): opts can be a function 2021-04-09 01:27:54 +02:00
Julien Fontanet
052aafd7cb fix(backups/DeltaBackupWriter): merge should be subtask of export
Introduced by f5024f0e7
2021-04-09 01:25:01 +02:00
Julien Fontanet
4abae578f4 feat(backups/Task): new implementation
- no longer requires logging
- supports cancelation (`Task.cancelToken` and `Task#cancel()`)
- supports running multiple functions in the same task
2021-04-09 01:19:09 +02:00
Julien Fontanet
4132d96591 chore(backups): remove unused deps 2021-04-09 01:13:22 +02:00
Julien Fontanet
8e4c90129e fix(backups/DeltaBackupWriter): dont overwrite prepare/cleanup in constructor
Introduced in e69b6c4dc
2021-04-08 23:52:21 +02:00
Julien Fontanet
31406927e6 chore: disable unused Jest coverage 2021-04-08 22:25:10 +02:00
Julien Fontanet
303646efd3 chore: remove unnecessary Jest transform setting 2021-04-08 22:25:10 +02:00
Julien Fontanet
9efc4f9113 chore: remove unnecessary babel-core 2021-04-08 22:25:10 +02:00
Julien Fontanet
31a5a42ec7 chore: use @babel/eslint-parser instead of babel-eslint
babel-eslint is no longer maintained and has issues with some recent syntaxes like private methods.
2021-04-08 22:25:10 +02:00
Yannick Achy
2d0ed3ec8a feat(doc): Host update revision (#5716)
* Host update revision

Co-authored-by: yannick Achy <yannick.achy@vates.fr>
2021-04-08 16:54:06 +02:00
592 changed files with 14935 additions and 14617 deletions

View File

@@ -13,19 +13,13 @@ module.exports = {
overrides: [
{
files: ['cli.js', '*-cli.js', '**/*cli*/**/*.js'],
files: ['cli.{,c,m}js', '*-cli.{,c,m}js', '**/*cli*/**/*.{,c,m}js'],
rules: {
'no-console': 'off',
},
},
],
parser: 'babel-eslint',
parserOptions: {
ecmaFeatures: {
legacyDecorators: true,
},
},
rules: {
// disabled because XAPI objects are using camel case
camelcase: ['off'],

33
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,33 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'status: triaging :triangular_flag_on_post:, type: bug :bug:'
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- Node: [e.g. 16.12.1]
- xo-server: [e.g. 5.82.3]
- xo-web: [e.g. 5.87.0]
- hypervisor: [e.g. XCP-ng 8.2.0]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

8
.gitignore vendored
View File

@@ -11,7 +11,7 @@
/packages/*/dist/
/packages/*/node_modules/
/@xen-orchestra/proxy/src/app/mixins/index.js
/@xen-orchestra/proxy/src/app/mixins/index.mjs
/packages/vhd-cli/src/commands/index.js
@@ -19,9 +19,9 @@
/packages/xen-api/plot.dat
/packages/xo-server/.xo-server.*
/packages/xo-server/src/api/index.js
/packages/xo-server/src/xapi/mixins/index.js
/packages/xo-server/src/xo-mixins/index.js
/packages/xo-server/src/api/index.mjs
/packages/xo-server/src/xapi/mixins/index.mjs
/packages/xo-server/src/xo-mixins/index.mjs
/packages/xo-server-auth-ldap/ldap.cache.conf

View File

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

View File

@@ -0,0 +1,68 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/async-each
[![Package Version](https://badgen.net/npm/v/@vates/async-each)](https://npmjs.org/package/@vates/async-each) ![License](https://badgen.net/npm/license/@vates/async-each) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/async-each)](https://bundlephobia.com/result?p=@vates/async-each) [![Node compatibility](https://badgen.net/npm/node/@vates/async-each)](https://npmjs.org/package/@vates/async-each)
> Run async fn for each item in (async) iterable
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/async-each):
```
> npm install --save @vates/async-each
```
## Usage
### `asyncEach(iterable, iteratee, [opts])`
Executes `iteratee` in order for each value yielded by `iterable`.
Returns a promise wich rejects as soon as a call to `iteratee` throws or a promise returned by it rejects, and which resolves when all promises returned by `iteratee` have resolved.
`iterable` must be an iterable or async iterable.
`iteratee` is called with the same `this` value as `asyncEach`, and with the following arguments:
- `value`: the value yielded by `iterable`
- `index`: the 0-based index for this value
- `iterable`: the iterable itself
`opts` is an object that can contains the following options:
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `1`
- `signal`: an abort signal to stop the iteration
- `stopOnError`: wether to stop iteration of first error, or wait for all calls to finish and throw an `AggregateError`, defaults to `true`
```js
import { asyncEach } from '@vates/async-each'
const contents = []
await asyncEach(
['foo.txt', 'bar.txt', 'baz.txt'],
async function (filename, i) {
contents[i] = await readFile(filename)
},
{
// reads two files at a time
concurrency: 2,
}
)
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -0,0 +1,35 @@
### `asyncEach(iterable, iteratee, [opts])`
Executes `iteratee` in order for each value yielded by `iterable`.
Returns a promise wich rejects as soon as a call to `iteratee` throws or a promise returned by it rejects, and which resolves when all promises returned by `iteratee` have resolved.
`iterable` must be an iterable or async iterable.
`iteratee` is called with the same `this` value as `asyncEach`, and with the following arguments:
- `value`: the value yielded by `iterable`
- `index`: the 0-based index for this value
- `iterable`: the iterable itself
`opts` is an object that can contains the following options:
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `1`
- `signal`: an abort signal to stop the iteration
- `stopOnError`: wether to stop iteration of first error, or wait for all calls to finish and throw an `AggregateError`, defaults to `true`
```js
import { asyncEach } from '@vates/async-each'
const contents = []
await asyncEach(
['foo.txt', 'bar.txt', 'baz.txt'],
async function (filename, i) {
contents[i] = await readFile(filename)
},
{
// reads two files at a time
concurrency: 2,
}
)
```

View File

@@ -0,0 +1,99 @@
'use strict'
const noop = Function.prototype
class AggregateError extends Error {
constructor(errors, message) {
super(message)
this.errors = errors
}
}
exports.asyncEach = function asyncEach(iterable, iteratee, { concurrency = 1, signal, stopOnError = true } = {}) {
return new Promise((resolve, reject) => {
const it = (iterable[Symbol.iterator] || iterable[Symbol.asyncIterator]).call(iterable)
const errors = []
let running = 0
let index = 0
let onAbort
if (signal !== undefined) {
onAbort = () => {
onRejectedWrapper(new Error('asyncEach aborted'))
}
signal.addEventListener('abort', onAbort)
}
const clean = () => {
onFulfilled = onRejected = noop
if (onAbort !== undefined) {
signal.removeEventListener('abort', onAbort)
}
}
resolve = (resolve =>
function resolveAndClean(value) {
resolve(value)
clean()
})(resolve)
reject = (reject =>
function rejectAndClean(reason) {
reject(reason)
clean()
})(reject)
let onFulfilled = value => {
--running
next()
}
const onFulfilledWrapper = value => onFulfilled(value)
let onRejected = stopOnError
? reject
: error => {
--running
errors.push(error)
next()
}
const onRejectedWrapper = reason => onRejected(reason)
let nextIsRunning = false
let next = async () => {
if (nextIsRunning) {
return
}
nextIsRunning = true
if (running < concurrency) {
const cursor = await it.next()
if (cursor.done) {
next = () => {
if (running === 0) {
if (errors.length === 0) {
resolve()
} else {
reject(new AggregateError(errors))
}
}
}
} else {
++running
try {
const result = iteratee.call(this, cursor.value, index++, iterable)
let then
if (result != null && typeof result === 'object' && typeof (then = result.then) === 'function') {
then.call(result, onFulfilledWrapper, onRejectedWrapper)
} else {
onFulfilled(result)
}
} catch (error) {
onRejected(error)
}
}
nextIsRunning = false
return next()
}
nextIsRunning = false
}
next()
})
}

View File

@@ -0,0 +1,99 @@
'use strict'
/* eslint-env jest */
const { asyncEach } = require('./')
const randomDelay = (max = 10) =>
new Promise(resolve => {
setTimeout(resolve, Math.floor(Math.random() * max + 1))
})
const rejectionOf = p =>
new Promise((resolve, reject) => {
p.then(reject, resolve)
})
describe('asyncEach', () => {
const thisArg = 'qux'
const values = ['foo', 'bar', 'baz']
Object.entries({
'sync iterable': () => values,
'async iterable': async function* () {
for (const value of values) {
await randomDelay()
yield value
}
},
}).forEach(([what, getIterable]) =>
describe('with ' + what, () => {
let iterable
beforeEach(() => {
iterable = getIterable()
})
it('works', async () => {
const iteratee = jest.fn(async () => {})
await asyncEach.call(thisArg, iterable, iteratee)
expect(iteratee.mock.instances).toEqual(Array.from(values, () => thisArg))
expect(iteratee.mock.calls).toEqual(Array.from(values, (value, index) => [value, index, iterable]))
})
;[1, 2, 4].forEach(concurrency => {
it('respects a concurrency of ' + concurrency, async () => {
let running = 0
await asyncEach(
values,
async () => {
++running
expect(running).toBeLessThanOrEqual(concurrency)
await randomDelay()
--running
},
{ concurrency }
)
})
})
it('stops on first error when stopOnError is true', async () => {
const error = new Error()
const iteratee = jest.fn((_, i) => {
if (i === 1) {
throw error
}
})
expect(await rejectionOf(asyncEach(iterable, iteratee, { stopOnError: true }))).toBe(error)
expect(iteratee).toHaveBeenCalledTimes(2)
})
it('rejects AggregateError when stopOnError is false', async () => {
const errors = []
const iteratee = jest.fn(() => {
const error = new Error()
errors.push(error)
throw error
})
const error = await rejectionOf(asyncEach(iterable, iteratee, { stopOnError: false }))
expect(error.errors).toEqual(errors)
expect(iteratee.mock.calls).toEqual(Array.from(values, (value, index) => [value, index, iterable]))
})
it('can be interrupted with an AbortSignal', async () => {
const ac = new AbortController()
const iteratee = jest.fn((_, i) => {
if (i === 1) {
ac.abort()
}
})
await expect(asyncEach(iterable, iteratee, { signal: ac.signal })).rejects.toThrow('asyncEach aborted')
expect(iteratee).toHaveBeenCalledTimes(2)
})
})
)
})

View File

@@ -0,0 +1,34 @@
{
"private": false,
"name": "@vates/async-each",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/async-each",
"description": "Run async fn for each item in (async) iterable",
"keywords": [
"array",
"async",
"collection",
"each",
"for",
"foreach",
"iterable",
"iterator"
],
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/async-each",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.1.0",
"engines": {
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public"
}
}

View File

@@ -20,9 +20,6 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"files": [
"index.js"
],
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"

View File

@@ -65,6 +65,23 @@ const f = compose(
)
```
Functions can receive extra parameters:
```js
const isIn = (value, min, max) => min <= value && value <= max
// Only compatible when `fns` is passed as an array!
const f = compose([
[add, 2],
[isIn, 3, 10],
])
console.log(f(1))
// → true
```
> Note: if the first function is defined with extra parameters, it will only receive the first value passed to the composed function, instead of all the parameters.
## Contributions
Contributions are _very_ welcomed, either on the documentation or on

View File

@@ -46,3 +46,20 @@ const f = compose(
[add2, mul3]
)
```
Functions can receive extra parameters:
```js
const isIn = (value, min, max) => min <= value && value <= max
// Only compatible when `fns` is passed as an array!
const f = compose([
[add, 2],
[isIn, 3, 10],
])
console.log(f(1))
// → true
```
> Note: if the first function is defined with extra parameters, it will only receive the first value passed to the composed function, instead of all the parameters.

View File

@@ -4,11 +4,13 @@ const defaultOpts = { async: false, right: false }
exports.compose = function compose(opts, fns) {
if (Array.isArray(opts)) {
fns = opts
fns = opts.slice() // don't mutate passed array
opts = defaultOpts
} else if (typeof opts === 'object') {
opts = Object.assign({}, defaultOpts, opts)
if (!Array.isArray(fns)) {
if (Array.isArray(fns)) {
fns = fns.slice() // don't mutate passed array
} else {
fns = Array.prototype.slice.call(arguments, 1)
}
} else {
@@ -20,6 +22,24 @@ exports.compose = function compose(opts, fns) {
if (n === 0) {
throw new TypeError('at least one function must be passed')
}
for (let i = 0; i < n; ++i) {
const entry = fns[i]
if (Array.isArray(entry)) {
const fn = entry[0]
const args = entry.slice()
args[0] = undefined
fns[i] = function composeWithArgs(value) {
args[0] = value
try {
return fn.apply(this, args)
} finally {
args[0] = undefined
}
}
}
}
if (n === 1) {
return fns[0]
}

View File

@@ -14,7 +14,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "2.0.0",
"version": "2.1.0",
"engines": {
"node": ">=7.6"
},

View File

@@ -16,7 +16,9 @@ Installation of the [npm package](https://npmjs.org/package/@vates/decorate-with
## Usage
For instance, allows using Lodash's functions as decorators:
### `decorateWith(fn, ...args)`
Creates a new ([legacy](https://babeljs.io/docs/en/babel-plugin-syntax-decorators#legacy)) method decorator from a function decorator, for instance, allows using Lodash's functions as decorators:
```js
import { decorateWith } from '@vates/decorate-with'
@@ -29,6 +31,45 @@ class Foo {
}
```
### `decorateMethodsWith(class, map)`
Decorates a number of methods directly, without using the decorator syntax:
```js
import { decorateMethodsWith } from '@vates/decorate-with'
class Foo {
bar() {
// body
}
baz() {
// body
}
}
decorateMethodsWith(Foo, {
// without arguments
bar: lodash.curry,
// with arguments
baz: [lodash.debounce, 150],
})
```
The decorated class is returned, so you can export it directly.
To apply multiple transforms to a method, you can either call `decorateMethodsWith` multiple times or use [`@vates/compose`](https://www.npmjs.com/package/@vates/compose):
```js
decorateMethodsWith(Foo, {
bar: compose([
[lodash.debounce, 150]
lodash.curry,
])
})
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on

View File

@@ -1,4 +1,6 @@
For instance, allows using Lodash's functions as decorators:
### `decorateWith(fn, ...args)`
Creates a new ([legacy](https://babeljs.io/docs/en/babel-plugin-syntax-decorators#legacy)) method decorator from a function decorator, for instance, allows using Lodash's functions as decorators:
```js
import { decorateWith } from '@vates/decorate-with'
@@ -10,3 +12,42 @@ class Foo {
}
}
```
### `decorateMethodsWith(class, map)`
Decorates a number of methods directly, without using the decorator syntax:
```js
import { decorateMethodsWith } from '@vates/decorate-with'
class Foo {
bar() {
// body
}
baz() {
// body
}
}
decorateMethodsWith(Foo, {
// without arguments
bar: lodash.curry,
// with arguments
baz: [lodash.debounce, 150],
})
```
The decorated class is returned, so you can export it directly.
To apply multiple transforms to a method, you can either call `decorateMethodsWith` multiple times or use [`@vates/compose`](https://www.npmjs.com/package/@vates/compose):
```js
decorateMethodsWith(Foo, {
bar: compose([
[lodash.debounce, 150]
lodash.curry,
])
})
```

View File

@@ -1,4 +1,21 @@
exports.decorateWith = (fn, ...args) => (target, name, descriptor) => ({
...descriptor,
value: fn(descriptor.value, ...args),
})
exports.decorateWith = function decorateWith(fn, ...args) {
return (target, name, descriptor) => ({
...descriptor,
value: fn(descriptor.value, ...args),
})
}
const { getOwnPropertyDescriptor, defineProperty } = Object
exports.decorateMethodsWith = function decorateMethodsWith(klass, map) {
const { prototype } = klass
for (const name of Object.keys(map)) {
const descriptor = getOwnPropertyDescriptor(prototype, name)
const { value } = descriptor
const decorator = map[name]
descriptor.value = typeof decorator === 'function' ? decorator(value) : decorator[0](value, ...decorator.slice(1))
defineProperty(prototype, name, descriptor)
}
return klass
}

View File

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

View File

@@ -48,7 +48,7 @@ import { createDebounceResource } from '@vates/disposable/debounceResource'
const debounceResource = createDebounceResource()
// it will wait for 10 seconds before calling the disposer
using(debounceResource(getConnection(host), 10e3), connection => {})
Disposable.use(debounceResource(getConnection(host), 10e3), connection => {})
```
### `debounceResource.flushAll()`

View File

@@ -30,7 +30,7 @@ import { createDebounceResource } from '@vates/disposable/debounceResource'
const debounceResource = createDebounceResource()
// it will wait for 10 seconds before calling the disposer
using(debounceResource(getConnection(host), 10e3), connection => {})
Disposable.use(debounceResource(getConnection(host), 10e3), connection => {})
```
### `debounceResource.flushAll()`

View File

@@ -23,7 +23,8 @@
},
"dependencies": {
"@vates/multi-key-map": "^0.1.0",
"@xen-orchestra/log": "^0.2.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.3.0",
"ensure-array": "^1.0.0"
}
}

View File

@@ -44,4 +44,4 @@ You may:
## License
[AGPL-3.0-or-later](https://spdx.org/licenses/AGPL-3.0-or-later) © [Vates SAS](https://vates.fr)
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -6,7 +6,7 @@ exports.parseDuration = value => {
}
const duration = ms(value)
if (duration === undefined) {
throw new TypeError(`not a valid duration: ${duration}`)
throw new TypeError(`not a valid duration: ${value}`)
}
return duration
}

View File

@@ -18,8 +18,8 @@
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "AGPL-3.0-or-later",
"version": "0.1.0",
"license": "ISC",
"version": "0.1.1",
"engines": {
"node": ">=8.10"
},

View File

@@ -23,9 +23,6 @@
"engines": {
"node": ">=8.10"
},
"files": [
"index.js"
],
"scripts": {
"postversion": "npm publish --access public"
},

View File

@@ -31,9 +31,6 @@
"engines": {
"node": ">=6"
},
"files": [
"index.js"
],
"bin": "./index.js",
"scripts": {
"postversion": "npm publish --access public"

View File

@@ -24,10 +24,6 @@
"url": "https://vates.fr"
},
"preferGlobal": false,
"files": [
"index.js",
"legacy.js"
],
"engines": {
"node": ">=6"
},

View File

@@ -0,0 +1 @@
../../scripts/babel-eslintrc.js

View File

@@ -9,7 +9,7 @@
},
"version": "0.2.0",
"engines": {
"node": ">=8.10"
"node": ">=10"
},
"main": "dist/",
"scripts": {
@@ -26,14 +26,13 @@
"@babel/plugin-proposal-decorators": "^7.8.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.0",
"@babel/preset-env": "^7.7.4",
"cross": "^1.0.0",
"cross-env": "^7.0.2",
"rimraf": "^3.0.0"
},
"dependencies": {
"@xen-orchestra/log": "^0.2.0",
"core-js": "^3.6.4",
"@vates/decorate-with": "^0.1.0",
"@xen-orchestra/log": "^0.3.0",
"golike-defer": "^0.5.1",
"lodash": "^4.17.15",
"object-hash": "^2.0.1"
},
"private": false,

View File

@@ -1,10 +1,8 @@
// see https://github.com/babel/babel/issues/8450
import 'core-js/features/symbol/async-iterator'
import assert from 'assert'
import defer from 'golike-defer'
import hash from 'object-hash'
import { createLogger } from '@xen-orchestra/log'
import { decorateWith } from '@vates/decorate-with'
import { defer } from 'golike-defer'
const log = createLogger('xo:audit-core')
@@ -65,7 +63,7 @@ export class AuditCore {
this._storage = storage
}
@defer
@decorateWith(defer)
async add($defer, subject, event, data) {
const time = Date.now()
$defer(await this._storage.acquireLock())
@@ -150,7 +148,7 @@ export class AuditCore {
}
}
@defer
@decorateWith(defer)
async deleteRangeAndRewrite($defer, newest, oldest) {
assert.notStrictEqual(newest, undefined)
assert.notStrictEqual(oldest, undefined)

View File

@@ -17,10 +17,10 @@ interface Record {
}
export class AuditCore {
constructor(storage: Storage) { }
public add(subject: any, event: string, data: any): Promise<Record> { }
public checkIntegrity(oldest: string, newest: string): Promise<number> { }
public getFrom(newest?: string): AsyncIterator { }
public deleteFrom(newest: string): Promise<void> { }
public deleteRangeAndRewrite(newest: string, oldest: string): Promise<void> { }
constructor(storage: Storage) {}
public add(subject: any, event: string, data: any): Promise<Record> {}
public checkIntegrity(oldest: string, newest: string): Promise<number> {}
public getFrom(newest?: string): AsyncIterator {}
public deleteFrom(newest: string): Promise<void> {}
public deleteRangeAndRewrite(newest: string, oldest: string): Promise<void> {}
}

View File

@@ -14,25 +14,13 @@ const configs = {
'@babel/plugin-proposal-pipeline-operator': {
proposal: 'minimal',
},
'@babel/preset-env'(pkg) {
return {
debug: !__TEST__,
'@babel/preset-env': {
debug: !__TEST__,
// disabled until https://github.com/babel/babel/issues/8323 is resolved
// loose: true,
// disabled until https://github.com/babel/babel/issues/8323 is resolved
// loose: true,
shippedProposals: true,
targets: (() => {
let node = (pkg.engines || {}).node
if (node !== undefined) {
const trimChars = '^=>~'
while (trimChars.includes(node[0])) {
node = node.slice(1)
}
}
return { browsers: pkg.browserslist, node }
})(),
}
shippedProposals: true,
},
}
@@ -44,21 +32,21 @@ const getConfig = (key, ...args) => {
// some plugins must be used in a specific order
const pluginsOrder = ['@babel/plugin-proposal-decorators', '@babel/plugin-proposal-class-properties']
module.exports = function (pkg, plugins, presets) {
plugins === undefined && (plugins = {})
presets === undefined && (presets = {})
module.exports = function (pkg, configs = {}) {
const plugins = {}
const presets = {}
Object.keys(pkg.devDependencies || {}).forEach(name => {
if (!(name in presets) && PLUGINS_RE.test(name)) {
plugins[name] = getConfig(name, pkg)
plugins[name] = { ...getConfig(name, pkg), ...configs[name] }
} else if (!(name in presets) && PRESETS_RE.test(name)) {
presets[name] = getConfig(name, pkg)
presets[name] = { ...getConfig(name, pkg), ...configs[name] }
}
})
return {
comments: !__PROD__,
ignore: __TEST__ ? undefined : [/\.spec\.js$/],
ignore: __PROD__ ? [/\btests?\//, /\.spec\.js$/] : undefined,
plugins: Object.keys(plugins)
.map(plugin => [plugin, plugins[plugin]])
.sort(([a], [b]) => {
@@ -67,5 +55,15 @@ module.exports = function (pkg, plugins, presets) {
return oA !== -1 && oB !== -1 ? oA - oB : a < b ? -1 : 1
}),
presets: Object.keys(presets).map(preset => [preset, presets[preset]]),
targets: (() => {
let node = (pkg.engines || {}).node
if (node !== undefined) {
const trimChars = '^=>~'
while (trimChars.includes(node[0])) {
node = node.slice(1)
}
}
return { browsers: pkg.browserslist, node }
})(),
}
}

View File

@@ -10,12 +10,13 @@ const { resolve } = require('path')
const adapter = new RemoteAdapter(require('@xen-orchestra/fs').getHandler({ url: 'file://' }))
module.exports = async function main(args) {
const { _, remove, merge } = getopts(args, {
const { _, fix, remove, merge } = getopts(args, {
alias: {
fix: 'f',
remove: 'r',
merge: 'm',
},
boolean: ['merge', 'remove'],
boolean: ['fix', 'merge', 'remove'],
default: {
merge: false,
remove: false,
@@ -25,7 +26,7 @@ module.exports = async function main(args) {
await asyncMap(_, async vmDir => {
vmDir = resolve(vmDir)
try {
await adapter.cleanVm(vmDir, { remove, merge, onLog: log => console.warn(log) })
await adapter.cleanVm(vmDir, { fixMetadata: fix, remove, merge, onLog: (...args) => console.warn(...args) })
} catch (error) {
console.error('adapter.cleanVm', vmDir, error)
}

View File

@@ -5,11 +5,12 @@ require('./_composeCommands')({
get main() {
return require('./commands/clean-vms')
},
usage: `[--merge] [--remove] xo-vm-backups/*
usage: `[--fix] [--merge] [--remove] xo-vm-backups/*
Detects and repair issues with VM backups.
Options:
-f, --fix Fix metadata issues (like size)
-m, --merge Merge (or continue merging) VHD files that are unused
-r, --remove Remove unused, incomplete, orphan, or corrupted files
`,

View File

@@ -7,21 +7,16 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.9.1",
"@xen-orchestra/fs": "^0.14.0",
"@xen-orchestra/backups": "^0.16.2",
"@xen-orchestra/fs": "^0.19.2",
"filenamify": "^4.1.0",
"getopts": "^2.2.5",
"lodash": "^4.17.15",
"promise-toolbox": "^0.18.0",
"vhd-lib": "^1.0.0"
"promise-toolbox": "^0.20.0"
},
"engines": {
"node": ">=7.10.1"
},
"files": [
"commands",
"*.js"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/backups-cli",
"name": "@xen-orchestra/backups-cli",
"repository": {
@@ -32,7 +27,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "0.5.0",
"version": "0.6.1",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -1,14 +1,14 @@
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 Disposable = require('promise-toolbox/Disposable.js')
const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
const { compileTemplate } = require('@xen-orchestra/template')
const { limitConcurrency } = require('limit-concurrency-decorator')
const { extractIdsFromSimplePattern } = require('./_extractIdsFromSimplePattern')
const { PoolMetadataBackup } = require('./_PoolMetadataBackup')
const { Task } = require('./Task')
const { VmBackup } = require('./_VmBackup')
const { XoMetadataBackup } = require('./_XoMetadataBackup')
const { extractIdsFromSimplePattern } = require('./_extractIdsFromSimplePattern.js')
const { PoolMetadataBackup } = require('./_PoolMetadataBackup.js')
const { Task } = require('./Task.js')
const { VmBackup } = require('./_VmBackup.js')
const { XoMetadataBackup } = require('./_XoMetadataBackup.js')
const noop = Function.prototype

View File

@@ -1,8 +1,9 @@
const assert = require('assert')
const { formatFilenameDate } = require('./_filenameDate')
const { importDeltaVm } = require('./_deltaVm')
const { Task } = require('./Task')
const { formatFilenameDate } = require('./_filenameDate.js')
const { importDeltaVm } = require('./_deltaVm.js')
const { Task } = require('./Task.js')
const { watchStreamSize } = require('./_watchStreamSize.js')
exports.ImportVmBackup = class ImportVmBackup {
constructor({ adapter, metadata, srUuid, xapi, settings: { newMacAddresses } = {} }) {
@@ -18,13 +19,17 @@ exports.ImportVmBackup = class ImportVmBackup {
const metadata = this._metadata
const isFull = metadata.mode === 'full'
const sizeContainer = { size: 0 }
let backup
if (isFull) {
backup = await adapter.readFullVmBackup(metadata)
watchStreamSize(backup, sizeContainer)
} else {
assert.strictEqual(metadata.mode, 'delta')
backup = await adapter.readDeltaVmBackup(metadata)
Object.values(backup.streams).forEach(stream => watchStreamSize(stream, sizeContainer))
}
return Task.run(
@@ -52,7 +57,7 @@ exports.ImportVmBackup = class ImportVmBackup {
])
return {
size: metadata.size,
size: sizeContainer.size,
id: await xapi.getField('VM', vmRef, 'uuid'),
}
}

View File

@@ -1,23 +1,24 @@
const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
const Disposable = require('promise-toolbox/Disposable')
const fromCallback = require('promise-toolbox/fromCallback')
const fromEvent = require('promise-toolbox/fromEvent')
const pDefer = require('promise-toolbox/defer')
const pump = require('pump')
const { basename, dirname, join, normalize, resolve } = require('path')
const Disposable = require('promise-toolbox/Disposable.js')
const fromCallback = require('promise-toolbox/fromCallback.js')
const fromEvent = require('promise-toolbox/fromEvent.js')
const pDefer = require('promise-toolbox/defer.js')
const { dirname, join, normalize, resolve } = require('path')
const { createLogger } = require('@xen-orchestra/log')
const { createSyntheticStream, mergeVhd, default: Vhd } = require('vhd-lib')
const { deduped } = require('@vates/disposable/deduped')
const { Constants, createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdSynthetic } = require('vhd-lib')
const { deduped } = require('@vates/disposable/deduped.js')
const { execFile } = require('child_process')
const { readdir, stat } = require('fs-extra')
const { v4: uuidv4 } = require('uuid')
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')
const { BACKUP_DIR } = require('./_getVmBackupDir.js')
const { cleanVm } = require('./_cleanVm.js')
const { getTmpDir } = require('./_getTmpDir.js')
const { isMetadataFile } = require('./_backupType.js')
const { isValidXva } = require('./_isValidXva.js')
const { listPartitions, LVM_PARTITION_TYPE } = require('./_listPartitions.js')
const { lvs, pvs } = require('./_lvm.js')
const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
exports.DIR_XO_CONFIG_BACKUPS = DIR_XO_CONFIG_BACKUPS
@@ -76,48 +77,6 @@ class RemoteAdapter {
return this._handler
}
async _deleteVhd(path) {
const handler = this._handler
const vhds = await asyncMapSettled(
await handler.list(dirname(path), {
filter: isVhdFile,
prependDir: true,
}),
async path => {
try {
const vhd = new Vhd(handler, path)
await vhd.readHeaderAndFooter()
return {
footer: vhd.footer,
header: vhd.header,
path,
}
} catch (error) {
// Do not fail on corrupted VHDs (usually uncleaned temporary files),
// they are probably inconsequent to the backup process and should not
// fail it.
warn(`BackupNg#_deleteVhd ${path}`, { error })
}
}
)
const base = basename(path)
const child = vhds.find(_ => _ !== undefined && _.header.parentUnicodeName === base)
if (child === undefined) {
await handler.unlink(path)
return 0
}
try {
const childPath = child.path
const mergedDataSize = await mergeVhd(handler, path, handler, childPath)
await handler.rename(path, childPath)
return mergedDataSize
} catch (error) {
handler.unlink(path).catch(warn)
throw error
}
}
async _findPartition(devicePath, partitionId) {
const partitions = await listPartitions(devicePath)
const partition = partitions.find(_ => _.id === partitionId)
@@ -252,16 +211,9 @@ class RemoteAdapter {
async deleteDeltaVmBackups(backups) {
const handler = this._handler
let mergedDataSize = 0
await asyncMapSettled(backups, ({ _filename, vhds }) =>
Promise.all([
handler.unlink(_filename),
asyncMap(Object.values(vhds), async _ => {
mergedDataSize += await this._deleteVhd(resolveRelativeFromFile(_filename, _))
}),
])
)
return mergedDataSize
// unused VHDs will be detected by `cleanVm`
await asyncMapSettled(backups, ({ _filename }) => VhdAbstract.unlink(handler, _filename))
}
async deleteMetadataBackup(backupId) {
@@ -360,6 +312,17 @@ class RemoteAdapter {
return yield this._getPartition(devicePath, await this._findPartition(devicePath, partitionId))
}
// this function will be the one where we plug the logic of the storage format by fs type/user settings
// if the file is named .vhd => vhd
// if the file is named alias.vhd => alias to a vhd
getVhdFileName(baseName) {
if (this._handler.type === 's3') {
return `${baseName}.alias.vhd` // we want an alias to a vhddirectory
}
return `${baseName}.vhd`
}
async listAllVmBackups() {
const handler = this._handler
@@ -504,22 +467,79 @@ class RemoteAdapter {
return backups.sort(compareTimestamp)
}
async outputStream(path, input, { checksum = true, validator = noop } = {}) {
async writeVhd(path, input, { checksum = true, validator = noop } = {}) {
const handler = this._handler
input = await input
const tmpPath = `${dirname(path)}/.${basename(path)}`
const output = await handler.createOutputStream(tmpPath, {
if (path.endsWith('.alias.vhd')) {
const dataPath = `${dirname(path)}/data/${uuidv4()}.vhd`
await createVhdDirectoryFromStream(handler, dataPath, input, {
concurrency: 16,
async validator() {
await input.task
return validator.apply(this, arguments)
},
})
await VhdAbstract.createAlias(handler, path, dataPath)
} else {
await this.outputStream(path, input, { checksum, validator })
}
}
async outputStream(path, input, { checksum = true, validator = noop } = {}) {
await this._handler.outputStream(path, input, {
checksum,
dirMode: this._dirMode,
async validator() {
await input.task
return validator.apply(this, arguments)
},
})
try {
await Promise.all([fromCallback(pump, input, output), output.checksumWritten, input.task])
await validator(tmpPath)
await handler.rename(tmpPath, path, { checksum })
} catch (error) {
await handler.unlink(tmpPath, { checksum })
throw error
}
async _createSyntheticStream(handler, paths) {
let disposableVhds = []
// if it's a path : open all hierarchy of parent
if (typeof paths === 'string') {
let vhd,
vhdPath = paths
do {
const disposable = await openVhd(handler, vhdPath)
vhd = disposable.value
disposableVhds.push(disposable)
vhdPath = resolveRelativeFromFile(vhdPath, vhd.header.parentUnicodeName)
} while (vhd.footer.diskType !== Constants.DISK_TYPES.DYNAMIC)
} else {
// only open the list of path given
disposableVhds = paths.map(path => openVhd(handler, path))
}
// I don't want the vhds to be disposed on return
// but only when the stream is done ( or failed )
const disposables = await Disposable.all(disposableVhds)
const vhds = disposables.value
let disposed = false
const disposeOnce = async () => {
if (!disposed) {
disposed = true
try {
await disposables.dispose()
} catch (error) {
warn('_createSyntheticStream: failed to dispose VHDs', { error })
}
}
}
const synthetic = new VhdSynthetic(vhds)
await synthetic.readHeaderAndFooter()
await synthetic.readBlockAllocationTable()
const stream = await synthetic.stream()
stream.on('end', disposeOnce)
stream.on('close', disposeOnce)
stream.on('error', disposeOnce)
return stream
}
async readDeltaVmBackup(metadata) {
@@ -528,8 +548,8 @@ class RemoteAdapter {
const dir = dirname(metadata._filename)
const streams = {}
await asyncMapSettled(Object.entries(vdis), async ([id, vdi]) => {
streams[`${id}.vhd`] = await createSyntheticStream(handler, join(dir, vhds[id]))
await asyncMapSettled(Object.keys(vdis), async id => {
streams[`${id}.vhd`] = await this._createSyntheticStream(handler, join(dir, vhds[id]))
})
return {
@@ -551,8 +571,15 @@ class RemoteAdapter {
}
}
RemoteAdapter.prototype.cleanVm = function (vmDir) {
return Disposable.use(this._handler.lock(vmDir), () => cleanVm.apply(this, arguments))
}
Object.assign(RemoteAdapter.prototype, {
cleanVm(vmDir, { lock = true } = {}) {
if (lock) {
return Disposable.use(this._handler.lock(vmDir), () => cleanVm.apply(this, arguments))
} else {
return cleanVm.apply(this, arguments)
}
},
isValidXva,
})
exports.RemoteAdapter = RemoteAdapter

View File

@@ -1,5 +1,5 @@
const { DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter')
const { PATH_DB_DUMP } = require('./_PoolMetadataBackup')
const { DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter.js')
const { PATH_DB_DUMP } = require('./_PoolMetadataBackup.js')
exports.RestoreMetadataBackup = class RestoreMetadataBackup {
constructor({ backupId, handler, xapi }) {

View File

@@ -1,11 +1,12 @@
const CancelToken = require('promise-toolbox/CancelToken.js')
const Zone = require('node-zone')
const { SyncThenable } = require('./_syncThenable')
const logAfterEnd = () => {
throw new Error('task has already ended')
}
const noop = Function.prototype
// Create a serializable object from an error.
//
// Otherwise some fields might be non-enumerable and missing from logs.
@@ -19,163 +20,132 @@ const serializeError = error =>
stack: error.stack,
}
: error
exports.serializeError = serializeError
class TaskLogger {
constructor(logFn, parentId) {
this._log = logFn
this._parentId = parentId
this._taskId = undefined
const $$task = Symbol('@xen-orchestra/backups/Task')
class Task {
static get cancelToken() {
const task = Zone.current.data[$$task]
return task !== undefined ? task.#cancelToken : CancelToken.none
}
get taskId() {
const taskId = this._taskId
if (taskId === undefined) {
throw new Error('start the task first')
}
return taskId
static run(opts, fn) {
return new this(opts).run(fn, true)
}
// create a subtask
fork() {
return new TaskLogger(this._log, this.taskId)
}
info(message, data) {
return this._log({
data,
event: 'info',
message,
taskId: this.taskId,
timestamp: Date.now(),
})
}
run(message, data, fn) {
if (arguments.length === 2) {
fn = data
data = undefined
}
return SyncThenable.tryUnwrap(
SyncThenable.fromFunction(() => {
if (this._taskId !== undefined) {
throw new Error('task has already started')
}
this._taskId = Math.random().toString(36).slice(2)
return this._log({
data,
event: 'start',
message,
parentId: this._parentId,
taskId: this.taskId,
timestamp: Date.now(),
})
})
.then(fn)
.then(
result => {
const log = this._log
this._log = logAfterEnd
return SyncThenable.resolve(
log({
event: 'end',
result,
status: 'success',
taskId: this.taskId,
timestamp: Date.now(),
})
).then(() => result)
},
error => {
const log = this._log
this._log = logAfterEnd
return SyncThenable.resolve(
log({
event: 'end',
result: serializeError(error),
status: 'failure',
taskId: this.taskId,
timestamp: Date.now(),
})
).then(() => {
throw error
})
}
)
)
}
warning(message, data) {
return this._log({
data,
event: 'warning',
message,
taskId: this.taskId,
timestamp: Date.now(),
})
}
wrapFn(fn, message, data) {
const logger = this
return function () {
const evaluate = v => (typeof v === 'function' ? v.apply(this, arguments) : v)
return logger.run(evaluate(message), evaluate(data), () => fn.apply(this, arguments))
}
}
}
const $$task = Symbol('current task logger')
const getCurrent = () => Zone.current.data[$$task]
const Task = {
info(message, data) {
const task = getCurrent()
if (task !== undefined) {
return task.info(message, data)
}
},
run({ name, data, onLog }, fn) {
let parentId
if (onLog === undefined) {
const parent = getCurrent()
if (parent === undefined) {
return fn()
}
onLog = parent._log
parentId = parent.taskId
}
const task = new TaskLogger(onLog, parentId)
const zone = Zone.current.fork('task')
zone.data[$$task] = task
return task.run(name, data, zone.wrap(fn))
},
warning(message, data) {
const task = getCurrent()
if (task !== undefined) {
return task.warning(message, data)
}
},
wrapFn(opts, fn) {
static 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))
return Task.run(typeof opts === 'function' ? opts.apply(this, arguments) : opts, () => fn.apply(this, arguments))
}
},
}
#cancelToken
#id = Math.random().toString(36).slice(2)
#onLog
#zone
constructor({ name, data, onLog }) {
let parentCancelToken, parentId
if (onLog === undefined) {
const parent = Zone.current.data[$$task]
if (parent === undefined) {
onLog = noop
} else {
onLog = log => parent.#onLog(log)
parentCancelToken = parent.#cancelToken
parentId = parent.#id
}
}
const zone = Zone.current.fork('@xen-orchestra/backups/Task')
zone.data[$$task] = this
this.#zone = zone
const { cancel, token } = CancelToken.source(parentCancelToken && [parentCancelToken])
this.#cancelToken = token
this.cancel = cancel
this.#onLog = onLog
this.#log('start', {
data,
message: name,
parentId,
})
}
failure(error) {
this.#end('failure', serializeError(error))
}
info(message, data) {
this.#log('info', { data, message })
}
/**
* Run a function in the context of this task
*
* In case of error, the task will be failed.
*
* @typedef Result
* @param {() => Result)} fn
* @param {boolean} last - Whether the task should succeed if there is no error
* @returns Result
*/
run(fn, last = false) {
return this.#zone.run(() => {
try {
const result = fn()
let then
if (result != null && typeof (then = result.then) === 'function') {
then.call(result, last && (value => this.success(value)), error => this.failure(error))
} else if (last) {
this.success(result)
}
return result
} catch (error) {
this.failure(error)
throw error
}
})
}
success(value) {
this.#end('success', value)
}
warning(message, data) {
this.#log('warning', { data, message })
}
wrapFn(fn, last) {
const task = this
return function () {
return task.run(() => fn.apply(this, arguments), last)
}
}
#end(status, result) {
this.#log('end', { result, status })
this.#onLog = logAfterEnd
}
#log(event, props) {
this.#onLog({
...props,
event,
taskId: this.#id,
timestamp: Date.now(),
})
}
}
exports.Task = Task
for (const method of ['info', 'warning']) {
Task[method] = (...args) => Zone.current.data[$$task]?.[method](...args)
}

View File

@@ -1,9 +1,9 @@
const { asyncMap } = require('@xen-orchestra/async-map')
const { DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter')
const { forkStreamUnpipe } = require('./_forkStreamUnpipe')
const { formatFilenameDate } = require('./_filenameDate')
const { Task } = require('./Task')
const { DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter.js')
const { forkStreamUnpipe } = require('./_forkStreamUnpipe.js')
const { formatFilenameDate } = require('./_filenameDate.js')
const { Task } = require('./Task.js')
const PATH_DB_DUMP = '/pool/xmldbdump'
exports.PATH_DB_DUMP = PATH_DB_DUMP

View File

@@ -1,23 +1,32 @@
const findLast = require('lodash/findLast')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const keyBy = require('lodash/keyBy')
const mapValues = require('lodash/mapValues')
const assert = require('assert')
const findLast = require('lodash/findLast.js')
const groupBy = require('lodash/groupBy.js')
const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
const keyBy = require('lodash/keyBy.js')
const mapValues = require('lodash/mapValues.js')
const { asyncMap } = require('@xen-orchestra/async-map')
const { createLogger } = require('@xen-orchestra/log')
const { defer } = require('golike-defer')
const { formatDateTime } = require('@xen-orchestra/xapi')
const { ContinuousReplicationWriter } = require('./_ContinuousReplicationWriter')
const { DeltaBackupWriter } = require('./_DeltaBackupWriter')
const { DisasterRecoveryWriter } = require('./_DisasterRecoveryWriter')
const { exportDeltaVm } = require('./_deltaVm')
const { forkStreamUnpipe } = require('./_forkStreamUnpipe')
const { FullBackupWriter } = require('./_FullBackupWriter')
const { getOldEntries } = require('./_getOldEntries')
const { Task } = require('./Task')
const { watchStreamSize } = require('./_watchStreamSize')
const { DeltaBackupWriter } = require('./writers/DeltaBackupWriter.js')
const { DeltaReplicationWriter } = require('./writers/DeltaReplicationWriter.js')
const { exportDeltaVm } = require('./_deltaVm.js')
const { forkStreamUnpipe } = require('./_forkStreamUnpipe.js')
const { FullBackupWriter } = require('./writers/FullBackupWriter.js')
const { FullReplicationWriter } = require('./writers/FullReplicationWriter.js')
const { getOldEntries } = require('./_getOldEntries.js')
const { Task } = require('./Task.js')
const { watchStreamSize } = require('./_watchStreamSize.js')
const { debug, warn } = createLogger('xo:backups:VmBackup')
const asyncEach = async (iterable, fn, thisArg = iterable) => {
for (const item of iterable) {
await fn.call(thisArg, item)
}
}
const forkDeltaExport = deltaExport =>
Object.create(deltaExport, {
streams: {
@@ -27,6 +36,11 @@ const forkDeltaExport = deltaExport =>
exports.VmBackup = class VmBackup {
constructor({ config, getSnapshotNameLabel, job, remoteAdapters, remotes, schedule, settings, srs, vm }) {
if (vm.other_config['xo:backup:job'] === job.id) {
// otherwise replicated VMs would be matched and replicated again and again
throw new Error('cannot backup a VM created by this very job')
}
this.config = config
this.job = job
this.remoteAdapters = remoteAdapters
@@ -62,12 +76,12 @@ exports.VmBackup = class VmBackup {
// Create writers
{
const writers = []
const writers = new Set()
this._writers = writers
const [BackupWriter, ReplicationWriter] = this._isDelta
? [DeltaBackupWriter, ContinuousReplicationWriter]
: [FullBackupWriter, DisasterRecoveryWriter]
? [DeltaBackupWriter, DeltaReplicationWriter]
: [FullBackupWriter, FullReplicationWriter]
const allSettings = job.settings
@@ -77,7 +91,7 @@ exports.VmBackup = class VmBackup {
...allSettings[remoteId],
}
if (targetSettings.exportRetention !== 0) {
writers.push(new BackupWriter(this, remoteId, targetSettings))
writers.add(new BackupWriter({ backup: this, remoteId, settings: targetSettings }))
}
})
srs.forEach(sr => {
@@ -86,12 +100,43 @@ exports.VmBackup = class VmBackup {
...allSettings[sr.uuid],
}
if (targetSettings.copyRetention !== 0) {
writers.push(new ReplicationWriter(this, sr, targetSettings))
writers.add(new ReplicationWriter({ backup: this, sr, settings: targetSettings }))
}
})
}
}
// calls fn for each function, warns of any errors, and throws only if there are no writers left
async _callWriters(fn, warnMessage, parallel = true) {
const writers = this._writers
const n = writers.size
if (n === 0) {
return
}
if (n === 1) {
const [writer] = writers
try {
await fn(writer)
} catch (error) {
writers.delete(writer)
throw error
}
return
}
await (parallel ? asyncMap : asyncEach)(writers, async function (writer) {
try {
await fn(writer)
} catch (error) {
this.delete(writer)
warn(warnMessage, { error, writer: writer.constructor.name })
}
})
if (writers.size === 0) {
throw new Error('all targets have failed, step: ' + warnMessage)
}
}
// ensure the VM itself does not have any backup metadata which would be
// copied on manual snapshots and interfere with the backup jobs
async _cleanMetadata() {
@@ -114,7 +159,8 @@ exports.VmBackup = class VmBackup {
const settings = this._settings
const doSnapshot = this._isDelta || vm.power_state === 'Running' || settings.snapshotRetention !== 0
const doSnapshot =
this._isDelta || (!settings.offlineBackup && vm.power_state === 'Running') || settings.snapshotRetention !== 0
if (doSnapshot) {
await Task.run({ name: 'snapshot' }, async () => {
if (!settings.bypassVdiChainsCheck) {
@@ -146,31 +192,28 @@ exports.VmBackup = class VmBackup {
async _copyDelta() {
const { exportedVm } = this
const baseVm = this._baseVm
const fullVdisRequired = this._fullVdisRequired
await asyncMap(this._writers, writer => writer.prepare && writer.prepare())
const isFull = fullVdisRequired === undefined || fullVdisRequired.size !== 0
await this._callWriters(writer => writer.prepare({ isFull }), 'writer.prepare()')
const deltaExport = await exportDeltaVm(exportedVm, baseVm, {
fullVdisRequired: this._fullVdisRequired,
fullVdisRequired,
})
const sizeContainers = mapValues(deltaExport.streams, watchStreamSize)
const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
const timestamp = Date.now()
await asyncMap(this._writers, async writer => {
try {
await writer.transfer({
await this._callWriters(
writer =>
writer.transfer({
deltaExport: forkDeltaExport(deltaExport),
sizeContainers,
timestamp,
})
} catch (error) {
warn('copy failure', {
error,
target: writer.target,
vm: this.vm,
})
}
})
}),
'writer.transfer()'
)
this._baseVm = exportedVm
@@ -195,7 +238,7 @@ exports.VmBackup = class VmBackup {
size,
})
await asyncMap(this._writers, writer => writer.cleanup && writer.cleanup())
await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
}
async _copyFull() {
@@ -208,21 +251,15 @@ exports.VmBackup = class VmBackup {
const timestamp = Date.now()
await asyncMap(this._writers, async writer => {
try {
await writer.run({
await this._callWriters(
writer =>
writer.run({
sizeContainer,
stream: forkStreamUnpipe(stream),
timestamp,
})
} catch (error) {
warn('copy failure', {
error,
target: writer.target,
vm: this.vm,
})
}
})
}),
'writer.run()'
)
const { size } = sizeContainer
const end = Date.now()
@@ -253,17 +290,28 @@ exports.VmBackup = class VmBackup {
}
async _removeUnusedSnapshots() {
// TODO: handle all schedules (no longer existing schedules default to 0 retention)
const { scheduleId } = this
const scheduleSnapshots = this._jobSnapshots.filter(_ => _.other_config['xo:backup:schedule'] === scheduleId)
const jobSettings = this.job.settings
const baseVmRef = this._baseVm?.$ref
const { config } = this
const baseSettings = {
...config.defaultSettings,
...config.metadata.defaultSettings,
...jobSettings[''],
}
const snapshotsPerSchedule = groupBy(this._jobSnapshots, _ => _.other_config['xo:backup:schedule'])
const xapi = this._xapi
await asyncMap(getOldEntries(this._settings.snapshotRetention, scheduleSnapshots), ({ $ref }) => {
if ($ref !== baseVmRef) {
return xapi.VM_destroy($ref)
await asyncMap(Object.entries(snapshotsPerSchedule), ([scheduleId, snapshots]) => {
const settings = {
...baseSettings,
...jobSettings[scheduleId],
...jobSettings[this.vm.uuid],
}
return asyncMap(getOldEntries(settings.snapshotRetention, snapshots), ({ $ref }) => {
if ($ref !== baseVmRef) {
return xapi.VM_destroy($ref)
}
})
})
}
@@ -272,12 +320,14 @@ exports.VmBackup = class VmBackup {
let baseVm = findLast(this._jobSnapshots, _ => 'xo:backup:exported' in _.other_config)
if (baseVm === undefined) {
debug('no base VM found')
return
}
const fullInterval = this._settings.fullInterval
const deltaChainLength = +(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1
if (!(fullInterval === 0 || fullInterval > deltaChainLength)) {
debug('not using base VM becaust fullInterval reached')
return
}
@@ -288,26 +338,44 @@ exports.VmBackup = class VmBackup {
const baseUuidToSrcVdi = new Map()
await asyncMap(await baseVm.$getDisks(), async baseRef => {
const snapshotOf = await xapi.getField('VDI', baseRef, 'snapshot_of')
const [baseUuid, snapshotOf] = await Promise.all([
xapi.getField('VDI', baseRef, 'uuid'),
xapi.getField('VDI', baseRef, 'snapshot_of'),
])
const srcVdi = srcVdis[snapshotOf]
if (srcVdi !== undefined) {
baseUuidToSrcVdi.set(await xapi.getField('VDI', baseRef, 'uuid'), srcVdi)
baseUuidToSrcVdi.set(baseUuid, srcVdi)
} else {
debug('ignore snapshot VDI because no longer present on VM', {
vdi: baseUuid,
})
}
})
const presentBaseVdis = new Map(baseUuidToSrcVdi)
const writers = this._writers
for (let i = 0, n = writers.length; presentBaseVdis.size !== 0 && i < n; ++i) {
await writers[i].checkBaseVdis(presentBaseVdis, baseVm)
}
await this._callWriters(
writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis, baseVm),
'writer.checkBaseVdis()',
false
)
if (presentBaseVdis.size === 0) {
debug('no base VM found')
return
}
const fullVdisRequired = new Set()
baseUuidToSrcVdi.forEach((srcVdi, baseUuid) => {
if (!presentBaseVdis.has(baseUuid)) {
if (presentBaseVdis.has(baseUuid)) {
debug('found base VDI', {
base: baseUuid,
vdi: srcVdi.uuid,
})
} else {
debug('missing base VDI', {
base: baseUuid,
vdi: srcVdi.uuid,
})
fullVdisRequired.add(srcVdi.uuid)
}
})
@@ -316,7 +384,19 @@ exports.VmBackup = class VmBackup {
this._fullVdisRequired = fullVdisRequired
}
async run() {
run = defer(this.run)
async run($defer) {
const settings = this._settings
assert(
!settings.offlineBackup || settings.snapshotRetention === 0,
'offlineBackup is not compatible with snapshotRetention'
)
await this._callWriters(async writer => {
await writer.beforeBackup()
$defer(() => writer.afterBackup())
}, 'writer.beforeBackup()')
await this._fetchJobSnapshots()
if (this._isDelta) {
@@ -326,7 +406,7 @@ exports.VmBackup = class VmBackup {
await this._cleanMetadata()
await this._removeUnusedSnapshots()
const { _settings: settings, vm } = this
const { vm } = this
const isRunning = vm.power_state === 'Running'
const startAfter = isRunning && (settings.offlineBackup ? 'backup' : settings.offlineSnapshot && 'snapshot')
if (startAfter) {
@@ -339,7 +419,7 @@ exports.VmBackup = class VmBackup {
ignoreErrors.call(vm.$callAsync('start', false, false))
}
if (this._writers.length !== 0) {
if (this._writers.size !== 0) {
await (this._isDelta ? this._copyDelta() : this._copyFull())
}
} finally {

View File

@@ -1,8 +1,8 @@
const { asyncMap } = require('@xen-orchestra/async-map')
const { DIR_XO_CONFIG_BACKUPS } = require('./RemoteAdapter')
const { formatFilenameDate } = require('./_filenameDate')
const { Task } = require('./Task')
const { DIR_XO_CONFIG_BACKUPS } = require('./RemoteAdapter.js')
const { formatFilenameDate } = require('./_filenameDate.js')
const { Task } = require('./Task.js')
exports.XoMetadataBackup = class XoMetadataBackup {
constructor({ config, job, remoteAdapters, schedule, settings }) {

View File

@@ -1,15 +1,19 @@
const Disposable = require('promise-toolbox/Disposable')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
require('@xen-orchestra/log/configure.js').catchGlobalErrors(
require('@xen-orchestra/log').createLogger('xo:backups:worker')
)
const Disposable = require('promise-toolbox/Disposable.js')
const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
const { compose } = require('@vates/compose')
const { createDebounceResource } = require('@vates/disposable/debounceResource')
const { deduped } = require('@vates/disposable/deduped')
const { createDebounceResource } = require('@vates/disposable/debounceResource.js')
const { deduped } = require('@vates/disposable/deduped.js')
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')
const { Backup } = require('./Backup.js')
const { RemoteAdapter } = require('./RemoteAdapter.js')
const { Task } = require('./Task.js')
class BackupWorker {
#config

View File

@@ -1,5 +1,5 @@
const cancelable = require('promise-toolbox/cancelable')
const CancelToken = require('promise-toolbox/CancelToken')
const cancelable = require('promise-toolbox/cancelable.js')
const CancelToken = require('promise-toolbox/CancelToken.js')
// Similar to `Promise.all` + `map` but pass a cancel token to the callback
//

View File

@@ -1,5 +0,0 @@
const Vhd = require('vhd-lib').default
exports.checkVhd = async function checkVhd(handler, path) {
await new Vhd(handler, path).readHeaderAndFooter()
}

View File

@@ -0,0 +1,390 @@
/* eslint-env jest */
const rimraf = require('rimraf')
const tmp = require('tmp')
const fs = require('fs-extra')
const { getHandler } = require('@xen-orchestra/fs')
const { pFromCallback } = require('promise-toolbox')
const crypto = require('crypto')
const { RemoteAdapter } = require('./RemoteAdapter')
const { VHDFOOTER, VHDHEADER } = require('./tests.fixtures.js')
const { VhdFile, Constants, VhdDirectory, VhdAbstract } = require('vhd-lib')
let tempDir, adapter, handler, jobId, vdiId, basePath
jest.setTimeout(60000)
beforeEach(async () => {
tempDir = await pFromCallback(cb => tmp.dir(cb))
handler = getHandler({ url: `file://${tempDir}` })
await handler.sync()
adapter = new RemoteAdapter(handler)
jobId = uniqueId()
vdiId = uniqueId()
basePath = `vdis/${jobId}/${vdiId}`
await fs.mkdirp(`${tempDir}/${basePath}`)
})
afterEach(async () => {
await pFromCallback(cb => rimraf(tempDir, cb))
await handler.forget()
})
const uniqueId = () => crypto.randomBytes(16).toString('hex')
async function generateVhd(path, opts = {}) {
let vhd
const dataPath = opts.useAlias ? path + '.data' : path
if (opts.mode === 'directory') {
await handler.mkdir(dataPath)
vhd = new VhdDirectory(handler, dataPath)
} else {
const fd = await handler.openFile(dataPath, 'wx')
vhd = new VhdFile(handler, fd)
}
vhd.header = { ...VHDHEADER, ...opts.header }
vhd.footer = { ...VHDFOOTER, ...opts.footer }
vhd.footer.uuid = Buffer.from(crypto.randomBytes(16))
if (vhd.header.parentUnicodeName) {
vhd.footer.diskType = Constants.DISK_TYPES.DIFFERENCING
} else {
vhd.footer.diskType = Constants.DISK_TYPES.DYNAMIC
}
if (opts.useAlias === true) {
await VhdAbstract.createAlias(handler, path + '.alias.vhd', dataPath)
}
await vhd.writeBlockAllocationTable()
await vhd.writeHeader()
await vhd.writeFooter()
return vhd
}
test('It remove broken vhd', async () => {
// todo also tests a directory and an alias
await handler.writeFile(`${basePath}/notReallyAVhd.vhd`, 'I AM NOT A VHD')
expect((await handler.list(basePath)).length).toEqual(1)
let loggued = ''
const onLog = message => {
loggued += message
}
await adapter.cleanVm('/', { remove: false, onLog })
expect(loggued).toEqual(`error while checking the VHD with path /${basePath}/notReallyAVhd.vhd`)
// not removed
expect((await handler.list(basePath)).length).toEqual(1)
// really remove it
await adapter.cleanVm('/', { remove: true, onLog })
expect((await handler.list(basePath)).length).toEqual(0)
})
test('it remove vhd with missing or multiple ancestors', async () => {
// one with a broken parent
await generateVhd(`${basePath}/abandonned.vhd`, {
header: {
parentUnicodeName: 'gone.vhd',
parentUid: Buffer.from(crypto.randomBytes(16)),
},
})
// one orphan, which is a full vhd, no parent
const orphan = await generateVhd(`${basePath}/orphan.vhd`)
// a child to the orphan
await generateVhd(`${basePath}/child.vhd`, {
header: {
parentUnicodeName: 'orphan.vhd',
parentUid: orphan.footer.uuid,
},
})
// clean
let loggued = ''
const onLog = message => {
loggued += message + '\n'
}
await adapter.cleanVm('/', { remove: true, onLog })
const deletedOrphanVhd = loggued.match(/deleting orphan VHD/g) || []
expect(deletedOrphanVhd.length).toEqual(1) // only one vhd should have been deleted
const deletedAbandonnedVhd = loggued.match(/abandonned.vhd is missing/g) || []
expect(deletedAbandonnedVhd.length).toEqual(1) // and it must be abandonned.vhd
// we don't test the filew on disk, since they will all be marker as unused and deleted without a metadata.json file
})
test('it remove backup meta data referencing a missing vhd in delta backup', async () => {
// create a metadata file marking child and orphan as ok
await handler.writeFile(
`metadata.json`,
JSON.stringify({
mode: 'delta',
vhds: [
`${basePath}/orphan.vhd`,
`${basePath}/child.vhd`,
// abandonned.json is not here
],
})
)
await generateVhd(`${basePath}/abandonned.vhd`)
// one orphan, which is a full vhd, no parent
const orphan = await generateVhd(`${basePath}/orphan.vhd`)
// a child to the orphan
await generateVhd(`${basePath}/child.vhd`, {
header: {
parentUnicodeName: 'orphan.vhd',
parentUid: orphan.footer.uuid,
},
})
let loggued = ''
const onLog = message => {
loggued += message + '\n'
}
await adapter.cleanVm('/', { remove: true, onLog })
let matched = loggued.match(/deleting unused VHD /g) || []
expect(matched.length).toEqual(1) // only one vhd should have been deleted
matched = loggued.match(/abandonned.vhd is unused/g) || []
expect(matched.length).toEqual(1) // and it must be abandonned.vhd
// a missing vhd cause clean to remove all vhds
await handler.writeFile(
`metadata.json`,
JSON.stringify({
mode: 'delta',
vhds: [
`${basePath}/deleted.vhd`, // in metadata but not in vhds
`${basePath}/orphan.vhd`,
`${basePath}/child.vhd`,
// abandonned.json is not here
],
}),
{ flags: 'w' }
)
loggued = ''
await adapter.cleanVm('/', { remove: true, onLog })
matched = loggued.match(/deleting unused VHD /g) || []
expect(matched.length).toEqual(2) // all vhds (orphan and child ) should have been deleted
})
test('it merges delta of non destroyed chain', async () => {
await handler.writeFile(
`metadata.json`,
JSON.stringify({
mode: 'delta',
size: 209920,
vhds: [
`${basePath}/grandchild.vhd`, // grand child should not be merged
`${basePath}/child.vhd`,
// orphan is not here, he should be merged in child
],
})
)
// one orphan, which is a full vhd, no parent
const orphan = await generateVhd(`${basePath}/orphan.vhd`)
// a child to the orphan
const child = await generateVhd(`${basePath}/child.vhd`, {
header: {
parentUnicodeName: 'orphan.vhd',
parentUid: orphan.footer.uuid,
},
})
// a grand child
await generateVhd(`${basePath}/grandchild.vhd`, {
header: {
parentUnicodeName: 'child.vhd',
parentUid: child.footer.uuid,
},
})
let loggued = ''
const onLog = message => {
loggued += message + '\n'
}
await adapter.cleanVm('/', { remove: true, onLog })
expect(loggued).toEqual(`the parent /${basePath}/orphan.vhd of the child /${basePath}/child.vhd is unused\n`)
loggued = ''
await adapter.cleanVm('/', { remove: true, merge: true, onLog })
const [unused, merging] = loggued.split('\n')
expect(unused).toEqual(`the parent /${basePath}/orphan.vhd of the child /${basePath}/child.vhd is unused`)
expect(merging).toEqual(`merging /${basePath}/child.vhd into /${basePath}/orphan.vhd`)
// merging is already tested in vhd-lib, don't retest it here (and theses vhd are as empty as my stomach at 12h12)
// only check deletion
const remainingVhds = await handler.list(basePath)
expect(remainingVhds.length).toEqual(2)
expect(remainingVhds.includes('child.vhd')).toEqual(true)
expect(remainingVhds.includes('grandchild.vhd')).toEqual(true)
})
test('it finish unterminated merge ', async () => {
await handler.writeFile(
`metadata.json`,
JSON.stringify({
mode: 'delta',
size: 209920,
vhds: [
`${basePath}/orphan.vhd`, // grand child should not be merged
`${basePath}/child.vhd`,
// orphan is not here, he should be merged in child
],
})
)
// one orphan, which is a full vhd, no parent
const orphan = await generateVhd(`${basePath}/orphan.vhd`)
// a child to the orphan
const child = await generateVhd(`${basePath}/child.vhd`, {
header: {
parentUnicodeName: 'orphan.vhd',
parentUid: orphan.footer.uuid,
},
})
// a merge in progress file
await handler.writeFile(
`${basePath}/.orphan.vhd.merge.json`,
JSON.stringify({
parent: {
header: orphan.header.checksum,
},
child: {
header: child.header.checksum,
},
})
)
// a unfinished merging
await adapter.cleanVm('/', { remove: true, merge: true })
// merging is already tested in vhd-lib, don't retest it here (and theses vhd are as empty as my stomach at 12h12)
// only check deletion
const remainingVhds = await handler.list(basePath)
expect(remainingVhds.length).toEqual(1)
expect(remainingVhds.includes('child.vhd')).toEqual(true)
})
// each of the vhd can be a file, a directory, an alias to a file or an alias to a directory
// the message an resulting files should be identical to the output with vhd files which is tested independantly
describe('tests mulitple combination ', () => {
for (const useAlias of [true, false]) {
for (const vhdMode of ['file', 'directory']) {
test(`alias : ${useAlias}, mode: ${vhdMode}`, async () => {
// a broken VHD
const brokenVhdDataPath = basePath + useAlias ? 'broken.data' : 'broken.vhd'
if (vhdMode === 'directory') {
await handler.mkdir(brokenVhdDataPath)
} else {
await handler.writeFile(brokenVhdDataPath, 'notreallyavhd')
}
if (useAlias) {
await VhdAbstract.createAlias(handler, 'broken.alias.vhd', brokenVhdDataPath)
}
// a vhd non referenced in metada
await generateVhd(`${basePath}/nonreference.vhd`, { useAlias, mode: vhdMode })
// an abandonded delta vhd without its parent
await generateVhd(`${basePath}/abandonned.vhd`, {
useAlias,
mode: vhdMode,
header: {
parentUnicodeName: 'gone.vhd',
parentUid: crypto.randomBytes(16),
},
})
// an ancestor of a vhd present in metadata
const ancestor = await generateVhd(`${basePath}/ancestor.vhd`, {
useAlias,
mode: vhdMode,
})
const child = await generateVhd(`${basePath}/child.vhd`, {
useAlias,
mode: vhdMode,
header: {
parentUnicodeName: 'ancestor.vhd' + (useAlias ? '.alias.vhd' : ''),
parentUid: ancestor.footer.uuid,
},
})
// a grand child vhd in metadata
await generateVhd(`${basePath}/grandchild.vhd`, {
useAlias,
mode: vhdMode,
header: {
parentUnicodeName: 'child.vhd' + (useAlias ? '.alias.vhd' : ''),
parentUid: child.footer.uuid,
},
})
// an older parent that was merging in clean
const cleanAncestor = await generateVhd(`${basePath}/cleanAncestor.vhd`, {
useAlias,
mode: vhdMode,
})
// a clean vhd in metadata
const clean = await generateVhd(`${basePath}/clean.vhd`, {
useAlias,
mode: vhdMode,
header: {
parentUnicodeName: 'cleanAncestor.vhd' + (useAlias ? '.alias.vhd' : ''),
parentUid: cleanAncestor.footer.uuid,
},
})
await handler.writeFile(
`${basePath}/.cleanAncestor.vhd${useAlias ? '.alias.vhd' : ''}.merge.json`,
JSON.stringify({
parent: {
header: cleanAncestor.header.checksum,
},
child: {
header: clean.header.checksum,
},
})
)
// the metadata file
await handler.writeFile(
`metadata.json`,
JSON.stringify({
mode: 'delta',
vhds: [
`${basePath}/grandchild.vhd` + (useAlias ? '.alias.vhd' : ''), // grand child should not be merged
`${basePath}/child.vhd` + (useAlias ? '.alias.vhd' : ''),
`${basePath}/clean.vhd` + (useAlias ? '.alias.vhd' : ''),
],
})
)
await adapter.cleanVm('/', { remove: true, merge: true })
// broken vhd, non referenced, abandonned should be deleted ( alias and data)
// ancestor and child should be merged
// grand child and clean vhd should not have changed
const survivors = await handler.list(basePath)
// console.log(survivors)
if (useAlias) {
// the goal of the alias : do not move a full folder
expect(survivors).toContain('ancestor.vhd.data')
expect(survivors).toContain('grandchild.vhd.data')
expect(survivors).toContain('cleanAncestor.vhd.data')
expect(survivors).toContain('clean.vhd.alias.vhd')
expect(survivors).toContain('child.vhd.alias.vhd')
expect(survivors).toContain('grandchild.vhd.alias.vhd')
expect(survivors.length).toEqual(6)
} else {
expect(survivors).toContain('clean.vhd')
expect(survivors).toContain('child.vhd')
expect(survivors).toContain('grandchild.vhd')
expect(survivors.length).toEqual(3)
}
})
}
}
})

View File

@@ -1,17 +1,20 @@
const assert = require('assert')
const limitConcurrency = require('limit-concurrency-decorator').default
const sum = require('lodash/sum')
const { asyncMap } = require('@xen-orchestra/async-map')
const { default: Vhd, mergeVhd } = require('vhd-lib')
const { Constants, mergeVhd, openVhd, VhdAbstract, VhdFile } = 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')
const { DISK_TYPES } = Constants
const { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } = require('./_backupType.js')
const { limitConcurrency } = require('limit-concurrency-decorator')
const { Task } = require('./Task.js')
const { Disposable } = require('promise-toolbox')
// 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 }) {
async function mergeVhdChain(chain, { handler, onLog, remove, merge }) {
assert(chain.length >= 2)
let child = chain[0]
@@ -35,6 +38,8 @@ const mergeVhdChain = limitConcurrency(1)(async function mergeVhdChain(chain, {
child = children[0]
}
onLog(`merging ${child} into ${parent}`)
let done, total
const handle = setInterval(() => {
if (done !== undefined) {
@@ -42,7 +47,7 @@ const mergeVhdChain = limitConcurrency(1)(async function mergeVhdChain(chain, {
}
}, 10e3)
await mergeVhd(
const mergedSize = await mergeVhd(
handler,
parent,
handler,
@@ -59,39 +64,84 @@ const mergeVhdChain = limitConcurrency(1)(async function mergeVhdChain(chain, {
)
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)
}),
])
})
await Promise.all([
VhdAbstract.rename(handler, parent, child),
asyncMap(children.slice(0, -1), child => {
onLog(`the VHD ${child} is unused`)
if (remove) {
onLog(`deleting unused VHD ${child}`)
return VhdAbstract.unlink(handler, child)
}
}),
])
return mergedSize
}
}
const noop = Function.prototype
exports.cleanVm = async function cleanVm(vmDir, { remove, merge, onLog = noop }) {
const INTERRUPTED_VHDS_REG = /^(?:(.+)\/)?\.(.+)\.merge.json$/
const listVhds = async (handler, vmDir) => {
const vhds = []
const interruptedVhds = new Set()
await asyncMap(
await handler.list(`${vmDir}/vdis`, {
ignoreMissing: true,
prependDir: true,
}),
async jobDir =>
asyncMap(
await handler.list(jobDir, {
prependDir: true,
}),
async vdiDir => {
const list = await handler.list(vdiDir, {
filter: file => isVhdFile(file) || INTERRUPTED_VHDS_REG.test(file),
prependDir: true,
})
list.forEach(file => {
const res = INTERRUPTED_VHDS_REG.exec(file)
if (res === null) {
vhds.push(file)
} else {
const [, dir, file] = res
interruptedVhds.add(`${dir}/${file}`)
}
})
}
)
)
return { vhds, interruptedVhds }
}
const defaultMergeLimiter = limitConcurrency(1)
exports.cleanVm = async function cleanVm(
vmDir,
{ fixMetadata, remove, merge, mergeLimiter = defaultMergeLimiter, onLog = noop }
) {
const limitedMergeVhdChain = mergeLimiter(mergeVhdChain)
const handler = this._handler
const vhds = new Set()
const vhdParents = { __proto__: null }
const vhdChildren = { __proto__: null }
const vhdsList = await listVhds(handler, vmDir)
// 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()
await asyncMap(vhdsList.vhds, async path => {
try {
await Disposable.use(openVhd(handler, path, { checkSecondFooter: !vhdsList.interruptedVhds.has(path) }), vhd => {
vhds.add(path)
if (vhd.footer.diskType === DISK_TYPE_DIFFERENCING) {
const parent = resolve(dirname(path), vhd.header.parentUnicodeName)
if (vhd.footer.diskType === DISK_TYPES.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')
@@ -102,37 +152,41 @@ exports.cleanVm = async function cleanVm(vmDir, { remove, merge, onLog = noop })
}
vhdChildren[parent] = path
}
} catch (error) {
onLog(`error while checking the VHD with path ${path}`)
if (error?.code === 'ERR_ASSERTION' && remove) {
await handler.unlink(path)
}
})
} catch (error) {
onLog(`error while checking the VHD with path ${path}`, { error })
if (error?.code === 'ERR_ASSERTION' && remove) {
onLog(`deleting broken ${path}`)
return VhdAbstract.unlink(handler, path)
}
}
)
})
// @todo : add check for data folder of alias not referenced in a valid alias
// 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]
const deleteIfOrphan = vhdPath => {
const parent = vhdParents[vhdPath]
if (parent === undefined) {
return
}
// no longer needs to be checked
delete vhdParents[vhd]
delete vhdParents[vhdPath]
deleteIfOrphan(parent)
if (!vhds.has(parent)) {
vhds.delete(vhd)
vhds.delete(vhdPath)
onLog(`the parent ${parent} of the VHD ${vhd} is missing`)
onLog(`the parent ${parent} of the VHD ${vhdPath} is missing`)
if (remove) {
deletions.push(handler.unlink(vhd))
onLog(`deleting orphan VHD ${vhdPath}`)
deletions.push(VhdAbstract.unlink(handler, vhdPath))
}
}
}
@@ -167,7 +221,7 @@ exports.cleanVm = async function cleanVm(vmDir, { remove, merge, onLog = noop })
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))) {
if (!(await this.isValidXva(path))) {
onLog(`the XVA with path ${path} is potentially broken`)
}
})
@@ -180,38 +234,80 @@ exports.cleanVm = async function cleanVm(vmDir, { remove, merge, onLog = noop })
await asyncMap(jsons, async json => {
const metadata = JSON.parse(await handler.readFile(json))
const { mode } = metadata
let size
if (mode === 'full') {
const linkedXva = resolve(vmDir, metadata.xva)
const linkedXva = resolve('/', vmDir, metadata.xva)
if (xvas.has(linkedXva)) {
unusedXvas.delete(linkedXva)
size = await handler.getSize(linkedXva).catch(error => {
onLog(`failed to get size of ${json}`, { error })
})
} else {
onLog(`the XVA linked to the metadata ${json} is missing`)
if (remove) {
onLog(`deleting incomplete backup ${json}`)
await handler.unlink(json)
}
}
} else if (mode === 'delta') {
const linkedVhds = (() => {
const { vhds } = metadata
return Object.keys(vhds).map(key => resolve(vmDir, vhds[key]))
return Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
})()
const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
// 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(_))) {
if (missingVhds.length === 0) {
linkedVhds.forEach(_ => unusedVhds.delete(_))
// checking the size of a vhd directory is costly
// 1 Http Query per 1000 blocks
// we only check size of all the vhd are VhdFiles
const shouldComputeSize = linkedVhds.every(vhd => vhd instanceof VhdFile)
if (shouldComputeSize) {
try {
await Disposable.use(Disposable.all(linkedVhds.map(vhdPath => openVhd(handler, vhdPath))), async vhds => {
const sizes = await asyncMap(vhds, vhd => vhd.getSize())
size = sum(sizes)
})
} catch (error) {
onLog(`failed to get size of ${json}`, { error })
}
}
} else {
onLog(`Some VHDs linked to the metadata ${json} are missing`)
onLog(`Some VHDs linked to the metadata ${json} are missing`, { missingVhds })
if (remove) {
onLog(`deleting incomplete backup ${json}`)
await handler.unlink(json)
}
}
}
const metadataSize = metadata.size
if (size !== undefined && metadataSize !== size) {
onLog(`incorrect size in metadata: ${metadataSize ?? 'none'} instead of ${size}`)
// don't update if the the stored size is greater than found files,
// it can indicates a problem
if (fixMetadata && (metadataSize === undefined || metadataSize < size)) {
try {
metadata.size = size
await handler.writeFile(json, JSON.stringify(metadata), { flags: 'w' })
} catch (error) {
onLog(`failed to update size in backup metadata ${json}`, { error })
}
}
}
})
// TODO: parallelize by vm/job/vdi
const unusedVhdsDeletion = []
const toMerge = []
{
// VHD chains (as list from child to ancestor) to merge indexed by last
// ancestor
@@ -244,7 +340,8 @@ exports.cleanVm = async function cleanVm(vmDir, { remove, merge, onLog = noop })
onLog(`the VHD ${vhd} is unused`)
if (remove) {
unusedVhdsDeletion.push(handler.unlink(vhd))
onLog(`deleting unused VHD ${vhd}`)
unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))
}
}
@@ -252,26 +349,47 @@ exports.cleanVm = async function cleanVm(vmDir, { remove, merge, onLog = noop })
vhdChainsToMerge[vhd] = getUsedChildChainOrDelete(vhd)
})
Object.keys(vhdChainsToMerge).forEach(key => {
const chain = vhdChainsToMerge[key]
// merge interrupted VHDs
vhdsList.interruptedVhds.forEach(parent => {
vhdChainsToMerge[parent] = [vhdChildren[parent], parent]
})
Object.values(vhdChainsToMerge).forEach(chain => {
if (chain !== undefined) {
unusedVhdsDeletion.push(mergeVhdChain(chain, { onLog, remove, merge }))
toMerge.push(chain)
}
})
}
const doMerge = () => {
const promise = asyncMap(toMerge, async chain => limitedMergeVhdChain(chain, { handler, onLog, remove, merge }))
return merge ? promise.then(sizes => ({ size: sum(sizes) })) : promise
}
await Promise.all([
unusedVhdsDeletion,
...unusedVhdsDeletion,
toMerge.length !== 0 && (merge ? Task.run({ name: 'merge' }, doMerge) : doMerge()),
asyncMap(unusedXvas, path => {
onLog(`the XVA ${path} is unused`)
return remove && handler.unlink(path)
if (remove) {
onLog(`deleting unused XVA ${path}`)
return 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)
if (remove) {
onLog(`deleting unused XVA checksum ${path}`)
return handler.unlink(path)
}
}
}),
])
return {
// boolean whether some VHDs were merged (or should be merged)
merge: toMerge.length !== 0,
}
}

View File

@@ -1,14 +1,14 @@
const compareVersions = require('compare-versions')
const defer = require('golike-defer').default
const find = require('lodash/find')
const groupBy = require('lodash/groupBy')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const omit = require('lodash/omit')
const find = require('lodash/find.js')
const groupBy = require('lodash/groupBy.js')
const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
const omit = require('lodash/omit.js')
const { asyncMap } = require('@xen-orchestra/async-map')
const { CancelToken } = require('promise-toolbox')
const { createVhdStreamWithLength } = require('vhd-lib')
const { defer } = require('golike-defer')
const { cancelableMap } = require('./_cancelableMap')
const { cancelableMap } = require('./_cancelableMap.js')
const TAG_BASE_DELTA = 'xo:base_delta'
exports.TAG_BASE_DELTA = TAG_BASE_DELTA
@@ -202,6 +202,7 @@ exports.importDeltaVm = defer(async function importDeltaVm(
blocked_operations: {
...vmRecord.blocked_operations,
start: 'Importing…',
start_on: 'Importing…',
},
ha_always_run: false,
is_a_template: false,
@@ -305,9 +306,6 @@ exports.importDeltaVm = defer(async function importDeltaVm(
}
}),
// Wait for VDI export tasks (if any) termination.
Promise.all(Object.values(streams).map(stream => stream.task)),
// Create VIFs.
asyncMap(Object.values(deltaVm.vifs), vif => {
let network = vif.$network$uuid && xapi.getObjectByUuid(vif.$network$uuid, undefined)

View File

@@ -1,4 +1,4 @@
const Disposable = require('promise-toolbox/Disposable')
const Disposable = require('promise-toolbox/Disposable.js')
const { join } = require('path')
const { mkdir, rmdir } = require('fs-extra')
const { tmpdir } = require('os')
@@ -10,7 +10,7 @@ exports.getTmpDir = async function getTmpDir() {
const path = join(tmpdir(), Math.random().toString(36).slice(2))
try {
await mkdir(path)
return new Disposable(path, () => rmdir(path))
return new Disposable(() => rmdir(path), path)
} catch (error) {
if (i === MAX_ATTEMPTS) {
throw error

View File

@@ -1,10 +1,10 @@
const assert = require('assert')
const fs = require('fs-extra')
const isGzipFile = async fd => {
const isGzipFile = async (handler, fd) => {
// https://tools.ietf.org/html/rfc1952.html#page-5
const magicNumber = Buffer.allocUnsafe(2)
assert.strictEqual((await fs.read(fd, magicNumber, 0, magicNumber.length, 0)).bytesRead, magicNumber.length)
assert.strictEqual((await handler.read(fd, magicNumber, 0)).bytesRead, magicNumber.length)
return magicNumber[0] === 31 && magicNumber[1] === 139
}
@@ -21,32 +21,33 @@ const isGzipFile = async fd => {
// /^Ref:\d+/\d+\.checksum$/ and then validating the tar structure from it
//
// https://github.com/npm/node-tar/issues/234#issuecomment-538190295
const isValidTar = async (size, fd) => {
const isValidTar = async (handler, size, fd) => {
if (size <= 1024 || size % 512 !== 0) {
return false
}
const buf = Buffer.allocUnsafe(1024)
assert.strictEqual((await fs.read(fd, buf, 0, buf.length, size - buf.length)).bytesRead, buf.length)
assert.strictEqual((await handler.read(fd, buf, size - buf.length)).bytesRead, buf.length)
return buf.every(_ => _ === 0)
}
// TODO: find an heuristic for compressed files
const isValidXva = async path => {
async function isValidXva(path) {
const handler = this._handler
try {
const fd = await fs.open(path, 'r')
const fd = await handler.openFile(path, 'r')
try {
const { size } = await fs.fstat(fd)
const size = await handler.getSize(fd)
if (size < 20) {
// neither a valid gzip not tar
return false
}
return (await isGzipFile(fd))
return (await isGzipFile(handler, fd))
? true // gzip files cannot be validated at this time
: await isValidTar(size, fd)
: await isValidTar(handler, size, fd)
} finally {
fs.close(fd).catch(noop)
handler.closeFile(fd).catch(noop)
}
} catch (error) {
// never throw, log and report as valid to avoid side effects

View File

@@ -1,4 +1,4 @@
const fromCallback = require('promise-toolbox/fromCallback')
const fromCallback = require('promise-toolbox/fromCallback.js')
const { createLogger } = require('@xen-orchestra/log')
const { createParser } = require('parse-pairs')
const { execFile } = require('child_process')

View File

@@ -1,4 +1,4 @@
const fromCallback = require('promise-toolbox/fromCallback')
const fromCallback = require('promise-toolbox/fromCallback.js')
const { createParser } = require('parse-pairs')
const { execFile } = require('child_process')
@@ -7,23 +7,25 @@ const { execFile } = require('child_process')
const parse = createParser({
keyTransform: key => key.slice(5).toLowerCase(),
})
const makeFunction = command => async (fields, ...args) => {
const info = await fromCallback(execFile, command, [
'--noheading',
'--nosuffix',
'--nameprefixes',
'--unbuffered',
'--units',
'b',
'-o',
String(fields),
...args,
])
return info
.trim()
.split(/\r?\n/)
.map(Array.isArray(fields) ? parse : line => parse(line)[fields])
}
const makeFunction =
command =>
async (fields, ...args) => {
const info = await fromCallback(execFile, command, [
'--noheading',
'--nosuffix',
'--nameprefixes',
'--unbuffered',
'--units',
'b',
'-o',
String(fields),
...args,
])
return info
.trim()
.split(/\r?\n/)
.map(Array.isArray(fields) ? parse : line => parse(line)[fields])
}
exports.lvs = makeFunction('lvs')
exports.pvs = makeFunction('pvs')

View File

@@ -1,46 +0,0 @@
function fulfilledThen(cb) {
return typeof cb === 'function' ? SyncThenable.fromFunction(cb, this.value) : this
}
function rejectedThen(_, cb) {
return typeof cb === 'function' ? SyncThenable.fromFunction(cb, this.value) : this
}
class SyncThenable {
static resolve(value) {
if (value != null && typeof value.then === 'function') {
return value
}
return new this(false, value)
}
static fromFunction(fn, ...arg) {
try {
return this.resolve(fn(...arg))
} catch (error) {
return this.reject(error)
}
}
static reject(reason) {
return new this(true, reason)
}
// unwrap if it's a SyncThenable
static tryUnwrap(value) {
if (value instanceof this) {
if (value.then === rejectedThen) {
throw value.value
}
return value.value
}
return value
}
constructor(rejected, value) {
this.then = rejected ? rejectedThen : fulfilledThen
this.value = value
}
}
exports.SyncThenable = SyncThenable

View File

@@ -1,5 +1,4 @@
exports.watchStreamSize = function watchStreamSize(stream) {
const container = { size: 0 }
exports.watchStreamSize = function watchStreamSize(stream, container = { size: 0 }) {
stream.on('data', data => {
container.size += data.length
})

View File

@@ -1,4 +1,4 @@
const mapValues = require('lodash/mapValues')
const mapValues = require('lodash/mapValues.js')
const { dirname } = require('path')
function formatVmBackup(backup) {

View File

@@ -0,0 +1,69 @@
#!/usr/bin/env node
const { catchGlobalErrors } = require('@xen-orchestra/log/configure.js')
const { createLogger } = require('@xen-orchestra/log')
const { getSyncedHandler } = require('@xen-orchestra/fs')
const { join } = require('path')
const Disposable = require('promise-toolbox/Disposable')
const min = require('lodash/min')
const { getVmBackupDir } = require('../_getVmBackupDir.js')
const { RemoteAdapter } = require('../RemoteAdapter.js')
const { CLEAN_VM_QUEUE } = require('./index.js')
// -------------------------------------------------------------------
catchGlobalErrors(createLogger('xo:backups:mergeWorker'))
const { fatal, info, warn } = createLogger('xo:backups:mergeWorker')
// -------------------------------------------------------------------
const main = Disposable.wrap(async function* main(args) {
const handler = yield getSyncedHandler({ url: 'file://' + process.cwd() })
yield handler.lock(CLEAN_VM_QUEUE)
const adapter = new RemoteAdapter(handler)
const listRetry = async () => {
const timeoutResolver = resolve => setTimeout(resolve, 10e3)
for (let i = 0; i < 10; ++i) {
const entries = await handler.list(CLEAN_VM_QUEUE)
if (entries.length !== 0) {
return entries
}
await new Promise(timeoutResolver)
}
}
let taskFiles
while ((taskFiles = await listRetry()) !== undefined) {
const taskFileBasename = min(taskFiles)
const taskFile = join(CLEAN_VM_QUEUE, '_' + taskFileBasename)
// move this task to the end
await handler.rename(join(CLEAN_VM_QUEUE, taskFileBasename), taskFile)
try {
const vmDir = getVmBackupDir(String(await handler.readFile(taskFile)))
await adapter.cleanVm(vmDir, { merge: true, onLog: info, remove: true })
handler.unlink(taskFile).catch(error => warn('deleting task failure', { error }))
} catch (error) {
warn('failure handling task', { error })
}
}
})
info('starting')
main(process.argv.slice(2)).then(
() => {
info('bye :-)')
},
error => {
fatal(error)
process.exit(1)
}
)

View File

@@ -0,0 +1,25 @@
const { join, resolve } = require('path')
const { spawn } = require('child_process')
const { check } = require('proper-lockfile')
const CLEAN_VM_QUEUE = (exports.CLEAN_VM_QUEUE = '/xo-vm-backups/.queue/clean-vm/')
const CLI_PATH = resolve(__dirname, 'cli.js')
exports.run = async function runMergeWorker(remotePath) {
try {
// TODO: find a way to pass the acquire the lock and then pass it down the worker
if (await check(join(remotePath, CLEAN_VM_QUEUE))) {
// already locked, don't start another worker
return
}
spawn(CLI_PATH, {
cwd: remotePath,
detached: true,
stdio: 'inherit',
}).unref()
} catch (error) {
// we usually don't want to throw if the merge worker failed to start
return error
}
}

View File

@@ -8,38 +8,39 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.9.1",
"version": "0.16.2",
"engines": {
"node": ">=14.5"
"node": ">=14.6"
},
"scripts": {
"postversion": "npm publish --access public"
},
"dependencies": {
"@vates/compose": "^2.0.0",
"@vates/compose": "^2.1.0",
"@vates/disposable": "^0.1.1",
"@vates/multi-key-map": "^0.1.0",
"@vates/parse-duration": "^0.1.0",
"@vates/parse-duration": "^0.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^0.14.0",
"@xen-orchestra/log": "^0.2.0",
"@xen-orchestra/fs": "^0.19.2",
"@xen-orchestra/log": "^0.3.0",
"@xen-orchestra/template": "^0.1.0",
"compare-versions": "^3.6.0",
"compare-versions": "^4.0.1",
"d3-time-format": "^3.0.0",
"end-of-stream": "^1.4.4",
"ensure-array": "^1.0.0",
"fs-extra": "^9.0.0",
"fs-extra": "^10.0.0",
"golike-defer": "^0.5.1",
"limit-concurrency-decorator": "^0.4.0",
"limit-concurrency-decorator": "^0.5.0",
"lodash": "^4.17.20",
"node-zone": "^0.4.0",
"parse-pairs": "^1.1.0",
"promise-toolbox": "^0.18.0",
"vhd-lib": "^1.0.0",
"promise-toolbox": "^0.20.0",
"proper-lockfile": "^4.1.2",
"pump": "^3.0.0",
"uuid": "^8.3.2",
"vhd-lib": "^2.0.3",
"yazl": "^2.5.1"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^0.6.0"
"@xen-orchestra/xapi": "^0.8.4"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -1,4 +1,4 @@
const { DIR_XO_CONFIG_BACKUPS, DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter')
const { DIR_XO_CONFIG_BACKUPS, DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter.js')
exports.parseMetadataBackupId = function parseMetadataBackupId(backupId) {
const [dir, ...rest] = backupId.split('/')

View File

@@ -0,0 +1,92 @@
// a valid footer of a 2
exports.VHDFOOTER = {
cookie: 'conectix',
features: 2,
fileFormatVersion: 65536,
dataOffset: 512,
timestamp: 0,
creatorApplication: 'caml',
creatorVersion: 1,
creatorHostOs: 0,
originalSize: 53687091200,
currentSize: 53687091200,
diskGeometry: { cylinders: 25700, heads: 16, sectorsPerTrackCylinder: 255 },
diskType: 3,
checksum: 4294962945,
uuid: Buffer.from('d8dbcad85265421e8b298d99c2eec551', 'utf-8'),
saved: '',
hidden: '',
reserved: '',
}
exports.VHDHEADER = {
cookie: 'cxsparse',
dataOffset: undefined,
tableOffset: 2048,
headerVersion: 65536,
maxTableEntries: 25600,
blockSize: 2097152,
checksum: 4294964241,
parentUuid: null,
parentTimestamp: 0,
reserved1: 0,
parentUnicodeName: '',
parentLocatorEntry: [
{
platformCode: 0,
platformDataSpace: 0,
platformDataLength: 0,
reserved: 0,
platformDataOffset: 0,
},
{
platformCode: 0,
platformDataSpace: 0,
platformDataLength: 0,
reserved: 0,
platformDataOffset: 0,
},
{
platformCode: 0,
platformDataSpace: 0,
platformDataLength: 0,
reserved: 0,
platformDataOffset: 0,
},
{
platformCode: 0,
platformDataSpace: 0,
platformDataLength: 0,
reserved: 0,
platformDataOffset: 0,
},
{
platformCode: 0,
platformDataSpace: 0,
platformDataLength: 0,
reserved: 0,
platformDataOffset: 0,
},
{
platformCode: 0,
platformDataSpace: 0,
platformDataLength: 0,
reserved: 0,
platformDataOffset: 0,
},
{
platformCode: 0,
platformDataSpace: 0,
platformDataLength: 0,
reserved: 0,
platformDataOffset: 0,
},
{
platformCode: 0,
platformDataSpace: 0,
platformDataLength: 0,
reserved: 0,
platformDataOffset: 0,
},
],
reserved2: '',
}

View File

@@ -1,42 +1,26 @@
const assert = require('assert')
const map = require('lodash/map')
const mapValues = require('lodash/mapValues')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const map = require('lodash/map.js')
const mapValues = require('lodash/mapValues.js')
const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
const { asyncMap } = require('@xen-orchestra/async-map')
const { chainVhd, checkVhdChain, default: Vhd } = require('vhd-lib')
const { chainVhd, checkVhdChain, openVhd, VhdAbstract } = require('vhd-lib')
const { createLogger } = require('@xen-orchestra/log')
const { dirname } = require('path')
const { checkVhd } = require('./_checkVhd')
const { formatFilenameDate } = require('./_filenameDate')
const { getOldEntries } = require('./_getOldEntries')
const { getVmBackupDir } = require('./_getVmBackupDir')
const { packUuid } = require('./_packUuid')
const { Task } = require('./Task')
const { formatFilenameDate } = require('../_filenameDate.js')
const { getOldEntries } = require('../_getOldEntries.js')
const { getVmBackupDir } = require('../_getVmBackupDir.js')
const { Task } = require('../Task.js')
const { MixinBackupWriter } = require('./_MixinBackupWriter.js')
const { AbstractDeltaWriter } = require('./_AbstractDeltaWriter.js')
const { checkVhd } = require('./_checkVhd.js')
const { packUuid } = require('./_packUuid.js')
const { Disposable } = require('promise-toolbox')
const { warn } = createLogger('xo:backups:DeltaBackupWriter')
exports.DeltaBackupWriter = class DeltaBackupWriter {
constructor(backup, remoteId, settings) {
this._adapter = backup.remoteAdapters[remoteId]
this._backup = backup
this._settings = settings
this.transfer = Task.wrapFn(
{
name: 'export',
data: ({ deltaExport }) => ({
id: remoteId,
isFull: Object.values(deltaExport.vdis).some(vdi => vdi.other_config['xo:base_delta'] === undefined),
type: 'remote',
}),
},
this.transfer
)
this[settings.deleteFirst ? 'prepare' : 'cleanup'] = this._deleteOldEntries
}
exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
async checkBaseVdis(baseUuidToSrcVdi) {
const { handler } = this._adapter
const backup = this._backup
@@ -54,13 +38,13 @@ exports.DeltaBackupWriter = class DeltaBackupWriter {
await asyncMap(vhds, async path => {
try {
await checkVhdChain(handler, path)
const vhd = new Vhd(handler, path)
await vhd.readHeaderAndFooter()
found = found || vhd.footer.uuid.equals(packUuid(baseUuid))
await Disposable.use(
openVhd(handler, path),
vhd => (found = found || vhd.footer.uuid.equals(packUuid(baseUuid)))
)
} catch (error) {
warn('checkBaseVdis', { error })
await ignoreErrors.call(handler.unlink(path))
await ignoreErrors.call(VhdAbstract.unlink(handler, path))
}
})
} catch (error) {
@@ -72,7 +56,28 @@ exports.DeltaBackupWriter = class DeltaBackupWriter {
})
}
async prepare() {
async beforeBackup() {
await super.beforeBackup()
return this._cleanVm({ merge: true })
}
prepare({ isFull }) {
// create the task related to this export and ensure all methods are called in this context
const task = new Task({
name: 'export',
data: {
id: this._remoteId,
isFull,
type: 'remote',
},
})
this.transfer = task.wrapFn(this.transfer)
this.cleanup = task.wrapFn(this.cleanup, true)
return task.run(() => this._prepare())
}
async _prepare() {
const adapter = this._adapter
const settings = this._settings
const { scheduleId, vm } = this._backup
@@ -99,28 +104,26 @@ exports.DeltaBackupWriter = class DeltaBackupWriter {
if (settings.deleteFirst) {
await this._deleteOldEntries()
} else {
this.cleanup = this._deleteOldEntries
}
}
async cleanup() {
if (!this._settings.deleteFirst) {
await this._deleteOldEntries()
}
}
async _deleteOldEntries() {
return Task.run({ name: 'merge' }, async () => {
const adapter = this._adapter
const oldEntries = this._oldEntries
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,
}
})
// delete sequentially from newest to oldest to avoid unnecessary merges
for (let i = oldEntries.length; i-- > 0; ) {
await adapter.deleteDeltaVmBackups([oldEntries[i]])
}
}
async transfer({ timestamp, deltaExport, sizeContainers }) {
async _transfer({ timestamp, deltaExport, sizeContainers }) {
const adapter = this._adapter
const backup = this._backup
@@ -142,7 +145,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter {
// don't do delta for it
vdi.uuid
: vdi.$snapshot_of$uuid
}/${basename}.vhd`
}/${adapter.getVhdFileName(basename)}`
)
const metadataFilename = `${backupDir}/${basename}.json`
@@ -186,7 +189,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter {
await checkVhd(handler, parentPath)
}
await adapter.outputStream(path, deltaExport.streams[`${id}.vhd`], {
await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
// no checksum for VHDs, because they will be invalidated by
// merges and chainings
checksum: false,
@@ -198,11 +201,11 @@ exports.DeltaBackupWriter = class DeltaBackupWriter {
}
// set the correct UUID in the VHD
const vhd = new Vhd(handler, path)
await vhd.readHeaderAndFooter()
vhd.footer.uuid = packUuid(vdi.uuid)
await vhd.readBlockAllocationTable() // required by writeFooter()
await vhd.writeFooter()
await Disposable.use(openVhd(handler, path), async vhd => {
vhd.footer.uuid = packUuid(vdi.uuid)
await vhd.readBlockAllocationTable() // required by writeFooter()
await vhd.writeFooter()
})
})
)
return {

View File

@@ -1,32 +1,17 @@
const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
const { formatDateTime } = require('@xen-orchestra/xapi')
const { formatFilenameDate } = require('./_filenameDate')
const { getOldEntries } = require('./_getOldEntries')
const { importDeltaVm, TAG_COPY_SRC } = require('./_deltaVm')
const { listReplicatedVms } = require('./_listReplicatedVms')
const { Task } = require('./Task')
const { formatFilenameDate } = require('../_filenameDate.js')
const { getOldEntries } = require('../_getOldEntries.js')
const { importDeltaVm, TAG_COPY_SRC } = require('../_deltaVm.js')
const { Task } = require('../Task.js')
exports.ContinuousReplicationWriter = class ContinuousReplicationWriter {
constructor(backup, sr, settings) {
this._backup = backup
this._settings = settings
this._sr = sr
this.transfer = Task.wrapFn(
{
name: 'export',
data: ({ deltaExport }) => ({
id: sr.uuid,
isFull: Object.values(deltaExport.vdis).some(vdi => vdi.other_config['xo:base_delta'] === undefined),
type: 'SR',
}),
},
this.transfer
)
}
const { AbstractDeltaWriter } = require('./_AbstractDeltaWriter.js')
const { MixinReplicationWriter } = require('./_MixinReplicationWriter.js')
const { listReplicatedVms } = require('./_listReplicatedVms.js')
exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinReplicationWriter(AbstractDeltaWriter) {
async checkBaseVdis(baseUuidToSrcVdi, baseVm) {
const sr = this._sr
const replicatedVm = listReplicatedVms(sr.$xapi, this._backup.job.id, sr.uuid, this._backup.vm.uuid).find(
@@ -51,7 +36,23 @@ exports.ContinuousReplicationWriter = class ContinuousReplicationWriter {
}
}
async prepare() {
prepare({ isFull }) {
// create the task related to this export and ensure all methods are called in this context
const task = new Task({
name: 'export',
data: {
id: this._sr.uuid,
isFull,
type: 'SR',
},
})
this.transfer = task.wrapFn(this.transfer)
this.cleanup = task.wrapFn(this.cleanup, true)
return task.run(() => this._prepare())
}
async _prepare() {
const settings = this._settings
const { uuid: srUuid, $xapi: xapi } = this._sr
const { scheduleId, vm } = this._backup
@@ -63,8 +64,12 @@ exports.ContinuousReplicationWriter = class ContinuousReplicationWriter {
if (settings.deleteFirst) {
await this._deleteOldEntries()
} else {
this.cleanup = this._deleteOldEntries
}
}
async cleanup() {
if (!this._settings.deleteFirst) {
await this._deleteOldEntries()
}
}
@@ -72,7 +77,7 @@ exports.ContinuousReplicationWriter = class ContinuousReplicationWriter {
return asyncMapSettled(this._oldEntries, vm => vm.$destroy())
}
async transfer({ timestamp, deltaExport, sizeContainers }) {
async _transfer({ timestamp, deltaExport, sizeContainers }) {
const sr = this._sr
const { job, scheduleId, vm } = this._backup
@@ -101,9 +106,11 @@ exports.ContinuousReplicationWriter = class ContinuousReplicationWriter {
targetVm.ha_restart_priority !== '' &&
Promise.all([targetVm.set_ha_restart_priority(''), targetVm.add_tags('HA disabled')]),
targetVm.set_name_label(`${vm.name_label} - ${job.name} - (${formatFilenameDate(timestamp)})`),
targetVm.update_blocked_operations(
'start',
'Start operation for this vm is blocked, clone it if you want to use it.'
asyncMap(['start', 'start_on'], op =>
targetVm.update_blocked_operations(
op,
'Start operation for this vm is blocked, clone it if you want to use it.'
)
),
targetVm.update_other_config({
'xo:backup:sr': srUuid,

View File

@@ -1,20 +1,20 @@
const { formatFilenameDate } = require('./_filenameDate')
const { getOldEntries } = require('./_getOldEntries')
const { getVmBackupDir } = require('./_getVmBackupDir')
const { isValidXva } = require('./isValidXva')
const { Task } = require('./Task')
const { formatFilenameDate } = require('../_filenameDate.js')
const { getOldEntries } = require('../_getOldEntries.js')
const { getVmBackupDir } = require('../_getVmBackupDir.js')
const { Task } = require('../Task.js')
exports.FullBackupWriter = class FullBackupWriter {
constructor(backup, remoteId, settings) {
this._backup = backup
this._remoteId = remoteId
this._settings = settings
const { MixinBackupWriter } = require('./_MixinBackupWriter.js')
const { AbstractFullWriter } = require('./_AbstractFullWriter.js')
exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(AbstractFullWriter) {
constructor(props) {
super(props)
this.run = Task.wrapFn(
{
name: 'export',
data: {
id: remoteId,
id: props.remoteId,
type: 'remote',
// necessary?
@@ -25,14 +25,13 @@ exports.FullBackupWriter = class FullBackupWriter {
)
}
async run({ timestamp, sizeContainer, stream }) {
async _run({ timestamp, sizeContainer, stream }) {
const backup = this._backup
const remoteId = this._remoteId
const settings = this._settings
const { job, scheduleId, vm } = backup
const adapter = backup.remoteAdapters[remoteId]
const adapter = this._adapter
const handler = adapter.handler
const backupDir = getVmBackupDir(vm.uuid)
@@ -68,11 +67,7 @@ exports.FullBackupWriter = class FullBackupWriter {
await Task.run({ name: 'transfer' }, async () => {
await adapter.outputStream(dataFilename, stream, {
validator: tmpPath => {
if (handler._getFilePath !== undefined) {
return isValidXva(handler._getFilePath('/' + tmpPath))
}
},
validator: tmpPath => adapter.isValidXva(tmpPath),
})
return { size: sizeContainer.size }
})

View File

@@ -1,23 +1,24 @@
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const { asyncMapSettled } = require('@xen-orchestra/async-map')
const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
const { formatDateTime } = require('@xen-orchestra/xapi')
const { formatFilenameDate } = require('./_filenameDate')
const { getOldEntries } = require('./_getOldEntries')
const { listReplicatedVms } = require('./_listReplicatedVms')
const { Task } = require('./Task')
const { formatFilenameDate } = require('../_filenameDate.js')
const { getOldEntries } = require('../_getOldEntries.js')
const { Task } = require('../Task.js')
exports.DisasterRecoveryWriter = class DisasterRecoveryWriter {
constructor(backup, sr, settings) {
this._backup = backup
this._settings = settings
this._sr = sr
const { AbstractFullWriter } = require('./_AbstractFullWriter.js')
const { MixinReplicationWriter } = require('./_MixinReplicationWriter.js')
const { listReplicatedVms } = require('./_listReplicatedVms.js')
exports.FullReplicationWriter = class FullReplicationWriter extends MixinReplicationWriter(AbstractFullWriter) {
constructor(props) {
super(props)
this.run = Task.wrapFn(
{
name: 'export',
data: {
id: sr.uuid,
id: props.sr.uuid,
type: 'SR',
// necessary?
@@ -28,7 +29,7 @@ exports.DisasterRecoveryWriter = class DisasterRecoveryWriter {
)
}
async run({ timestamp, sizeContainer, stream }) {
async _run({ timestamp, sizeContainer, stream }) {
const sr = this._sr
const settings = this._settings
const { job, scheduleId, vm } = this._backup
@@ -63,9 +64,11 @@ exports.DisasterRecoveryWriter = class DisasterRecoveryWriter {
const targetVm = await xapi.getRecord('VM', targetVmRef)
await Promise.all([
targetVm.update_blocked_operations(
'start',
'Start operation for this vm is blocked, clone it if you want to use it.'
asyncMap(['start', 'start_on'], op =>
targetVm.update_blocked_operations(
op,
'Start operation for this vm is blocked, clone it if you want to use it.'
)
),
targetVm.update_other_config({
'xo:backup:sr': srUuid,

View File

@@ -0,0 +1,26 @@
const { AbstractWriter } = require('./_AbstractWriter.js')
exports.AbstractDeltaWriter = class AbstractDeltaWriter extends AbstractWriter {
checkBaseVdis(baseUuidToSrcVdi, baseVm) {
throw new Error('Not implemented')
}
cleanup() {
throw new Error('Not implemented')
}
prepare({ isFull }) {
throw new Error('Not implemented')
}
async transfer({ timestamp, deltaExport, sizeContainers }) {
try {
return await this._transfer({ timestamp, deltaExport, sizeContainers })
} finally {
// ensure all streams are properly closed
for (const stream of Object.values(deltaExport.streams)) {
stream.destroy()
}
}
}
}

View File

@@ -0,0 +1,12 @@
const { AbstractWriter } = require('./_AbstractWriter.js')
exports.AbstractFullWriter = class AbstractFullWriter extends AbstractWriter {
async run({ timestamp, sizeContainer, stream }) {
try {
return await this._run({ timestamp, sizeContainer, stream })
} finally {
// ensure stream is properly closed
stream.destroy()
}
}
}

View File

@@ -0,0 +1,10 @@
exports.AbstractWriter = class AbstractWriter {
constructor({ backup, settings }) {
this._backup = backup
this._settings = settings
}
beforeBackup() {}
afterBackup() {}
}

View File

@@ -0,0 +1,65 @@
const { createLogger } = require('@xen-orchestra/log')
const { join } = require('path')
const { BACKUP_DIR, getVmBackupDir } = require('../_getVmBackupDir.js')
const MergeWorker = require('../merge-worker/index.js')
const { formatFilenameDate } = require('../_filenameDate.js')
const { warn } = createLogger('xo:backups:MixinBackupWriter')
exports.MixinBackupWriter = (BaseClass = Object) =>
class MixinBackupWriter extends BaseClass {
#lock
#vmBackupDir
constructor({ remoteId, ...rest }) {
super(rest)
this._adapter = rest.backup.remoteAdapters[remoteId]
this._remoteId = remoteId
this.#vmBackupDir = getVmBackupDir(this._backup.vm.uuid)
}
async _cleanVm(options) {
try {
return await this._adapter.cleanVm(this.#vmBackupDir, {
...options,
fixMetadata: true,
onLog: warn,
lock: false,
})
} catch (error) {
warn(error)
return {}
}
}
async beforeBackup() {
const { handler } = this._adapter
const vmBackupDir = this.#vmBackupDir
await handler.mktree(vmBackupDir)
this.#lock = await handler.lock(vmBackupDir)
}
async afterBackup() {
const { disableMergeWorker } = this._backup.config
const { merge } = await this._cleanVm({ remove: true, merge: disableMergeWorker })
await this.#lock.dispose()
// merge worker only compatible with local remotes
const { handler } = this._adapter
if (merge && !disableMergeWorker && typeof handler._getRealPath === 'function') {
const taskFile =
join(MergeWorker.CLEAN_VM_QUEUE, formatFilenameDate(new Date())) +
'-' +
// add a random suffix to avoid collision in case multiple tasks are created at the same second
Math.random().toString(36).slice(2)
await handler.outputFile(taskFile, this._backup.vm.uuid)
const remotePath = handler._getRealPath()
await MergeWorker.run(remotePath)
}
}
}

View File

@@ -0,0 +1,8 @@
exports.MixinReplicationWriter = (BaseClass = Object) =>
class MixinReplicationWriter extends BaseClass {
constructor({ sr, ...rest }) {
super(rest)
this._sr = sr
}
}

View File

@@ -0,0 +1,6 @@
const openVhd = require('vhd-lib').openVhd
const Disposable = require('promise-toolbox/Disposable')
exports.checkVhd = async function checkVhd(handler, path) {
await Disposable.use(openVhd(handler, path), () => {})
}

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
const defer = require('golike-defer').default
const { Ref, Xapi } = require('xen-api')
const { defer } = require('golike-defer')
const pkg = require('./package.json')
@@ -77,7 +77,11 @@ ${cliName} v${pkg.version}
'xo:backup:sr': tgtSr.uuid,
'xo:copy_of': srcSnapshotUuid,
}),
tgtVm.update_blocked_operations('start', 'Start operation for this vm is blocked, clone it if you want to use it.'),
Promise.all(
['start', 'start_on'].map(op =>
tgtVm.update_blocked_operations(op, 'Start operation for this vm is blocked, clone it if you want to use it.')
)
),
Promise.all(
userDevices.map(userDevice => {
const srcDisk = srcDisks[userDevice]

View File

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

View File

@@ -0,0 +1 @@
../../scripts/babel-eslintrc.js

View File

@@ -28,9 +28,6 @@
},
"preferGlobal": false,
"main": "dist/",
"files": [
"dist/"
],
"browserslist": [
">2%"
],
@@ -45,7 +42,6 @@
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"cross-env": "^7.0.2",
"rimraf": "^3.0.0"
},

View File

@@ -2,6 +2,8 @@
import { createSchedule } from './'
jest.useFakeTimers()
const wrap = value => () => value
describe('issues', () => {

View File

@@ -11,7 +11,7 @@
// process.env.http_proxy
// ])
// ```
export default function defined() {
function defined() {
let args = arguments
let n = args.length
if (n === 1) {
@@ -29,6 +29,7 @@ export default function defined() {
}
}
}
module.exports = exports = defined
// Usage:
//
@@ -39,7 +40,7 @@ export default function defined() {
// const getFriendName = _ => _.friends[0].name
// const friendName = get(getFriendName, props.user)
// ```
export const get = (accessor, arg) => {
function get(accessor, arg) {
try {
return accessor(arg)
} catch (error) {
@@ -49,6 +50,7 @@ export const get = (accessor, arg) => {
}
}
}
exports.get = get
// Usage:
//
@@ -58,4 +60,6 @@ export const get = (accessor, arg) => {
// _ => new ProxyAgent(_)
// )
// ```
export const ifDef = (value, thenFn) => (value !== undefined ? thenFn(value) : value)
exports.ifDef = function ifDef(value, thenFn) {
return value !== undefined ? thenFn(value) : value
}

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/defined",
"version": "0.0.0",
"version": "0.0.1",
"license": "ISC",
"description": "Utilities to help handling (possibly) undefined values",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/defined",
@@ -16,30 +16,13 @@
"url": "https://vates.fr"
},
"preferGlobal": false,
"main": "dist/",
"files": [
"dist/"
],
"browserslist": [
">2%"
],
"engines": {
"node": ">=6"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"cross-env": "^7.0.2",
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -1,4 +1,4 @@
export default function emitAsync(event) {
module.exports = function emitAsync(event) {
let opts
let i = 1

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/emit-async",
"version": "0.0.0",
"version": "0.1.0",
"license": "ISC",
"description": "Emit an event for async listeners to settle",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/emit-async",
@@ -16,30 +16,13 @@
"url": "https://vates.fr"
},
"preferGlobal": false,
"main": "dist/",
"files": [
"dist/"
],
"browserslist": [
">2%"
],
"engines": {
"node": ">=6"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"cross-env": "^7.0.2",
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -0,0 +1 @@
../../scripts/babel-eslintrc.js

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/fs",
"version": "0.14.0",
"version": "0.19.2",
"license": "AGPL-3.0-or-later",
"description": "The File System for Xen Orchestra backups.",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",
@@ -13,30 +13,27 @@
},
"preferGlobal": true,
"main": "dist/",
"files": [
"dist/"
],
"engines": {
"node": ">=8.10"
"node": ">=14"
},
"dependencies": {
"@marsaud/smb2": "^0.17.2",
"@marsaud/smb2": "^0.18.0",
"@sindresorhus/df": "^3.1.1",
"@sullux/aws-sdk": "^1.0.5",
"@vates/coalesce-calls": "^0.1.0",
"@xen-orchestra/async-map": "^0.1.2",
"aws-sdk": "^2.686.0",
"decorator-synchronized": "^0.5.0",
"decorator-synchronized": "^0.6.0",
"execa": "^5.0.0",
"fs-extra": "^9.0.0",
"fs-extra": "^10.0.0",
"get-stream": "^6.0.0",
"limit-concurrency-decorator": "^0.4.0",
"limit-concurrency-decorator": "^0.5.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.18.0",
"promise-toolbox": "^0.20.0",
"proper-lockfile": "^4.1.2",
"readable-stream": "^3.0.6",
"through2": "^4.0.2",
"tmp": "^0.2.1",
"xo-remote-parser": "^0.6.0"
"xo-remote-parser": "^0.8.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
@@ -45,12 +42,10 @@
"@babel/plugin-proposal-function-bind": "^7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.4.4",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"async-iterator-to-stream": "^1.1.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^7.0.2",
"dotenv": "^8.0.0",
"index-modules": "^0.3.0",
"dotenv": "^10.0.0",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -1,33 +1,21 @@
// @flow
// $FlowFixMe
import getStream from 'get-stream'
import asyncMapSettled from '@xen-orchestra/async-map/legacy'
import limit from 'limit-concurrency-decorator'
import getStream from 'get-stream'
import path, { basename } from 'path'
import synchronized from 'decorator-synchronized'
import { coalesceCalls } from '@vates/coalesce-calls'
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
import { limitConcurrency } from 'limit-concurrency-decorator'
import { parse } from 'xo-remote-parser'
import { pipeline } from 'stream'
import { randomBytes } from 'crypto'
import { type Readable, type Writable } from 'stream'
import { synchronized } from 'decorator-synchronized'
import normalizePath from './_normalizePath'
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
type RemoteInfo = { used?: number, size?: number }
type File = FileDescriptor | string
const checksumFile = file => file + '.checksum'
const computeRate = (hrtime: number[], size: number) => {
const computeRate = (hrtime, size) => {
const seconds = hrtime[0] + hrtime[1] / 1e9
return size / seconds
}
@@ -41,6 +29,8 @@ const ignoreEnoent = error => {
}
}
const noop = Function.prototype
class PrefixWrapper {
constructor(handler, prefix) {
this._prefix = prefix
@@ -73,11 +63,7 @@ class PrefixWrapper {
}
export default class RemoteHandlerAbstract {
_highWaterMark: number
_remote: Object
_timeout: number
constructor(remote: any, options: Object = {}) {
constructor(remote, options = {}) {
if (remote.url === 'test://') {
this._remote = remote
} else {
@@ -88,8 +74,9 @@ export default class RemoteHandlerAbstract {
}
;({ highWaterMark: this._highWaterMark, timeout: this._timeout = DEFAULT_TIMEOUT } = options)
const sharedLimit = limit(options.maxParallelOperations ?? DEFAULT_MAX_PARALLEL_OPERATIONS)
const sharedLimit = limitConcurrency(options.maxParallelOperations ?? DEFAULT_MAX_PARALLEL_OPERATIONS)
this.closeFile = sharedLimit(this.closeFile)
this.copy = sharedLimit(this.copy)
this.getInfo = sharedLimit(this.getInfo)
this.getSize = sharedLimit(this.getSize)
this.list = sharedLimit(this.list)
@@ -104,25 +91,28 @@ export default class RemoteHandlerAbstract {
this.unlink = sharedLimit(this.unlink)
this.write = sharedLimit(this.write)
this.writeFile = sharedLimit(this.writeFile)
this._forget = coalesceCalls(this._forget)
this._sync = coalesceCalls(this._sync)
}
// Public members
get type(): string {
get type() {
throw new Error('Not implemented')
}
addPrefix(prefix: string) {
addPrefix(prefix) {
prefix = normalizePath(prefix)
return prefix === '/' ? this : new PrefixWrapper(this, prefix)
}
async closeFile(fd: FileDescriptor): Promise<void> {
async closeFile(fd) {
await this.__closeFile(fd)
}
// TODO: remove method
async createOutputStream(file: File, { checksum = false, dirMode, ...options }: Object = {}): Promise<LaxWritable> {
async createOutputStream(file, { checksum = false, dirMode, ...options } = {}) {
if (typeof file === 'string') {
file = normalizePath(file)
}
@@ -149,7 +139,6 @@ export default class RemoteHandlerAbstract {
stream.on('error', forwardError)
checksumStream.pipe(stream)
// $FlowFixMe
checksumStream.checksumWritten = checksumStream.checksum
.then(value => this._outputFile(checksumFile(path), value, { flags: 'wx' }))
.catch(forwardError)
@@ -157,10 +146,7 @@ export default class RemoteHandlerAbstract {
return checksumStream
}
createReadStream(
file: File,
{ checksum = false, ignoreMissingChecksum = false, ...options }: Object = {}
): Promise<LaxReadable> {
createReadStream(file, { checksum = false, ignoreMissingChecksum = false, ...options } = {}) {
if (typeof file === 'string') {
file = normalizePath(file)
}
@@ -197,7 +183,7 @@ export default class RemoteHandlerAbstract {
checksum =>
streamP.then(stream => {
const { length } = stream
stream = (validChecksumOfReadStream(stream, String(checksum).trim()): LaxReadable)
stream = validChecksumOfReadStream(stream, String(checksum).trim())
stream.length = length
return stream
@@ -211,16 +197,31 @@ export default class RemoteHandlerAbstract {
)
}
// write a stream to a file using a temporary file
async outputStream(
path: string,
input: Readable | Promise<Readable>,
{ checksum = true, dirMode }: { checksum?: boolean, dirMode?: number } = {}
): Promise<void> {
return this._outputStream(normalizePath(path), await input, {
checksum,
/**
* write a stream to a file using a temporary file
*
* @param {string} path
* @param {ReadableStream} input
* @param {object} [options]
* @param {boolean} [options.checksum]
* @param {number} [options.dirMode]
* @param {(this: RemoteHandlerAbstract, path: string) => Promise<undefined>} [options.validator] Function that will be called before the data is commited to the remote, if it fails, file should not exist
*/
async outputStream(path, input, { checksum = true, dirMode, validator } = {}) {
path = normalizePath(path)
let checksumStream
if (checksum) {
checksumStream = createChecksumStream()
pipeline(input, checksumStream, noop)
input = checksumStream
}
await this._outputStream(path, input, {
dirMode,
validator,
})
if (checksum) {
await this._outputFile(checksumFile(path), await checksumStream.checksum, { dirMode, flags: 'wx' })
}
}
// Free the resources possibly dedicated to put the remote at work, when it
@@ -230,73 +231,73 @@ export default class RemoteHandlerAbstract {
// as mount), forgetting them might breaking other processes using the same
// remote.
@synchronized()
async forget(): Promise<void> {
async forget() {
await this._forget()
}
async getInfo(): Promise<RemoteInfo> {
async getInfo() {
return timeout.call(this._getInfo(), this._timeout)
}
async getSize(file: File): Promise<number> {
async getSize(file) {
return timeout.call(this._getSize(typeof file === 'string' ? normalizePath(file) : file), this._timeout)
}
async list(
dir: string,
{ filter, prependDir = false }: { filter?: (name: string) => boolean, prependDir?: boolean } = {}
): Promise<string[]> {
const virtualDir = normalizePath(dir)
dir = normalizePath(dir)
async list(dir, { filter, ignoreMissing = false, prependDir = false } = {}) {
try {
const virtualDir = normalizePath(dir)
dir = normalizePath(dir)
let entries = await timeout.call(this._list(dir), this._timeout)
if (filter !== undefined) {
entries = entries.filter(filter)
let entries = await timeout.call(this._list(dir), this._timeout)
if (filter !== undefined) {
entries = entries.filter(filter)
}
if (prependDir) {
entries.forEach((entry, i) => {
entries[i] = virtualDir + '/' + entry
})
}
return entries
} catch (error) {
if (ignoreMissing && error?.code === 'ENOENT') {
return []
}
throw error
}
if (prependDir) {
entries.forEach((entry, i) => {
entries[i] = virtualDir + '/' + entry
})
}
return entries
}
async lock(path: string): Promise<Disposable> {
async lock(path) {
path = normalizePath(path)
return { dispose: await this._lock(path) }
}
async mkdir(dir: string, { mode }: { mode?: number } = {}): Promise<void> {
async mkdir(dir, { mode } = {}) {
await this.__mkdir(normalizePath(dir), { mode })
}
async mktree(dir: string, { mode }: { mode?: number } = {}): Promise<void> {
async mktree(dir, { mode } = {}) {
await this._mktree(normalizePath(dir), { mode })
}
openFile(path: string, flags: string): Promise<FileDescriptor> {
openFile(path, flags) {
return this.__openFile(path, flags)
}
async outputFile(
file: string,
data: Data,
{ dirMode, flags = 'wx' }: { dirMode?: number, flags?: string } = {}
): Promise<void> {
async outputFile(file, data, { dirMode, flags = 'wx' } = {}) {
await this._outputFile(normalizePath(file), data, { dirMode, flags })
}
async read(file: File, buffer: Buffer, position?: number): Promise<{| bytesRead: number, buffer: Buffer |}> {
async read(file, buffer, position) {
return this._read(typeof file === 'string' ? normalizePath(file) : file, buffer, position)
}
async readFile(file: string, { flags = 'r' }: { flags?: string } = {}): Promise<Buffer> {
async readFile(file, { flags = 'r' } = {}) {
return this._readFile(normalizePath(file), { flags })
}
async rename(oldPath: string, newPath: string, { checksum = false }: Object = {}) {
async rename(oldPath, newPath, { checksum = false } = {}) {
oldPath = normalizePath(oldPath)
newPath = normalizePath(newPath)
@@ -307,11 +308,22 @@ export default class RemoteHandlerAbstract {
return p
}
async rmdir(dir: string): Promise<void> {
async copy(oldPath, newPath, { checksum = false } = {}) {
oldPath = normalizePath(oldPath)
newPath = normalizePath(newPath)
let p = timeout.call(this._copy(oldPath, newPath), this._timeout)
if (checksum) {
p = Promise.all([p, this._copy(checksumFile(oldPath), checksumFile(newPath))])
}
return p
}
async rmdir(dir) {
await timeout.call(this._rmdir(normalizePath(dir)).catch(ignoreEnoent), this._timeout)
}
async rmtree(dir: string): Promise<void> {
async rmtree(dir) {
await this._rmtree(normalizePath(dir))
}
@@ -320,11 +332,11 @@ export default class RemoteHandlerAbstract {
//
// This method MUST ALWAYS be called before using the handler.
@synchronized()
async sync(): Promise<void> {
async sync() {
await this._sync()
}
async test(): Promise<Object> {
async test() {
const SIZE = 1024 * 1024 * 10
const testFileName = normalizePath(`${Date.now()}.test`)
const data = await fromCallback(randomBytes, SIZE)
@@ -359,11 +371,11 @@ export default class RemoteHandlerAbstract {
}
}
async truncate(file: string, len: number): Promise<void> {
async truncate(file, len) {
await this._truncate(file, len)
}
async unlink(file: string, { checksum = true }: Object = {}): Promise<void> {
async unlink(file, { checksum = true } = {}) {
file = normalizePath(file)
if (checksum) {
@@ -373,21 +385,21 @@ export default class RemoteHandlerAbstract {
await this._unlink(file).catch(ignoreEnoent)
}
async write(file: File, buffer: Buffer, position: number): Promise<{| bytesWritten: number, buffer: Buffer |}> {
async write(file, buffer, position) {
await this._write(typeof file === 'string' ? normalizePath(file) : file, buffer, position)
}
async writeFile(file: string, data: Data, { flags = 'wx' }: { flags?: string } = {}): Promise<void> {
async writeFile(file, data, { flags = 'wx' } = {}) {
await this._writeFile(normalizePath(file), data, { flags })
}
// Methods that can be called by private methods to avoid parallel limit on public methods
async __closeFile(fd: FileDescriptor): Promise<void> {
async __closeFile(fd) {
await timeout.call(this._closeFile(fd.fd), this._timeout)
}
async __mkdir(dir: string, { mode }: { mode?: number } = {}): Promise<void> {
async __mkdir(dir, { mode } = {}) {
try {
await this._mkdir(dir, { mode })
} catch (error) {
@@ -400,7 +412,7 @@ export default class RemoteHandlerAbstract {
}
}
async __openFile(path: string, flags: string): Promise<FileDescriptor> {
async __openFile(path, flags) {
path = normalizePath(path)
return {
@@ -411,11 +423,11 @@ export default class RemoteHandlerAbstract {
// Methods that can be implemented by inheriting classes
async _closeFile(fd: mixed): Promise<void> {
async _closeFile(fd) {
throw new Error('Not implemented')
}
async _createOutputStream(file: File, { dirMode, ...options }: Object = {}): Promise<LaxWritable> {
async _createOutputStream(file, { dirMode, ...options } = {}) {
try {
return await this._createWriteStream(file, { ...options, highWaterMark: this._highWaterMark })
} catch (error) {
@@ -428,40 +440,40 @@ export default class RemoteHandlerAbstract {
return this._createOutputStream(file, options)
}
async _createReadStream(file: File, options?: Object): Promise<LaxReadable> {
async _createReadStream(file, options) {
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> {
async _createWriteStream(file, options) {
throw new Error('Not implemented')
}
// called to finalize the remote
async _forget(): Promise<void> {}
async _forget() {}
async _getInfo(): Promise<Object> {
async _getInfo() {
return {}
}
async _lock(path: string): Promise<Function> {
async _lock(path) {
return () => Promise.resolve()
}
async _getSize(file: File): Promise<number> {
async _getSize(file) {
throw new Error('Not implemented')
}
async _list(dir: string): Promise<string[]> {
async _list(dir) {
throw new Error('Not implemented')
}
async _mkdir(dir: string): Promise<void> {
async _mkdir(dir) {
throw new Error('Not implemented')
}
async _mktree(dir: string, { mode }: { mode?: number } = {}): Promise<void> {
async _mktree(dir, { mode } = {}) {
try {
return await this.__mkdir(dir, { mode })
} catch (error) {
@@ -474,11 +486,11 @@ export default class RemoteHandlerAbstract {
return this._mktree(dir, { mode })
}
async _openFile(path: string, flags: string): Promise<mixed> {
async _openFile(path, flags) {
throw new Error('Not implemented')
}
async _outputFile(file: string, data: Data, { dirMode, flags }: { dirMode?: number, flags?: string }): Promise<void> {
async _outputFile(file, data, { dirMode, flags }) {
try {
return await this._writeFile(file, data, { flags })
} catch (error) {
@@ -491,42 +503,43 @@ export default class RemoteHandlerAbstract {
return this._outputFile(file, data, { flags })
}
async _outputStream(path: string, input: Readable, { checksum, dirMode }: { checksum?: boolean, dirMode?: number }) {
async _outputStream(path, input, { dirMode, validator }) {
const tmpPath = `${dirname(path)}/.${basename(path)}`
const output = await this.createOutputStream(tmpPath, {
checksum,
dirMode,
})
try {
input.pipe(output)
await fromEvent(output, 'finish')
await output.checksumWritten
// $FlowFixMe
await input.task
await this.rename(tmpPath, path, { checksum })
await fromCallback(pipeline, input, output)
if (validator !== undefined) {
await validator.call(this, tmpPath)
}
await this.rename(tmpPath, path)
} catch (error) {
await this.unlink(tmpPath, { checksum })
await this.unlink(tmpPath)
throw error
}
}
_read(file: File, buffer: Buffer, position?: number): Promise<{| bytesRead: number, buffer: Buffer |}> {
_read(file, buffer, position) {
throw new Error('Not implemented')
}
_readFile(file: string, options?: Object): Promise<Buffer> {
_readFile(file, options) {
return this._createReadStream(file, { ...options, highWaterMark: this._highWaterMark }).then(getStream.buffer)
}
async _rename(oldPath: string, newPath: string) {
async _rename(oldPath, newPath) {
throw new Error('Not implemented')
}
async _copy(oldPath, newPath) {
throw new Error('Not implemented')
}
async _rmdir(dir: string) {
async _rmdir(dir) {
throw new Error('Not implemented')
}
async _rmtree(dir: string) {
async _rmtree(dir) {
try {
return await this._rmdir(dir)
} catch (error) {
@@ -548,13 +561,13 @@ export default class RemoteHandlerAbstract {
}
// called to initialize the remote
async _sync(): Promise<void> {}
async _sync() {}
async _unlink(file: string): Promise<void> {
async _unlink(file) {
throw new Error('Not implemented')
}
async _write(file: File, buffer: Buffer, position: number): Promise<void> {
async _write(file, buffer, position) {
const isPath = typeof file === 'string'
if (isPath) {
file = await this.__openFile(file, 'r+')
@@ -568,11 +581,11 @@ export default class RemoteHandlerAbstract {
}
}
async _writeFd(fd: FileDescriptor, buffer: Buffer, position: number): Promise<void> {
async _writeFd(fd, buffer, position) {
throw new Error('Not implemented')
}
async _writeFile(file: string, data: Data, options: { flags?: string }): Promise<void> {
async _writeFile(file, data, options) {
throw new Error('Not implemented')
}
}

View File

@@ -1,10 +1,7 @@
// @flow
import through2 from 'through2'
import { createHash } from 'crypto'
import { defer, fromEvent } from 'promise-toolbox'
import { invert } from 'lodash'
import { type Readable, type Transform } from 'stream'
// Format: $<algorithm>$<salt>$<encrypted>
//
@@ -27,7 +24,7 @@ const ID_TO_ALGORITHM = invert(ALGORITHM_TO_ID)
// const checksumStream = source.pipe(createChecksumStream())
// checksumStream.resume() // make the data flow without an output
// console.log(await checksumStream.checksum)
export const createChecksumStream = (algorithm: string = 'md5'): Transform & { checksum: Promise<string> } => {
export const createChecksumStream = (algorithm = 'md5') => {
const algorithmId = ALGORITHM_TO_ID[algorithm]
if (!algorithmId) {
@@ -54,10 +51,7 @@ export const createChecksumStream = (algorithm: string = 'md5'): Transform & { c
// Check if the checksum of a readable stream is equals to an expected checksum.
// The given stream is wrapped in a stream which emits an error event
// if the computed checksum is not equals to the expected checksum.
export const validChecksumOfReadStream = (
stream: Readable,
expectedChecksum: string
): Readable & { checksumVerified: Promise<void> } => {
export const validChecksumOfReadStream = (stream, expectedChecksum) => {
const algorithmId = expectedChecksum.slice(1, expectedChecksum.indexOf('$', 1))
if (!algorithmId) {
@@ -66,7 +60,7 @@ export const validChecksumOfReadStream = (
const hash = createHash(ID_TO_ALGORITHM[algorithmId])
const wrapper: any = stream.pipe(
const wrapper = stream.pipe(
through2(
{ highWaterMark: 0 },
(chunk, enc, callback) => {

View File

@@ -133,6 +133,14 @@ handlers.forEach(url => {
await handler.outputFile('dir/file', '')
expect(await handler.list('dir', { prependDir: true })).toEqual(['/dir/file'])
})
it('throws ENOENT if no such directory', async () => {
expect((await rejectionOf(handler.list('dir'))).code).toBe('ENOENT')
})
it('can returns empty for missing directory', async () => {
expect(await handler.list('dir', { ignoreMissing: true })).toEqual([])
})
})
describe('#mkdir()', () => {

View File

@@ -1,16 +1,12 @@
// @flow
import execa from 'execa'
import { parse } from 'xo-remote-parser'
import type RemoteHandler from './abstract'
import RemoteHandlerLocal from './local'
import RemoteHandlerNfs from './nfs'
import RemoteHandlerS3 from './s3'
import RemoteHandlerSmb from './smb'
import RemoteHandlerSmbMount from './smb-mount'
export type { default as RemoteHandler } from './abstract'
export type Remote = { url: string }
const HANDLERS = {
file: RemoteHandlerLocal,
nfs: RemoteHandlerNfs,
@@ -24,13 +20,19 @@ try {
HANDLERS.smb = RemoteHandlerSmb
}
export const getHandler = (remote: Remote, ...rest: any): RemoteHandler => {
// FIXME: should be done in xo-remote-parser.
const type = remote.url.split('://')[0]
const Handler = HANDLERS[type]
export const getHandler = (remote, ...rest) => {
const Handler = HANDLERS[parse(remote.url).type]
if (!Handler) {
throw new Error('Unhandled remote type')
}
return new Handler(remote, ...rest)
}
export const getSyncedHandler = async (...opts) => {
const handler = getHandler(...opts)
await handler.sync()
return {
dispose: () => handler.forget(),
value: handler,
}
}

View File

@@ -33,6 +33,10 @@ export default class LocalHandler extends RemoteHandlerAbstract {
return fs.close(fd)
}
async _copy(oldPath, newPath) {
return fs.copy(this._getFilePath(oldPath), this._getFilePath(newPath))
}
async _createReadStream(file, options) {
if (typeof file === 'string') {
const stream = fs.createReadStream(this._getFilePath(file), options)
@@ -84,7 +88,7 @@ export default class LocalHandler extends RemoteHandlerAbstract {
}
_lock(path) {
return lockfile.lock(path)
return lockfile.lock(this._getFilePath(path))
}
_mkdir(dir, { mode }) {

View File

@@ -8,7 +8,6 @@ export default class NfsHandler extends MountHandler {
super(remote, opts, {
type: 'nfs',
device: `${host}${port !== undefined ? ':' + port : ''}:${path}`,
defaultOptions: 'vers=3',
})
}

View File

@@ -1,9 +1,10 @@
import aws from '@sullux/aws-sdk'
import assert from 'assert'
import http from 'http'
import https from 'https'
import { parse } from 'xo-remote-parser'
import RemoteHandlerAbstract from './abstract'
import { createChecksumStream } from './checksum'
// endpoints https://docs.aws.amazon.com/general/latest/gr/s3.html
@@ -16,9 +17,8 @@ const IDEAL_FRAGMENT_SIZE = Math.ceil(MAX_OBJECT_SIZE / MAX_PARTS_COUNT) // the
export default class S3Handler extends RemoteHandlerAbstract {
constructor(remote, _opts) {
super(remote)
const { host, path, username, password } = parse(remote.url)
// https://www.zenko.io/blog/first-things-first-getting-started-scality-s3-server/
this._s3 = aws({
const { allowUnauthorized, host, path, username, password, protocol, region } = parse(remote.url)
const params = {
accessKeyId: username,
apiVersion: '2006-03-01',
endpoint: host,
@@ -28,7 +28,21 @@ export default class S3Handler extends RemoteHandlerAbstract {
httpOptions: {
timeout: 600000,
},
}).s3
}
if (protocol === 'http') {
params.httpOptions.agent = new http.Agent({ keepAlive: true })
params.sslEnabled = false
} else if (protocol === 'https') {
params.httpOptions.agent = new https.Agent({
rejectUnauthorized: !allowUnauthorized,
keepAlive: true,
})
}
if (region !== undefined) {
params.region = region
}
this._s3 = aws(params).s3
const splitPath = path.split('/').filter(s => s.length)
this._bucket = splitPath.shift()
@@ -43,70 +57,7 @@ export default class S3Handler extends RemoteHandlerAbstract {
return { Bucket: this._bucket, Key: this._dir + file }
}
async _outputStream(path, input, { checksum }) {
let inputStream = input
if (checksum) {
const checksumStream = createChecksumStream()
const forwardError = error => {
checksumStream.emit('error', error)
}
input.pipe(checksumStream)
input.on('error', forwardError)
inputStream = checksumStream
}
await this._s3.upload(
{
...this._createParams(path),
Body: inputStream,
},
{ partSize: IDEAL_FRAGMENT_SIZE, queueSize: 1 }
)
if (checksum) {
const checksum = await inputStream.checksum
const params = {
...this._createParams(path + '.checksum'),
Body: checksum,
}
await this._s3.upload(params)
}
await input.task
}
async _writeFile(file, data, options) {
return this._s3.putObject({ ...this._createParams(file), Body: data })
}
async _createReadStream(file, options) {
// https://github.com/Sullux/aws-sdk/issues/11
return this._s3.getObject.raw(this._createParams(file)).createReadStream()
}
async _unlink(file) {
return this._s3.deleteObject(this._createParams(file))
}
async _list(dir) {
function splitPath(path) {
return path.split('/').filter(d => d.length)
}
const prefix = [this._dir, dir].join('/')
const splitPrefix = splitPath(prefix)
const result = await this._s3.listObjectsV2({
Bucket: this._bucket,
Prefix: splitPrefix.join('/'),
})
const uniq = new Set()
for (const entry of result.Contents) {
const line = splitPath(entry.Key)
if (line.length > splitPrefix.length) {
uniq.add(line[splitPrefix.length])
}
}
return [...uniq]
}
async _rename(oldPath, newPath) {
async _copy(oldPath, newPath) {
const size = await this._getSize(oldPath)
const multipartParams = await this._s3.createMultipartUpload({ ...this._createParams(newPath) })
const param2 = { ...multipartParams, CopySource: `/${this._bucket}/${this._dir}${oldPath}` }
@@ -125,6 +76,121 @@ export default class S3Handler extends RemoteHandlerAbstract {
await this._s3.abortMultipartUpload(multipartParams)
throw e
}
}
async _isNotEmptyDir(path) {
const result = await this._s3.listObjectsV2({
Bucket: this._bucket,
MaxKeys: 1,
Prefix: this._dir + path + '/',
})
return result.Contents.length !== 0
}
async _isFile(path) {
try {
await this._s3.headObject(this._createParams(path))
return true
} catch (error) {
if (error.code === 'NotFound') {
return false
}
throw error
}
}
async _outputStream(path, input, { validator }) {
await this._s3.upload(
{
...this._createParams(path),
Body: input,
},
{ partSize: IDEAL_FRAGMENT_SIZE, queueSize: 1 }
)
if (validator !== undefined) {
try {
await validator.call(this, path)
} catch (error) {
await this.unlink(path)
throw error
}
}
}
async _writeFile(file, data, options) {
return this._s3.putObject({ ...this._createParams(file), Body: data })
}
async _createReadStream(path, options) {
if (!(await this._isFile(path))) {
const error = new Error(`ENOENT: no such file '${path}'`)
error.code = 'ENOENT'
error.path = path
throw error
}
// https://github.com/Sullux/aws-sdk/issues/11
return this._s3.getObject.raw(this._createParams(path)).createReadStream()
}
async _unlink(path) {
await this._s3.deleteObject(this._createParams(path))
if (await this._isNotEmptyDir(path)) {
const error = new Error(`EISDIR: illegal operation on a directory, unlink '${path}'`)
error.code = 'EISDIR'
error.path = path
throw error
}
}
async _list(dir) {
function splitPath(path) {
return path.split('/').filter(d => d.length)
}
const prefix = [this._dir, dir].join('/')
const splitPrefix = splitPath(prefix)
const result = await this._s3.listObjectsV2({
Bucket: this._bucket,
Prefix: splitPrefix.join('/') + '/', // need slash at the end with the use of delimiters
Delimiter: '/', // will only return path until delimiters
})
if (result.isTruncated) {
const error = new Error('more than 1000 objects, unsupported in this implementation')
error.dir = dir
throw error
}
const uniq = []
// sub directories
for (const entry of result.CommonPrefixes) {
const line = splitPath(entry.Prefix)
uniq.push(line[line.length - 1])
}
// files
for (const entry of result.Contents) {
const line = splitPath(entry.Key)
uniq.push(line[line.length - 1])
}
return uniq
}
async _mkdir(path) {
if (await this._isFile(path)) {
const error = new Error(`ENOTDIR: file already exists, mkdir '${path}'`)
error.code = 'ENOTDIR'
error.path = path
throw error
}
// nothing to do, directories do not exist, they are part of the files' path
}
// s3 doesn't have a rename operation, so copy + delete source
async _rename(oldPath, newPath) {
await this.copy(oldPath, newPath)
await this._s3.deleteObject(this._createParams(oldPath))
}
@@ -142,9 +208,54 @@ export default class S3Handler extends RemoteHandlerAbstract {
}
const params = this._createParams(file)
params.Range = `bytes=${position}-${position + buffer.length - 1}`
const result = await this._s3.getObject(params)
result.Body.copy(buffer)
return { bytesRead: result.Body.length, buffer }
try {
const result = await this._s3.getObject(params)
result.Body.copy(buffer)
return { bytesRead: result.Body.length, buffer }
} catch (e) {
if (e.code === 'NoSuchKey') {
if (await this._isNotEmptyDir(file)) {
const error = new Error(`${file} is a directory`)
error.code = 'EISDIR'
error.path = file
throw error
}
}
throw e
}
}
async _rmdir(path) {
if (await this._isNotEmptyDir(path)) {
const error = new Error(`ENOTEMPTY: directory not empty, rmdir '${path}`)
error.code = 'ENOTEMPTY'
error.path = path
throw error
}
// nothing to do, directories do not exist, they are part of the files' path
}
// reimplement _rmtree to handle efficiantly path with more than 1000 entries in trees
// @todo : use parallel processing for unlink
async _rmtree(path) {
let NextContinuationToken
do {
const result = await this._s3.listObjectsV2({
Bucket: this._bucket,
Prefix: this._dir + path + '/',
ContinuationToken: NextContinuationToken,
})
NextContinuationToken = result.isTruncated ? null : result.NextContinuationToken
for (const { Key } of result.Contents) {
// _unlink will add the prefix, but Key contains everything
// also we don't need to check if we delete a directory, since the list only return files
await this._s3.deleteObject({
Bucket: this._bucket,
Key,
})
}
} while (NextContinuationToken !== null)
}
async _write(file, buffer, position) {

View File

@@ -1 +0,0 @@
module.exports = require('../../@xen-orchestra/babel-config')(require('./package.json'))

View File

@@ -66,6 +66,10 @@ configure([
// if filter is a string, then it is pattern
// (https://github.com/visionmedia/debug#wildcards) which is
// matched against the namespace of the logs
//
// If it's an array, it will be handled as an array of filters
// and the transport will be used if any one of them match the
// current log
filter: process.env.DEBUG,
transport: transportConsole(),

View File

@@ -48,6 +48,10 @@ configure([
// if filter is a string, then it is pattern
// (https://github.com/visionmedia/debug#wildcards) which is
// matched against the namespace of the logs
//
// If it's an array, it will be handled as an array of filters
// and the transport will be used if any one of them match the
// current log
filter: process.env.DEBUG,
transport: transportConsole(),

View File

@@ -1 +1,132 @@
module.exports = require('./dist/configure')
const createConsoleTransport = require('./transports/console')
const { LEVELS, resolve } = require('./levels')
const { compileGlobPattern } = require('./utils')
// ===================================================================
const compileFilter = filter => {
if (filter === undefined) {
return
}
const type = typeof filter
if (type === 'function') {
return filter
}
if (type === 'string') {
const re = compileGlobPattern(filter)
return log => re.test(log.namespace)
}
if (Array.isArray(filter)) {
const filters = filter.map(compileFilter).filter(_ => _ !== undefined)
const { length } = filters
if (length === 0) {
return
}
if (length === 1) {
return filters[0]
}
return log => {
for (let i = 0; i < length; ++i) {
if (filters[i](log)) {
return true
}
}
return false
}
}
throw new TypeError('unsupported `filter`')
}
const createTransport = config => {
if (typeof config === 'function') {
return config
}
if (Array.isArray(config)) {
const transports = config.map(createTransport)
const { length } = transports
return function () {
for (let i = 0; i < length; ++i) {
transports[i].apply(this, arguments)
}
}
}
const level = resolve(config.level)
const filter = compileFilter([config.filter, level === undefined ? undefined : log => log.level >= level])
let transport = createTransport(config.transport)
if (filter !== undefined) {
const orig = transport
transport = function (log) {
if (filter(log)) {
return orig.apply(this, arguments)
}
}
}
return transport
}
const symbol = typeof Symbol !== 'undefined' ? Symbol.for('@xen-orchestra/log') : '@@@xen-orchestra/log'
const { env } = process
global[symbol] = createTransport({
// display warnings or above, and all that are enabled via DEBUG or
// NODE_DEBUG env
filter: [env.DEBUG, env.NODE_DEBUG].filter(Boolean).join(','),
level: resolve(env.LOG_LEVEL, LEVELS.INFO),
transport: createConsoleTransport(),
})
const configure = config => {
global[symbol] = createTransport(config)
}
exports.configure = configure
// -------------------------------------------------------------------
const catchGlobalErrors = logger => {
// patch process
const onUncaughtException = error => {
logger.error('uncaught exception', { error })
}
const onUnhandledRejection = error => {
logger.warn('possibly unhandled rejection', { error })
}
const onWarning = error => {
logger.warn('Node warning', { error })
}
process.on('uncaughtException', onUncaughtException)
process.on('unhandledRejection', onUnhandledRejection)
process.on('warning', onWarning)
// patch EventEmitter
const EventEmitter = require('events')
const { prototype } = EventEmitter
const { emit } = prototype
function patchedEmit(event, error) {
if (event === 'error' && this.listenerCount(event) === 0) {
logger.error('unhandled error event', { error })
return false
}
return emit.apply(this, arguments)
}
prototype.emit = patchedEmit
return () => {
process.removeListener('uncaughtException', onUncaughtException)
process.removeListener('unhandledRejection', onUnhandledRejection)
process.removeListener('warning', onWarning)
if (prototype.emit === patchedEmit) {
prototype.emit = emit
}
}
}
exports.catchGlobalErrors = catchGlobalErrors

View File

@@ -1,5 +1,5 @@
import createTransport from './transports/console'
import LEVELS, { resolve } from './levels'
const createTransport = require('./transports/console')
const { LEVELS, resolve } = require('./levels')
const symbol = typeof Symbol !== 'undefined' ? Symbol.for('@xen-orchestra/log') : '@@@xen-orchestra/log'
if (!(symbol in global)) {
@@ -68,5 +68,7 @@ prototype.wrap = function (message, fn) {
}
}
export const createLogger = namespace => new Logger(namespace)
export { createLogger as default }
const createLogger = namespace => new Logger(namespace)
module.exports = exports = createLogger
exports.createLogger = createLogger

View File

@@ -1,5 +1,5 @@
const LEVELS = Object.create(null)
export { LEVELS as default }
exports.LEVELS = LEVELS
// https://github.com/trentm/node-bunyan#levels
LEVELS.FATAL = 60 // service/app is going down
@@ -8,7 +8,8 @@ LEVELS.WARN = 40 // something went wrong but it's not fatal
LEVELS.INFO = 30 // detail on unusual but normal operation
LEVELS.DEBUG = 20
export const NAMES = Object.create(null)
const NAMES = Object.create(null)
exports.NAMES = NAMES
for (const name in LEVELS) {
NAMES[LEVELS[name]] = name
}
@@ -16,7 +17,7 @@ for (const name in LEVELS) {
// resolves to the number representation of a level
//
// returns `defaultLevel` if invalid
export const resolve = (level, defaultLevel) => {
const resolve = (level, defaultLevel) => {
const type = typeof level
if (type === 'number') {
if (level in NAMES) {
@@ -30,6 +31,7 @@ export const resolve = (level, defaultLevel) => {
}
return defaultLevel
}
exports.resolve = resolve
Object.freeze(LEVELS)
Object.freeze(NAMES)

View File

@@ -1,8 +1,8 @@
/* eslint-env jest */
import { forEach, isInteger } from 'lodash'
const { forEach, isInteger } = require('lodash')
import LEVELS, { NAMES, resolve } from './levels'
const { LEVELS, NAMES, resolve } = require('./levels')
describe('LEVELS', () => {
it('maps level names to their integer values', () => {

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/log",
"version": "0.2.0",
"version": "0.3.0",
"license": "ISC",
"description": "Logging system with decoupled producers/consumer",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/log",
@@ -16,12 +16,6 @@
"url": "https://vates.fr"
},
"preferGlobal": false,
"main": "dist/",
"files": [
"configure.js",
"dist/",
"transports/"
],
"browserslist": [
">2%"
],
@@ -30,24 +24,9 @@
},
"dependencies": {
"lodash": "^4.17.4",
"promise-toolbox": "^0.18.0"
},
"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",
"index-modules": "^0.3.0",
"rimraf": "^3.0.0"
"promise-toolbox": "^0.20.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

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