Compare commits

...

616 Commits

Author SHA1 Message Date
Olivier Floch
8d2dd32e87 feat(xo6/core): update tree-view ui 2024-02-16 17:03:31 +01:00
Olivier Floch
a0a6b73bce feedback 2024-02-16 16:28:39 +01:00
Olivier Floch
64bba27923 feat(xo6/core): add ui tree-view components in web-core 2024-02-16 16:28:39 +01:00
Julien Fontanet
eedaca0195 feat(xo-server/remotes): detect, log and fix incorrect params (#7343) 2024-02-16 16:23:06 +01:00
Julien Fontanet
9ffa52cc01 docs(xoa): manual network config 2024-02-16 11:25:34 +01:00
Julien Fontanet
e9a23755b6 test(fs/path/normalizePath): test relative paths handling
Related to 5712f29a5
2024-02-15 10:10:44 +01:00
Julien Fontanet
5712f29a58 fix(vhd-lib/chainVhd): correctly handle relative paths 2024-02-15 09:14:32 +01:00
Julien Fontanet
509ebf900e fix(fs/path/relativeFromFile): correctly handle relative paths 2024-02-15 09:13:10 +01:00
Julien Fontanet
757a8915d9 feat(xo-server/xapi-stats): handle new format
Starting from XAPI 23.31, stats are in valid JSON but numbers are encoded as strings.
2024-02-14 16:14:43 +01:00
Thierry Goettelmann
35c660dbf6 feat(xo-stack): add @core alias to import Core from Web and Lite (#7375) 2024-02-14 14:43:23 +01:00
Julien Fontanet
f23fd69e7e fix(xapi/VIF_create): fetch power_state and MTU in parallel 2024-02-14 11:48:07 +01:00
Julien Fontanet
39c10a7197 fix(xapi/VIF_create): explicit error when no allowed devices
Related to #7380
2024-02-14 11:48:07 +01:00
Julien Fontanet
7a1bc16468 fix: respect logger method signature
This is a minor fix that should not have major impacts.

It's not necessary to release impacted packages.
2024-02-13 17:38:03 +01:00
Julien Fontanet
93dd1a63da docs(log): document method signature 2024-02-13 17:35:58 +01:00
Florent Beauchamp
b4e1064914 fix(backups): _isAlreadyTransferred is async
This leads to a retransfer and a EEXIST error while writing the metadata.

It can happen when a mirror transfer to multiple remotes, fails on one remote and is restarted/resumed.
2024-02-13 16:03:45 +01:00
Florent Beauchamp
810cdc1a77 fix(backups): really skip already transferred backups 2024-02-13 16:03:45 +01:00
Julien Fontanet
1023131828 chore: update dev deps 2024-02-12 20:47:05 +01:00
Smultar
e2d83324ac chore: add name and version to root package.json (#7372)
Fixes #7371
2024-02-12 16:59:50 +01:00
Julien Fontanet
7cea445c21 fix(xo-web/remotes): don't merge all properties into url
Related to #7343

Introduced by fb1bf6a1e7
2024-02-12 14:51:04 +01:00
Julien Fontanet
b5d9d9a9e1 fix(xo-server-audit): ignore tag.getAllConfigured
Introduced by 25e270edb4
2024-02-12 10:58:06 +01:00
Julien Fontanet
3a4e9b8f8e chore(xo-web/config): remove unused computeds
Introduced by 01302d7a60
2024-02-12 10:55:58 +01:00
Julien Fontanet
92efd28b33 fix(xo-web/config): sort backups from newest to oldest
Introduced by 01302d7a60
2024-02-12 10:55:26 +01:00
Julien Fontanet
a2c36c0832 feat(xo-server): add robots.txt
Fixes zammad#21489
2024-02-09 11:25:06 +01:00
Florent BEAUCHAMP
2eb49cfdf1 feat: release 5.91.2 (#7367) 2024-02-09 11:10:59 +01:00
Florent BEAUCHAMP
ba9d4d4bb5 feat: technical release (#7365) 2024-02-09 10:09:09 +01:00
b-Nollet
18dea2f2fe fix(xo-server-load-balancer): create simple plan as in config (#7358)
Previously, a density plan was created when simple plan was selected in load-balancer configuration.
2024-02-08 18:14:08 +01:00
Julien Fontanet
70c51227bf feat(xo-server/rest-api): expose messages
Fixes zammad#21415
2024-02-08 11:25:05 +01:00
Julien Fontanet
e162fd835b feat(xo-server/rest-api): add /groups/:id/users and /users/:id/groups collections
Fixes https://xcp-ng.org/forum/post/70500
2024-02-08 11:23:17 +01:00
Julien Fontanet
bcdcfbf20b feat(xo-server/rest-api): add groups collection
See https://xcp-ng.org/forum/post/70500
2024-02-08 11:23:17 +01:00
Julien Fontanet
a6e93c895c chore(xo-server/rest-api): unify collections handling 2024-02-08 11:23:17 +01:00
Julien Fontanet
5c4f907358 chore(xo-server/rest-api): move :object handling to the collection 2024-02-08 09:56:04 +01:00
Julien Fontanet
e19dbc06fe chore(xo-server/rest-api): uniformize collections creation 2024-02-08 09:06:53 +01:00
Julien Fontanet
287378f9c6 fix(xo-web): changing items per page should select page 1
Fixes #7350
2024-02-07 16:03:53 +01:00
b-Nollet
83a94eefd6 fix(xo-server-load-balancer): error during optimize (#7362)
Introduced by d949112

Fixes #7359
2024-02-07 15:48:10 +01:00
Julien Fontanet
92fc19e2e3 fix(xo-server/api): never log proxy.getApplianceUpdaterState
Due to xo-web's subscription, there can be a lot of errors in case of XO Proxy failures.
2024-02-07 11:42:21 +01:00
Julien Fontanet
521d31ac84 feat(xo-server/logs-cli): display number of deleted entries 2024-02-07 11:42:21 +01:00
Florent BEAUCHAMP
2b3ccb4b0e fix(xo-server/vm.importFromEsxi): userdevice is a string (#7361)
Introduced by 59cc418973

From zammad#82017 and other tickets
2024-02-07 10:24:50 +01:00
Julien Fontanet
2498a4f47c feat: release 5.91.1 2024-02-06 17:18:47 +01:00
Julien Fontanet
dd61feeaf3 feat(xo-server): 5.135.1 2024-02-06 16:41:06 +01:00
Julien Fontanet
7851f8c196 feat(@xen-orchestra/xva): 1.0.2 2024-02-06 16:41:05 +01:00
Julien Fontanet
404a764821 feat(@xen-orchestra/immutable-backups): 1.0.1 2024-02-06 16:40:52 +01:00
Florent Beauchamp
59cc418973 fix(xva): disk order import 2024-02-06 15:00:08 +01:00
Florent Beauchamp
bc00551cb3 fix(xva): off by one
if last block is not already written we must write it, not the next bloxk
2024-02-06 15:00:08 +01:00
Florent Beauchamp
4d24248ea2 fix(xva): missing await 2024-02-06 15:00:08 +01:00
Florent BEAUCHAMP
5c731fd56e fix(xva): align block size 2024-02-06 15:00:08 +01:00
Florent BEAUCHAMP
79abb97b1f fix(xva): last block path
the last block may be added by a different code path where the block counter wasn't padded, leading to trying to allocate a few Ettabytes
2024-02-06 15:00:08 +01:00
Florent BEAUCHAMP
3314ba6e08 fix(xva): write block from time to time to handle timeout
when the source VM is very sparse, a long time can pass between write to xapi, triggering a timeout
2024-02-06 15:00:08 +01:00
Florent Beauchamp
0fe8f8cac3 fix(xo-server/migrate-vms): use thin mode for vhdraw
when importing a snapshot, the parent (raw) does not need to go through a full read
to generate a thin BAT
2024-02-06 15:00:08 +01:00
Julien Fontanet
aaffe8c872 fix(xo-server/rest-api): fix incorrect object's tasks href
Introduced by f3c5e817a

`req.baseUrl` is already handled by `sendObjects`.
2024-02-05 13:38:14 +01:00
Julien Fontanet
cf0e820632 fix(xo-server/rest-api): fix empty object's tasks list
Introduced by 1da05e239

Since task data were moved into properties, the relation between an object and
its tasks was incorrectly checked which led to an abnormal empty tasks list.
2024-02-05 13:38:14 +01:00
Julien Fontanet
82fdfbd18b fix(xo-server/_createProxyVm): {,_}getOrWaitObject
Introduced by 48c3a65cc
2024-02-05 13:38:14 +01:00
OlivierFL
116f2ec47a fix(ISSUE_TEMPLATE/bug_report): verbatim error field (#7357) 2024-02-05 12:42:16 +01:00
Julien Fontanet
5d4723ec06 fix(xo-server/vm.import): fix fetching UUID from ref
Introduced by 70b09839c
Reported by @ydirson

This led to `UUID_INVALID(VM, OpaqueRef:...)` when importing from URL.
2024-02-02 16:47:55 +01:00
Julien Fontanet
092475d57f chore: update to tap@18 2024-02-02 12:52:11 +01:00
Julien Fontanet
e7a97fd175 test(audit-core,predicates,otp,xo-server): move from tap to test
Support of Mocha syntax is much easier than in latest versions of tap
2024-02-02 12:52:11 +01:00
Julien Fontanet
2262ce8814 chore(async-each): remove unused dev dep tap 2024-02-02 12:52:11 +01:00
Julien Fontanet
7ec64476a4 chore: remove unused script ci 2024-02-02 12:52:02 +01:00
Julien Fontanet
bbb43808dd fix(ci): run unit tests 2024-02-02 12:52:02 +01:00
Florent Beauchamp
02d7f0f20b fix(immutable-backups): handle index files too big 2024-02-02 11:28:28 +01:00
Florent Beauchamp
e226db708a fix(immutable-backups): since index is not immutable anymore we should consider an EPERM Error as relevantt 2024-02-02 11:28:28 +01:00
Florent Beauchamp
b7af74e27e fix(immutable-backups): fix variable name errors 2024-02-02 11:28:28 +01:00
Florent Beauchamp
70f2e50274 fix(immutable-backups): index files aren't immutable anymore 2024-02-02 11:28:28 +01:00
Florent Beauchamp
619b3f3c78 test(immutable-backups): add missing tests 2024-02-02 11:28:28 +01:00
Florent Beauchamp
ac182374d6 fix(immutable-backups): tests
introduced by ed7046c1ab
these tests need to be run by root in a linux system => move them to integ.mjs
2024-02-02 11:28:28 +01:00
Florent BEAUCHAMP
8461d68d40 fix(xva): use a buildless dependency for hashes (#7348)
Native dependencies can have some problems on closed systems

Introduced by 2d047c4fef
2024-02-01 16:30:57 +01:00
Julien Fontanet
838f9a42d1 feat: release 5.91.0 2024-01-31 17:59:34 +01:00
Julien Fontanet
a9dbc01b84 feat(xo-web): 5.136.0 2024-01-31 17:34:53 +01:00
Julien Fontanet
5daf269ab3 feat(xo-server): 5.135.0 2024-01-31 17:34:41 +01:00
Julien Fontanet
383677f4dd feat(xo-cli): 0.26.0 2024-01-31 17:33:13 +01:00
Julien Fontanet
28ad2af9b0 feat(@xen-orchestra/proxy): 0.26.45 2024-01-31 17:25:02 +01:00
Julien Fontanet
53b25508af feat(@xen-orchestra/immutable-backups): 1.0.0 2024-01-31 17:22:51 +01:00
Julien Fontanet
54836ec576 fix(immutable-backups): package should be public
Introduced by ed7046c1a
2024-01-31 17:22:04 +01:00
Julien Fontanet
c9897166e0 feat(@xen-orchestra/backups): 0.44.6 2024-01-31 17:18:52 +01:00
Julien Fontanet
1b35fddf3d feat(@xen-orchestra/fs): 4.1.4 2024-01-31 17:16:15 +01:00
MlssFrncJrg
fc2ca1f4d4 fix(xo-web/pool/advanced): added delete button in backup/migration network (#7349)
Introduced by 0f1f459
2024-01-31 17:00:45 +01:00
Florent BEAUCHAMP
ed7046c1ab feat: immutable backups (#6928) 2024-01-31 16:57:28 +01:00
Julien Fontanet
215579ff4d fix(fs/Local#readFile): fix flags handling 2024-01-31 16:50:29 +01:00
Julien Fontanet
4c79a78a05 fix(fs): remove unnecessary/incorrect readFile param 2024-01-31 16:47:33 +01:00
Julien Fontanet
7864c05ee1 fix: race condition between normalize-packages and usage-to-readme
The first was writting `package.json` at the same time the second one was reading it.

The file is now read only once by `normalize-packages`.
2024-01-31 16:22:07 +01:00
Julien Fontanet
1026d18e4b fix(usage-to-readme): remove another debug log
Introduced by a62433081
2024-01-31 16:10:42 +01:00
Julien Fontanet
d62acd3fe7 fix: format with Prettier 2024-01-31 15:54:43 +01:00
Julien Fontanet
cc1b4bc06c feat(xo-server/rest-api): add pool action create_vm
Fixes #6749
2024-01-31 15:07:01 +01:00
Julien Fontanet
d45f843308 feat(xo-cli/rest): dot syntax for deep properties
`foo.bar=baz` can be used instead of `foo=json:'{ "bar": "baz" }'`.
2024-01-31 15:04:34 +01:00
b-Nollet
3c7fa05c43 feat: technical release (#7347) 2024-01-31 13:56:02 +01:00
MlssFrncJrg
05df055552 feat(xo-web/xostor): expose ignoreFileSystems at creation (#7338) 2024-01-31 13:07:24 +01:00
Pierre Donias
755b206901 chore(web): use relative asset URLs (#7337) 2024-01-31 11:51:40 +01:00
Julien Fontanet
3360399b2a fix(usage-to-readme): remove debug logs
Introduced by a62433081
2024-01-31 10:15:37 +01:00
Florent BEAUCHAMP
091bc04ace fix(xo-server): import of @xen-orchestra/xva package (#7346)
Fix https://github.com/vatesfr/xen-orchestra/issues/7344
Introduced by 2d047c4fef

Cause: we did not run the normalize-package command after renaming
2024-01-31 09:58:34 +01:00
Florent Beauchamp
3ab2a8354b feat(xo-web/import): all the import are thin, remove toggle 2024-01-30 18:54:46 +01:00
Florent Beauchamp
2d047c4fef feat(vmware/import): use xva to load base disk 2024-01-30 18:54:46 +01:00
Florent Beauchamp
0f1dcda7db feat(vmware-explorer): don't read from parent when delta block is already complete 2024-01-30 18:54:46 +01:00
Florent Beauchamp
44760668a3 feat(vmware-explorer): batch reading for sesparse delta 2024-01-30 18:54:46 +01:00
Julien Fontanet
a4023dbc54 feat(ci): ensure yarn.lock is up-to-date 2024-01-30 18:33:25 +01:00
Julien Fontanet
3cad7e2467 fix(yarn.lock): refresh
Introduced by 2e6f7d35e
2024-01-30 17:49:46 +01:00
Julien Fontanet
ad5f37436a fix(CHANGELOG.unreleased): add xo-server@minor
Introduced by 06d411543
2024-01-30 17:35:46 +01:00
MlssFrncJrg
8c05eab720 feat(xo-web/pool/patches): disable rolling pool update button (#7294)
Fixes https://github.com/vatesfr/xen-orchestra/issues/6415
2024-01-30 17:30:36 +01:00
Florent BEAUCHAMP
4db605f14a fix(backups/formatVmBackup): handle snapshotless backups (#7342)
Introduced by b48d955c44
2024-01-30 17:25:59 +01:00
MlssFrncJrg
06d411543a feat(xo-web/SR): add possibility to create SMB shared SR (#7330)
Fixes #991
2024-01-30 17:24:31 +01:00
MlssFrncJrg
6084db22a9 fix(xo-web/import/disk): can't change file name when importing from URL (#7332)
Fixes [#7326](https://github.com/vatesfr/xen-orchestra/issues/7326)
2024-01-30 15:04:27 +01:00
Thierry Goettelmann
2e6f7d35ef feat(xo6): setup file based router (#7318) 2024-01-30 11:31:58 +01:00
MlssFrncJrg
0f1f45953c fix(xo-web/pool/advanced): show backup/migration network even when deleted (#7303) 2024-01-30 11:28:43 +01:00
b-Nollet
89a4de5b21 feat: technical release (#7341) 2024-01-30 09:51:24 +01:00
Julien Fontanet
32dd16114e fix(xo-web/backup): smart mode preview should ignore xo:no-bak tags (#7331)
Introduced by 87a9fbe23

Fixes https://xcp-ng.org/forum/post/69797
2024-01-29 15:56:43 +01:00
Julien Fontanet
f5a3cc0cdb fix(xo-server/rest-api): handle empty fields param
Introduced by c18373bb0
2024-01-29 11:57:47 +01:00
Julien Fontanet
5cabf9916a feat(xo-server/rest-api): expose actions' params schema 2024-01-29 11:08:38 +01:00
OlivierFL
8e65ef7dbc feat(xo-web/logs): transform objects UUIDs into clickable links (#7300)
In Settings/Logs modals : transform objects UUIDs into clickable links, leading
to the corresponding object page.
For objects that are not found, UUID can be copied to clipboard.
2024-01-26 17:28:30 +01:00
Mathieu
0c0251082d feat(xo-web/pool): ability to do a rolling pool reboot (#7243)
Fixes #6885
See #7242
2024-01-26 17:08:52 +01:00
Pierre Donias
c250cd9b89 feat(xo-web/VM): ability to add custom notes (#7322)
Fixes #5792
2024-01-26 14:59:32 +01:00
Florent BEAUCHAMP
d6abdb246b feat(xo-server): implement rolling pool reboot (#7242) 2024-01-25 17:50:34 +01:00
Pierre Donias
5769da3ebc feat(xo-web/tags): add tooltips on xo:no-bak and xo:notify-on-snapshot tags (#7335) 2024-01-25 10:27:42 +01:00
Julien Fontanet
4f383635ef feat(xo-server/rest-api): validate params 2024-01-24 11:41:22 +01:00
Julien Fontanet
8a7abc2e54 feat(xo-cli/rest): display error response body 2024-01-24 11:23:43 +01:00
Julien Fontanet
af1650bd14 chore: update dev deps 2024-01-24 10:54:02 +01:00
Julien Fontanet
c6fdef33c4 feat(xo-server): web signin with auth token in query string (#7314)
Potential issue: the token stays in the browser history.
2024-01-24 10:02:38 +01:00
Florent BEAUCHAMP
5f73f09f59 feat(fuse-vhd): implement cli (#7310) 2024-01-23 17:14:18 +01:00
Thierry Goettelmann
1b0fc62e2e feat(xo6): introducing @xen-orchestra/web (#7309)
Introduces `@xen-orchestra/web`, which will be the home for the new incoming
Xen Orchestra v6.

It uses `@xen-orchestra/web-core` as a foundation.

Upgraded common dependencies of Lite/Web/Core.
2024-01-23 11:25:18 +01:00
Julien Fontanet
aa2dc9206d chore: format with Prettier
Introduced by 85ec26194
2024-01-23 11:02:28 +01:00
Mathieu
2b1562da81 fix(xo-server/PIF): IPv4 reconfiguration only worked when mode was updated (#7324) 2024-01-23 10:55:37 +01:00
Mathieu
25e270edb4 feat(xo-web,xo-server/tag): add colored tag (#7262) 2024-01-23 09:46:06 +01:00
MlssFrncJrg
51c11c15a8 feat(xo-web/pool): disable Rolling Pool Update if pool has 1 host (#7286)
See #6415
2024-01-22 11:04:38 +01:00
Julien Fontanet
47922dee56 fix(xo-server/patching): fix typo
Introduced by 901f7b3fe

Fixes #7325
2024-01-20 09:35:48 +01:00
Julien Fontanet
3dda4dbaad chore: format with Prettier 2024-01-19 16:42:10 +01:00
Julien Fontanet
901f7b3fe2 chore(xo-server): remove object properties decorator syntax
This syntax is not supported by Prettier.
2024-01-19 16:42:10 +01:00
Julien Fontanet
774d66512e feat(decorate-with): decorateObject() 2024-01-19 16:42:10 +01:00
b-Nollet
ec1669a32e feat(xo-server-load-balancer): limit concurrent migrations (#7297)
Fixes #7084
2024-01-19 16:37:55 +01:00
Florent BEAUCHAMP
bbcd4184b0 feat(xo-web,backups): fix dynamic disks count with suspend_VDI (#7315)
`suspend_VDI` was counted as a dynamic VHD.

This commit ignores it in the computation, but add a tag if the backup is with memory.
2024-01-19 16:34:06 +01:00
Mathieu
85ec26194b feat(xo-web/host): ask for confirmation to reboot the updated slave host if the master is not (#7293)
Fixes #7059

In case a slave host requires a reboot to apply updates and the master is using
the same version as the slave host, a confirmation modal is triggered.
2024-01-19 11:18:40 +01:00
MlssFrncJrg
f0242380ca feat(xo-web/tab-advanced): allow to update VM creator (#7276)
Related to [forum#7313](https://xcp-ng.org/forum/topic/7313/change-created-by-and-date-information)
2024-01-19 10:40:08 +01:00
Julien Fontanet
a624330818 fix(usage-to-readme): fix multiple .USAGE.md changes 2024-01-19 10:23:52 +01:00
Julien Fontanet
3892efcca2 feat(xo-web/plugins): auto-load follow load/unload (#7317)
Loading, or unloading, will respectively enable, or disable, _Auto-load at server start_,
this should lead to least surprising behaviors.
2024-01-19 09:55:36 +01:00
Mathieu
c1c122d92c feat(xo-web/pool/host): add warning if hosts don't have the same version (#7280)
Fixes #7059
2024-01-18 17:13:57 +01:00
Julien Fontanet
b7a66e9f73 chore(web-core): add missing npmignore
Follow-up of d92d2efc7
2024-01-18 13:29:30 +01:00
Julien Fontanet
d92d2efc78 chore(web-core): normalize package.json 2024-01-18 12:39:39 +01:00
Julien Fontanet
c2cb51a470 feat(lint-staged): add .USAGE.md → README.md
So that it's not needed to manually runs from `normalize-packages.js`.
2024-01-18 12:34:23 +01:00
Thierry Goettelmann
5242affdc1 feat(lite): introducing @xen-orchestra/web-core (#7302)
This PR introduces `@xen-orchestra/web-core`, which will be the common base for
XO Lite and XO 6.

This package is not meant to be distributed and will be used as-is in other
packages thanks to Yarn Workspace. This mean that the files of XO Web Core will
not be built by themselves but by either the package which use it.

Styles have been moved from XO Lite to XO Web Core.

Colors variable have been renamed and updated according to the new Design
System. XO Lite has been updated accordingly.
- `extra-blue` → `purple`
- `green-infra` → `green`
- `orange-world` → `orange`
- `red-vates` → `red`
- `blue-scale` → `grey`

⚠️ A new intermediate shade has been introduced (`--color-grey-400`). So
`--color-blue-scale-400` is now `--color-grey-500` and `--color-blue-scale-500`
is now `--color-grey-600`.

PostCSS color function plugin is used to generate the shades of color like it is
done on the Figma mockup (with blending the base color with black or white at
different degrees).

PostCSS custom media are now loaded globally thanks to a plugin and no longer
require to import `_responsive.pcss` file manually in each file where a custom
media was needed.
2024-01-18 10:04:25 +01:00
Dom Del Nano
71f3be288b feat(xo-server): add xenStoreData to XO VM objects (#7316)
The initial support added in #7055 to support terraform resource support doesn't provide read access to a VM's state.

Since Tterraform's model requires read and write access to the resource it's managing, this PR implements the missing piece for terra-farm/terraform-provider-xenorchestra#261.
2024-01-17 10:24:08 +01:00
OlivierFL
58769815b0 fix(xo-web/modal): close modal when navigating to another url (#7301) 2024-01-16 20:47:21 +01:00
Florent BEAUCHAMP
c81c23c0d0 fix(fuse-vhd): potential race condition in mount/unmount (#7312)
The code was not properly waiting mount/unmount to be done.
2024-01-16 18:27:30 +01:00
Julien Fontanet
f06f89b5b4 feat(self-signed): readCert utility (#7282)
Expired certificates are not automatically detected, which is not a big deal for user certificates because they can still be used and it's their responsibility to update them.

But automatic certificates must be regenerated in that case which was not the case until now.

This commit unifies certificate/key reading, checking and generation for both xo-server and xo-proxy.
2024-01-16 16:58:15 +01:00
Julien Fontanet
fa748ed9de feat(xo-server): load plugins from mono-repo
Contrary to 3e3ce543a8, which was reverted,
this implentation properly handle duplicates.
2024-01-16 15:54:12 +01:00
Julien Fontanet
cd753acff7 feat(xo-server): move plugin lookup paths to config 2024-01-16 15:51:45 +01:00
Julien Fontanet
8ff861e2be feat(xo-server): find plugins sequentially
This provides a deterministic order.

In case of duplicate plugins (with the same name), the first found plugin takes precedence.
2024-01-16 15:31:03 +01:00
Julien Fontanet
95ccb2e0ae feat(gitignore): ignore .tap/ 2024-01-16 14:02:33 +01:00
John P. Cooper
b0e5846ad1 feat(lite): introduce PWA manifest (#7291)
This implements the initial PWA manifest for XO Lite. I requested this several
months to a year ago, so decided to do it myself in the end.
2024-01-16 11:19:10 +01:00
OlivierFL
19fd456ccf fix(xo-web/metadata-restore-modal): add check to hide pool select when restoring xo config backup (#7287)
See https://xcp-ng.org/forum/topic/8130/xo-configbackup-restore
2024-01-16 11:13:01 +01:00
Julien Fontanet
7946a7db68 feat(xo-server/signin): uniformize sign in buttons
Make sign in with password button the same as external providers.
2024-01-16 10:36:04 +01:00
Julien Fontanet
6127e30574 feat(xo-server/signin): remember me with external providers
It works the same as password signin.
2024-01-16 10:36:04 +01:00
Julien Fontanet
4aad9d8e32 fix(xo-server-backup-reports): require Node>=15
Introduced by 2af74008b

Due to using AggregateError.
2024-01-16 10:10:42 +01:00
Julien Fontanet
78d15ddf96 chore: update dev deps 2024-01-16 09:59:16 +01:00
Pierre Donias
302f7fb85e fix(xo-web/new-vm): isDiskTemplate → _isDiskTemplate (#7311) 2024-01-15 16:51:55 +01:00
Thierry Goettelmann
ea19b0851f feat(lite): upgrade deps + root eslint config (#7292) 2024-01-15 11:12:53 +01:00
Julien Fontanet
b0c37df8d7 fix(xo-server/rest-api): /backup/log/<id>
Introduced by 037e1c1df

Fixes https://xcp-ng.org/forum/post/69426
2024-01-11 11:05:00 +01:00
Julien Fontanet
beba6f7e8d chore: format with Prettier 2024-01-11 09:57:28 +01:00
Julien Fontanet
9388b5500c chore(xo-server/signin): remove empty div 2024-01-10 17:17:42 +01:00
Julien Fontanet
bae8ad25e9 feat(xo-web/tasks): hide /rrd_updates by default
After an internal discussion with @Darkbeldin and @olivierlambert.
2024-01-10 16:50:03 +01:00
Julien Fontanet
c96b29fe96 docs(troubleshooting): explicit sudo with xo-server-recover-account 2024-01-10 16:48:34 +01:00
Julien Fontanet
9888013aff feat(xo-server/rest-api): add pool action emergency_shutdown
Fixes #7277
2024-01-10 15:55:14 +01:00
Julien Fontanet
0bbb0c289d feat(xapi/pool_emergencyShutdown): new method
Related to #7277
2024-01-10 15:55:14 +01:00
Julien Fontanet
80097ea777 fix(backups/RestoreMetadataBackup): fix data path resolution
Introduced by ad46bde30

Fixes https://xcp-ng.org/forum/post/68999
2024-01-10 15:39:41 +01:00
Julien Fontanet
be452a5d63 fix(xo-web/jobs/new): reset params on method change
Fixes https://xcp-ng.org/forum/post/69299
2024-01-10 14:05:02 +01:00
Julien Fontanet
bcc0452646 feat(CODE_OF_CONDUCT): update to Contributor Covenant 2.1 2024-01-09 16:36:29 +01:00
Julien Fontanet
9d9691c5a3 fix(xen-api/setFieldEntry): avoid unnecessary MAP_DUPLICATE_KEY error
Fixes https://xcp-ng.org/forum/post/68761
2024-01-09 15:10:37 +01:00
Julien Fontanet
e56edc70d5 feat(xo-cli): 0.24.0 2024-01-09 14:29:24 +01:00
Julien Fontanet
d7f4d0f5e0 feat(xo-cli rest get): support NDJSON responses
Fixes https://xcp-ng.org/forum/post/69326
2024-01-09 14:24:48 +01:00
Julien Fontanet
8c24dd1732 fix(xapi/host_smartReboot): disable the host before fetching resident VMs
Otherwise it might leads to race condition where new VMs appear on the
host but are ignored by this method.
2024-01-08 17:11:21 +01:00
Julien Fontanet
575a423edf fix(xapi/host_smartReboot): resume VMs even if host was originally disabled
The host will always be enabled after this method anyway.
2024-01-08 17:09:53 +01:00
Julien Fontanet
e311860bb5 fix(xapi/host/waitAgentRestart): wait for enabled status 2024-01-08 17:05:30 +01:00
Julien Fontanet
e6289ebc16 docs(rest-api): update TOC 2024-01-08 16:15:32 +01:00
Julien Fontanet
013e20aa0f docs(rest-api): task monitoring 2024-01-08 16:14:20 +01:00
Julien Fontanet
45a0a83fa4 chore(CHANGELOG.unreleased): sort packages 2024-01-08 14:46:17 +01:00
Guillaume de Lafond
ae518399fa docs(configuration): useForwardedHeaders (#7289) 2024-01-08 11:35:24 +01:00
Ronan Abhamon
d949112921 fix(load-balancer): bad comparison to evaluate migration in perf plan (#7288)
Memory is compared to CPU usage to migrate VM in performance plan context.

This condition can cause unwanted migrations.
2024-01-08 11:25:40 +01:00
Manon Mercier
bb19afc45c Update backups.md (#7283)
This change follows a discussion with Marc Pezin and Yannick on Mattermost.

As Yannick pointed out, the doc refers to a remote while there is no such option in XO GUI.
2024-01-06 15:25:51 +01:00
Julien Fontanet
7780cb176a fix(backups/_MixinXapiWriter#healthCheck): add_tag → add_tags
Fixes https://xcp-ng.org/forum/post/69156

Introduced by a5acc7d26
2024-01-06 15:16:51 +01:00
Julien Fontanet
74ff64dfb4 fix(xo-server/collection/redis#_extract): properly ignore missing entries
Introduced by d8280087a

Fixes #7281
2024-01-05 13:53:46 +01:00
Julien Fontanet
9be3c40ead feat(xo-server/collection/redis#_get): return undefined if missing
Related to #7281
2024-01-05 13:52:45 +01:00
OlivierFL
0f00c7e393 fix(lite): typings errors when running yarn type-check (#7278) 2024-01-04 11:33:30 +01:00
OlivierFL
95492f6f89 fix(xo-web/menu): don't subscribe to proxies if not admin (#7249) 2024-01-04 11:05:53 +01:00
Olivier Floch
046fa7282b feat(xo-web): open github issue url with query params when clicking on bug report button 2024-01-04 11:04:27 +01:00
Olivier Floch
6cd99c39f4 feat(github): add github issue form template 2024-01-04 11:04:27 +01:00
Julien Fontanet
48c3a65cc6 fix(xo-server): VM_import() returns ref, not record
Introduced by 70b0983
2024-01-04 09:36:42 +01:00
OlivierFL
8b0b2d7c31 fix(xo-web/menu): don't subscribe to unhealthy vdi chains if not admin (#7265) 2024-01-03 18:11:34 +01:00
Julien Fontanet
d8280087a4 fix(xo-server/collection/redis#_extract): don't ignore empty records 2024-01-03 17:11:50 +01:00
Julien Fontanet
c14261a0bc fix(xo-cli): close connection on sign in error
Otherwise the CLI does not stop.
2024-01-03 17:06:29 +01:00
Julien Fontanet
3d6defca37 fix(xo-server/emergencyShutdownhost): disable host first 2024-01-03 16:26:07 +01:00
Julien Fontanet
d062a5175a chore(xo-server/emergencyShutdownhost): unnecessary var 2024-01-03 16:25:12 +01:00
Julien Fontanet
f218874c4b fix(xo-server/_createProxyVm): {this → _app}.getObject
Fixes zammad#20646

Introduced by 70b0983
2024-01-03 10:58:19 +01:00
Julien Fontanet
b1e879ca2f feat: release 5.90.0 2023-12-29 11:03:07 +01:00
Julien Fontanet
c5010c2caa feat(xo-web): 5.133.0 2023-12-29 10:48:02 +01:00
Julien Fontanet
2c40b99d8b feat(xo-web): scoped tags (#7270)
Based on #7258 developed by @fbeauchamp.

- use inline blocks to respect all paddings/margins
- main settings are in easily modifiable variables
- text color is either black or white based on the background color luminance
- make sure tags and surrounding action buttons are aligned
- always display value in black on white
- delete button use tag color if dark, otherwise black
- Tag component accept color param
2023-12-28 23:10:35 +01:00
Mathieu
0d127f2b92 fix(lite): fix changelog entry (#7269) 2023-12-28 15:56:21 +01:00
Mathieu
0464886e80 feat(lite): 0.1.7 (#7268) 2023-12-28 15:49:09 +01:00
Mathieu
d655a3e222 feat: technical release (#7266) 2023-12-27 16:07:51 +01:00
b-Nollet
579f0b91d5 feat(xo-web,xo-server): restart VM to change memory (#7244)
Fixes #7069

Add modal to restart VM to increase memory.
2023-12-26 23:46:43 +01:00
Florent BEAUCHAMP
72b1878254 fix(vhd-lib/createStreamNbd): skip original table offset before overwriting (#7264)
Introduced by fc1357db93
2023-12-26 22:29:24 +01:00
MlssFrncJrg
74dd4c8db7 feat(lite/nav): display VM count in host when menu is minimized (#7185) 2023-12-26 13:30:23 +01:00
mathieuRA
ef4ecce572 feat(xo-server/PIF): add XO tasks for PIF.reconfigureIp 2023-12-26 11:22:35 +01:00
mathieuRA
1becccffbc feat(xo-web/host/network): display and edit the IPv6 PIF field 2023-12-26 11:22:35 +01:00
mathieuRA
b95b1622b1 feat(xo-server/PIF): PIF.reconfigureIp handle IPv6 2023-12-26 11:22:35 +01:00
Manon Mercier
36d6e3779d docs: XenServer → XCP-ng/XenServer (#7255)
I would like to replace every "XenServer" I find in the doc by "XCP-ng/XenServer".

This follows an internal conversation we had with Olivier and Yann.
2023-12-26 11:21:16 +01:00
Pierre Donias
b0e000328d feat(lite): XOA quick deploy (#7245) 2023-12-22 15:58:54 +01:00
Pierre Donias
cc080ec681 feat: technical release (#7259) 2023-12-22 15:05:17 +01:00
Julien Fontanet
0d4cf48410 feat(xo-cli rest): explicit error if not registered
Fixes https://xcp-ng.org/forum/post/68698
2023-12-22 11:33:08 +01:00
OlivierFL
2ec164c560 feat(xo-web/menu): add warning icon in proxies menu (#7237)
See Zammad#19126

Add a warning icon in Proxies menu if unable to check proxy upgrade
2023-12-22 09:53:00 +01:00
b-Nollet
d38dce9302 feat(xo-server/vm.stop): add forceShutdownDelay (#7247) 2023-12-22 09:13:36 +01:00
Florent BEAUCHAMP
4c775f6b86 fix(xo-server): missing data in mirror backup job (#7254)
Mirror Backup jobs weren't storing all the data necessary for sending the report
2023-12-21 17:39:39 +01:00
b-Nollet
dd6c858737 fix(xo-server): replacing wrong param name (#7256)
Introduced by 1da05e239d

When the `Task` class is imported from `@vates/tasks`, `Task.run()` parameter uses `properties` key instead of `data` . This is corrected in this PR.
2023-12-21 17:38:58 +01:00
Julien Fontanet
194db8d0dd fix(xo-server/Redis#{add,update}): return complete record
Introduced by 32afd5c46

Fixes https://xcp-ng.org/forum/post/68664

The returned record should not be serialized and should contain the identifier.
2023-12-21 15:47:11 +01:00
Julien Fontanet
5b00cf3ecd chore(xo-server/PluginMetadata#merge): don't return record 2023-12-21 15:47:11 +01:00
Julien Fontanet
afa957b305 chore(xo-server/updateCloudConfig): don't return record 2023-12-21 15:47:11 +01:00
Julien Fontanet
ea1921625e chore(xo-server/create{Group,User}): simplify code 2023-12-21 15:47:11 +01:00
Julien Fontanet
65a154a1b3 chore(xo-server/updateJob): don't return record 2023-12-21 15:47:11 +01:00
Julien Fontanet
2249c90be9 chore(xo-server/subjects): update accept array 2023-12-21 15:47:11 +01:00
Julien Fontanet
1b55d5e2b4 chore(xo-server/scheduling): use builtin (un)serialize 2023-12-21 15:47:11 +01:00
Julien Fontanet
9f5be8029a chore(xo-server/remotes): update accept array 2023-12-21 15:47:11 +01:00
Julien Fontanet
16458b953f chore(xo-server/jobs): use builtin (un)serialize 2023-12-21 15:47:11 +01:00
Julien Fontanet
d63a567a1c chore(xo-server/CloudConfigs): remove useless method 2023-12-21 15:47:11 +01:00
Florent Beauchamp
d36e10e73f feat(xo-server/rest-api): expose NBD settings in VDI exports 2023-12-21 11:54:08 +01:00
Florent Beauchamp
5d80a58754 fix(vhd-lib/createStreamNbd): fix off by one block length computation 2023-12-21 11:54:08 +01:00
Florent Beauchamp
77b14a316f feat(xo-server/vdi.exportContent): expose NBD settings 2023-12-21 11:54:08 +01:00
Florent BEAUCHAMP
213eb6a56a feat(xo-web/sr): show coalescing status (#7241) 2023-12-21 11:29:32 +01:00
OlivierFL
2c298ef47a fix(lite): infinite loader when no stats on pool dashboard (#7236) 2023-12-21 11:27:06 +01:00
Pierre Donias
b7b7af8cff fix(CHANGELOG): more specific description for mirror backup report fix (#7253)
Introduced by #7235
See https://github.com/vatesfr/xen-orchestra/pull/7235#discussion_r1431612789
2023-12-21 11:21:06 +01:00
Pierre Donias
5cf5d14449 feat: technical release (#7252) 2023-12-20 16:39:27 +01:00
Julien Fontanet
e0bf9ee9d5 fix(proxy): wait the HTTP server to listen 2023-12-20 15:23:48 +01:00
Florent BEAUCHAMP
54808967f6 fix(backups): handle undefined isVhdDifferencing (#7248)
Introduced by e13d55bfa9

Fixes https://xcp-ng.org/forum/post/68600
2023-12-20 15:23:23 +01:00
Pierre Donias
c63d38dc0f chore(lite): release script (#7219) 2023-12-20 15:07:10 +01:00
Mathieu
41ed5625be fix(xo-server/RPU): correctly migrate VMs to their original host (#7238)
See zamma#19772
Introduced by bea771
2023-12-19 19:22:50 +01:00
Mathieu
e66bcf2a5c feat(xapi/VDI_exportContent): create XAPI task during NBD export (#7228)
See zammad#19003
2023-12-19 19:16:44 +01:00
b-Nollet
c40e71ed49 fix(xo-web/backup): fix reportWhen being reset to undefined (#7235)
See Zammad#17506

Preventing `reportWhen` value from becoming undefined (and resetting to the
default value _Failure_) when a mirror backup is edited without modifying this
field.
2023-12-19 17:08:19 +01:00
Florent BEAUCHAMP
439c721472 refactor(backups/cleanVm) use of merge parameter 2023-12-19 15:42:59 +01:00
Florent Beauchamp
99429edf23 test(backups/cleanVm): fix after changes 2023-12-19 15:42:59 +01:00
Florent Beauchamp
cec8237a47 feat(xo-web): show if an incremental backup is a delta or a full 2023-12-19 15:42:59 +01:00
Florent Beauchamp
e13d55bfa9 feat(backups): store if disks of incremental backups are differencing in metadata 2023-12-19 15:42:59 +01:00
Florent Beauchamp
141c141516 refactor(backups): differentialVhds to isVhdDifferencing 2023-12-19 15:42:59 +01:00
Florent Beauchamp
7a47d23191 feat(backups): show warning message if xva are truncated 2023-12-19 15:42:59 +01:00
Florent Beauchamp
7a8bf671fb feat(backups): put back warning message on truncated vhds 2023-12-19 15:42:59 +01:00
Florent Beauchamp
7f83a3e55e fix(backup, vhd-lib): merge now return the resulting size and merged size 2023-12-19 15:42:59 +01:00
Florent Beauchamp
7f8ab07692 fix(backup): transferred backup size for incremental backup 2023-12-19 15:42:59 +01:00
Julien Fontanet
2634008a6a docs(VM backups): fix settings link 2023-12-19 15:41:39 +01:00
Florent Beauchamp
4c652a457f fixup! refactor(nbd-client): remove unused method and default iterator 2023-12-19 15:28:32 +01:00
Florent Beauchamp
89dc40a1c5 fixup! feat(nbd-client,xapi): implement multiple connections NBD 2023-12-19 15:28:32 +01:00
Florent Beauchamp
04a7982801 test(nbd-client): add test for unaligned export size 2023-12-19 15:28:32 +01:00
Florent Beauchamp
df9b59f980 fix(nbd-client): better handling of multiple disconnection 2023-12-19 15:28:32 +01:00
Florent Beauchamp
fe215a53af feat(nbd-client): expose exportSize on multi nbd client 2023-12-19 15:28:32 +01:00
Florent Beauchamp
0559c843c4 fix(nbd-client): put back retry on block read 2023-12-19 15:28:32 +01:00
Florent Beauchamp
79967e0eec feat(nbd-client/multi): allow partial connexion 2023-12-19 15:28:32 +01:00
Florent Beauchamp
847ad63c09 feat(backups,xo-server,xo-web): make NBD concurrency configurable 2023-12-19 15:28:32 +01:00
Florent Beauchamp
fc1357db93 refactor(nbd-client): remove unused method and default iterator 2023-12-19 15:28:32 +01:00
Florent Beauchamp
b644cbe28d feat(nbd-client,xapi): implement multiple connections NBD 2023-12-19 15:28:32 +01:00
Florent Beauchamp
7ddfb2a684 fix(vhd-lib/createStreamNbd): create binary instead of object stream
This reduces memory consumption.
2023-12-19 15:28:32 +01:00
Julien Fontanet
5a0cfd86c7 fix(xo-server/remote#_unserialize): handle boolean enabled
Introduced by 32afd5c46

Fixes #7246
Fixes https://xcp-ng.org/forum/post/68575
2023-12-19 15:11:34 +01:00
Julien Fontanet
70e3ba17af fix(CHANGELOG.unreleased): metadata → mirror
Introduced by 2b973275c
2023-12-19 15:07:01 +01:00
Julien Fontanet
4784bbfb99 fix(xo-server/vm.migrate): dont hide original error 2023-12-18 15:02:51 +01:00
MlssFrncJrg
ceddddd7f2 fix(xo-server/PIF): call PIF.forget to delete PIF (#7221)
Fixes #7193

Calling `pif.destroy` generates PIF_IS_PHYSICAL error when the PIF has been
physically removed
2023-12-18 14:41:58 +01:00
Julien Fontanet
32afd5c463 feat(xo-server): save JSON records in Redis (#7113)
Better support of non-string properties without having to handle them one by one.

To avoid breaking compatibility with older versions, this should not be merged after reading JSON records has been supported for at least a month.
2023-12-18 14:26:51 +01:00
Julien Fontanet
ac391f6a0f docs(incremental_backups): fix image path
Introduced by a0b50b47e
2023-12-18 11:45:23 +01:00
Manon Mercier
a0b50b47ef docs(incremental_backups): explain NBD (#7240) 2023-12-18 11:41:20 +01:00
b-Nollet
e3618416bf fix(xo-server-transport-slack): slack-node → @slack/webhook (#7220)
Fixes #7130

Changing the package used for Slack webhooks, to ensure compatibility with slack-compatible services like Discord or Mattermost.
2023-12-18 10:56:54 +01:00
Julien Fontanet
37fd6d13db chore(xo-server-transport-email): dont use deprecated nodemailer-markdown
BREAKING CHANGE: now requires Node >=18.'

Permits the use of a more recent `marked`.
2023-12-17 11:56:18 +01:00
Julien Fontanet
eb56666f98 chore(xen-api,xo-server): update to proxy-agent@6.3.1 2023-12-16 16:41:10 +01:00
Julien Fontanet
b7daee81c0 feat(xo-server): http.useForwardedHeaders (#7233)
Fixes https://xcp-ng.org/forum/post/67625

This setting can be enabled when XO is behind a reverse proxy to
fetch clients IP addresses from `X-Forwarded-*` headers.
2023-12-16 12:34:58 +01:00
Julien Fontanet
bee0eb9091 chore: update dev deps 2023-12-15 14:09:18 +01:00
Julien Fontanet
59a9a63971 feat(xo-server/store): ensure leveldb only accessible to current user 2023-12-13 11:36:31 +01:00
Julien Fontanet
a2e8b999da feat(xo-server-auth-saml): forceAuthn setting (#7232)
Fixes https://xcp-ng.org/forum/post/67764
2023-12-13 11:25:16 +01:00
OlivierFL
489ad51b4d feat(lite): add new UiStatusPanel component (#7227) 2023-12-12 11:44:22 +01:00
Julien Fontanet
7db2516a38 chore: update dev deps 2023-12-12 10:30:11 +01:00
Julien Fontanet
1141ef524f fix(xapi/host_smartReboot): retries when HOST_STILL_BOOTING (#7231)
Fixes #7194
2023-12-11 16:04:43 +01:00
OlivierFL
f449258ed3 feat(lite): add indeterminate state on FormToggle component (#7230) 2023-12-11 14:48:24 +01:00
Julien Fontanet
bb3b83c690 fix(xo-server/rest-api): proper 404 in case of missing backup job 2023-12-08 15:19:48 +01:00
Julien Fontanet
2b973275c0 feat(xo-server/rest-api): expose metadata & mirror backup jobs 2023-12-08 15:17:51 +01:00
Julien Fontanet
037e1c1dfa feat(xo-server/rest-api): /backups → /backup 2023-12-08 15:14:06 +01:00
Julien Fontanet
f0da94081b feat(gen-deps-list): detect duplicate packages
Prevents a bug where a second entry would override the previous one and possibly
decrease the release type (e.g. `major + patch → patch`).
2023-12-07 17:15:09 +01:00
Julien Fontanet
cd44a6e28c feat(eslint): enable require-atomic-updates rule 2023-12-07 17:05:21 +01:00
Julien Fontanet
70b09839c7 chore(xo-server): use @xen-orchestra/xapi/VM_import when possible 2023-12-07 16:50:45 +01:00
OlivierFL
12140143d2 feat(lite): added tooltip on CPU provisioning warning icon (#7223) 2023-12-07 09:07:15 +01:00
b-Nollet
e68236c9f2 docs(installation): update Debian & Fedora packages (#7207)
Fixes #7095
2023-12-06 15:39:50 +01:00
Julien Fontanet
8a1a0d76f7 chore: update dev deps 2023-12-06 11:09:54 +01:00
Mathieu
4a5bc5dccc feat(lite): override host address with 'master' query param (#7187) 2023-12-04 11:31:35 +01:00
MlssFrncJrg
0ccdfbd6f4 feat(xo-web/SR): improve forget SR modal message (#7155) 2023-12-04 09:33:50 +01:00
Mathieu
75af7668b5 fix(lite/changelog): fix xolite changelog (#7215) 2023-12-01 10:48:22 +01:00
Thierry Goettelmann
0b454fa670 feat(lite/VM): ability to migrate a VM (#7164) 2023-12-01 10:38:55 +01:00
Pierre Donias
2dcb5cb7cd feat(lite): 0.1.6 (#7213) 2023-11-30 16:01:06 +01:00
Thierry Goettelmann
a5aeeceb7f chore(lite): upgrade dependencies (#7170)
1. Since the project is built-only, all deps have been moved to `devDependencies`
2. TypeScript has been upgraded from 4.9 to 5.2
3. `engines.node` requirement was set to `>=8.10`. It has been updated to `>=18` to be aligned with deps requirements
2023-11-30 15:13:36 +01:00
Florent BEAUCHAMP
b2f2c3cbc4 feat: release 5.89.0 (#7212) 2023-11-30 13:57:49 +01:00
Florent BEAUCHAMP
0f7ac004ad feat: technical release (#7211) 2023-11-30 10:42:54 +01:00
Florent Beauchamp
7faa82a9c8 feat(xo-web): add UX for differential backup 2023-11-30 10:12:20 +01:00
Florent Beauchamp
4b3f60b280 feat(backups): implement differential restore
When restoring a backup, try to reuse data from an existing snapshot.
We use this snasphot and apply a reverse differential of the changes
between the backup and the snapshot

Prerequisite : a uninterrupted delta chain from the backup being
restored to a backup that still have its snapshot on the host
2023-11-30 10:12:20 +01:00
Florent Beauchamp
b29d5ba95c feat(vhd-lib): implement a limit in VhdSynthetic.fromVhdChain 2023-11-30 10:12:20 +01:00
Florent Beauchamp
408fc5af84 feat(vhd-lib): implement VhdNegative
it's a virtual Vhd that contains all the changes to be applied
to reset a child to its parent value
2023-11-30 10:12:20 +01:00
Florent BEAUCHAMP
2748aea4e9 feat: technical release (#7210) 2023-11-29 15:39:25 +01:00
Florent Beauchamp
a5acc7d267 fix(backups,xo-server): don't backup VMs created by Health Check 2023-11-29 14:46:05 +01:00
Florent Beauchamp
87a9fbe237 feat(xo-server,xo-web): don't backup VMs with xo:no-bak tag 2023-11-29 14:46:05 +01:00
Julien Fontanet
9d0b7242f0 fix(xapi-explore-sr): use xen-api@2.0.0 2023-11-29 14:42:50 +01:00
Julien Fontanet
20ec44c3b3 fix(xo-server/registerHttpRequestHandler): match on path
Instead of full URL, so that handlers can manage query string.
2023-11-29 14:42:50 +01:00
Julien Fontanet
6f68456bae feat(xo-server/registerHttpRequestHandler): returns teardown function
`unregisterHttpRequestHandler` is no longer useful and has been removed.
2023-11-29 14:42:50 +01:00
Florent BEAUCHAMP
b856c1a6b4 feat(xo-server,xo-web): show link to the SR for the garbage collector (coalesce) task (#7189)
See https://github.com/vatesfr/xen-orchestra/issues/5379#issuecomment-1765170973
2023-11-29 09:07:05 +01:00
Julien Fontanet
61e1f83a9f feat(xo-server/rest-api): possibility to import a VM 2023-11-28 17:54:10 +01:00
Mathieu
5820e19731 feat(xo-web/VM): display task information on VDI import (#7197) 2023-11-28 15:41:10 +01:00
Pierre Donias
cdb51f8fe3 chore(lite/settings): use FormSelect instead of select (#7206) 2023-11-28 14:46:25 +01:00
Florent BEAUCHAMP
57940e0a52 fix(backups): import on non default SR (#7209) 2023-11-28 14:35:08 +01:00
Florent BEAUCHAMP
6cc95efe51 feat: technical release (#7208) 2023-11-28 09:30:32 +01:00
Pierre Donias
b0ff2342ab chore(netbox): remove null-indexed entries from keyed-by collections (#7156) 2023-11-27 16:26:53 +01:00
Mathieu
0f67692be4 feat(xo-server/xostor): add XO tasks (#7201) 2023-11-27 16:11:53 +01:00
Julien Fontanet
865461bfb9 feat(xo-server/api): backupNg.{,un}mountPartition (#7176)
Manual method to mount a backup partition on the XOA.
2023-11-24 09:47:23 +01:00
Julien Fontanet
e108cb0990 feat(xo-server/rest-api): possibility to import in an existing VDI (#7199) 2023-11-23 17:07:40 +01:00
Florent BEAUCHAMP
c4535c6bae fix(fs/s3): enable md5 if object lock status is unknown (#7195)
From https://xcp-ng.org/forum/topic/7939/unable-to-connect-to-backblaze-b2/7?_=1700572613725
Following 796e2ab674 

User report it fixes the issue https://xcp-ng.org/forum/post/67633
2023-11-23 16:43:25 +01:00
Julien Fontanet
ad8eaaa771 feat(xo-cli): support REST PUT method 2023-11-23 16:30:03 +01:00
Julien Fontanet
9419cade3d feat(xo-server/rest-api): tags property can be updated 2023-11-23 16:30:03 +01:00
Julien Fontanet
272e6422bd chore(xapi/VM_import): typo snapshots → snapshot 2023-11-23 16:28:30 +01:00
Julien Fontanet
547908a8f9 chore(xo-server/proxy.checkHealth): call checkProxyHealth 2023-11-23 16:28:29 +01:00
Mathieu
8abfaa0bd5 feat(lite/VM): ability to export a VM (#7190) 2023-11-23 11:00:38 +01:00
MlssFrncJrg
a9fbcf3962 feat(xo-web/new VM): always show ISO selector (#7166)
Fixes #3464
2023-11-22 11:04:30 +01:00
Michael Bennett
887b49ebbf docs(installation): Fedora & CentOS wrong package libvhd-utils (#7200)
Under Packages the installation of package `libvhdi-utils` is incorrect for Fedora/CentOS. This should be replaced by `libvhdi-tools` instead.
2023-11-21 17:46:17 +01:00
Florent BEAUCHAMP
858ecbc217 fix(xapi/VDI_importContent): other_config entries must be strings (#7198)
Introduced byffd523679de80b36b2eacd30cc98de3c588a2b77
2023-11-21 16:55:53 +01:00
Florent BEAUCHAMP
ffd523679d feat(backups): update VDI importing status its name_label 2023-11-21 14:38:49 +01:00
Florent BEAUCHAMP
bd9db437f1 feat(xapi/VDI_importContent): store task UUID and stream length into other_config 2023-11-21 14:38:49 +01:00
Florent BEAUCHAMP
0365bacfbb feat(backups): show more detail on restored VM (#7186) 2023-11-21 12:28:53 +01:00
MlssFrncJrg
f3e0227c55 feat(xo-web/console): add disabled console message (#7161)
Fixes #6319
2023-11-21 10:39:35 +01:00
Florent BEAUCHAMP
4504141cbf refactor(backups/importIncrementalVm): move base detection to callers (#7165) 2023-11-20 14:52:59 +01:00
b-Nollet
ecbbf878d0 chore(xen-api): convert to ESM (#7181) 2023-11-20 14:32:44 +01:00
MlssFrncJrg
c1faaa3107 fix(xo-server/resource-set): fix error when changing VM resource set (#7144) 2023-11-20 14:19:27 +01:00
Julien Fontanet
59f04b4a6b chore: format with Prettier 2023-11-20 12:34:30 +01:00
Julien Fontanet
781b070e74 fix(xen-api/examples/import-vdi): params handling 2023-11-20 11:45:45 +01:00
Julien Fontanet
1911386aba chore: refresh yarn.lock 2023-11-20 09:55:07 +01:00
MlssFrncJrg
5b0339315f docs(Support): remove Partner Program (#7099) 2023-11-20 09:42:21 +01:00
b-Nollet
5fe53dfa99 refactor(xapi-explore-sr): convert to EM (#7191) 2023-11-17 16:55:11 +01:00
b-Nollet
06068cdcc6 refactor(cr-seed-cli): convert to ESM (#7192) 2023-11-17 16:46:36 +01:00
Julien Fontanet
c88cc2b020 chore(xo-server/token.create): allow 60s for expiresIn
It makes more sense for the min accepted value to be 60s than 60,001ms.
2023-11-17 10:57:48 +01:00
Pierre Donias
03de8ad481 docs(netbox): update steps and screenshots with latest version (#7182) 2023-11-16 16:06:01 +01:00
Julien Fontanet
08ba7e7253 chore: refresh yarn.lock 2023-11-16 10:21:17 +01:00
Florent BEAUCHAMP
9ca3f3df26 fix(xo-vmdk-to-vhd): improve compatibilty of ova with disk bigger than 8.2GB (#7183)
following #7047, from https://xcp-ng.org/forum/topic/7946/ova-export-not-functional?_=1700051758755

ova exported from xo with more than 8.2G data per disk can't be imported in virtual box 

tar-stream@3 pack and entry are now streams
2023-11-15 16:23:33 +01:00
Mathieu
511908bb7d feat(lite/pool/VMs): ability to export selected VMs (#7174) 2023-11-15 15:29:25 +01:00
Thierry Goettelmann
4351aad312 feat(lite): new FormByteSize component (#6741) 2023-11-15 15:28:46 +01:00
Florent BEAUCHAMP
af7aa29c91 feat(nbd-client): various fixes (#6964) 2023-11-15 10:04:09 +01:00
Thierry Goettelmann
315d626055 fix(lite/story): code highlight modal path (#7180) 2023-11-15 09:58:15 +01:00
Pierre Donias
7af0899800 feat(netbox): sync XO users as Netbox tenants (#7158)
See Zammad#11356
See Zammad#17364
See Zammad#18409
2023-11-14 15:25:56 +01:00
Florent BEAUCHAMP
46ec2dfd56 fix(vmware-explorer): better handling of VM import without any storage (#7168) 2023-11-14 15:14:13 +01:00
Thierry Goettelmann
b2348474c3 fix(lite/pool): host patches list is broken if changelog property is empty (#7169) 2023-11-14 15:08:56 +01:00
Julien Fontanet
836300755a feat: release 5.88.2 2023-11-13 15:07:43 +01:00
Julien Fontanet
55c8c8a6e9 feat(xo-server): 5.126.0 2023-11-13 11:41:08 +01:00
Julien Fontanet
38e32cd24c chore: update dev deps 2023-11-13 09:48:54 +01:00
Julien Fontanet
5ceacfaf5a fix(xo-server/redis): fix searching with multiple indexes
Introduced by 36b94f745
2023-11-12 22:18:19 +01:00
Thierry Goettelmann
1ee6b106b9 feat(lite/ui): compact layout (#7159) 2023-11-10 16:26:20 +01:00
Julien Fontanet
eaef4f22d2 fix(xo-web/settings/logs): use template when reporting
Related to #7142
2023-11-10 11:33:01 +01:00
Julien Fontanet
96025df12f feat(xo-server): only create a single token per web client (and user)
Related to e07e2d3cc

Similar to 581b42fa9
2023-11-09 17:13:10 +01:00
Mathieu
a8aac295eb fix(lite/login): correctly handle login from slave (#7110) 2023-11-09 15:00:29 +01:00
Mathieu
83141989f0 feat(xolite/modals): add onClose event (#7167) 2023-11-09 10:57:24 +01:00
Julien Fontanet
9dea52281d docs(installation): explicit Redis should be started 2023-11-07 17:10:35 +01:00
Julien Fontanet
2164c72034 fix(xo-server): log redis errors
Avoid unhandled error events.
2023-11-07 16:08:41 +01:00
Julien Fontanet
0d0c38f3b5 fix(backups): create suspend VDI on correct SR
Introduced by a958fe86d
2023-11-07 15:55:14 +01:00
Pierre Donias
e5be21a590 feat(lite): 0.1.5 (#7162) 2023-11-07 15:49:12 +01:00
Julien Fontanet
bc1a8be862 chore: fix formatting 2023-11-07 14:33:16 +01:00
Mathieu
3df4dbaae7 feat: release 5.88.1 (#7163) 2023-11-07 14:30:53 +01:00
Julien Fontanet
8f2cfebda6 feat(xo-server/rest-api): add users collection 2023-11-07 12:51:12 +01:00
Julien Fontanet
0d00c1c45f chore: update dev deps 2023-11-07 12:32:38 +01:00
Mathieu
9886e06d6a feat: technical release (#7160) 2023-11-07 10:18:09 +01:00
Thierry Goettelmann
478dbdfe41 feat(lite): new modal management (#7134) 2023-11-07 09:49:02 +01:00
Florent BEAUCHAMP
2bfdb60dda fix(fs): handle object storage server not implementing Object lock (#7157) 2023-11-06 17:18:00 +01:00
Pierre Donias
cabd04470d feat(xo-server-netbox): use Netbox version instead of Netbox API version (#7138)
Netbox version is more precise (X.Y.Z instead of X.Y)
2023-11-06 17:10:10 +01:00
Florent BEAUCHAMP
f6819b23f9 fix(xo-web/dashboard): empty VDIs shouldn't be flagged as orphan (#7102)
Fixes zammad#15524
2023-11-06 13:57:44 +01:00
Julien Fontanet
c9dbcf1384 fix(proxy/backup.importVmBackup): only dispose resources at the end (#7152)
Fixes #7052

Fixes zammad#17383

When a stream is returned, the handler immediately returned a stream which disposed the resource.

Due to the disposable having a 5 mins debounce delay, the problem was only apparent after 5 mins.
2023-11-06 10:30:34 +01:00
Pierre Donias
457fec0bc8 fix(xo-server-netbox): properly delete all interfaces that don't have a UUID (#7153)
Fixes zammad#18812

Introduced by 3b1bcc67ae

The first step of synchronizing VIFs with Netbox interfaces is to clean up any interface attached to the Netbox VM that isn't found on the XO VM, *based on their UUID*, including the interfaces that don't have a UUID at all (`uuid` is `null`).

But by looping over the keyed-by-UUID collection, we could only ever delete at most *one* null-UUID interface, the other ones being dropped by `keyBy`.

Using the flat-array collection instead makes sure all the interfaces are handled.
2023-11-06 10:28:43 +01:00
Pierre Donias
db99a22244 fix(xo-web/New network): only hide bond-PIFs when creating a bonded network (#7151)
Fixes #7150
See https://xcp-ng.org/forum/topic/7918
Introduced by dbdc5f3e3b
2023-11-03 11:22:10 +01:00
Pierre Donias
89d8adc6c6 feat(netbox): expose raw HTTP body if it cannot be JSON-parsed (#7146) 2023-11-02 14:28:06 +01:00
Pierre Donias
a3ea70c61c fix(xo-server-netbox): fix site property null/undefined cases (#7145)
Introduced by 1d7559ded2
2023-10-31 16:16:38 +01:00
Mathieu
ae0f3b4fe0 feat: release 5.88.0 (#7143) 2023-10-31 14:58:50 +01:00
Mathieu
2552ef37d2 feat: technical release (#7141) 2023-10-31 10:09:35 +01:00
Pierre Donias
9803e8c6cb feat(xo-web/patches): warning about updating pool master first (#7140) 2023-10-31 09:51:17 +01:00
Florent BEAUCHAMP
3410cbc3b9 fix(backups): use VDI virtual_size instead of physical_size
`physical_size` appears to be broken
2023-10-30 15:55:30 +01:00
Florent BEAUCHAMP
93fce0d4bf fix(backups): pass type to Xapi#getRecord
Fixes #7131

Introduced by 37b211376
2023-10-30 15:55:30 +01:00
MlssFrncJrg
dbdc5f3e3b feat(xo-web/New network): don't show PIFs that belong to a bond (#7136) 2023-10-30 15:47:38 +01:00
Julien Fontanet
581b42fa9d feat(xo-cli): only create a single token per instance (and user) 2023-10-30 15:47:17 +01:00
Julien Fontanet
e07e2d3ccd feat(xo-server/token): client info support 2023-10-30 15:47:17 +01:00
Mathieu
ad928ec23d fix(xo-web/licenses/XOSTOR): various fixes on XOSTOR licenses (#7137)
Introduced by #6983
2023-10-30 14:55:11 +01:00
Pierre Donias
1d7559ded2 fix(xo-server-netbox/VM): explicitly assign site (#7124)
See Zammad#17766
See https://xcp-ng.org/forum/topic/7887
2023-10-30 11:32:12 +01:00
Mathieu
9099b58557 feat: technical release (#7132) 2023-10-27 16:13:04 +02:00
Julien Fontanet
9e70397240 fix(xo-server/redis): fix indexes handling
Introduced by 225a67ae3
2023-10-27 11:27:25 +02:00
Thierry Goettelmann
5f69b0e9a0 feat(lite/console): new console toolbar (#7088) 2023-10-27 10:27:51 +02:00
Julien Fontanet
2a9bff1607 chore(xo-server/importConfig): don't use deptree 2023-10-27 10:14:02 +02:00
Pierre Donias
9e621d7de8 feat(lite/header): replace logo with "XO LITE" (#7118) 2023-10-27 09:16:28 +02:00
Mathieu
3e5c73528d feat(xo-server,xo-web/XOSTOR): XOSTOR implementation (#6983)
See https://xcp-ng.org/forum/topic/5361
2023-10-26 16:58:59 +02:00
Pierre Donias
397b5cd56d fix(xo-server/snapshot): allow self user that is member of a group to snapshot (#7129)
Introduced by a88798cc22
See Zammad#18478
2023-10-26 16:08:43 +02:00
Julien Fontanet
55cb6042e8 chore(yarn.lock): update dev deps 2023-10-26 11:00:14 +02:00
Pierre Donias
339d920b78 feat(xo-web/proxy): ability to open support tunnel on XO Proxy (#7127)
Requires #7126
2023-10-25 17:26:06 +02:00
Julien Fontanet
f14f716f3d feat(xo-server/api): proxy.openSupportTunnel (#7126)
The goal is to provide an easier way for the support team to open a tunnel on a proxy appliance.

This is the server side of this feature.
2023-10-25 17:12:17 +02:00
Julien Fontanet
fb83d1fc98 feat(xo-server/api): ignorable parameters (#7125) 2023-10-25 15:49:41 +02:00
Julien Fontanet
62208e7847 fix(xo-server-transport-xmpp): fix loading (#7082)
Fixes https://xcp-ng.org/forum/post/66402

Introduced by d6fc86b6b
2023-10-25 14:36:40 +02:00
Julien Fontanet
df91772f5c chore(xo-server/server): use builtin (un)serialize 2023-10-25 11:48:53 +02:00
Julien Fontanet
cf8a9d40be chore(xo-server/remote): use builtin (un)serialize 2023-10-25 11:48:53 +02:00
Julien Fontanet
93d1c6c3fc chore(xo-server/plugin-metadata): use builtin (un)serialize 2023-10-25 11:48:53 +02:00
Julien Fontanet
f1fa811e5c chore(xo-server/user): use builtin (un)serialize 2023-10-25 11:48:53 +02:00
Julien Fontanet
5a9812c492 chore(xo-server/group): use builtin (un)serialize 2023-10-25 11:48:53 +02:00
Julien Fontanet
b53d613a64 chore(xo-server/token): use builtin unserialize 2023-10-25 11:48:53 +02:00
Julien Fontanet
225a67ae3b chore(xo-server/redis): proper (un)serialization support 2023-10-25 11:48:53 +02:00
Mathieu
c7eb7db463 feat(xo-web/about): display if XO from source is up to date (#7091)
Fixes #5934
2023-10-24 17:14:01 +02:00
Pierre Donias
edfa729672 chore(lite/assets): remove darkreader properties in SVG files (#7121) 2023-10-24 16:40:37 +02:00
Mathieu
77d9798319 fix(xo-web/vtpm): fix various an error has occured (#7122)
Introduced by 8834af65f7
Introduced by 1a1dd0531d

Fix `an error has occurred` in the VM advanced tab and on the VM creation form
if the user does not have pool permission.
2023-10-24 16:26:36 +02:00
Pierre Donias
680f1e2f07 chore(lite): serve Poppins font internally (#7117) 2023-10-24 15:19:54 +02:00
Julien Fontanet
7c009b0fc0 feat(xo-server): support reading JSON records in Redis
This allows forward compatibility with future versions which will use JSON records in the future.
2023-10-23 15:13:28 +02:00
Pierre Donias
eb7de4f2dd feat(xo-web/self): show # of VMs that belong to each Resource Set (#7114)
See Zammad#17568
2023-10-23 15:03:30 +02:00
Olivier Lambert
2378399981 docs: update project's README (#7116) 2023-10-23 14:25:03 +02:00
Florent BEAUCHAMP
37b2113763 feat(fs/s3): compute sensible chunk size for uploads 2023-10-23 10:23:50 +02:00
Florent BEAUCHAMP
5048485a85 feat(fs/s3): object lock mode need content md5
and the middleware consume addiitionnal memory
2023-10-23 10:23:50 +02:00
Florent BEAUCHAMP
9e667533e9 fix(fs/s3): throw error if upload >50GB 2023-10-23 10:23:50 +02:00
MlssFrncJrg
1fac7922b4 feat(xo-web/dashboard/health): VDIs to coalesce warning contains the number (#7111)
Fixes Zammad#17577
2023-10-20 15:53:24 +02:00
Julien Fontanet
1a0e5eb6fc chore: format with Prettier 2023-10-20 15:52:10 +02:00
Pierre Donias
321e322492 feat(xo-server/clearHost): pass optional batch size arg (#7107)
Fixes #7105
See https://github.com/xapi-project/xen-api/issues/5202
See https://github.com/xapi-project/xen-api/pull/5203

`host.evacuate`: try passing optional batch size argument.
If not supported: remove it and try again.
2023-10-19 17:03:14 +02:00
Mathieu
8834af65f7 feat(xo-server/xo-web/VM/new): VTPM creation (#7077)
See #7066
See #6802
See #7085
2023-10-19 16:48:56 +02:00
Mathieu
1a1dd0531d feat(xo-web/VM/advanced): VTPM management (#7085)
See #7066
See #6802
See #7074
2023-10-19 15:46:03 +02:00
Pierre Donias
8752487280 docs(installation): add nfs-common dependency for Debian/Ubuntu (#7108) 2023-10-18 22:50:29 +02:00
Pierre Donias
4b12a6d31d fix(xo-server-usage-report): handle null and nested stats (#7092)
Introduced by 083483645e

Fixes Zammad#18120
Fixes Zammad#18266

- Always assume that data can be `null`
- Handle edge cases where all values are `null`
- Properly handle nested RRD collections: collections have different depths (`memory`: 1, `cpus[0]`: 2, `pifs.rx[0]`: 3, ...). This PR replaces `getLastDays` which wouldn't handle those depths properly, with `getDeepLastValues` which is run on the whole stat object and doesn't assume the depth of the collections. It finds any Array at any depth and slices it to only keep the last N values.
2023-10-18 22:50:08 +02:00
Julien Fontanet
2924f82754 fix(xo-web): don't sign out on connection error (#7103)
May fix zammad#17717

Introduced by 005ab47d9
2023-10-18 18:07:16 +02:00
Pierre Donias
9b236a6191 fix(netbox/test): test custom fields first (#7104)
More atomic and it makes more sense for users to check that the Netbox
configuration is correct before doing any write operations
2023-10-18 11:56:10 +02:00
Julien Fontanet
a3b8553cec fix(xo-server,xo-web): fix total number of VDIs to coalesce (#7098)
Fixes #7016

Summing all chains does take not common chains into account, the total must be computed on the server side.
2023-10-18 11:52:43 +02:00
Pierre Donias
00a1778a6d feat(lite): set color-scheme CSS property to "dark" in dark mode (#7101) 2023-10-17 16:50:13 +02:00
MlssFrncJrg
3b6bc629bc fix(xo-web/home): fix misaligned descriptions (#7090) 2023-10-16 15:53:35 +02:00
Pierre Donias
04dfd9a02c fix(xo-server-usage-report): use @xen-orchestra/log to log errors (#7096)
Fixes Zammad#14579
Fixes Zammad#18183

Better handles error objects with a circular structure and avoids "Converting
circular structure to JSON" error on stringify
2023-10-16 10:07:57 +02:00
Pierre Donias
fb52868074 fix(xo-server/patching): always check that XS credentials are configured on XS (#7093)
Introduced by a30d962b1d
2023-10-13 16:49:04 +02:00
Pierre Donias
77d53d2abf fix(xo-server/patching): always pass xsCredentials to installPatches on XS (#7089)
Fixes Zammad#18284

Introduced by a30d962b1d
2023-10-13 11:45:17 +02:00
Julien Fontanet
6afb87def1 feat(xo-server/vm.set): support xenStoreData
Fixes #7055
2023-10-13 11:26:48 +02:00
Mathieu
8bfe293414 feat(lite/VM): add copy, snapshot single action (#7087) 2023-10-12 11:09:11 +02:00
Mathieu
2e634a9d1c feat(xapi/VTPM): ability to create, destroy VTPM (#7074) 2023-10-12 09:19:38 +02:00
Pierre Donias
bea771ca90 fix(xo-server/RPU): do not migrate VM back if already on host (#7071)
See https://xcp-ng.org/forum/topic/7802
2023-10-11 16:16:44 +02:00
Pierre Donias
99e3622f31 feat(xo-web/SelectPif): show network name (#7081)
See Zammad#17381
2023-10-10 15:59:24 +02:00
Pizzosaure
a16522241e docs(netbox): remove extra backtick (#7083)
Introduced by 3b3f927e4b
2023-10-10 14:14:15 +02:00
Julien Fontanet
b86cb12649 chore(yarn.lock): update dev deps 2023-10-09 17:06:54 +02:00
Julien Fontanet
2af74008b2 feat(xo-server-backup-reports): errors are logged as XO tasks 2023-10-09 09:35:24 +02:00
Julien Fontanet
2e689592f1 feat(xo-server-backup-reports): error when transports not enabled 2023-10-09 09:35:24 +02:00
Julien Fontanet
3f8436b58b fix(xo-server/authenticateUser): use clearLogOnSuccess
This fixes success logs not deleted due to race conditions.
2023-10-09 09:35:24 +02:00
Julien Fontanet
e3dd59d684 feat(mixins/Tasks#create): clearLogOnSuccess option 2023-10-09 09:35:24 +02:00
mathieuRA
549d9b70a9 feat(xo-web/host): allow to force smartReboot 2023-10-06 16:52:26 +02:00
mathieuRA
3bf6aae103 feat(xapi/host_smartReboot): ability to bypass blocked operations 2023-10-06 16:52:26 +02:00
Julien Fontanet
afb110c473 fix(fs/rmtree): fix huge memory usage (#7073)
Fixes zammad#15258

This adds a sane concurrency limit of 2 per depth level.

Co-authored-by: Florent BEAUCHAMP <florent.beauchamp@vates.fr>
2023-10-06 09:52:11 +02:00
Pierre Donias
8727c3cf96 docs(patches): update URLs that need to be accessible from XOA (#7075) 2023-10-05 09:45:50 +02:00
Julien Fontanet
b13302ddeb fix(xen-api/cli): dont run default export when imported by ESM
Fix a bug in `@xen-orchestra/xapi` introduced by c3e0308ad

`module.parent` is `null` when the module is the entry point but `undefined` when imported via ESM.
2023-10-04 10:06:17 +02:00
Julien Fontanet
e89ed06314 docs(installation): Node 18 required
XO is not compatible with Node > 18 for the moment, as Node 20 will
likely graduate to LTS soon, the docs must explicitly recommend 18.
2023-10-04 09:25:37 +02:00
Malcolm Scott
e3f57998f7 fix(signin): try to preserve current page across reauthentication (#7013)
If an authentication session expires or is lost for whatever reason, XO redirects to `/signin`.  This redirect generally preserves the URL fragment (hash) which contains the page selected prior to reauthentication, i.e. if the user had been in settings/servers just beforehand, they end up at `/signin#settings/servers`.  However, currently when they log back in they end up on the home page; the page they were on is forgotten.

This commit tries to send the user back to the page they were viewing before reauthentication, by preserving the URL fragment in the login form action / by appending it to the links to authentication plugins.  (Not all authentication plugins will necessarily preserve it internally, but we can optimistically try it and see; at worst the old behaviour will remain.)
2023-10-03 12:39:57 +02:00
Julien Fontanet
8cdb5ee31b chore: update dev deps 2023-10-03 11:24:51 +02:00
Pierre Donias
5b734db656 feat(lite): 0.1.4 (#7068) 2023-10-03 10:26:05 +02:00
rbarhtaoui
e853f9d04f feat(lite): display loading icon and error message when data is not fetched (#6775) 2023-10-03 10:03:44 +02:00
Mathieu
2a5e09719e feat(lite/login): add remember me checkbox (#7030) 2023-10-03 10:01:07 +02:00
Pierre Donias
3c0477e0da feat: release 5.87.0 (#7064) 2023-09-29 11:35:23 +02:00
Pierre Donias
060d1c5297 feat: technical release (#7063) 2023-09-29 10:01:45 +02:00
Julien Fontanet
55dd7bfb9c feat(backups): don't snapshot migrating VMs
Related to zammad#16108
2023-09-28 17:42:43 +02:00
Julien Fontanet
b00cf13029 feat(backups): block snapshot migration during backup
Related to zammad#16108
2023-09-28 17:42:43 +02:00
Julien Fontanet
73755e4ccf feat(xo-server/authenticateUser): log failed attempts
Related to zammad#16318
2023-09-28 17:38:57 +02:00
Julien Fontanet
a1bd96da6a feat(mixins/Tasks#create): allow any properties 2023-09-28 17:38:57 +02:00
mathieuRA
0e934c1413 feat(xo-web/host/advanced): display system disks health 2023-09-28 17:14:09 +02:00
Florent BEAUCHAMP
eb69234a8e feat(xo-server/host): implement smartctl api call 2023-09-28 17:14:09 +02:00
mathieuRA
7659d9c0be fix(xo-web/host/advanced): catch error for ACLs users on hyper threading plugin
it broke the componentDidMount methode and didn't update the state correctly
2023-09-28 17:14:09 +02:00
Florent BEAUCHAMP
2ba81d55f8 fix(vhd-lib/test): collision during tests (#7062)
multiple tests use the same temporary files
2023-09-28 16:49:00 +02:00
Gabriel Gunullu
2e1abad255 feat(xapi/VDI_importContent): add SR name_label to task name_label (#6979) 2023-09-28 16:10:29 +02:00
Julien Fontanet
c7d5b4b063 fix(xo-web/messages): clarify *forget tokens* description
Introduced by c7df11cc6
2023-09-28 15:41:10 +02:00
Julien Fontanet
cc5f4b0996 fix(xo-web/messages): connection token → authentication token
Uniformize naming.
2023-09-28 15:41:06 +02:00
Julien Fontanet
55f627ed83 chore: fix formatting
Introduced by 869f7ffab
2023-09-28 15:37:45 +02:00
Florent BEAUCHAMP
988179a3f0 fix(xo-server): add mbr for cloud-init only for windows VM (#7050)
Fixes zammad#16808
2023-09-28 09:09:13 +02:00
Julien Fontanet
ce617e0732 fix(xo-server/host.restart): make force defaults to false
Introduced by 5ee11c7b6
2023-09-27 17:39:10 +02:00
Florent BEAUCHAMP
f0f429a473 fix(xo-server-backup-report): send report for Mirror Backup (#7049) 2023-09-27 16:39:27 +02:00
Thierry Goettelmann
bb6e158301 feat(lite): host patches (#6709) 2023-09-27 11:44:03 +02:00
Pierre Donias
7ff304a042 feat: technical release (#7058) 2023-09-27 11:30:16 +02:00
Julien Fontanet
7df1994d7f fix(xo-server/sr.getAllUnhealthyVdiChainsLength): require admin permission
Introduced by 0975863d9
2023-09-27 10:37:30 +02:00
Mathieu
a3a2fda157 feat(lite/pool/VMs): ability to snapshot selected VMs (#7021) 2023-09-26 17:28:15 +02:00
Thierry Goettelmann
d8530f9518 chore(lite): update changelog (#7057)
Fixes [#7040](https://github.com/vatesfr/xen-orchestra/pull/7040)
2023-09-26 17:13:29 +02:00
Thierry Goettelmann
d3062ac35c feat(lite/pool/VMs): ability to migrate selected VMs (#7040) 2023-09-26 17:06:00 +02:00
Thierry Goettelmann
b11f11f4db feat(lite): rework modal system (#6994) 2023-09-26 16:25:23 +02:00
Thierry Goettelmann
79d48f3b56 feat(lite/xapi): update XenApi types and enums (#7018) 2023-09-26 15:19:33 +02:00
Pierre Donias
869f7ffab0 feat(xo-web/XOA/Support): button to restart xo-server service (#7056) 2023-09-26 14:35:17 +02:00
Julien Fontanet
6665d6a8e6 chore: format with Prettier 2023-09-26 14:34:47 +02:00
Pierre Donias
8eb0bdbda7 feat(xo-server,xo-web/SR): reclaim space (#7054)
Fixes #1204
2023-09-26 14:21:43 +02:00
Mathieu
710689db0b feat(xo-web/home/host,pool): display product brand and version (#7027) 2023-09-26 11:16:08 +02:00
mathieuRA
801eea7e75 feat(xo-web/host/advanced): confirmation modal for download system logs 2023-09-26 11:10:22 +02:00
Julien Fontanet
7885e1e6e7 feat(xo-web/host/advanced): button do download system logs
Fixes #3968
2023-09-26 11:10:22 +02:00
Julien Fontanet
d384c746ca feat(xo-server/rest-api): export host audit and system logs
See #3968
2023-09-26 11:10:22 +02:00
Pierre Donias
a30d962b1d feat(xo-server,xo-web/patching): support new XS Updates system (#7044)
See Zammad#13416

Support for new XenServer Updates system with authentication:
- User downloads Client ID JSON file from XenServer account
- User uploads it to XO in their user preferences
- XO uses `username` and `apikey` from that file to authenticate and download updates
2023-09-26 10:29:07 +02:00
Pierre Donias
b6e078716b docs(users/auth): update GitHub plugin screenshots (#7035) 2023-09-25 16:10:00 +02:00
Julien Fontanet
34b69c7ee8 chore: refresh yarn.lock
Introduced by 90e0f2684
2023-09-25 09:10:54 +02:00
Julien Fontanet
70bf8d9620 fix(xo-web/kubernetes): handle empty searches domain field
Do not send `['']` if empty.
2023-09-25 09:08:33 +02:00
Florent BEAUCHAMP
c8bfda9cf5 fix(xo-vmdk-to-vhd): handle ova with disk position collision (#7051)
Some OVA have multiple disks with the same position, which prevent the VM from being created (error while creating VBD). Renumeroting the problematic disk works around the issue.

This may lead to unbootable VM in case the renumeroted disk was the bootable one (VMware-VirtualSAN-Witness-7.0.0-15843807.ova for example).

Fixes #7046
2023-09-22 11:44:12 +02:00
Gabriel Gunullu
1eb4c20844 fix(xo-web/kubernetes): remove required property from search domain (#7028)
Make this field optional for the cluster creation.
2023-09-22 09:46:13 +02:00
Florent BEAUCHAMP
e5c5f19219 fix(backups): mirror must not replicate themselves (#7043)
Fixes zammad#16871
2023-09-21 14:45:29 +02:00
Florent BEAUCHAMP
db92f0e365 fix(vhd-lib): VhdFile implementation is not compatible with encrypted remote (#7045) 2023-09-21 11:18:44 +02:00
Adocentyn
570de7c0fe feat: add licenses (#7042) 2023-09-21 10:28:31 +02:00
Florent BEAUCHAMP
90e0f26845 fix(xo-server): ova export with files bigger than 8.2GB (#7047)
Following 15f69a1

Updating tar-stream to latest version fixes support of files bigger than 8.2GB
2023-09-20 17:17:45 +02:00
Julien Fontanet
c714bc3518 fix(stream-reader): requires Node >=12.3 2023-09-18 09:51:14 +02:00
Julien Fontanet
48e0acda32 chore: update dev deps 2023-09-18 09:43:13 +02:00
Thierry Goettelmann
013cdbcd96 fix(lite/composable): useSubscriber is disabled by default (#7041) 2023-09-15 12:01:20 +02:00
Julien Fontanet
fdd886f213 chore(xo-web/jobs): use set for user ids 2023-09-15 11:05:23 +02:00
Julien Fontanet
de70ef3064 chore(xo-web/jobs): use addSubscriptions for all subs 2023-09-15 11:05:23 +02:00
Julien Fontanet
9142a95f79 feat(xo-web/addSubscriptions): support initial values 2023-09-15 11:05:23 +02:00
Julien Fontanet
1c6aebf997 fix(xo-web/jobs): make schedules a computed
Fixes #6968

The schedules did not appear if the jobs subscription triggered after the schedules one.

The logic has been moved to a computed depending on both subscriptions.
2023-09-15 11:05:23 +02:00
Julien Fontanet
7b9ec4b7a7 chore(xo-web/_getScheduleJob): remove unnecessary sort 2023-09-15 11:05:23 +02:00
Julien Fontanet
decb87f0c9 chore(xo-web/_getScheduleJob): explicit comparison 2023-09-15 11:05:23 +02:00
Julien Fontanet
e17470f56c chore(xo-web/_getScheduleJob): fix comment 2023-09-15 11:05:23 +02:00
Julien Fontanet
99ddbcdc67 fix(xo-web/_getScheduleJob): jobs can be undefined
Related to #6968
2023-09-15 11:05:23 +02:00
Pierre Donias
6953e2fe7b fix(xo-web/backup/mirror): submit button: "Edit" → "Save" (#7036) 2023-09-13 10:06:30 +02:00
Pierre Donias
beb1063ba1 fix(xo-server-auth-github): bad argument passed to registerUser2 (#7032)
Introduced by 562401ebe4
2023-09-12 11:39:55 +02:00
Pierre Donias
7773edd590 fix(xo-server-auth-google): bad argument passed to registerUser2 (#7031)
Introduced by 91b19d9bc4
See https://xcp-ng.org/forum/topic/7729
2023-09-12 11:21:28 +02:00
Julien Fontanet
0104649b84 fix(xo-server/importVmBackupNg): set result when restoring via XO Proxy (#7026) 2023-09-10 18:32:34 +02:00
Pierre Donias
1c9d1049e0 fix(xo-web/render-xo-item/PIF): hide parenthesis if no info inside (#7022)
See Zammad#17381
2023-09-08 10:45:28 +02:00
Pierre Donias
d992a4cb87 feat(netbox): don't delete VMs and interfaces that don't have a UUID (#7008)
See https://xcp-ng.org/forum/topic/7639

In an effort of not deleting or overwriting useful data that has been added
manually by the user, this reverts the feature of deleting VMs and interfaces
that are not bound to an XO object via their custom field UUID. Such objects:
- shouldn't exist in normal use cases anyway
- aren't an issue for the Netbox sync
- are easy to clean manually
2023-09-08 10:36:16 +02:00
Julien Fontanet
52114ad4b0 docs(backup_troubleshooting): unexpected key/full (#7023)
Co-authored-by: Jon Sands <fohdeesha@gmail.com>
2023-09-08 09:56:21 +02:00
Julien Fontanet
bcc62cfcaf feat: release 5.86.1 2023-09-07 16:45:50 +02:00
Julien Fontanet
60434b136a feat(xo-web): 5.124.1 2023-09-06 16:55:52 +02:00
Julien Fontanet
13f3c8851d feat(xo-server): 5.122.0 2023-09-06 16:55:33 +02:00
Julien Fontanet
f386f94dc2 feat(@xen-orchestra/proxy): 0.26.33 2023-09-06 16:52:02 +02:00
Julien Fontanet
fda1fd1a04 feat(xen-api): 1.3.6 2023-09-06 16:51:34 +02:00
Thierry
0b17bdd9bc fix(lite/types): type of ObjectLink component is broken 2023-09-06 09:03:32 +00:00
Thierry
2c5706a89b fix(lite/types): issue with createUseCollection typing in VSCode 2023-09-06 09:03:32 +00:00
rbarhtaoui
5448452b71 fix(xo-web): fix naming conflict for duplicate variables (#7019)
Introduced by c9244b2
2023-09-05 12:28:25 +02:00
Julien Fontanet
22e7c126e6 fix(xen-api): set hostnameRaw before creating transport
Fixes zammad#17423

Introduced by 158a8e14a

Fix XML-RPC transport.
2023-09-05 10:52:11 +02:00
Julien Fontanet
750fefe957 fix(xo-web): don't delete other user's auth tokens
Fixes zammad#17276
2023-09-05 10:39:33 +02:00
Julien Fontanet
025e671989 feat(xo-server/api): split token.delete to token.deleteOwn
So that the behavior is more consistent.
2023-09-05 10:39:33 +02:00
Thierry Goettelmann
df0ed5e794 feat(lite): implement useContext composable (#6991) 2023-09-05 10:05:42 +02:00
Manon Mercier
da45ace7c1 docs(manage_infrastructure): info about Dashboard/Health (#7003)
Related to #5678 

Co-authored-by: Jon Sands <fohdeesha@gmail.com>
2023-09-04 15:48:18 +02:00
Manon Mercier
2a623b8ae7 docs(manage_infrastructure): manage dom0 memory (#6916) 2023-09-04 15:26:55 +02:00
Pierre Donias
f034ec45f3 feat(lite): 0.1.3 (#7011) 2023-09-01 13:42:38 +02:00
Pierre Donias
970bc0ac5d fix(lite/stories): bad import path for POWER_STATE (#7012)
Introduced by 5e8539865f
2023-09-01 11:07:49 +02:00
Mathieu
3abbc8d57e feat: release 5.86.0 (#7010) 2023-08-31 14:53:04 +02:00
Mathieu
06570d78a0 feat: technical release (#7009) 2023-08-31 10:24:11 +02:00
Florent BEAUCHAMP
6a0df7aec2 feat(fs/s3): retry on failures (#6966) 2023-08-31 09:51:28 +02:00
Julien Fontanet
30aeb95f3a fix(xo-server-audit): ignore more side-effects free methods 2023-08-31 09:26:23 +02:00
Julien Fontanet
36d6d53a26 fix(xo-web/jobs/schedules): order jobs by name
Fixes https://xcp-ng.org/forum/post/64825
2023-08-30 22:27:33 +02:00
Thierry Goettelmann
895773b6c6 feat(lite): add alarms to pool dashboard (#6976)
* feat(lite): new iteration for XenApi, stores and subscriptions

* feat(lite/xen-api): enhance XenApi typings and utils

* feat(lite): add subscription dependencies for xen-api records stores

* feat(lite/xen-api): use generics for XenApiEvent

* feat(lite/xen-api): add load error handling

* feat(lite/store): simplify alarm store onRemove

* feat(lite): add alarms to pool dashboard

* feat(lite/alarms): rename type

* feat(lite/object-link): merge useStore and routeName configs + better naming

* feat(lite/object-link): typing enhancement + loader

* feat(lite): feedback on pool dashboard alarms

* feedback
2023-08-30 17:22:41 +02:00
Pierre Donias
8ebc0dba4f feat(netbox): primary IP: fallback to next IPs in the list of addresses
Fixes #6978
2023-08-30 17:02:46 +02:00
Pierre Donias
006f12f17f feat(netbox): do not throw when IP cannot be parsed
See https://xcp-ng.org/forum/topic/7625
2023-08-30 17:02:46 +02:00
Pierre Donias
b22239804a fix(netbox): properly remove deleted Netbox IPs from local collection 2023-08-30 17:02:46 +02:00
Pierre Donias
afd174ca21 feat(netbox): handle empty collections in request() method 2023-08-30 17:02:46 +02:00
Pierre Donias
27c6c1b896 chore(netbox): use lodash find/filter where relevant 2023-08-30 17:02:46 +02:00
Florent BEAUCHAMP
311b420b74 feat(backups): expose preferNbd setting per backup job (#6995) 2023-08-30 16:59:36 +02:00
Julien Fontanet
e403298140 fix(CHANGELOG.unreleased): add missing xo-server
Introduced by 9c7fd94a9
2023-08-30 16:13:50 +02:00
Julien Fontanet
9c7fd94a9b fix(xo-server/rest-api): limit applies at the end for {backups,restore}/logs
Fixes https://xcp-ng.org/forum/post/64880
2023-08-30 16:02:42 +02:00
Mathieu
8cdae83150 feat: technical release (#7007) 2023-08-30 10:37:46 +02:00
Pierre Donias
5b1cc7415e fix(xo-server/normalizeVmNetworks): always assume multiple space-delimited IPs (#6990)
See https://xcp-ng.org/forum/topic/7625
2023-08-30 09:56:08 +02:00
Florent BEAUCHAMP
f5d3bc1f2d feat(backups): merge worker concurrency (#6965) 2023-08-30 09:38:37 +02:00
Julien Fontanet
ba81d0e08a feat(xo-server-transport-email): add local hostname to config (#6988)
Fixes https://xcp-ng.org/forum/topic/7579/
2023-08-29 23:22:31 +02:00
Pierre Donias
3b3f927e4b docs(netbox): steps and labels better match Netbox UI (#6986)
See https://xcp-ng.org/forum/topic/7625
2023-08-29 15:42:17 +02:00
Thierry Goettelmann
5e8539865f feat(lite): new iteration for XenApi, stores and subscriptions (#6998) 2023-08-29 15:23:18 +02:00
Florent BEAUCHAMP
3a3fa2882c fix(backup/healthcheck): mirror backup appeared detached (#7000) 2023-08-29 14:52:44 +02:00
Thierry Goettelmann
3baa37846e feat(lite/stories): allow to organize stories in subdirectories (#6992) 2023-08-21 11:12:16 +02:00
Mathieu
999fba2030 feat(xo-web/pool/advanced): ability to set a crash dump SR (#6973)
Fixes #5060
2023-08-18 15:34:05 +02:00
Mathieu
785a5857ef fix(xapi/host_smartReboot): resume VMs after enabling host (#6980)
Found when investigating https://xcp-ng.org/forum/post/60372
2023-08-17 16:22:35 +02:00
Thierry Goettelmann
067f4ac882 feat(lite): new XenApi records collection system (#6975) 2023-08-17 15:22:33 +02:00
Julien Fontanet
8a26e08102 feat(xo-server/rest-api): filter/limit support for {backups/restore}/logs
Fixes https://xcp-ng.org/forum/post/64789
2023-08-17 13:59:32 +02:00
Julien Fontanet
42aa202f7a fix(xo-server/job.set): accept userId
Fixes https://xcp-ng.org/forum/post/64668
2023-08-16 15:13:00 +02:00
Julien Fontanet
403d2c8e7b fix(mixins/Tasks): behave when no user connected to API
Introduced by 1ddbe87d0
2023-08-11 11:27:08 +02:00
Julien Fontanet
ad46bde302 feat(backups/XO metadata): transfer binary config in base64 2023-08-10 15:39:34 +02:00
Julien Fontanet
1b6ec2c545 fix(xo-web/home): don't search in linked objects (#6881)
Introduced by 5928984069

For instance, searching the UUID of a running VM was showing all other VMs on the same host due to the UUID being present in their `container.residentVms`.
2023-08-10 14:42:07 +02:00
Julien Fontanet
56388557cb fix(xo-server): increase timeout when file restore via XO Proxy
Related to zammad#13396
2023-08-10 11:37:08 +02:00
Julien Fontanet
1ddbe87d0f feat(mixins/Tasks): inject userId in tasks 2023-08-09 16:18:29 +02:00
Pierre Donias
3081810450 feat(xo-server-netbox): synchronize VM tags
Fixes #5899
See Zammad#12478
See https://xcp-ng.org/forum/topic/6902
2023-08-08 15:23:57 +02:00
Pierre Donias
155be7fd95 fix(netbox): add missing trailing / in URL 2023-08-08 15:23:57 +02:00
Pierre Donias
ef960e94d3 chore(netbox): namespace all XO objects as xo* 2023-08-08 15:23:57 +02:00
Pierre Donias
bfd99a48fe chore(netbox): namespace all Netbox objects as nb* 2023-08-08 15:23:57 +02:00
Florent BEAUCHAMP
a13fda5fe9 fix(backups/_MixinXapiWriter): typo _heathCheckSr → _healthCheckSr (#6969)
Fix `TypeError: Cannot read properties of undefined (reading 'uuid') at #isAlreadyOnHealthCheckSr`
2023-08-08 09:48:53 +02:00
Florent BEAUCHAMP
66bee59774 fix(xen-api/getResource): don't fail silently when HTTP request fails without response (#6970)
Seen while investigating zammad#16309
2023-08-08 09:39:18 +02:00
Julien Fontanet
685400bbf8 fix(xo-server): fix get-stream@3 usage
Fixes #6971

Introduced by 3dca7f2a7
2023-08-08 08:05:38 +02:00
Julien Fontanet
5bef8fc411 fix(lite): disable linting because it's broken
Introduced by 3dca7f2a7
2023-08-05 17:05:02 +02:00
Julien Fontanet
aa7ff1449a fix(lite): adapt ESLint config to prettier@3
Introduced by 3dca7f2a7
2023-08-04 22:09:55 +02:00
Julien Fontanet
3dca7f2a71 chore: update deps 2023-08-03 17:56:24 +02:00
Julien Fontanet
3dc2f649f6 chore: format with Prettier 2023-08-03 17:56:24 +02:00
Julien Fontanet
9eb537c2f9 chore: update dev deps 2023-08-03 17:56:24 +02:00
Thierry Goettelmann
dfd5f6882f feat(lite): enhance typings for improved type safety (#6949) 2023-08-03 11:33:29 +02:00
Julien Fontanet
7214016338 fix(xo-server/_authenticateUser): don't use registerUser()
Introduced by 99605bf18
2023-08-03 10:25:15 +02:00
Julien Fontanet
606e3c4ce5 docs(xo-server-test-plugin): explain configurationPresets 2023-08-03 10:21:15 +02:00
Julien Fontanet
fb04d3d25d docs(xo-server-test-plugin): show title/description for settings 2023-08-03 10:20:51 +02:00
Julien Fontanet
db8c042131 fix(xo-web/plugins): merge preset with existing config
Instead of replacing it.
2023-08-03 10:14:07 +02:00
Julien Fontanet
fd9005fba8 fix(xo-web/plugins): don't disable presets when config not edited 2023-08-03 10:12:52 +02:00
Julien Fontanet
2d25413b8d fix(xo-server-auth-ldap): mark userIdAttribute as required
It can no longer be ommited since 99605bf18
2023-08-03 09:56:33 +02:00
Julien Fontanet
035679800a chore(xo-server-auth-ldap): defaults are merged automatically by xo-server
Related to 8c7d25424
2023-08-03 09:53:01 +02:00
Thierry Goettelmann
abd0a3035a feat(lite/component): created UiResources + UiResource (#6932) 2023-08-01 11:18:10 +02:00
Julien Fontanet
d307730c68 feat: release 5.85.0 2023-07-31 17:06:25 +02:00
Julien Fontanet
1b44de4958 feat(xo-server): 5.120.2 2023-07-31 16:52:03 +02:00
Julien Fontanet
ec78a1ce8b feat(xo-web): 5.122.2 2023-07-31 16:32:42 +02:00
Julien Fontanet
19c82ab30d feat(xo-server): 5.120.1 2023-07-31 16:32:41 +02:00
Julien Fontanet
9986f3fb18 fix(xo-web/removeUserAuthProvider): notify on error
Introduced by 52cf2d151
2023-07-31 16:29:09 +02:00
Julien Fontanet
d24e9c093d fix(xo-server/updaterUser): fix current user auth protection
Introduced by 2d52aee95
2023-07-31 16:28:16 +02:00
Julien Fontanet
70c8b24fac feat(xo-web): 5.122.1 2023-07-31 15:58:15 +02:00
Julien Fontanet
9c9c11104b feat(xo-server-auth-google): 0.3.0 2023-07-31 15:58:05 +02:00
Julien Fontanet
cba90b27f4 feat(xo-server-auth-github): 0.3.0 2023-07-31 15:57:44 +02:00
Julien Fontanet
46cbced570 feat(xo-server): 5.120.0 2023-07-31 15:56:48 +02:00
Julien Fontanet
52cf2d1514 feat(xo-web/settings/users): auth providers can be removed 2023-07-31 15:48:49 +02:00
Julien Fontanet
e51351be8d feat(xo-server/api): user.removeAuthProvider 2023-07-31 15:48:49 +02:00
Julien Fontanet
2a42e0ff94 feat(xo-web/users): display users auth providers
Related to zammad#16318
2023-07-31 15:48:49 +02:00
Julien Fontanet
3a824a2bfc fix(xo-server/updateUser): check password xor auth providers 2023-07-31 15:48:49 +02:00
Julien Fontanet
fc1c809a18 fix(xo-server): remove password when sign in with provider
Password can no longer be used/edited when an auth provider is registered.

For security concerns, this useless password should be removed from the database.
2023-07-31 15:48:49 +02:00
Julien Fontanet
221cd40199 fix(xo-server/updateUser): can remove password 2023-07-31 15:48:49 +02:00
Julien Fontanet
aca19d9a81 fix(xo-server): user pass disabled when associated auth providers 2023-07-31 15:48:49 +02:00
Julien Fontanet
0601bbe18d fix(xo-server/recover-account): remove all auth providers 2023-07-31 15:48:49 +02:00
Julien Fontanet
2d52aee952 fix(xo-server/updateUser): can remove all auth providers with null 2023-07-31 15:48:49 +02:00
Julien Fontanet
99605bf185 feat(xo-server/registerUser): completely disable 2023-07-31 15:48:49 +02:00
Julien Fontanet
91b19d9bc4 feat(xo-server-auth-google): use registerUser2 2023-07-31 15:48:49 +02:00
Julien Fontanet
562401ebe4 feat(xo-server-auth-github): use registerUser2 2023-07-31 15:48:49 +02:00
Julien Fontanet
6fd2f2610d fix(xo-web/new-vm): don't send device in VIFs
Introduced by 6ae19b064

Fixes #6960
2023-07-31 09:34:30 +02:00
Gabriel Gunullu
6ae19b0640 fix(xo-web/new-vm): list VIFs ordered by device (#6944)
Fixes zammad#15920
2023-07-28 18:51:48 +02:00
Pierre Donias
6b936d8a8c feat(lite): 0.1.2 (#6958) 2023-07-28 17:37:07 +02:00
Thierry Goettelmann
8f2cfaae00 feat(lite): open console in new window (#6868)
Add a link to open the console in a new window.
2023-07-28 14:04:06 +02:00
Thierry Goettelmann
5c215e1a8a feat(lite/console): rework VM console page (#6863)
Rework the VM Console page to be better aligned with Figma mockup.
- Spinner while loading the console
- Added the "monitor" image with correct message when VM is powered off
- Better screen space usage
2023-07-28 11:39:33 +02:00
Pierre Donias
e3cb98124f feat: technical release (#6956) 2023-07-28 10:05:26 +02:00
Julien Fontanet
90c3319880 feat(xo-web/backup/file-restore): add export format selection 2023-07-27 17:22:58 +02:00
Julien Fontanet
348db876d2 feat(xo-server/backupNg.fetchFiles): add format param 2023-07-27 17:22:58 +02:00
Julien Fontanet
408fd7ec03 feat(proxy/backup.fetchPartitionFiles): add format param 2023-07-27 17:22:58 +02:00
Julien Fontanet
1fd84836b1 feat(backups/fetchPartitionFiles): add tgz (tar+gzip) support
Around 6 times faster than ZIP export.
2023-07-27 17:22:58 +02:00
Julien Fontanet
522204795f fix(backups/fetchPartitionFiles): rewrite ZIP creation
It's now sequential which leads to better performance and less memory consumption.

Empty directories are now included and all entries have correct mode and modification time.
2023-07-27 17:22:58 +02:00
Julien Fontanet
e29c422ac9 fix(xo-server/_handleHttpRequest): use pipeline between result and response
Properly closes one stream if the other is destroyed.
2023-07-27 17:22:58 +02:00
Florent BEAUCHAMP
152cf09b7e feat(vmware-explorer): handle sesparse files (#6909) 2023-07-27 17:15:29 +02:00
Pierre Donias
ff728099dc docs(netbox): update screenshot (#6955) 2023-07-27 17:13:57 +02:00
Mathieu
706d94221d feat(xo-server/pool/rpu): avoid unnecessary VMs migration (#6943) 2023-07-27 17:12:31 +02:00
Gabriel Gunullu
340e9af7f4 fix(backups): handle incremental replication to multiple SRs (#6811)
Fix matching previous replications when multiple SRs.

Fixes #6582
2023-07-27 17:09:15 +02:00
Pierre Donias
40e536ba61 feat(xo-server-netbox): synchronize VM platform (#6954)
See Zammad#12478
See https://xcp-ng.org/forum/topic/6902
2023-07-27 16:59:50 +02:00
Thierry Goettelmann
fd4c56c8c2 feat(lite/pool): add tasks to Pool Dashboard (#6713)
Other updates:
- Move pending/finished tasks logic to store subscription
- Add `count` prop to `UiCardTitle`
- Add "No tasks" message on Task table if empty
- Make the `finishedTasks` prop optional
- Add ability to have full width dashboard cards
2023-07-27 16:23:52 +02:00
Thierry Goettelmann
20d04ba956 feat(lite): dynamic page title (#6853)
See #6793

ℹ️ This PR adds a `pageTitleStore` which allows defining the current page title
according to 3 parts: an object, a string, and a count. Each part is optional.

 The page title is **reactive** when function argument is a `Ref`, a `Computed`
or a getter. For example, when updating a VM name, the page title will be
updated in every tabs.

🪄 Each title part is automatically unset when the component that set it is
unmounted.
2023-07-27 11:41:33 +02:00
Pierre Donias
3b1bcc67ae feat(xo-server-netbox): rewrite (#6950)
Fixes #6038, Fixes #6135, Fixes #6024, Fixes #6036
See https://xcp-ng.org/forum/topic/6070
See zammad#5695
See https://xcp-ng.org/forum/topic/6149
See https://xcp-ng.org/forum/topic/6332

Complete rewrite of the plugin. Main functional changes:
- Synchronize VM description
- Fix duplicated VMs in Netbox after disconnecting one pool
- Migrating a VM from one pool to another keeps VM data added manually
- Fix largest IP prefix being picked instead of smallest
- Fix synchronization not working if some pools are unavailable
- Better error messages
2023-07-27 10:07:26 +02:00
Julien Fontanet
1add3fbf9d fix(yarn.lock): refresh
Introduced by 1c23bd5ff
2023-07-26 13:36:28 +02:00
Julien Fontanet
97f0759de0 feat(mixins/Hooks): warning every 5s if listener still running
This helps diagnosticate issues when a hook is stuck.'
2023-07-25 16:41:29 +02:00
Julien Fontanet
005ab47d9b fix(xo-web): clear token on authentication failure (#6937)
This prevents infinite refreshes when the token is deemed valid by the server
but the authentication failed for any reasons.
2023-07-25 09:49:11 +02:00
868 changed files with 37357 additions and 16781 deletions

View File

@@ -15,9 +15,10 @@ module.exports = {
overrides: [
{
files: ['cli.{,c,m}js', '*-cli.{,c,m}js', '**/*cli*/**/*.{,c,m}js'],
files: ['cli.{,c,m}js', '*-cli.{,c,m}js', '**/*cli*/**/*.{,c,m}js', '**/scripts/**.{,c,m}js'],
rules: {
'n/no-process-exit': 'off',
'n/shebang': 'off',
'no-console': 'off',
},
},
@@ -46,6 +47,58 @@ module.exports = {
],
},
},
{
files: ['@xen-orchestra/{web-core,lite,web}/**/*.{vue,ts}'],
parserOptions: {
sourceType: 'module',
},
plugins: ['import'],
extends: [
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:vue/vue3-recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier',
],
settings: {
'import/resolver': {
typescript: true,
'eslint-import-resolver-custom-alias': {
alias: {
'@core': '../web-core/lib',
'@': './src',
},
extensions: ['.ts'],
packages: ['@xen-orchestra/lite', '@xen-orchestra/web'],
},
},
},
rules: {
'no-void': 'off',
'n/no-missing-import': 'off', // using 'import' plugin instead to support TS aliases
'@typescript-eslint/no-explicit-any': 'off',
'vue/require-default-prop': 'off', // https://github.com/vuejs/eslint-plugin-vue/issues/2051
},
},
{
files: ['@xen-orchestra/{web-core,lite,web}/src/pages/**/*.vue'],
parserOptions: {
sourceType: 'module',
},
rules: {
'vue/multi-word-component-names': 'off',
},
},
{
files: ['@xen-orchestra/{web-core,lite,web}/typed-router.d.ts'],
parserOptions: {
sourceType: 'module',
},
rules: {
'eslint-comments/disable-enable-pair': 'off',
'eslint-comments/no-unlimited-disable': 'off',
},
},
],
parserOptions: {
@@ -68,6 +121,11 @@ module.exports = {
'no-console': ['error', { allow: ['warn', 'error'] }],
// this rule can prevent race condition bugs like parallel `a += await foo()`
//
// as it has a lots of false positive, it is only enabled as a warning for now
'require-atomic-updates': 'warn',
strict: 'error',
},
}

View File

@@ -1,48 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'status: triaging :triangular_flag_on_post:, type: bug :bug:'
assignees: ''
---
1. ⚠️ **If you don't follow this template, the issue will be closed**.
2. ⚠️ **If your issue can't be easily reproduced, please report it [on the forum first](https://xcp-ng.org/forum/category/12/xen-orchestra)**.
Are you using XOA or XO from the sources?
If XOA:
- which release channel? (`stable` vs `latest`)
- please consider creating a support ticket in [your dedicated support area](https://xen-orchestra.com/#!/member/support)
If XO from the sources:
- Provide **your commit number**. If it's older than a week, we won't investigate
- Don't forget to [read this first](https://xen-orchestra.com/docs/community.html)
- As well as follow [this guide](https://xen-orchestra.com/docs/community.html#report-a-bug)
**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.
**Environment (please provide the following information):**
- Node: [e.g. 16.12.1]
- hypervisor: [e.g. XCP-ng 8.2.0]
**Additional context**
Add any other context about the problem here.

119
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,119 @@
name: Bug Report
description: Create a report to help us improve
labels: ['type: bug :bug:', 'status: triaging :triangular_flag_on_post:']
body:
- type: markdown
attributes:
value: |
1. ⚠️ **If you don't follow this template, the issue will be closed**.
2. ⚠️ **If your issue can't be easily reproduced, please report it [on the forum first](https://xcp-ng.org/forum/category/12/xen-orchestra)**.
- type: markdown
attributes:
value: '## Are you using XOA or XO from the sources?'
- type: dropdown
id: xo-origin
attributes:
label: Are you using XOA or XO from the sources?
options:
- XOA
- XO from the sources
- both
validations:
required: false
- type: markdown
attributes:
value: '### If XOA:'
- type: dropdown
id: xoa-channel
attributes:
label: Which release channel?
description: please consider creating a support ticket in [your dedicated support area](https://xen-orchestra.com/#!/member/support)
options:
- stable
- latest
- both
validations:
required: false
- type: markdown
attributes:
value: '### If XO from the sources:'
- type: markdown
attributes:
value: |
- Don't forget to [read this first](https://xen-orchestra.com/docs/community.html)
- As well as follow [this guide](https://xen-orchestra.com/docs/community.html#report-a-bug)
- type: input
id: xo-sources-commit-number
attributes:
label: Provide your commit number
description: If it's older than a week, we won't investigate
placeholder: e.g. 579f0
validations:
required: false
- type: markdown
attributes:
value: '## Bug description:'
- type: textarea
id: bug-description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is
validations:
required: true
- type: textarea
id: error-message
attributes:
label: Error message
render: Text
validations:
required: false
- type: textarea
id: steps
attributes:
label: To reproduce
description: 'Steps to reproduce the behavior:'
value: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
validations:
required: false
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem
validations:
required: false
- type: markdown
attributes:
value: '## Environment (please provide the following information):'
- type: input
id: node-version
attributes:
label: Node
placeholder: e.g. 16.12.1
validations:
required: true
- type: input
id: hypervisor-version
attributes:
label: Hypervisor
placeholder: e.g. XCP-ng 8.2.0
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context about the problem here
validations:
required: false

View File

@@ -24,8 +24,12 @@ jobs:
cache: 'yarn'
- name: Install project dependencies
run: yarn
- name: Ensure yarn.lock is up-to-date
run: git diff --exit-code yarn.lock
- name: Build the project
run: yarn build
- name: Unit tests
run: yarn test-unit
- name: Lint tests
run: yarn test-lint
- name: Integration tests

4
.gitignore vendored
View File

@@ -30,8 +30,12 @@ pnpm-debug.log.*
yarn-error.log
yarn-error.log.*
.env
*.tsbuildinfo
# code coverage
.nyc_output/
coverage/
.turbo/
# https://node-tap.org/dot-tap-folder/
.tap/

View File

@@ -1,8 +1,11 @@
'use strict'
module.exports = {
arrowParens: 'avoid',
jsxSingleQuote: true,
semi: false,
singleQuote: true,
trailingComma: 'es5',
// 2020-11-24: Requested by nraynaud and approved by the rest of the team
//

View File

@@ -33,8 +33,7 @@
"test": "node--test"
},
"devDependencies": {
"sinon": "^15.0.1",
"tap": "^16.3.0",
"sinon": "^17.0.1",
"test": "^3.2.1"
}
}

View File

@@ -62,6 +62,42 @@ decorateClass(Foo, {
})
```
### `decorateObject(object, map)`
Decorates an object the same way `decorateClass()` decorates a class:
```js
import { decorateObject } from '@vates/decorate-with'
const object = {
get bar() {
// body
},
set bar(value) {
// body
},
baz() {
// body
},
}
decorateObject(object, {
// getter and/or setter
bar: {
// without arguments
get: lodash.memoize,
// with arguments
set: [lodash.debounce, 150],
},
// method (with or without arguments)
baz: lodash.curry,
})
```
### `perInstance(fn, ...args)`
Helper to decorate the method by instance instead of for the whole class.

View File

@@ -80,6 +80,42 @@ decorateClass(Foo, {
})
```
### `decorateObject(object, map)`
Decorates an object the same way `decorateClass()` decorates a class:
```js
import { decorateObject } from '@vates/decorate-with'
const object = {
get bar() {
// body
},
set bar(value) {
// body
},
baz() {
// body
},
}
decorateObject(object, {
// getter and/or setter
bar: {
// without arguments
get: lodash.memoize,
// with arguments
set: [lodash.debounce, 150],
},
// method (with or without arguments)
baz: lodash.curry,
})
```
### `perInstance(fn, ...args)`
Helper to decorate the method by instance instead of for the whole class.

View File

@@ -14,10 +14,13 @@ function applyDecorator(decorator, value) {
}
exports.decorateClass = exports.decorateMethodsWith = function decorateClass(klass, map) {
const { prototype } = klass
return decorateObject(klass.prototype, map)
}
function decorateObject(object, map) {
for (const name of Object.keys(map)) {
const decorator = map[name]
const descriptor = getOwnPropertyDescriptor(prototype, name)
const descriptor = getOwnPropertyDescriptor(object, name)
if (typeof decorator === 'function' || Array.isArray(decorator)) {
descriptor.value = applyDecorator(decorator, descriptor.value)
} else {
@@ -30,10 +33,11 @@ exports.decorateClass = exports.decorateMethodsWith = function decorateClass(kla
}
}
defineProperty(prototype, name, descriptor)
defineProperty(object, name, descriptor)
}
return klass
return object
}
exports.decorateObject = decorateObject
exports.perInstance = function perInstance(fn, decorator, ...args) {
const map = new WeakMap()

View File

@@ -13,12 +13,15 @@ describe('decorateWith', () => {
const expectedFn = Function.prototype
const newFn = () => {}
const decorator = decorateWith(function wrapper(fn, ...args) {
assert.deepStrictEqual(fn, expectedFn)
assert.deepStrictEqual(args, expectedArgs)
const decorator = decorateWith(
function wrapper(fn, ...args) {
assert.deepStrictEqual(fn, expectedFn)
assert.deepStrictEqual(args, expectedArgs)
return newFn
}, ...expectedArgs)
return newFn
},
...expectedArgs
)
const descriptor = {
configurable: true,

View File

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

View File

@@ -14,7 +14,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.1.4",
"version": "0.1.5",
"engines": {
"node": ">=8.10"
},
@@ -23,13 +23,13 @@
"test": "node--test"
},
"dependencies": {
"@vates/multi-key-map": "^0.1.0",
"@vates/multi-key-map": "^0.2.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"ensure-array": "^1.0.0"
},
"devDependencies": {
"sinon": "^15.0.1",
"sinon": "^17.0.1",
"test": "^3.2.1"
}
}

View File

@@ -20,6 +20,9 @@ function assertListeners(t, event, listeners) {
}
t.beforeEach(function (t) {
// work around https://github.com/tapjs/tapjs/issues/998
t.context = {}
t.context.ee = new EventEmitter()
t.context.em = new EventListenersManager(t.context.ee)
})

View File

@@ -38,9 +38,9 @@
"version": "1.0.1",
"scripts": {
"postversion": "npm publish --access public",
"test": "tap --branches=72"
"test": "tap --allow-incomplete-coverage"
},
"devDependencies": {
"tap": "^16.2.0"
"tap": "^18.7.0"
}
}

28
@vates/fuse-vhd/.USAGE.md Normal file
View File

@@ -0,0 +1,28 @@
Mount a vhd generated by xen-orchestra to filesystem
### Library
```js
import { mount } from 'fuse-vhd'
// return a disposable, see promise-toolbox/Disposable
// unmount automatically when disposable is disposed
// in case of differencing VHD, it mounts the full chain
await mount(handler, diskId, mountPoint)
```
### cli
From the install folder :
```
cli.mjs <remoteUrl> <vhdPathInRemote> <mountPoint>
```
After installing the package
```
xo-fuse-vhd <remoteUrl> <vhdPathInRemote> <mountPoint>
```
remoteUrl can be found by using cli in `@xen-orchestra/fs` , for example a local remote will have a url like `file:///path/to/remote/root`

59
@vates/fuse-vhd/README.md Normal file
View File

@@ -0,0 +1,59 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/fuse-vhd
[![Package Version](https://badgen.net/npm/v/@vates/fuse-vhd)](https://npmjs.org/package/@vates/fuse-vhd) ![License](https://badgen.net/npm/license/@vates/fuse-vhd) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/fuse-vhd)](https://bundlephobia.com/result?p=@vates/fuse-vhd) [![Node compatibility](https://badgen.net/npm/node/@vates/fuse-vhd)](https://npmjs.org/package/@vates/fuse-vhd)
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/fuse-vhd):
```sh
npm install --save @vates/fuse-vhd
```
## Usage
Mount a vhd generated by xen-orchestra to filesystem
### Library
```js
import { mount } from 'fuse-vhd'
// return a disposable, see promise-toolbox/Disposable
// unmount automatically when disposable is disposed
// in case of differencing VHD, it mounts the full chain
await mount(handler, diskId, mountPoint)
```
### cli
From the install folder :
```
cli.mjs <remoteUrl> <vhdPathInRemote> <mountPoint>
```
After installing the package
```
xo-fuse-vhd <remoteUrl> <vhdPathInRemote> <mountPoint>
```
remoteUrl can be found by using cli in `@xen-orchestra/fs` , for example a local remote will have a url like `file:///path/to/remote/root`
## Contributions
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)

26
@vates/fuse-vhd/cli.mjs Executable file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env node
import Disposable from 'promise-toolbox/Disposable'
import { getSyncedHandler } from '@xen-orchestra/fs'
import { mount } from './index.mjs'
async function* main([remoteUrl, vhdPathInRemote, mountPoint]) {
if (mountPoint === undefined) {
throw new TypeError('missing arg: cli <remoteUrl> <vhdPathInRemote> <mountPoint>')
}
const handler = yield getSyncedHandler({ url: remoteUrl })
const mounted = await mount(handler, vhdPathInRemote, mountPoint)
let disposePromise
process.on('SIGINT', async () => {
// ensure single dispose
if (!disposePromise) {
disposePromise = mounted.dispose()
}
await disposePromise
process.exit()
})
}
Disposable.wrap(main)(process.argv.slice(2))

View File

@@ -58,7 +58,7 @@ export const mount = Disposable.factory(async function* mount(handler, diskPath,
},
})
return new Disposable(
() => fromCallback(() => fuse.unmount()),
fromCallback(() => fuse.mount())
() => fromCallback(cb => fuse.unmount(cb)),
fromCallback(cb => fuse.mount(cb))
)
})

View File

@@ -1,6 +1,6 @@
{
"name": "@vates/fuse-vhd",
"version": "1.0.0",
"version": "2.1.0",
"license": "ISC",
"private": false,
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/fuse-vhd",
@@ -19,10 +19,14 @@
},
"main": "./index.mjs",
"dependencies": {
"@xen-orchestra/fs": "^4.1.4",
"fuse-native": "^2.2.6",
"lru-cache": "^7.14.0",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^4.5.0"
"vhd-lib": "^4.9.0"
},
"bin": {
"xo-fuse-vhd": "./cli.mjs"
},
"scripts": {
"postversion": "npm publish --access public"

View File

@@ -17,4 +17,14 @@ map.get(['foo', 'bar']) // 2
map.get(['bar', 'foo']) // 3
map.get([OBJ]) // 4
map.get([{}]) // undefined
map.delete([])
for (const [key, value] of map.entries() {
console.log(key, value)
}
for (const value of map.values()) {
console.log(value)
}
```

View File

@@ -35,6 +35,16 @@ map.get(['foo', 'bar']) // 2
map.get(['bar', 'foo']) // 3
map.get([OBJ]) // 4
map.get([{}]) // undefined
map.delete([])
for (const [key, value] of map.entries() {
console.log(key, value)
}
for (const value of map.values()) {
console.log(value)
}
```
## Contributions

View File

@@ -36,14 +36,31 @@ function del(node, i, keys) {
return node
}
function* entries(node, key) {
if (node !== undefined) {
if (node instanceof Node) {
const { value } = node
if (value !== undefined) {
yield [key, node.value]
}
for (const [childKey, child] of node.children.entries()) {
yield* entries(child, key.concat(childKey))
}
} else {
yield [key, node]
}
}
}
function get(node, i, keys) {
return i === keys.length
? node instanceof Node
? node.value
: node
: node instanceof Node
? get(node.children.get(keys[i]), i + 1, keys)
: undefined
? get(node.children.get(keys[i]), i + 1, keys)
: undefined
}
function set(node, i, keys, value) {
@@ -69,6 +86,22 @@ function set(node, i, keys, value) {
return node
}
function* values(node) {
if (node !== undefined) {
if (node instanceof Node) {
const { value } = node
if (value !== undefined) {
yield node.value
}
for (const child of node.children.values()) {
yield* values(child)
}
} else {
yield node
}
}
}
exports.MultiKeyMap = class MultiKeyMap {
constructor() {
// each node is either a value or a Node if it contains children
@@ -79,6 +112,10 @@ exports.MultiKeyMap = class MultiKeyMap {
this._root = del(this._root, 0, keys)
}
entries() {
return entries(this._root, [])
}
get(keys) {
return get(this._root, 0, keys)
}
@@ -86,4 +123,8 @@ exports.MultiKeyMap = class MultiKeyMap {
set(keys, value) {
this._root = set(this._root, 0, keys, value)
}
values() {
return values(this._root)
}
}

View File

@@ -19,7 +19,7 @@ describe('MultiKeyMap', () => {
// reverse composite key
['bar', 'foo'],
]
const values = keys.map(() => ({}))
const values = keys.map(() => Math.random())
// set all values first to make sure they are all stored and not only the
// last one
@@ -27,6 +27,12 @@ describe('MultiKeyMap', () => {
map.set(key, values[i])
})
assert.deepEqual(
Array.from(map.entries()),
keys.map((key, i) => [key, values[i]])
)
assert.deepEqual(Array.from(map.values()), values)
keys.forEach((key, i) => {
// copy the key to make sure the array itself is not the key
assert.strictEqual(map.get(key.slice()), values[i])

View File

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

View File

@@ -1,10 +1,9 @@
import assert from 'node:assert'
import { Socket } from 'node:net'
import { connect } from 'node:tls'
import { fromCallback, pRetry, pDelay, pTimeout } from 'promise-toolbox'
import { fromCallback, pRetry, pDelay, pTimeout, pFromCallback } from 'promise-toolbox'
import { readChunkStrict } from '@vates/read-chunk'
import { createLogger } from '@xen-orchestra/log'
import {
INIT_PASSWD,
NBD_CMD_READ,
@@ -21,7 +20,6 @@ import {
OPTS_MAGIC,
NBD_CMD_DISC,
} from './constants.mjs'
const { warn } = createLogger('vates:nbd-client')
// documentation is here : https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md
@@ -40,6 +38,7 @@ export default class NbdClient {
#readBlockRetries
#reconnectRetry
#connectTimeout
#messageTimeout
// AFAIK, there is no guaranty the server answers in the same order as the queries
// so we handle a backlog of command waiting for response and handle concurrency manually
@@ -52,7 +51,14 @@ export default class NbdClient {
#reconnectingPromise
constructor(
{ address, port = NBD_DEFAULT_PORT, exportname, cert },
{ connectTimeout = 6e4, waitBeforeReconnect = 1e3, readAhead = 10, readBlockRetries = 5, reconnectRetry = 5 } = {}
{
connectTimeout = 6e4,
messageTimeout = 6e4,
waitBeforeReconnect = 1e3,
readAhead = 10,
readBlockRetries = 5,
reconnectRetry = 5,
} = {}
) {
this.#serverAddress = address
this.#serverPort = port
@@ -63,6 +69,7 @@ export default class NbdClient {
this.#readBlockRetries = readBlockRetries
this.#reconnectRetry = reconnectRetry
this.#connectTimeout = connectTimeout
this.#messageTimeout = messageTimeout
}
get exportSize() {
@@ -115,13 +122,27 @@ export default class NbdClient {
if (!this.#connected) {
return
}
this.#connected = false
const socket = this.#serverSocket
const queryId = this.#nextCommandQueryId
this.#nextCommandQueryId++
const buffer = Buffer.alloc(28)
buffer.writeInt32BE(NBD_REQUEST_MAGIC, 0) // it is a nbd request
buffer.writeInt16BE(0, 4) // no command flags for a disconnect
buffer.writeInt16BE(NBD_CMD_DISC, 6) // we want to disconnect from nbd server
await this.#write(buffer)
await this.#serverSocket.destroy()
buffer.writeBigUInt64BE(queryId, 8)
buffer.writeBigUInt64BE(0n, 16)
buffer.writeInt32BE(0, 24)
const promise = pFromCallback(cb => {
socket.end(buffer, 'utf8', cb)
})
try {
await pTimeout.call(promise, this.#messageTimeout)
} catch (error) {
socket.destroy()
}
this.#serverSocket = undefined
this.#connected = false
}
@@ -195,11 +216,13 @@ export default class NbdClient {
}
#read(length) {
return readChunkStrict(this.#serverSocket, length)
const promise = readChunkStrict(this.#serverSocket, length)
return pTimeout.call(promise, this.#messageTimeout)
}
#write(buffer) {
return fromCallback.call(this.#serverSocket, 'write', buffer)
const promise = fromCallback.call(this.#serverSocket, 'write', buffer)
return pTimeout.call(promise, this.#messageTimeout)
}
async #readInt32() {
@@ -232,19 +255,20 @@ export default class NbdClient {
}
try {
this.#waitingForResponse = true
const magic = await this.#readInt32()
const buffer = await this.#read(16)
const magic = buffer.readInt32BE(0)
if (magic !== NBD_REPLY_MAGIC) {
throw new Error(`magic number for block answer is wrong : ${magic} ${NBD_REPLY_MAGIC}`)
}
const error = await this.#readInt32()
const error = buffer.readInt32BE(4)
if (error !== 0) {
// @todo use error code from constants.mjs
throw new Error(`GOT ERROR CODE : ${error}`)
}
const blockQueryId = await this.#readInt64()
const blockQueryId = buffer.readBigUInt64BE(8)
const query = this.#commandQueryBacklog.get(blockQueryId)
if (!query) {
throw new Error(` no query associated with id ${blockQueryId}`)
@@ -265,7 +289,7 @@ export default class NbdClient {
}
}
async readBlock(index, size = NBD_DEFAULT_BLOCK_SIZE) {
async #readBlock(index, size) {
// we don't want to add anything in backlog while reconnecting
if (this.#reconnectingPromise) {
await this.#reconnectingPromise
@@ -281,7 +305,13 @@ export default class NbdClient {
buffer.writeInt16BE(NBD_CMD_READ, 6) // we want to read a data block
buffer.writeBigUInt64BE(queryId, 8)
// byte offset in the raw disk
buffer.writeBigUInt64BE(BigInt(index) * BigInt(size), 16)
const offset = BigInt(index) * BigInt(size)
const remaining = this.#exportSize - offset
if (remaining < BigInt(size)) {
size = Number(remaining)
}
buffer.writeBigUInt64BE(offset, 16)
buffer.writeInt32BE(size, 24)
return new Promise((resolve, reject) => {
@@ -307,45 +337,13 @@ export default class NbdClient {
})
}
async *readBlocks(indexGenerator) {
// default : read all blocks
if (indexGenerator === undefined) {
const exportSize = this.#exportSize
const chunkSize = 2 * 1024 * 1024
indexGenerator = function* () {
const nbBlocks = Math.ceil(Number(exportSize / BigInt(chunkSize)))
for (let index = 0; BigInt(index) < nbBlocks; index++) {
yield { index, size: chunkSize }
}
}
}
const readAhead = []
const readAheadMaxLength = this.#readAhead
const makeReadBlockPromise = (index, size) => {
const promise = pRetry(() => this.readBlock(index, size), {
tries: this.#readBlockRetries,
onRetry: async err => {
warn('will retry reading block ', index, err)
await this.reconnect()
},
})
// error is handled during unshift
promise.catch(() => {})
return promise
}
// read all blocks, but try to keep readAheadMaxLength promise waiting ahead
for (const { index, size } of indexGenerator()) {
// stack readAheadMaxLength promises before starting to handle the results
if (readAhead.length === readAheadMaxLength) {
// any error will stop reading blocks
yield readAhead.shift()
}
readAhead.push(makeReadBlockPromise(index, size))
}
while (readAhead.length > 0) {
yield readAhead.shift()
}
async readBlock(index, size = NBD_DEFAULT_BLOCK_SIZE) {
return pRetry(() => this.#readBlock(index, size), {
tries: this.#readBlockRetries,
onRetry: async err => {
warn('will retry reading block ', index, err)
await this.reconnect()
},
})
}
}

View File

@@ -0,0 +1,85 @@
import { asyncEach } from '@vates/async-each'
import { NBD_DEFAULT_BLOCK_SIZE } from './constants.mjs'
import NbdClient from './index.mjs'
import { createLogger } from '@xen-orchestra/log'
const { warn } = createLogger('vates:nbd-client:multi')
export default class MultiNbdClient {
#clients = []
#readAhead
get exportSize() {
return this.#clients[0].exportSize
}
constructor(settings, { nbdConcurrency = 8, readAhead = 16, ...options } = {}) {
this.#readAhead = readAhead
if (!Array.isArray(settings)) {
settings = [settings]
}
for (let i = 0; i < nbdConcurrency; i++) {
this.#clients.push(
new NbdClient(settings[i % settings.length], { ...options, readAhead: Math.ceil(readAhead / nbdConcurrency) })
)
}
}
async connect() {
const connectedClients = []
for (const clientId in this.#clients) {
const client = this.#clients[clientId]
try {
await client.connect()
connectedClients.push(client)
} catch (err) {
client.disconnect().catch(() => {})
warn(`can't connect to one nbd client`, { err })
}
}
if (connectedClients.length === 0) {
throw new Error(`Fail to connect to any Nbd client`)
}
if (connectedClients.length < this.#clients.length) {
warn(
`incomplete connection by multi Nbd, only ${connectedClients.length} over ${this.#clients.length} expected clients`
)
this.#clients = connectedClients
}
}
async disconnect() {
await asyncEach(this.#clients, client => client.disconnect(), {
stopOnError: false,
})
}
async readBlock(index, size = NBD_DEFAULT_BLOCK_SIZE) {
const clientId = index % this.#clients.length
return this.#clients[clientId].readBlock(index, size)
}
async *readBlocks(indexGenerator) {
// default : read all blocks
const readAhead = []
const makeReadBlockPromise = (index, size) => {
const promise = this.readBlock(index, size)
// error is handled during unshift
promise.catch(() => {})
return promise
}
// read all blocks, but try to keep readAheadMaxLength promise waiting ahead
for (const { index, size } of indexGenerator()) {
// stack readAheadMaxLength promises before starting to handle the results
if (readAhead.length === this.#readAhead) {
// any error will stop reading blocks
yield readAhead.shift()
}
readAhead.push(makeReadBlockPromise(index, size))
}
while (readAhead.length > 0) {
yield readAhead.shift()
}
}
}

View File

@@ -13,25 +13,25 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "1.2.1",
"version": "3.0.0",
"engines": {
"node": ">=14.0"
},
"main": "./index.mjs",
"dependencies": {
"@vates/async-each": "^1.0.0",
"@vates/read-chunk": "^1.1.1",
"@vates/read-chunk": "^1.2.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"promise-toolbox": "^0.21.0",
"xen-api": "^1.3.3"
"xen-api": "^2.0.1"
},
"devDependencies": {
"tap": "^16.3.0",
"tap": "^18.7.0",
"tmp": "^0.2.1"
},
"scripts": {
"postversion": "npm publish --access public",
"test-integration": "tap --lines 97 --functions 95 --branches 74 --statements 97 tests/*.integ.mjs"
"test-integration": "tap --allow-incomplete-coverage"
}
}

View File

@@ -1,4 +1,3 @@
import NbdClient from '../index.mjs'
import { spawn, exec } from 'node:child_process'
import fs from 'node:fs/promises'
import { test } from 'tap'
@@ -7,8 +6,10 @@ import { pFromCallback } from 'promise-toolbox'
import { Socket } from 'node:net'
import { NBD_DEFAULT_PORT } from '../constants.mjs'
import assert from 'node:assert'
import MultiNbdClient from '../multi.mjs'
const FILE_SIZE = 10 * 1024 * 1024
const CHUNK_SIZE = 1024 * 1024 // non default size
const FILE_SIZE = 1024 * 1024 * 9.5 // non aligned file size
async function createTempFile(size) {
const tmpPath = await pFromCallback(cb => tmp.file(cb))
@@ -81,7 +82,7 @@ test('it works with unsecured network', async tap => {
const path = await createTempFile(FILE_SIZE)
let nbdServer = await spawnNbdKit(path)
const client = new NbdClient(
const client = new MultiNbdClient(
{
address: '127.0.0.1',
exportname: 'MY_SECRET_EXPORT',
@@ -109,13 +110,13 @@ CYu1Xn/FVPx1HoRgWc7E8wFhDcA/P3SJtfIQWHB9FzSaBflKGR4t8WCE2eE8+cTB
`,
},
{
nbdConcurrency: 1,
readAhead: 2,
}
)
await client.connect()
tap.equal(client.exportSize, BigInt(FILE_SIZE))
const CHUNK_SIZE = 1024 * 1024 // non default size
const indexes = []
for (let i = 0; i < FILE_SIZE / CHUNK_SIZE; i++) {
indexes.push(i)
@@ -127,9 +128,9 @@ CYu1Xn/FVPx1HoRgWc7E8wFhDcA/P3SJtfIQWHB9FzSaBflKGR4t8WCE2eE8+cTB
})
let i = 0
for await (const block of nbdIterator) {
let blockOk = true
let blockOk = block.length === Math.min(CHUNK_SIZE, FILE_SIZE - CHUNK_SIZE * i)
let firstFail
for (let j = 0; j < CHUNK_SIZE; j += 4) {
for (let j = 0; j < block.length; j += 4) {
const wanted = i * CHUNK_SIZE + j
const found = block.readUInt32BE(j)
blockOk = blockOk && found === wanted
@@ -137,7 +138,7 @@ CYu1Xn/FVPx1HoRgWc7E8wFhDcA/P3SJtfIQWHB9FzSaBflKGR4t8WCE2eE8+cTB
firstFail = j
}
}
tap.ok(blockOk, `check block ${i} content`)
tap.ok(blockOk, `check block ${i} content ${block.length}`)
i++
// flaky server is flaky
@@ -147,17 +148,6 @@ CYu1Xn/FVPx1HoRgWc7E8wFhDcA/P3SJtfIQWHB9FzSaBflKGR4t8WCE2eE8+cTB
nbdServer = await spawnNbdKit(path)
}
}
// we can reuse the conneciton to read other blocks
// default iterator
const nbdIteratorWithDefaultBlockIterator = client.readBlocks()
let nb = 0
for await (const block of nbdIteratorWithDefaultBlockIterator) {
nb++
tap.equal(block.length, 2 * 1024 * 1024)
}
tap.equal(nb, 5)
assert.rejects(() => client.readBlock(100, CHUNK_SIZE))
await client.disconnect()

View File

@@ -1,6 +1,6 @@
{
"name": "@vates/node-vsphere-soap",
"version": "1.0.0",
"version": "2.0.0",
"description": "interface to vSphere SOAP/WSDL from node for interfacing with vCenter or ESXi, forked from node-vsphere-soap",
"main": "lib/client.mjs",
"author": "reedog117",

View File

@@ -1,5 +1,5 @@
import { strict as assert } from 'node:assert'
import { describe, it } from 'tap/mocha'
import test from 'test'
import {
generateHotp,
@@ -11,6 +11,8 @@ import {
verifyTotp,
} from './index.mjs'
const { describe, it } = test
describe('generateSecret', function () {
it('generates a string of 32 chars', async function () {
const secret = generateSecret()

View File

@@ -31,9 +31,9 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "tap"
"test": "node--test"
},
"devDependencies": {
"tap": "^16.3.0"
"test": "^3.3.0"
}
}

View File

@@ -1,7 +1,7 @@
'use strict'
const assert = require('assert/strict')
const { describe, it } = require('tap').mocha
const { describe, it } = require('test')
const { every, not, some } = require('./')

View File

@@ -32,9 +32,9 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "tap"
"test": "node--test"
},
"devDependencies": {
"tap": "^16.0.1"
"test": "^3.3.0"
}
}

View File

@@ -22,41 +22,41 @@ const readChunk = (stream, size) =>
stream.errored != null
? Promise.reject(stream.errored)
: stream.closed || stream.readableEnded
? Promise.resolve(null)
: new Promise((resolve, reject) => {
if (size !== undefined) {
assert(size > 0)
? Promise.resolve(null)
: new Promise((resolve, reject) => {
if (size !== undefined) {
assert(size > 0)
// per Node documentation:
// > The size argument must be less than or equal to 1 GiB.
assert(size < 1073741824)
}
// per Node documentation:
// > The size argument must be less than or equal to 1 GiB.
assert(size < 1073741824)
}
function onEnd() {
resolve(null)
removeListeners()
}
function onError(error) {
reject(error)
removeListeners()
}
function onReadable() {
const data = stream.read(size)
if (data !== null) {
resolve(data)
function onEnd() {
resolve(null)
removeListeners()
}
}
function removeListeners() {
stream.removeListener('end', onEnd)
stream.removeListener('error', onError)
stream.removeListener('readable', onReadable)
}
stream.on('end', onEnd)
stream.on('error', onError)
stream.on('readable', onReadable)
onReadable()
})
function onError(error) {
reject(error)
removeListeners()
}
function onReadable() {
const data = stream.read(size)
if (data !== null) {
resolve(data)
removeListeners()
}
}
function removeListeners() {
stream.removeListener('end', onEnd)
stream.removeListener('error', onError)
stream.removeListener('readable', onReadable)
}
stream.on('end', onEnd)
stream.on('error', onError)
stream.on('readable', onReadable)
onReadable()
})
exports.readChunk = readChunk
/**
@@ -111,42 +111,42 @@ async function skip(stream, size) {
return stream.errored != null
? Promise.reject(stream.errored)
: size === 0 || stream.closed || stream.readableEnded
? Promise.resolve(0)
: new Promise((resolve, reject) => {
let left = size
function onEnd() {
resolve(size - left)
removeListeners()
}
function onError(error) {
reject(error)
removeListeners()
}
function onReadable() {
const data = stream.read()
left -= data === null ? 0 : data.length
if (left > 0) {
// continue to read
} else {
// if more than wanted has been read, push back the rest
if (left < 0) {
stream.unshift(data.slice(left))
}
resolve(size)
? Promise.resolve(0)
: new Promise((resolve, reject) => {
let left = size
function onEnd() {
resolve(size - left)
removeListeners()
}
}
function removeListeners() {
stream.removeListener('end', onEnd)
stream.removeListener('error', onError)
stream.removeListener('readable', onReadable)
}
stream.on('end', onEnd)
stream.on('error', onError)
stream.on('readable', onReadable)
onReadable()
})
function onError(error) {
reject(error)
removeListeners()
}
function onReadable() {
const data = stream.read()
left -= data === null ? 0 : data.length
if (left > 0) {
// continue to read
} else {
// if more than wanted has been read, push back the rest
if (left < 0) {
stream.unshift(data.slice(left))
}
resolve(size)
removeListeners()
}
}
function removeListeners() {
stream.removeListener('end', onEnd)
stream.removeListener('error', onError)
stream.removeListener('readable', onReadable)
}
stream.on('end', onEnd)
stream.on('error', onError)
stream.on('readable', onReadable)
onReadable()
})
}
exports.skip = skip

View File

@@ -19,7 +19,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "1.1.1",
"version": "1.2.0",
"engines": {
"node": ">=8.10"
},

View File

@@ -27,7 +27,7 @@
"license": "ISC",
"version": "0.1.0",
"engines": {
"node": ">=10"
"node": ">=12.3"
},
"scripts": {
"postversion": "npm publish --access public",

View File

@@ -123,7 +123,7 @@ const onProgress = makeOnProgress({
onTaskUpdate(taskLog) {},
})
Task.run({ data: { name: 'my task' }, onProgress }, asyncFn)
Task.run({ properties: { name: 'my task' }, onProgress }, asyncFn)
```
It can also be fed event logs directly:

View File

@@ -111,7 +111,7 @@ const onProgress = makeOnProgress({
// current status of the task as described in the previous section
taskLog.status
// undefined or a dictionnary of properties attached to the task
// undefined or a dictionary of properties attached to the task
taskLog.properties
// timestamp at which the abortion was requested, undefined otherwise
@@ -139,7 +139,7 @@ const onProgress = makeOnProgress({
onTaskUpdate(taskLog) {},
})
Task.run({ data: { name: 'my task' }, onProgress }, asyncFn)
Task.run({ properties: { name: 'my task' }, onProgress }, asyncFn)
```
It can also be fed event logs directly:

View File

@@ -35,7 +35,7 @@
"test": "node--test"
},
"devDependencies": {
"sinon": "^15.0.1",
"sinon": "^17.0.1",
"test": "^3.2.1"
}
}

View File

@@ -1,7 +1,7 @@
'use strict'
const assert = require('assert/strict')
const { afterEach, describe, it } = require('tap').mocha
const { afterEach, describe, it } = require('test')
const { AlteredRecordError, AuditCore, MissingRecordError, NULL_ID, Storage } = require('.')

View File

@@ -13,10 +13,10 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "tap --lines 67 --functions 92 --branches 52 --statements 67"
"test": "node--test"
},
"dependencies": {
"@vates/decorate-with": "^2.0.0",
"@vates/decorate-with": "^2.1.0",
"@xen-orchestra/log": "^0.6.0",
"golike-defer": "^0.5.1",
"object-hash": "^2.0.1"
@@ -28,6 +28,6 @@
"url": "https://vates.fr"
},
"devDependencies": {
"tap": "^16.0.1"
"test": "^3.3.0"
}
}

View File

@@ -7,9 +7,9 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.39.0",
"@xen-orchestra/fs": "^4.0.1",
"filenamify": "^4.1.0",
"@xen-orchestra/backups": "^0.44.6",
"@xen-orchestra/fs": "^4.1.4",
"filenamify": "^6.0.0",
"getopts": "^2.2.5",
"lodash": "^4.17.15",
"promise-toolbox": "^0.21.0"
@@ -27,7 +27,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "1.0.9",
"version": "1.0.14",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -4,23 +4,229 @@ import { formatFilenameDate } from './_filenameDate.mjs'
import { importIncrementalVm } from './_incrementalVm.mjs'
import { Task } from './Task.mjs'
import { watchStreamSize } from './_watchStreamSize.mjs'
import { VhdNegative, VhdSynthetic } from 'vhd-lib'
import { decorateClass } from '@vates/decorate-with'
import { createLogger } from '@xen-orchestra/log'
import { dirname, join } from 'node:path'
import pickBy from 'lodash/pickBy.js'
import { defer } from 'golike-defer'
const { debug, info, warn } = createLogger('xo:backups:importVmBackup')
async function resolveUuid(xapi, cache, uuid, type) {
if (uuid == null) {
return uuid
}
const ref = cache.get(uuid)
if (ref === undefined) {
cache.set(uuid, xapi.call(`${type}.get_by_uuid`, uuid))
}
return cache.get(uuid)
}
export class ImportVmBackup {
constructor({ adapter, metadata, srUuid, xapi, settings: { newMacAddresses, mapVdisSrs = {} } = {} }) {
constructor({
adapter,
metadata,
srUuid,
xapi,
settings: { additionnalVmTag, newMacAddresses, mapVdisSrs = {}, useDifferentialRestore = false } = {},
}) {
this._adapter = adapter
this._importIncrementalVmSettings = { newMacAddresses, mapVdisSrs }
this._importIncrementalVmSettings = { additionnalVmTag, newMacAddresses, mapVdisSrs, useDifferentialRestore }
this._metadata = metadata
this._srUuid = srUuid
this._xapi = xapi
}
async #getPathOfVdiSnapshot(snapshotUuid) {
const metadata = this._metadata
if (this._pathToVdis === undefined) {
const backups = await this._adapter.listVmBackups(
this._metadata.vm.uuid,
({ mode, timestamp }) => mode === 'delta' && timestamp >= metadata.timestamp
)
const map = new Map()
for (const backup of backups) {
for (const [vdiRef, vdi] of Object.entries(backup.vdis)) {
map.set(vdi.uuid, backup.vhds[vdiRef])
}
}
this._pathToVdis = map
}
return this._pathToVdis.get(snapshotUuid)
}
async _reuseNearestSnapshot($defer, ignoredVdis) {
const metadata = this._metadata
const { mapVdisSrs } = this._importIncrementalVmSettings
const { vbds, vhds, vifs, vm, vmSnapshot } = metadata
const streams = {}
const metdataDir = dirname(metadata._filename)
const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
for (const [vdiRef, vdi] of Object.entries(vdis)) {
const vhdPath = join(metdataDir, vhds[vdiRef])
let xapiDisk
try {
xapiDisk = await this._xapi.getRecordByUuid('VDI', vdi.$snapshot_of$uuid)
} catch (err) {
// if this disk is not present anymore, fall back to default restore
warn(err)
}
let snapshotCandidate, backupCandidate
if (xapiDisk !== undefined) {
debug('found disks, wlll search its snapshots', { snapshots: xapiDisk.snapshots })
for (const snapshotRef of xapiDisk.snapshots) {
const snapshot = await this._xapi.getRecord('VDI', snapshotRef)
debug('handling snapshot', { snapshot })
// take only the first snapshot
if (snapshotCandidate && snapshotCandidate.snapshot_time < snapshot.snapshot_time) {
debug('already got a better candidate')
continue
}
// have a corresponding backup more recent than metadata ?
const pathToSnapshotData = await this.#getPathOfVdiSnapshot(snapshot.uuid)
if (pathToSnapshotData === undefined) {
debug('no backup linked to this snaphot')
continue
}
if (snapshot.$SR.uuid !== (mapVdisSrs[vdi.$snapshot_of$uuid] ?? this._srUuid)) {
debug('not restored on the same SR', { snapshotSr: snapshot.$SR.uuid, mapVdisSrs, srUuid: this._srUuid })
continue
}
debug('got a candidate', pathToSnapshotData)
snapshotCandidate = snapshot
backupCandidate = pathToSnapshotData
}
}
let stream
const backupWithSnapshotPath = join(metdataDir, backupCandidate ?? '')
if (vhdPath === backupWithSnapshotPath) {
// all the data are already on the host
debug('direct reuse of a snapshot')
stream = null
vdis[vdiRef].baseVdi = snapshotCandidate
// go next disk , we won't use this stream
continue
}
let disposableDescendants
const disposableSynthetic = await VhdSynthetic.fromVhdChain(this._adapter._handler, vhdPath)
// this will also clean if another disk of this VM backup fails
// if user really only need to restore non failing disks he can retry with ignoredVdis
let disposed = false
const disposeOnce = async () => {
if (!disposed) {
disposed = true
try {
await disposableDescendants?.dispose()
await disposableSynthetic?.dispose()
} catch (error) {
warn('openVhd: failed to dispose VHDs', { error })
}
}
}
$defer.onFailure(() => disposeOnce())
const parentVhd = disposableSynthetic.value
await parentVhd.readBlockAllocationTable()
debug('got vhd synthetic of parents', parentVhd.length)
if (snapshotCandidate !== undefined) {
try {
debug('will try to use differential restore', {
backupWithSnapshotPath,
vhdPath,
vdiRef,
})
disposableDescendants = await VhdSynthetic.fromVhdChain(this._adapter._handler, backupWithSnapshotPath, {
until: vhdPath,
})
const descendantsVhd = disposableDescendants.value
await descendantsVhd.readBlockAllocationTable()
debug('got vhd synthetic of descendants')
const negativeVhd = new VhdNegative(parentVhd, descendantsVhd)
debug('got vhd negative')
// update the stream with the negative vhd stream
stream = await negativeVhd.stream()
vdis[vdiRef].baseVdi = snapshotCandidate
} catch (error) {
// can be a broken VHD chain, a vhd chain with a key backup, ....
// not an irrecuperable error, don't dispose parentVhd, and fallback to full restore
warn(`can't use differential restore`, { error })
disposableDescendants?.dispose()
}
}
// didn't make a negative stream : fallback to classic stream
if (stream === undefined) {
debug('use legacy restore')
stream = await parentVhd.stream()
}
stream.on('end', disposeOnce)
stream.on('close', disposeOnce)
stream.on('error', disposeOnce)
info('everything is ready, will transfer', stream.length)
streams[`${vdiRef}.vhd`] = stream
}
return {
streams,
vbds,
vdis,
version: '1.0.0',
vifs,
vm: { ...vm, suspend_VDI: vmSnapshot.suspend_VDI },
}
}
async #decorateIncrementalVmMetadata() {
const { additionnalVmTag, mapVdisSrs, useDifferentialRestore } = this._importIncrementalVmSettings
const ignoredVdis = new Set(
Object.entries(mapVdisSrs)
.filter(([_, srUuid]) => srUuid === null)
.map(([vdiUuid]) => vdiUuid)
)
let backup
if (useDifferentialRestore) {
backup = await this._reuseNearestSnapshot(ignoredVdis)
} else {
backup = await this._adapter.readIncrementalVmBackup(this._metadata, ignoredVdis)
}
const xapi = this._xapi
const cache = new Map()
const mapVdisSrRefs = {}
if (additionnalVmTag !== undefined) {
backup.vm.tags.push(additionnalVmTag)
}
for (const [vdiUuid, srUuid] of Object.entries(mapVdisSrs)) {
mapVdisSrRefs[vdiUuid] = await resolveUuid(xapi, cache, srUuid, 'SR')
}
const srRef = await resolveUuid(xapi, cache, this._srUuid, 'SR')
Object.values(backup.vdis).forEach(vdi => {
vdi.SR = mapVdisSrRefs[vdi.uuid] ?? srRef
})
return backup
}
async run() {
const adapter = this._adapter
const metadata = this._metadata
const isFull = metadata.mode === 'full'
const sizeContainer = { size: 0 }
const { newMacAddresses } = this._importIncrementalVmSettings
let backup
if (isFull) {
backup = await adapter.readFullVmBackup(metadata)
@@ -28,12 +234,7 @@ export class ImportVmBackup {
} else {
assert.strictEqual(metadata.mode, 'delta')
const ignoredVdis = new Set(
Object.entries(this._importIncrementalVmSettings.mapVdisSrs)
.filter(([_, srUuid]) => srUuid === null)
.map(([vdiUuid]) => vdiUuid)
)
backup = await adapter.readIncrementalVmBackup(metadata, ignoredVdis)
backup = await this.#decorateIncrementalVmMetadata()
Object.values(backup.streams).forEach(stream => watchStreamSize(stream, sizeContainer))
}
@@ -48,8 +249,7 @@ export class ImportVmBackup {
const vmRef = isFull
? await xapi.VM_import(backup, srRef)
: await importIncrementalVm(backup, await xapi.getRecord('SR', srRef), {
...this._importIncrementalVmSettings,
detectBase: false,
newMacAddresses,
})
await Promise.all([
@@ -59,6 +259,13 @@ export class ImportVmBackup {
vmRef,
`${metadata.vm.name_label} (${formatFilenameDate(metadata.timestamp)})`
),
xapi.call(
'VM.set_name_description',
vmRef,
`Restored on ${formatFilenameDate(+new Date())} from ${adapter._handler._remote.name} -
${metadata.vm.name_description}
`
),
])
return {
@@ -69,3 +276,5 @@ export class ImportVmBackup {
)
}
}
decorateClass(ImportVmBackup, { _reuseNearestSnapshot: defer })

View File

@@ -5,7 +5,7 @@ import { createLogger } from '@xen-orchestra/log'
import { createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic } from 'vhd-lib'
import { decorateMethodsWith } from '@vates/decorate-with'
import { deduped } from '@vates/disposable/deduped.js'
import { dirname, join, normalize, resolve } from 'node:path'
import { dirname, join, resolve } from 'node:path'
import { execFile } from 'child_process'
import { mount } from '@vates/fuse-vhd'
import { readdir, lstat } from 'node:fs/promises'
@@ -18,6 +18,7 @@ import fromEvent from 'promise-toolbox/fromEvent'
import groupBy from 'lodash/groupBy.js'
import pDefer from 'promise-toolbox/defer'
import pickBy from 'lodash/pickBy.js'
import tar from 'tar'
import zlib from 'zlib'
import { BACKUP_DIR } from './_getVmBackupDir.mjs'
@@ -34,6 +35,8 @@ export const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
export const DIR_XO_POOL_METADATA_BACKUPS = 'xo-pool-metadata-backups'
const IMMUTABILTY_METADATA_FILENAME = '/immutability.json'
const { debug, warn } = createLogger('xo:backups:RemoteAdapter')
const compareTimestamp = (a, b) => a.timestamp - b.timestamp
@@ -41,20 +44,23 @@ const compareTimestamp = (a, b) => a.timestamp - b.timestamp
const noop = Function.prototype
const resolveRelativeFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)
const makeRelative = path => resolve('/', path).slice(1)
const resolveSubpath = (root, path) => resolve(root, makeRelative(path))
const resolveSubpath = (root, path) => resolve(root, `.${resolve('/', path)}`)
async function addZipEntries(zip, realBasePath, virtualBasePath, relativePaths) {
for (const relativePath of relativePaths) {
const realPath = join(realBasePath, relativePath)
const virtualPath = join(virtualBasePath, relativePath)
async function addDirectory(files, realPath, metadataPath) {
const stats = await lstat(realPath)
if (stats.isDirectory()) {
await asyncMap(await readdir(realPath), file =>
addDirectory(files, realPath + '/' + file, metadataPath + '/' + file)
)
} else if (stats.isFile()) {
files.push({
realPath,
metadataPath,
})
const stats = await lstat(realPath)
const { mode, mtime } = stats
const opts = { mode, mtime }
if (stats.isDirectory()) {
zip.addEmptyDirectory(virtualPath, opts)
await addZipEntries(zip, realPath, virtualPath, await readdir(realPath))
} else if (stats.isFile()) {
zip.addFile(realPath, virtualPath, opts)
}
}
}
@@ -182,17 +188,6 @@ export class RemoteAdapter {
})
}
async *_usePartitionFiles(diskId, partitionId, paths) {
const path = yield this.getPartition(diskId, partitionId)
const files = []
await asyncMap(paths, file =>
addDirectory(files, resolveSubpath(path, file), normalize('./' + file).replace(/\/+$/, ''))
)
return files
}
// check if we will be allowed to merge a a vhd created in this adapter
// with the vhd at path `path`
async isMergeableParent(packedParentUid, path) {
@@ -209,15 +204,24 @@ export class RemoteAdapter {
})
}
fetchPartitionFiles(diskId, partitionId, paths) {
fetchPartitionFiles(diskId, partitionId, paths, format) {
const { promise, reject, resolve } = pDefer()
Disposable.use(
async function* () {
const files = yield this._usePartitionFiles(diskId, partitionId, paths)
const zip = new ZipFile()
files.forEach(({ realPath, metadataPath }) => zip.addFile(realPath, metadataPath))
zip.end()
const { outputStream } = zip
const path = yield this.getPartition(diskId, partitionId)
let outputStream
if (format === 'tgz') {
outputStream = tar.c({ cwd: path, gzip: true }, paths.map(makeRelative))
} else if (format === 'zip') {
const zip = new ZipFile()
await addZipEntries(zip, path, '', paths.map(makeRelative))
zip.end()
;({ outputStream } = zip)
} else {
throw new Error('unsupported format ' + format)
}
resolve(outputStream)
await fromEvent(outputStream, 'end')
}.bind(this)
@@ -679,11 +683,13 @@ export class RemoteAdapter {
}
}
async outputStream(path, input, { checksum = true, validator = noop } = {}) {
async outputStream(path, input, { checksum = true, maxStreamLength, streamLength, validator = noop } = {}) {
const container = watchStreamSize(input)
await this._handler.outputStream(path, input, {
checksum,
dirMode: this._dirMode,
maxStreamLength,
streamLength,
async validator() {
await input.task
return validator.apply(this, arguments)
@@ -745,10 +751,37 @@ export class RemoteAdapter {
}
async readVmBackupMetadata(path) {
let json
let isImmutable = false
let remoteIsImmutable = false
// if the remote is immutable, check if this metadatas are also immutables
try {
// this file is not encrypted
await this._handler._readFile(IMMUTABILTY_METADATA_FILENAME)
remoteIsImmutable = true
} catch (error) {
if (error.code !== 'ENOENT') {
throw error
}
}
try {
// this will trigger an EPERM error if the file is immutable
json = await this.handler.readFile(path, { flag: 'r+' })
// s3 handler don't respect flags
} catch (err) {
// retry without triggerring immutbaility check ,only on immutable remote
if (err.code === 'EPERM' && remoteIsImmutable) {
isImmutable = true
json = await this._handler.readFile(path, { flag: 'r' })
} else {
throw err
}
}
// _filename is a private field used to compute the backup id
//
// it's enumerable to make it cacheable
const metadata = { ...JSON.parse(await this._handler.readFile(path)), _filename: path }
const metadata = { ...JSON.parse(json), _filename: path, isImmutable }
// backups created on XenServer < 7.1 via JSON in XML-RPC transports have boolean values encoded as integers, which make them unusable with more recent XAPIs
if (typeof metadata.vm.is_a_template === 'number') {
@@ -824,8 +857,6 @@ decorateMethodsWith(RemoteAdapter, {
debounceResourceFactory,
]),
_usePartitionFiles: Disposable.factory,
getDisk: compose([Disposable.factory, [deduped, diskId => [diskId]], debounceResourceFactory]),
getPartition: Disposable.factory,

View File

@@ -21,7 +21,12 @@ export class RestoreMetadataBackup {
})
} else {
const metadata = JSON.parse(await handler.readFile(join(backupId, 'metadata.json')))
return String(await handler.readFile(resolve(backupId, metadata.data ?? 'data.json')))
const dataFileName = resolve('/', backupId, metadata.data ?? 'data.json').slice(1)
const data = await handler.readFile(dataFileName)
// if data is JSON, sent it as a plain string, otherwise, consider the data as binary and encode it
const isJson = dataFileName.endsWith('.json')
return isJson ? data.toString() : { encoding: 'base64', data: data.toString('base64') }
}
}
}

View File

@@ -67,6 +67,11 @@ async function generateVhd(path, opts = {}) {
await VhdAbstract.createAlias(handler, path + '.alias.vhd', dataPath)
}
if (opts.blocks) {
for (const blockId of opts.blocks) {
await vhd.writeEntireBlock({ id: blockId, buffer: Buffer.alloc(2 * 1024 * 1024 + 512, blockId) })
}
}
await vhd.writeBlockAllocationTable()
await vhd.writeHeader()
await vhd.writeFooter()
@@ -230,7 +235,7 @@ test('it merges delta of non destroyed chain', async () => {
const metadata = JSON.parse(await handler.readFile(`${rootPath}/metadata.json`))
// size should be the size of children + grand children after the merge
assert.equal(metadata.size, 209920)
assert.equal(metadata.size, 104960)
// 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
@@ -320,6 +325,7 @@ describe('tests multiple combination ', () => {
const ancestor = await generateVhd(`${basePath}/ancestor.vhd`, {
useAlias,
mode: vhdMode,
blocks: [1, 3],
})
const child = await generateVhd(`${basePath}/child.vhd`, {
useAlias,
@@ -328,6 +334,7 @@ describe('tests multiple combination ', () => {
parentUnicodeName: 'ancestor.vhd' + (useAlias ? '.alias.vhd' : ''),
parentUuid: ancestor.footer.uuid,
},
blocks: [1, 2],
})
// a grand child vhd in metadata
await generateVhd(`${basePath}/grandchild.vhd`, {
@@ -337,6 +344,7 @@ describe('tests multiple combination ', () => {
parentUnicodeName: 'child.vhd' + (useAlias ? '.alias.vhd' : ''),
parentUuid: child.footer.uuid,
},
blocks: [2, 3],
})
// an older parent that was merging in clean
@@ -395,7 +403,7 @@ describe('tests multiple combination ', () => {
const metadata = JSON.parse(await handler.readFile(`${rootPath}/metadata.json`))
// size should be the size of children + grand children + clean after the merge
assert.deepEqual(metadata.size, vhdMode === 'file' ? 314880 : undefined)
assert.deepEqual(metadata.size, vhdMode === 'file' ? 6502400 : 6501888)
// broken vhd, non referenced, abandonned should be deleted ( alias and data)
// ancestor and child should be merged

View File

@@ -36,34 +36,32 @@ const computeVhdsSize = (handler, vhdPaths) =>
)
// chain is [ ancestor, child_1, ..., child_n ]
async function _mergeVhdChain(handler, chain, { logInfo, remove, merge, mergeBlockConcurrency }) {
if (merge) {
logInfo(`merging VHD chain`, { chain })
async function _mergeVhdChain(handler, chain, { logInfo, remove, mergeBlockConcurrency }) {
logInfo(`merging VHD chain`, { chain })
let done, total
const handle = setInterval(() => {
if (done !== undefined) {
logInfo('merge in progress', {
done,
parent: chain[0],
progress: Math.round((100 * done) / total),
total,
})
}
}, 10e3)
try {
return await mergeVhdChain(handler, chain, {
logInfo,
mergeBlockConcurrency,
onProgress({ done: d, total: t }) {
done = d
total = t
},
removeUnused: remove,
let done, total
const handle = setInterval(() => {
if (done !== undefined) {
logInfo('merge in progress', {
done,
parent: chain[0],
progress: Math.round((100 * done) / total),
total,
})
} finally {
clearInterval(handle)
}
}, 10e3)
try {
return await mergeVhdChain(handler, chain, {
logInfo,
mergeBlockConcurrency,
onProgress({ done: d, total: t }) {
done = d
total = t
},
removeUnused: remove,
})
} finally {
clearInterval(handle)
}
}
@@ -471,23 +469,20 @@ export async function cleanVm(
const metadataWithMergedVhd = {}
const doMerge = async () => {
await asyncMap(toMerge, async chain => {
const merged = await limitedMergeVhdChain(handler, chain, {
const { finalVhdSize } = await limitedMergeVhdChain(handler, chain, {
logInfo,
logWarn,
remove,
merge,
mergeBlockConcurrency,
})
if (merged !== undefined) {
const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metada file
metadataWithMergedVhd[metadataPath] = true
}
const metadataPath = vhdsToJSons[chain[chain.length - 1]] // all the chain should have the same metada file
metadataWithMergedVhd[metadataPath] = (metadataWithMergedVhd[metadataPath] ?? 0) + finalVhdSize
})
}
await Promise.all([
...unusedVhdsDeletion,
toMerge.length !== 0 && (merge ? Task.run({ name: 'merge' }, doMerge) : doMerge()),
toMerge.length !== 0 && (merge ? Task.run({ name: 'merge' }, doMerge) : () => Promise.resolve()),
asyncMap(unusedXvas, path => {
logWarn('unused XVA', { path })
if (remove) {
@@ -509,12 +504,11 @@ export async function cleanVm(
// update size for delta metadata with merged VHD
// check for the other that the size is the same as the real file size
await asyncMap(jsons, async metadataPath => {
const metadata = backups.get(metadataPath)
let fileSystemSize
const merged = metadataWithMergedVhd[metadataPath] !== undefined
const mergedSize = metadataWithMergedVhd[metadataPath]
const { mode, size, vhds, xva } = metadata
@@ -524,26 +518,29 @@ export async function cleanVm(
const linkedXva = resolve('/', vmDir, xva)
try {
fileSystemSize = await handler.getSize(linkedXva)
if (fileSystemSize !== size && fileSystemSize !== undefined) {
logWarn('cleanVm: incorrect backup size in metadata', {
path: metadataPath,
actual: size ?? 'none',
expected: fileSystemSize,
})
}
} catch (error) {
// can fail with encrypted remote
}
} else if (mode === 'delta') {
const linkedVhds = Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
fileSystemSize = await computeVhdsSize(handler, linkedVhds)
// the size is not computed in some cases (e.g. VhdDirectory)
if (fileSystemSize === undefined) {
return
}
// don't warn if the size has changed after a merge
if (!merged && fileSystemSize !== size) {
// FIXME: figure out why it occurs so often and, once fixed, log the real problems with `logWarn`
console.warn('cleanVm: incorrect backup size in metadata', {
path: metadataPath,
actual: size ?? 'none',
expected: fileSystemSize,
})
if (mergedSize === undefined) {
const linkedVhds = Object.keys(vhds).map(key => resolve('/', vmDir, vhds[key]))
fileSystemSize = await computeVhdsSize(handler, linkedVhds)
// the size is not computed in some cases (e.g. VhdDirectory)
if (fileSystemSize !== undefined && fileSystemSize !== size) {
logWarn('cleanVm: incorrect backup size in metadata', {
path: metadataPath,
actual: size ?? 'none',
expected: fileSystemSize,
})
}
}
}
} catch (error) {
@@ -551,9 +548,19 @@ export async function cleanVm(
return
}
// systematically update size after a merge
if ((merged || fixMetadata) && size !== fileSystemSize) {
metadata.size = fileSystemSize
// systematically update size and differentials after a merge
// @todo : after 2024-04-01 remove the fixmetadata options since the size computation is fixed
if (mergedSize || (fixMetadata && fileSystemSize !== size)) {
metadata.size = mergedSize ?? fileSystemSize ?? size
if (mergedSize) {
// all disks are now key disk
metadata.isVhdDifferencing = {}
for (const id of Object.values(metadata.vdis ?? {})) {
metadata.isVhdDifferencing[`${id}.vhd`] = false
}
}
mustRegenerateCache = true
try {
await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })

View File

@@ -1,4 +1,3 @@
import find from 'lodash/find.js'
import groupBy from 'lodash/groupBy.js'
import ignoreErrors from 'promise-toolbox/ignoreErrors'
import omit from 'lodash/omit.js'
@@ -12,22 +11,18 @@ import { cancelableMap } from './_cancelableMap.mjs'
import { Task } from './Task.mjs'
import pick from 'lodash/pick.js'
// in `other_config` of an incrementally replicated VM, contains the UUID of the source VM
export const TAG_BASE_DELTA = 'xo:base_delta'
// in `other_config` of an incrementally replicated VM, contains the UUID of the target SR used for replication
//
// added after the complete replication
export const TAG_BACKUP_SR = 'xo:backup:sr'
// in other_config of VDIs of an incrementally replicated VM, contains the UUID of the source VDI
export const TAG_COPY_SRC = 'xo:copy_of'
const ensureArray = value => (value === undefined ? [] : Array.isArray(value) ? value : [value])
const resolveUuid = async (xapi, cache, uuid, type) => {
if (uuid == null) {
return uuid
}
let ref = cache.get(uuid)
if (ref === undefined) {
ref = await xapi.call(`${type}.get_by_uuid`, uuid)
cache.set(uuid, ref)
}
return ref
}
export async function exportIncrementalVm(
vm,
@@ -39,6 +34,8 @@ export async function exportIncrementalVm(
fullVdisRequired = new Set(),
disableBaseTags = false,
nbdConcurrency = 1,
preferNbd,
} = {}
) {
// refs of VM's VDIs → base's VDIs.
@@ -86,6 +83,8 @@ export async function exportIncrementalVm(
baseRef: baseVdi?.$ref,
cancelToken,
format: 'vhd',
nbdConcurrency,
preferNbd,
})
})
@@ -143,7 +142,7 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
$defer,
incrementalVm,
sr,
{ cancelToken = CancelToken.none, detectBase = true, mapVdisSrs = {}, newMacAddresses = false } = {}
{ cancelToken = CancelToken.none, newMacAddresses = false } = {}
) {
const { version } = incrementalVm
if (compareVersions(version, '1.0.0') < 0) {
@@ -153,32 +152,6 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
const vmRecord = incrementalVm.vm
const xapi = sr.$xapi
let baseVm
if (detectBase) {
const remoteBaseVmUuid = vmRecord.other_config[TAG_BASE_DELTA]
if (remoteBaseVmUuid) {
baseVm = find(xapi.objects.all, obj => (obj = obj.other_config) && obj[TAG_COPY_SRC] === remoteBaseVmUuid)
if (!baseVm) {
throw new Error(`could not find the base VM (copy of ${remoteBaseVmUuid})`)
}
}
}
const cache = new Map()
const mapVdisSrRefs = {}
for (const [vdiUuid, srUuid] of Object.entries(mapVdisSrs)) {
mapVdisSrRefs[vdiUuid] = await resolveUuid(xapi, cache, srUuid, 'SR')
}
const baseVdis = {}
baseVm &&
baseVm.$VBDs.forEach(vbd => {
const vdi = vbd.$VDI
if (vdi !== undefined) {
baseVdis[vbd.VDI] = vbd.$VDI
}
})
const vdiRecords = incrementalVm.vdis
// 0. Create suspend_VDI
@@ -190,18 +163,7 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
vm: pick(vmRecord, 'uuid', 'name_label', 'suspend_VDI'),
})
} else {
suspendVdi = await xapi.getRecord(
'VDI',
await xapi.VDI_create({
...vdi,
other_config: {
...vdi.other_config,
[TAG_BASE_DELTA]: undefined,
[TAG_COPY_SRC]: vdi.uuid,
},
sr: mapVdisSrRefs[vdi.uuid] ?? sr.$ref,
})
)
suspendVdi = await xapi.getRecord('VDI', await xapi.VDI_create(vdi))
$defer.onFailure(() => suspendVdi.$destroy())
}
}
@@ -219,10 +181,6 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
ha_always_run: false,
is_a_template: false,
name_label: '[Importing…] ' + vmRecord.name_label,
other_config: {
...vmRecord.other_config,
[TAG_COPY_SRC]: vmRecord.uuid,
},
},
{
bios_strings: vmRecord.bios_strings,
@@ -243,14 +201,8 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
const vdi = vdiRecords[vdiRef]
let newVdi
const remoteBaseVdiUuid = detectBase && vdi.other_config[TAG_BASE_DELTA]
if (remoteBaseVdiUuid) {
const baseVdi = find(baseVdis, vdi => vdi.other_config[TAG_COPY_SRC] === remoteBaseVdiUuid)
if (!baseVdi) {
throw new Error(`missing base VDI (copy of ${remoteBaseVdiUuid})`)
}
newVdi = await xapi.getRecord('VDI', await baseVdi.$clone())
if (vdi.baseVdi !== undefined) {
newVdi = await xapi.getRecord('VDI', await vdi.baseVdi.$clone())
$defer.onFailure(() => newVdi.$destroy())
await newVdi.update_other_config(TAG_COPY_SRC, vdi.uuid)
@@ -261,18 +213,7 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
// suspendVDI has already created
newVdi = suspendVdi
} else {
newVdi = await xapi.getRecord(
'VDI',
await xapi.VDI_create({
...vdi,
other_config: {
...vdi.other_config,
[TAG_BASE_DELTA]: undefined,
[TAG_COPY_SRC]: vdi.uuid,
},
SR: mapVdisSrRefs[vdi.uuid] ?? sr.$ref,
})
)
newVdi = await xapi.getRecord('VDI', await xapi.VDI_create(vdi))
$defer.onFailure(() => newVdi.$destroy())
}
@@ -311,13 +252,19 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
// Import VDI contents.
cancelableMap(cancelToken, Object.entries(newVdis), async (cancelToken, [id, vdi]) => {
for (let stream of ensureArray(streams[`${id}.vhd`])) {
if (stream === null) {
// we restore a backup and reuse completly a local snapshot
continue
}
if (typeof stream === 'function') {
stream = await stream()
}
if (stream.length === undefined) {
stream = await createVhdStreamWithLength(stream)
}
await xapi.setField('VDI', vdi.$ref, 'name_label', `[Importing] ${vdiRecords[id].name_label}`)
await vdi.$importContent(stream, { cancelToken, format: 'vhd' })
await xapi.setField('VDI', vdi.$ref, 'name_label', vdiRecords[id].name_label)
}
}),

View File

@@ -22,7 +22,13 @@ export class XoMetadataBackup {
const dir = `${scheduleDir}/${formatFilenameDate(timestamp)}`
const data = job.xoMetadata
const dataBaseName = './data.json'
let dataBaseName = './data'
// JSON data is sent as plain string, binary data is sent as an object with `data` and `encoding properties
const isJson = typeof data === 'string'
if (isJson) {
dataBaseName += '.json'
}
const metadata = JSON.stringify(
{
@@ -54,7 +60,7 @@ export class XoMetadataBackup {
async () => {
const handler = adapter.handler
const dirMode = this._config.dirMode
await handler.outputFile(dataFileName, data, { dirMode })
await handler.outputFile(dataFileName, isJson ? data : Buffer.from(data.data, data.encoding), { dirMode })
await handler.outputFile(metaDataFileName, metadata, {
dirMode,
})

View File

@@ -29,6 +29,8 @@ export const FullRemote = class FullRemoteVmBackupRunner extends AbstractRemote
writer =>
writer.run({
stream: forkStreamUnpipe(stream),
// stream will be forked and transformed, it's not safe to attach additionnal properties to it
streamLength: stream.length,
timestamp: metadata.timestamp,
vm: metadata.vm,
vmSnapshot: metadata.vmSnapshot,

View File

@@ -35,13 +35,25 @@ export const FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
useSnapshot: false,
})
)
const vdis = await exportedVm.$getDisks()
let maxStreamLength = 1024 * 1024 // Ovf file and tar headers are a few KB, let's stay safe
for (const vdiRef of vdis) {
const vdi = await this._xapi.getRecord('VDI', vdiRef)
// the size a of fully allocated vdi will be virtual_size exaclty, it's a gross over evaluation
// of the real stream size in general, since a disk is never completly full
// vdi.physical_size seems to underevaluate a lot the real disk usage of a VDI, as of 2023-10-30
maxStreamLength += vdi.virtual_size
}
const sizeContainer = watchStreamSize(stream)
const timestamp = Date.now()
await this._callWriters(
writer =>
writer.run({
maxStreamLength,
sizeContainer,
stream: forkStreamUnpipe(stream),
timestamp,

View File

@@ -32,10 +32,10 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
useChain: false,
})
const differentialVhds = {}
const isVhdDifferencing = {}
await asyncEach(Object.entries(incrementalExport.streams), async ([key, stream]) => {
differentialVhds[key] = await isVhdDifferencingDisk(stream)
isVhdDifferencing[key] = await isVhdDifferencingDisk(stream)
})
incrementalExport.streams = mapValues(incrementalExport.streams, this._throttleStream)
@@ -43,7 +43,7 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
writer =>
writer.transfer({
deltaExport: forkDeltaExport(incrementalExport),
differentialVhds,
isVhdDifferencing,
timestamp: metadata.timestamp,
vm: metadata.vm,
vmSnapshot: metadata.vmSnapshot,

View File

@@ -41,6 +41,8 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
const deltaExport = await exportIncrementalVm(exportedVm, baseVm, {
fullVdisRequired,
nbdConcurrency: this._settings.nbdConcurrency,
preferNbd: this._settings.preferNbd,
})
// since NBD is network based, if one disk use nbd , all the disk use them
// except the suspended VDI
@@ -48,11 +50,11 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
Task.info('Transfer data using NBD')
}
const differentialVhds = {}
const isVhdDifferencing = {}
// since isVhdDifferencingDisk is reading and unshifting data in stream
// it should be done BEFORE any other stream transform
await asyncEach(Object.entries(deltaExport.streams), async ([key, stream]) => {
differentialVhds[key] = await isVhdDifferencingDisk(stream)
isVhdDifferencing[key] = await isVhdDifferencingDisk(stream)
})
const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
@@ -67,7 +69,7 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
writer =>
writer.transfer({
deltaExport: forkDeltaExport(deltaExport),
differentialVhds,
isVhdDifferencing,
sizeContainers,
timestamp,
vm,

View File

@@ -4,6 +4,7 @@ import { Disposable } from 'promise-toolbox'
import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
import { Abstract } from './_Abstract.mjs'
import { extractIdsFromSimplePattern } from '../../extractIdsFromSimplePattern.mjs'
export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstract {
constructor({
@@ -34,7 +35,8 @@ export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstrac
this._writers = writers
const RemoteWriter = this._getRemoteWriter()
Object.entries(remoteAdapters).forEach(([remoteId, adapter]) => {
extractIdsFromSimplePattern(job.remotes).forEach(remoteId => {
const adapter = remoteAdapters[remoteId]
const targetSettings = {
...settings,
...allSettings[remoteId],

View File

@@ -31,6 +31,11 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
throw new Error('cannot backup a VM created by this very job')
}
const currentOperations = Object.values(vm.current_operations)
if (currentOperations.some(_ => _ === 'migrate_send' || _ === 'pool_migrate')) {
throw new Error('cannot backup a VM currently being migrated')
}
this.config = config
this.job = job
this.remoteAdapters = remoteAdapters
@@ -256,7 +261,15 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
}
if (this._writers.size !== 0) {
await this._copy()
const { pool_migrate = null, migrate_send = null } = this._exportedVm.blocked_operations
const reason = 'VM migration is blocked during backup'
await this._exportedVm.update_blocked_operations({ pool_migrate: reason, migrate_send: reason })
try {
await this._copy()
} finally {
await this._exportedVm.update_blocked_operations({ pool_migrate, migrate_send })
}
}
} finally {
if (startAfter) {

View File

@@ -1,11 +1,11 @@
import cloneDeep from 'lodash/cloneDeep.js'
import mapValues from 'lodash/mapValues.js'
import { forkStreamUnpipe } from '../_forkStreamUnpipe.mjs'
export function forkDeltaExport(deltaExport) {
return Object.create(deltaExport, {
streams: {
value: mapValues(deltaExport.streams, forkStreamUnpipe),
},
})
const { streams, ...rest } = deltaExport
const newMetadata = cloneDeep(rest)
newMetadata.streams = mapValues(streams, forkStreamUnpipe)
return newMetadata
}

View File

@@ -24,7 +24,7 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
)
}
async _run({ timestamp, sizeContainer, stream, vm, vmSnapshot }) {
async _run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot }) {
const settings = this._settings
const job = this._job
const scheduleId = this._scheduleId
@@ -65,6 +65,8 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
await Task.run({ name: 'transfer' }, async () => {
await adapter.outputStream(dataFilename, stream, {
maxStreamLength,
streamLength,
validator: tmpPath => adapter.isValidXva(tmpPath),
})
return { size: sizeContainer.size }

View File

@@ -11,6 +11,7 @@ import { dirname } from 'node:path'
import { formatFilenameDate } from '../../_filenameDate.mjs'
import { getOldEntries } from '../../_getOldEntries.mjs'
import { TAG_BASE_DELTA } from '../../_incrementalVm.mjs'
import { Task } from '../../Task.mjs'
import { MixinRemoteWriter } from './_MixinRemoteWriter.mjs'
@@ -132,7 +133,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
}
}
async _transfer($defer, { differentialVhds, timestamp, deltaExport, vm, vmSnapshot }) {
async _transfer($defer, { isVhdDifferencing, timestamp, deltaExport, vm, vmSnapshot }) {
const adapter = this._adapter
const job = this._job
const scheduleId = this._scheduleId
@@ -142,8 +143,10 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
let metadataContent = await this._isAlreadyTransferred(timestamp)
if (metadataContent !== undefined) {
// @todo : should skip backup while being vigilant to not stuck the forked stream
// skip backup while being vigilant to not stuck the forked stream
Task.info('This backup has already been transfered')
Object.values(deltaExport.streams).forEach(stream => stream.destroy())
return { size: 0 }
}
const basename = formatFilenameDate(timestamp)
@@ -160,6 +163,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
)
metadataContent = {
isVhdDifferencing,
jobId,
mode: job.mode,
scheduleId,
@@ -179,9 +183,9 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
async ([id, vdi]) => {
const path = `${this._vmBackupDir}/${vhds[id]}`
const isDelta = differentialVhds[`${id}.vhd`]
const isDifferencing = isVhdDifferencing[`${id}.vhd`]
let parentPath
if (isDelta) {
if (isDifferencing) {
const vdiDir = dirname(path)
parentPath = (
await handler.list(vdiDir, {
@@ -195,7 +199,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
assert.notStrictEqual(
parentPath,
undefined,
`missing parent of ${id} in ${dirname(path)}, looking for ${vdi.other_config['xo:base_delta']}`
`missing parent of ${id} in ${dirname(path)}, looking for ${vdi.other_config[TAG_BASE_DELTA]}`
)
parentPath = parentPath.slice(1) // remove leading slash
@@ -204,15 +208,19 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
await checkVhd(handler, parentPath)
}
transferSize += await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
// don't write it as transferSize += await async function
// since i += await asyncFun lead to race condition
// as explained : https://eslint.org/docs/latest/rules/require-atomic-updates
const transferSizeOneDisk = await adapter.writeVhd(path, deltaExport.streams[`${id}.vhd`], {
// no checksum for VHDs, because they will be invalidated by
// merges and chainings
checksum: false,
validator: tmpPath => checkVhd(handler, tmpPath),
writeBlockConcurrency: this._config.writeBlockConcurrency,
})
transferSize += transferSizeOneDisk
if (isDelta) {
if (isDifferencing) {
await chainVhd(handler, parentPath, handler, path)
}

View File

@@ -4,12 +4,13 @@ import { formatDateTime } from '@xen-orchestra/xapi'
import { formatFilenameDate } from '../../_filenameDate.mjs'
import { getOldEntries } from '../../_getOldEntries.mjs'
import { importIncrementalVm, TAG_COPY_SRC } from '../../_incrementalVm.mjs'
import { importIncrementalVm, TAG_BACKUP_SR, TAG_BASE_DELTA, TAG_COPY_SRC } from '../../_incrementalVm.mjs'
import { Task } from '../../Task.mjs'
import { AbstractIncrementalWriter } from './_AbstractIncrementalWriter.mjs'
import { MixinXapiWriter } from './_MixinXapiWriter.mjs'
import { listReplicatedVms } from './_listReplicatedVms.mjs'
import find from 'lodash/find.js'
export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
async checkBaseVdis(baseUuidToSrcVdi, baseVm) {
@@ -81,6 +82,54 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
return asyncMapSettled(this._oldEntries, vm => vm.$destroy())
}
#decorateVmMetadata(backup) {
const { _warmMigration } = this._settings
const sr = this._sr
const xapi = sr.$xapi
const vm = backup.vm
vm.other_config[TAG_COPY_SRC] = vm.uuid
const remoteBaseVmUuid = vm.other_config[TAG_BASE_DELTA]
let baseVm
if (remoteBaseVmUuid) {
baseVm = find(
xapi.objects.all,
obj => (obj = obj.other_config) && obj[TAG_COPY_SRC] === remoteBaseVmUuid && obj[TAG_BACKUP_SR] === sr.$id
)
if (!baseVm) {
throw new Error(`could not find the base VM (copy of ${remoteBaseVmUuid})`)
}
}
const baseVdis = {}
baseVm?.$VBDs.forEach(vbd => {
const vdi = vbd.$VDI
if (vdi !== undefined) {
baseVdis[vbd.VDI] = vbd.$VDI
}
})
vm.other_config[TAG_COPY_SRC] = vm.uuid
if (!_warmMigration) {
vm.tags.push('Continuous Replication')
}
Object.values(backup.vdis).forEach(vdi => {
vdi.other_config[TAG_COPY_SRC] = vdi.uuid
vdi.SR = sr.$ref
// vdi.other_config[TAG_BASE_DELTA] is never defined on a suspend vdi
if (vdi.other_config[TAG_BASE_DELTA]) {
const remoteBaseVdiUuid = vdi.other_config[TAG_BASE_DELTA]
const baseVdi = find(baseVdis, vdi => vdi.other_config[TAG_COPY_SRC] === remoteBaseVdiUuid)
if (!baseVdi) {
throw new Error(`missing base VDI (copy of ${remoteBaseVdiUuid})`)
}
vdi.baseVdi = baseVdi
}
})
return backup
}
async _transfer({ timestamp, deltaExport, sizeContainers, vm }) {
const { _warmMigration } = this._settings
const sr = this._sr
@@ -91,16 +140,7 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
let targetVmRef
await Task.run({ name: 'transfer' }, async () => {
targetVmRef = await importIncrementalVm(
{
__proto__: deltaExport,
vm: {
...deltaExport.vm,
tags: _warmMigration ? deltaExport.vm.tags : [...deltaExport.vm.tags, 'Continuous Replication'],
},
},
sr
)
targetVmRef = await importIncrementalVm(this.#decorateVmMetadata(deltaExport), sr)
return {
size: Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0),
}
@@ -121,13 +161,13 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
)
),
targetVm.update_other_config({
'xo:backup:sr': srUuid,
[TAG_BACKUP_SR]: srUuid,
// these entries need to be added in case of offline backup
'xo:backup:datetime': formatDateTime(timestamp),
'xo:backup:job': job.id,
'xo:backup:schedule': scheduleId,
'xo:backup:vm': vm.uuid,
[TAG_BASE_DELTA]: vm.uuid,
}),
])
}

View File

@@ -1,9 +1,9 @@
import { AbstractWriter } from './_AbstractWriter.mjs'
export class AbstractFullWriter extends AbstractWriter {
async run({ timestamp, sizeContainer, stream, vm, vmSnapshot }) {
async run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot }) {
try {
return await this._run({ timestamp, sizeContainer, stream, vm, vmSnapshot })
return await this._run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot })
} finally {
// ensure stream is properly closed
stream.destroy()

View File

@@ -96,6 +96,9 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
metadata,
srUuid,
xapi,
settings: {
additionnalVmTag: 'xo:no-bak=Health Check',
},
}).run()
const restoredVm = xapi.getObject(restoredId)
try {
@@ -110,13 +113,13 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
)
}
_isAlreadyTransferred(timestamp) {
async _isAlreadyTransferred(timestamp) {
const vmUuid = this._vmUuid
const adapter = this._adapter
const backupDir = getVmBackupDir(vmUuid)
try {
const actualMetadata = JSON.parse(
adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
await adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
)
return actualMetadata
} catch (error) {}

View File

@@ -18,7 +18,7 @@ export const MixinXapiWriter = (BaseClass = Object) =>
const vdiRefs = await xapi.VM_getDisks(baseVm.$ref)
for (const vdiRef of vdiRefs) {
const vdi = xapi.getObject(vdiRef)
if (vdi.$SR.uuid !== this._heathCheckSr.uuid) {
if (vdi.$SR.uuid !== this._healthCheckSr.uuid) {
return false
}
}
@@ -58,7 +58,7 @@ export const MixinXapiWriter = (BaseClass = Object) =>
)
}
const healthCheckVm = xapi.getObject(healthCheckVmRef) ?? (await xapi.waitObject(healthCheckVmRef))
await healthCheckVm.add_tags('xo:no-bak=Health Check')
await new HealthCheckVmBackup({
restoredVm: healthCheckVm,
xapi,

View File

@@ -221,7 +221,7 @@ For multiple objects:
### Settings
Settings are described in [`@xen-orchestra/backups/Backup.js](https://github.com/vatesfr/xen-orchestra/blob/master/%40xen-orchestra/backups/Backup.js).
Settings are described in [`@xen-orchestra/backups/\_runners/VmsXapi.mjs``](https://github.com/vatesfr/xen-orchestra/blob/master/%40xen-orchestra/backups/_runners/VmsXapi.mjs).
## Writer API

View File

@@ -2,6 +2,21 @@ import mapValues from 'lodash/mapValues.js'
import { dirname } from 'node:path'
function formatVmBackup(backup) {
const { isVhdDifferencing, vmSnapshot } = backup
let differencingVhds
let dynamicVhds
// some backups don't use snapshots, therefore cannot be with memory
const withMemory = vmSnapshot !== undefined && vmSnapshot.suspend_VDI !== 'OpaqueRef:NULL'
// isVhdDifferencing is either undefined or an object
if (isVhdDifferencing !== undefined) {
differencingVhds = Object.values(isVhdDifferencing).filter(t => t).length
dynamicVhds = Object.values(isVhdDifferencing).filter(t => !t).length
if (withMemory) {
// the suspend VDI (memory) is always a dynamic
dynamicVhds -= 1
}
}
return {
disks:
backup.vhds === undefined
@@ -16,6 +31,7 @@ function formatVmBackup(backup) {
}),
id: backup.id,
isImmutable: backup.isImmutable,
jobId: backup.jobId,
mode: backup.mode,
scheduleId: backup.scheduleId,
@@ -25,6 +41,10 @@ function formatVmBackup(backup) {
name_description: backup.vm.name_description,
name_label: backup.vm.name_label,
},
differencingVhds,
dynamicVhds,
withMemory,
}
}

View File

@@ -2,18 +2,21 @@
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable n/shebang */
import { asyncEach } from '@vates/async-each'
import { catchGlobalErrors } from '@xen-orchestra/log/configure'
import { createLogger } from '@xen-orchestra/log'
import { getSyncedHandler } from '@xen-orchestra/fs'
import { join } from 'node:path'
import { load as loadConfig } from 'app-conf'
import Disposable from 'promise-toolbox/Disposable'
import min from 'lodash/min.js'
import { getVmBackupDir } from '../_getVmBackupDir.mjs'
import { RemoteAdapter } from '../RemoteAdapter.mjs'
import { CLEAN_VM_QUEUE } from './index.mjs'
const APP_NAME = 'xo-merge-worker'
const APP_DIR = new URL('.', import.meta.url).pathname
// -------------------------------------------------------------------
catchGlobalErrors(createLogger('xo:backups:mergeWorker'))
@@ -34,6 +37,7 @@ const main = Disposable.wrap(async function* main(args) {
for (let i = 0; i < 10; ++i) {
const entries = await handler.list(CLEAN_VM_QUEUE)
if (entries.length !== 0) {
entries.sort()
return entries
}
await new Promise(timeoutResolver)
@@ -42,38 +46,47 @@ const main = Disposable.wrap(async function* main(args) {
let taskFiles
while ((taskFiles = await listRetry()) !== undefined) {
const taskFileBasename = min(taskFiles)
const previousTaskFile = join(CLEAN_VM_QUEUE, taskFileBasename)
const taskFile = join(CLEAN_VM_QUEUE, '_' + taskFileBasename)
const { concurrency } = await loadConfig(APP_NAME, {
appDir: APP_DIR,
ignoreUnknownFormats: true,
})
await asyncEach(
taskFiles,
async taskFileBasename => {
const previousTaskFile = join(CLEAN_VM_QUEUE, taskFileBasename)
const taskFile = join(CLEAN_VM_QUEUE, '_' + taskFileBasename)
// move this task to the end
try {
await handler.rename(previousTaskFile, taskFile)
} catch (error) {
// this error occurs if the task failed too many times (i.e. too many `_` prefixes)
// there is nothing more that can be done
if (error.code === 'ENAMETOOLONG') {
await handler.unlink(previousTaskFile)
}
// move this task to the end
try {
await handler.rename(previousTaskFile, taskFile)
} catch (error) {
// this error occurs if the task failed too many times (i.e. too many `_` prefixes)
// there is nothing more that can be done
if (error.code === 'ENAMETOOLONG') {
await handler.unlink(previousTaskFile)
}
throw error
}
try {
const vmDir = getVmBackupDir(String(await handler.readFile(taskFile)))
try {
await adapter.cleanVm(vmDir, { merge: true, logInfo: info, logWarn: warn, remove: true })
} catch (error) {
// consider the clean successful if the VM dir is missing
if (error.code !== 'ENOENT') {
throw error
}
}
handler.unlink(taskFile).catch(error => warn('deleting task failure', { error }))
} catch (error) {
warn('failure handling task', { error })
}
try {
const vmDir = getVmBackupDir(String(await handler.readFile(taskFile)))
try {
await adapter.cleanVm(vmDir, { merge: true, logInfo: info, logWarn: warn, remove: true })
} catch (error) {
// consider the clean successful if the VM dir is missing
if (error.code !== 'ENOENT') {
throw error
}
}
handler.unlink(taskFile).catch(error => warn('deleting task failure', { error }))
} catch (error) {
warn('failure handling task', { error })
}
},
{ concurrency }
)
}
})

View File

@@ -0,0 +1 @@
concurrency = 1

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.39.0",
"version": "0.44.6",
"engines": {
"node": ">=14.18"
},
@@ -17,21 +17,23 @@
"test-integration": "node--test *.integ.mjs"
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"@kldzj/stream-throttle": "^1.1.1",
"@vates/async-each": "^1.0.0",
"@vates/cached-dns.lookup": "^1.0.0",
"@vates/compose": "^2.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.4",
"@vates/fuse-vhd": "^1.0.0",
"@vates/nbd-client": "^1.2.1",
"@vates/decorate-with": "^2.1.0",
"@vates/disposable": "^0.1.5",
"@vates/fuse-vhd": "^2.1.0",
"@vates/nbd-client": "^3.0.0",
"@vates/parse-duration": "^0.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^4.0.1",
"@xen-orchestra/fs": "^4.1.4",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/template": "^0.1.0",
"compare-versions": "^5.0.1",
"d3-time-format": "^3.0.0",
"app-conf": "^2.3.0",
"compare-versions": "^6.0.0",
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",
"golike-defer": "^0.5.1",
"limit-concurrency-decorator": "^0.5.0",
@@ -40,20 +42,21 @@
"parse-pairs": "^2.0.0",
"promise-toolbox": "^0.21.0",
"proper-lockfile": "^4.1.2",
"tar": "^6.1.15",
"uuid": "^9.0.0",
"vhd-lib": "^4.5.0",
"xen-api": "^1.3.3",
"vhd-lib": "^4.9.0",
"xen-api": "^2.0.1",
"yazl": "^2.5.1"
},
"devDependencies": {
"fs-extra": "^11.1.0",
"rimraf": "^5.0.1",
"sinon": "^15.0.1",
"sinon": "^17.0.1",
"test": "^3.2.1",
"tmp": "^0.2.1"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^2.2.1"
"@xen-orchestra/xapi": "^4.2.0"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -1,11 +1,10 @@
#!/usr/bin/env node
'use strict'
import { defer } from 'golike-defer'
import { readFileSync } from 'fs'
import { Ref, Xapi } from 'xen-api'
const { Ref, Xapi } = require('xen-api')
const { defer } = require('golike-defer')
const pkg = require('./package.json')
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url)))
Xapi.prototype.getVmDisks = async function (vm) {
const disks = { __proto__: null }

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/cr-seed-cli",
"version": "0.2.0",
"version": "1.0.0",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/cr-seed-cli",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
@@ -10,15 +10,15 @@
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"engines": {
"node": ">=8"
"node": ">=10"
},
"bin": {
"xo-cr-seed": "./index.js"
"xo-cr-seed": "./index.mjs"
},
"preferGlobal": true,
"dependencies": {
"golike-defer": "^0.5.1",
"xen-api": "^1.3.3"
"xen-api": "^2.0.1"
},
"scripts": {
"postversion": "npm publish"

View File

@@ -42,7 +42,7 @@
"test": "node--test"
},
"devDependencies": {
"sinon": "^15.0.1",
"sinon": "^17.0.1",
"test": "^3.2.1"
}
}

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/fs",
"version": "4.0.1",
"version": "4.1.4",
"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",
@@ -28,8 +28,8 @@
"@sindresorhus/df": "^3.1.1",
"@vates/async-each": "^1.0.0",
"@vates/coalesce-calls": "^0.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/read-chunk": "^1.1.1",
"@vates/decorate-with": "^2.1.0",
"@vates/read-chunk": "^1.2.0",
"@xen-orchestra/log": "^0.6.0",
"bind-property-descriptor": "^2.0.0",
"decorator-synchronized": "^0.6.0",
@@ -53,7 +53,7 @@
"cross-env": "^7.0.2",
"dotenv": "^16.0.0",
"rimraf": "^5.0.1",
"sinon": "^15.0.4",
"sinon": "^17.0.1",
"test": "^3.3.0",
"tmp": "^0.2.1"
},

View File

@@ -189,7 +189,7 @@ export default class RemoteHandlerAbstract {
* @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 } = {}) {
async outputStream(path, input, { checksum = true, dirMode, maxStreamLength, streamLength, validator } = {}) {
path = normalizePath(path)
let checksumStream
@@ -201,6 +201,8 @@ export default class RemoteHandlerAbstract {
}
await this._outputStream(path, input, {
dirMode,
maxStreamLength,
streamLength,
validator,
})
if (checksum) {
@@ -362,7 +364,7 @@ export default class RemoteHandlerAbstract {
let data
try {
// this file is not encrypted
data = await this._readFile(normalizePath(ENCRYPTION_DESC_FILENAME), 'utf-8')
data = await this._readFile(normalizePath(ENCRYPTION_DESC_FILENAME))
const json = JSON.parse(data)
encryptionAlgorithm = json.algorithm
} catch (error) {
@@ -375,7 +377,7 @@ export default class RemoteHandlerAbstract {
try {
this.#rawEncryptor = _getEncryptor(encryptionAlgorithm, this._remote.encryptionKey)
// this file is encrypted
const data = await this.__readFile(ENCRYPTION_METADATA_FILENAME, 'utf-8')
const data = await this.__readFile(ENCRYPTION_METADATA_FILENAME)
JSON.parse(data)
} catch (error) {
// can be enoent, bad algorithm, or broeken json ( bad key or algorithm)
@@ -624,14 +626,18 @@ export default class RemoteHandlerAbstract {
const files = await this._list(dir)
await asyncEach(files, file =>
this._unlink(`${dir}/${file}`).catch(error => {
// Unlink dir behavior is not consistent across platforms
// https://github.com/nodejs/node-v0.x-archive/issues/5791
if (error.code === 'EISDIR' || error.code === 'EPERM') {
return this._rmtree(`${dir}/${file}`)
}
throw error
})
this._unlink(`${dir}/${file}`).catch(
error => {
// Unlink dir behavior is not consistent across platforms
// https://github.com/nodejs/node-v0.x-archive/issues/5791
if (error.code === 'EISDIR' || error.code === 'EPERM') {
return this._rmtree(`${dir}/${file}`)
}
throw error
},
// real unlink concurrency will be 2**max directory depth
{ concurrency: 2 }
)
)
return this._rmtree(dir)
}

View File

@@ -171,7 +171,12 @@ export default class LocalHandler extends RemoteHandlerAbstract {
}
}
async _readFile(file, options) {
async _readFile(file, { flags, ...options } = {}) {
// contrary to createReadStream, readFile expect singular `flag`
if (flags !== undefined) {
options.flag = flags
}
const filePath = this.getFilePath(file)
return await this.#addSyncStackTrace(retry, () => fs.readFile(filePath, options), this.#retriesOnEagain)
}

View File

@@ -20,5 +20,7 @@ export function split(path) {
return parts
}
export const relativeFromFile = (file, path) => relative(dirname(file), path)
// paths are made absolute otherwise fs.relative() would resolve them against working directory
export const relativeFromFile = (file, path) => relative(dirname(normalize(file)), normalize(path))
export const resolveFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)

View File

@@ -0,0 +1,17 @@
import { describe, it } from 'test'
import { strict as assert } from 'assert'
import { relativeFromFile } from './path.js'
describe('relativeFromFile()', function () {
for (const [title, args] of Object.entries({
'file absolute and path absolute': ['/foo/bar/file.vhd', '/foo/baz/path.vhd'],
'file relative and path absolute': ['foo/bar/file.vhd', '/foo/baz/path.vhd'],
'file absolute and path relative': ['/foo/bar/file.vhd', 'foo/baz/path.vhd'],
'file relative and path relative': ['foo/bar/file.vhd', 'foo/baz/path.vhd'],
})) {
it('works with ' + title, function () {
assert.equal(relativeFromFile(...args), '../baz/path.vhd')
})
}
})

View File

@@ -5,6 +5,7 @@ import {
CreateMultipartUploadCommand,
DeleteObjectCommand,
GetObjectCommand,
GetObjectLockConfigurationCommand,
HeadObjectCommand,
ListObjectsV2Command,
PutObjectCommand,
@@ -16,22 +17,23 @@ import { NodeHttpHandler } from '@aws-sdk/node-http-handler'
import { getApplyMd5BodyChecksumPlugin } from '@aws-sdk/middleware-apply-body-checksum'
import { Agent as HttpAgent } from 'http'
import { Agent as HttpsAgent } from 'https'
import pRetry from 'promise-toolbox/retry'
import { createLogger } from '@xen-orchestra/log'
import { decorateWith } from '@vates/decorate-with'
import { PassThrough, pipeline } from 'stream'
import { PassThrough, Transform, pipeline } from 'stream'
import { parse } from 'xo-remote-parser'
import copyStreamToBuffer from './_copyStreamToBuffer.js'
import guessAwsRegion from './_guessAwsRegion.js'
import RemoteHandlerAbstract from './abstract'
import { basename, join, split } from './path'
import { asyncEach } from '@vates/async-each'
import { pRetry } from 'promise-toolbox'
// endpoints https://docs.aws.amazon.com/general/latest/gr/s3.html
// limits: https://docs.aws.amazon.com/AmazonS3/latest/dev/qfacts.html
const MAX_PART_SIZE = 1024 * 1024 * 1024 * 5 // 5GB
const { warn } = createLogger('xo:fs:s3')
const MAX_PART_NUMBER = 10000
const MIN_PART_SIZE = 5 * 1024 * 1024
const { debug, info, warn } = createLogger('xo:fs:s3')
export default class S3Handler extends RemoteHandlerAbstract {
#bucket
@@ -72,12 +74,47 @@ export default class S3Handler extends RemoteHandlerAbstract {
}),
})
// Workaround for https://github.com/aws/aws-sdk-js-v3/issues/2673
this.#s3.middlewareStack.use(getApplyMd5BodyChecksumPlugin(this.#s3.config))
const parts = split(path)
this.#bucket = parts.shift()
this.#dir = join(...parts)
const WITH_RETRY = [
'_closeFile',
'_copy',
'_getInfo',
'_getSize',
'_list',
'_mkdir',
'_openFile',
'_outputFile',
'_read',
'_readFile',
'_rename',
'_rmdir',
'_truncate',
'_unlink',
'_write',
'_writeFile',
]
WITH_RETRY.forEach(functionName => {
if (this[functionName] !== undefined) {
// adding the retry on the top level mtehod won't
// cover when _functionName are called internally
this[functionName] = pRetry.wrap(this[functionName], {
delays: [100, 200, 500, 1000, 2000],
// these errors should not change on retry
when: err => !['EEXIST', 'EISDIR', 'ENOTEMPTY', 'ENOENT', 'ENOTDIR', 'EISDIR'].includes(err?.code),
onRetry(error) {
warn('retrying method on fs ', {
method: functionName,
attemptNumber: this.attemptNumber,
delay: this.delay,
error,
file: this.arguments?.[0],
})
},
})
}
})
}
get type() {
@@ -186,11 +223,35 @@ export default class S3Handler extends RemoteHandlerAbstract {
}
}
async _outputStream(path, input, { validator }) {
async _outputStream(path, input, { streamLength, maxStreamLength = streamLength, validator }) {
// S3 storage is limited to 10K part, each part is limited to 5GB. And the total upload must be smaller than 5TB
// a bigger partSize increase the memory consumption of aws/lib-storage exponentially
let partSize
if (maxStreamLength === undefined) {
warn(`Writing ${path} to a S3 remote without a max size set will cut it to 50GB`, { path })
partSize = MIN_PART_SIZE // min size for S3
} else {
partSize = Math.min(Math.max(Math.ceil(maxStreamLength / MAX_PART_NUMBER), MIN_PART_SIZE), MAX_PART_SIZE)
}
// ensure we don't try to upload a stream to big for this partSize
let readCounter = 0
const MAX_SIZE = MAX_PART_NUMBER * partSize
const streamCutter = new Transform({
transform(chunk, encoding, callback) {
readCounter += chunk.length
if (readCounter > MAX_SIZE) {
callback(new Error(`read ${readCounter} bytes, maximum size allowed is ${MAX_SIZE} `))
} else {
callback(null, chunk)
}
},
})
// Workaround for "ReferenceError: ReadableStream is not defined"
// https://github.com/aws/aws-sdk-js-v3/issues/2522
const Body = new PassThrough()
pipeline(input, Body, () => {})
pipeline(input, streamCutter, Body, () => {})
const upload = new Upload({
client: this.#s3,
@@ -198,6 +259,8 @@ export default class S3Handler extends RemoteHandlerAbstract {
...this.#createParams(path),
Body,
},
partSize,
leavePartsOnError: false,
})
await upload.done()
@@ -212,21 +275,6 @@ export default class S3Handler extends RemoteHandlerAbstract {
}
}
// some objectstorage provider like backblaze, can answer a 500/503 routinely
// in this case we should retry, and let their load balancing do its magic
// https://www.backblaze.com/b2/docs/calling.html#error_handling
@decorateWith(pRetry.wrap, {
delays: [100, 200, 500, 1000, 2000],
when: e => e.$metadata?.httpStatusCode === 500,
onRetry(error) {
warn('retrying writing file', {
attemptNumber: this.attemptNumber,
delay: this.delay,
error,
file: this.arguments[0],
})
},
})
async _writeFile(file, data, options) {
return this.#s3.send(
new PutObjectCommand({
@@ -396,6 +444,32 @@ export default class S3Handler extends RemoteHandlerAbstract {
async _closeFile(fd) {}
async _sync() {
await super._sync()
try {
// if Object Lock is enabled, each upload must come with a contentMD5 header
// the computation of this md5 is memory-intensive, especially when uploading a stream
const res = await this.#s3.send(new GetObjectLockConfigurationCommand({ Bucket: this.#bucket }))
if (res.ObjectLockConfiguration?.ObjectLockEnabled === 'Enabled') {
// Workaround for https://github.com/aws/aws-sdk-js-v3/issues/2673
// will automatically add the contentMD5 header to any upload to S3
debug(`Object Lock is enable, enable content md5 header`)
this.#s3.middlewareStack.use(getApplyMd5BodyChecksumPlugin(this.#s3.config))
}
} catch (error) {
// maybe the account doesn't have enought privilege to query the object lock configuration
// be defensive and apply the md5 just in case
if (error.$metadata.httpStatusCode === 403) {
info(`s3 user doesnt have enough privilege to check for Object Lock, enable content MD5 header`)
this.#s3.middlewareStack.use(getApplyMd5BodyChecksumPlugin(this.#s3.config))
} else if (error.Code === 'ObjectLockConfigurationNotFoundError' || error.$metadata.httpStatusCode === 501) {
info(`Object lock is not available or not configured, don't add the content MD5 header`)
} else {
throw error
}
}
}
useVhdDirectory() {
return true
}

View File

@@ -0,0 +1,10 @@
### make a remote immutable
launch the `xo-immutable-remote` command. The configuration is stored in the config file.
This script must be kept running to make file immutable reliably.
### make file mutable
launch the `xo-lift-remote-immutability` cli. The configuration is stored in the config file .
If the config file have a `liftEvery`, this script will contiue to run and check regularly if there are files to update.

View File

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

View File

@@ -0,0 +1,41 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @xen-orchestra/immutable-backups
[![Package Version](https://badgen.net/npm/v/@xen-orchestra/immutable-backups)](https://npmjs.org/package/@xen-orchestra/immutable-backups) ![License](https://badgen.net/npm/license/@xen-orchestra/immutable-backups) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@xen-orchestra/immutable-backups)](https://bundlephobia.com/result?p=@xen-orchestra/immutable-backups) [![Node compatibility](https://badgen.net/npm/node/@xen-orchestra/immutable-backups)](https://npmjs.org/package/@xen-orchestra/immutable-backups)
## Install
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/immutable-backups):
```sh
npm install --save @xen-orchestra/immutable-backups
```
## Usage
### make a remote immutable
launch the `xo-immutable-remote` command. The configuration is stored in the config file.
This script must be kept running to make file immutable reliably.
### make file mutable
launch the `xo-lift-remote-immutability` cli. The configuration is stored in the config file .
If the config file have a `liftEvery`, this script will contiue to run and check regularly if there are files to update.
## 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
[AGPL-3.0-or-later](https://spdx.org/licenses/AGPL-3.0-or-later) © [Vates SAS](https://vates.fr)

View File

@@ -0,0 +1,10 @@
import fs from 'node:fs/promises'
import { dirname, join } from 'node:path'
import isBackupMetadata from './isBackupMetadata.mjs'
export default async path => {
if (isBackupMetadata(path)) {
// snipe vm metadata cache to force XO to update it
await fs.unlink(join(dirname(path), 'cache.json.gz'))
}
}

View File

@@ -0,0 +1,4 @@
import { dirname } from 'node:path'
// check if we are handling file directly under a vhd directory ( bat, headr, footer,..)
export default path => dirname(path).endsWith('.vhd')

View File

@@ -0,0 +1,46 @@
import { load } from 'app-conf'
import { homedir } from 'os'
import { join } from 'node:path'
import ms from 'ms'
const APP_NAME = 'xo-immutable-backups'
const APP_DIR = new URL('.', import.meta.url).pathname
export default async function loadConfig() {
const config = await load(APP_NAME, {
appDir: APP_DIR,
ignoreUnknownFormats: true,
})
if (config.remotes === undefined || config.remotes?.length < 1) {
throw new Error(
'No remotes are configured in the config file, please add at least one [remotes.<remoteid>] with a root property pointing to the absolute path of the remote to watch'
)
}
if (config.liftEvery) {
config.liftEvery = ms(config.liftEvery)
}
for (const [remoteId, { indexPath, immutabilityDuration, root }] of Object.entries(config.remotes)) {
if (!root) {
throw new Error(
`Remote ${remoteId} don't have a root property,containing the absolute path to the root of a backup repository `
)
}
if (!immutabilityDuration) {
throw new Error(
`Remote ${remoteId} don't have a immutabilityDuration property to indicate the minimal duration the backups should be protected by immutability `
)
}
if (ms(immutabilityDuration) < ms('1d')) {
throw new Error(
`Remote ${remoteId} immutability duration is smaller than the minimum allowed (1d), current : ${immutabilityDuration}`
)
}
if (!indexPath) {
const basePath = indexPath ?? process.env.XDG_DATA_HOME ?? join(homedir(), '.local', 'share')
const immutabilityIndexPath = join(basePath, APP_NAME, remoteId)
config.remotes[remoteId].indexPath = immutabilityIndexPath
}
config.remotes[remoteId].immutabilityDuration = ms(immutabilityDuration)
}
return config
}

View File

@@ -0,0 +1,14 @@
# how often does the lift immutability script will run to check if
# some files need to be made mutable
liftEvery = 1h
# you can add as many remote as you want, if you change the id ( here : remote1)
#[remotes.remote1]
#root = "/mnt/ssd/vhdblock/" # the absolute path of the root of the backup repository
#immutabilityDuration = 7d # mandatory
# optional, default value is false will scan and update the index on start, can be expensive
#rebuildIndexOnStart = true
# the index path is optional, default in XDG_DATA_HOME, or if this is not set, in ~/.local/share
#indexPath = "/var/lib/" # will add automatically the application name immutable-backup

View File

@@ -0,0 +1,33 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import { tmpdir } from 'node:os'
import * as Directory from './directory.mjs'
import { rimraf } from 'rimraf'
describe('immutable-backups/file', async () => {
it('really lock a directory', async () => {
const dir = await fs.mkdtemp(path.join(tmpdir(), 'immutable-backups-tests'))
const dataDir = path.join(dir, 'data')
await fs.mkdir(dataDir)
const immutDir = path.join(dir, '.immutable')
const filePath = path.join(dataDir, 'test')
await fs.writeFile(filePath, 'data')
await Directory.makeImmutable(dataDir, immutDir)
assert.strictEqual(await Directory.isImmutable(dataDir), true)
await assert.rejects(() => fs.writeFile(filePath, 'data'))
await assert.rejects(() => fs.appendFile(filePath, 'data'))
await assert.rejects(() => fs.unlink(filePath))
await assert.rejects(() => fs.rename(filePath, filePath + 'copy'))
await assert.rejects(() => fs.writeFile(path.join(dataDir, 'test2'), 'data'))
await assert.rejects(() => fs.rename(dataDir, dataDir + 'copy'))
await Directory.liftImmutability(dataDir, immutDir)
assert.strictEqual(await Directory.isImmutable(dataDir), false)
await fs.writeFile(filePath, 'data')
await fs.appendFile(filePath, 'data')
await fs.unlink(filePath)
await fs.rename(dataDir, dataDir + 'copy')
await rimraf(dir)
})
})

View File

@@ -0,0 +1,21 @@
import execa from 'execa'
import { unindexFile, indexFile } from './fileIndex.mjs'
export async function makeImmutable(dirPath, immutabilityCachePath) {
if (immutabilityCachePath) {
await indexFile(dirPath, immutabilityCachePath)
}
await execa('chattr', ['+i', '-R', dirPath])
}
export async function liftImmutability(dirPath, immutabilityCachePath) {
if (immutabilityCachePath) {
await unindexFile(dirPath, immutabilityCachePath)
}
await execa('chattr', ['-i', '-R', dirPath])
}
export async function isImmutable(path) {
const { stdout } = await execa('lsattr', ['-d', path])
return stdout[4] === 'i'
}

View File

@@ -0,0 +1,114 @@
# Imutability
the goal is to make a remote that XO can write, but not modify during the immutability duration set on the remote. That way, it's not possible for XO to delete or encrypt any backup during this period. It protects your backup agains ransomware, at least as long as the attacker does not have a root access to the remote server.
We target `governance` type of immutability, **the local root account of the remote server will be able to lift immutability**.
We use the file system capabilities, they are tested on the protection process start.
It is compatible with encryption at rest made by XO.
## Prerequisites
The commands must be run as root on the remote, or by a user with the `CAP_LINUX_IMMUTABLE` capability . On start, the protect process writes into the remote `imutability.json` file its status and the immutability duration.
the `chattr` and `lsattr` should be installed on the system
## Configuring
this package uses app-conf to store its config. The application name is `xo-immutable-backup`. A sample config file is provided in this package.
## Making a file immutable
when marking a file or a folder immutable, it create an alias file in the `<indexPath>/<DayOfFileCreation>/<sha256(fullpath)>`.
`indexPath` can be defined in the config file, otherwise `XDG_HOME` is used. If not available it goes to `~/.local/share`
This index is used when lifting the immutability of the remote, it will only look at the old enough `<indexPath>/<DayOfFileCreation>/` folders.
## Real time protecting
On start, the watcher will create the index if it does not exists.
It will also do a checkup to ensure immutability could work on this remote and handle the easiest issues.
The watching process depends on the backup type, since we don't want to make temporary files and cache immutable.
It won't protect files during upload, only when the files have been completly written on disk. Real time, in this case, means "protecting critical files as soon as possible after they are uploaded"
This can be alleviated by :
- Coupling immutability with encryption to ensure the file is not modified
- Making health check to ensure the data are exactly as the snapshot data
List of protected files :
```js
const PATHS = [
// xo configuration backupq
'xo-config-backups/*/*/data',
'xo-config-backups/*/*/data.json',
'xo-config-backups/*/*/metadata.json',
// pool backupq
'xo-pool-metadata-backups/*/metadata.json',
'xo-pool-metadata-backups/*/data',
// vm backups , xo-vm-backups/<vmuuid>/
'xo-vm-backups/*/*.json',
'xo-vm-backups/*/*.xva',
'xo-vm-backups/*/*.xva.checksum',
// xo-vm-backups/<vmuuid>/vdis/<jobid>/<vdiUuid>
'xo-vm-backups/*/vdis/*/*/*.vhd', // can be an alias or a vhd file
// for vhd directory :
'xo-vm-backups/*/vdis/*/*/data/*.vhd/bat',
'xo-vm-backups/*/vdis/*/*/data/*.vhd/header',
'xo-vm-backups/*/vdis/*/*/data/*.vhd/footer',
]
```
## Releasing protection on old enough files on a remote
the watcher will periodically check if some file must by unlocked
## Troubleshooting
### some files are still locked
add the `rebuildIndexOnStart` option to the config file
### make remote fully mutable again
- Update the immutability setting with a 0 duration
- launch the `liftProtection` cli.
- remove the `protectRemotes` service
### increasing the immutability duration
this will prolong immutable file, but won't protect files that are already out of immutability
### reducing the immutability duration
change the setting, and launch the `liftProtection` cli , or wait for next planed execution
### why are my incremental backups not marked as protected in XO ?
are not marked as protected in XO ?
For incremental backups to be marked as protected in XO, the entire chain must be under protection. To ensure at least 7 days of backups are protected, you need to set the immutability duration and retention at 14 days, the full backup interval at 7 days
That means that if the last backup chain is complete ( 7 backup ) it is completely under protection, and if not, the precedent chain is also under protection. K are key backups, and are delta
```
Kd Kdddddd Kdddddd K # 8 backups protected, 2 chains
K Kdddddd Kdddddd Kd # 9 backups protected, 2 chains
Kdddddd Kdddddd Kdd # 10 backups protected, 2 chains
Kddddd Kdddddd Kddd # 11 backups protected, 2 chains
Kdddd Kdddddd Kdddd # 12 backups protected, 2 chains
Kddd Kdddddd Kddddd # 13 backups protected, 2 chains
Kdd Kdddddd Kdddddd # 7 backups protected, 1 chain since precedent full is now mutable
Kd Kdddddd Kdddddd K # 8 backups protected, 2 chains
```
### Why doesn't the protect process start ?
- it should be run as root or by a user with the `CAP_LINUX_IMMUTABLE` capability
- the underlying file system should support immutability, especially the `chattr` and `lsattr` command
- logs are in journalctl

View File

@@ -0,0 +1,29 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import * as File from './file.mjs'
import { tmpdir } from 'node:os'
import { rimraf } from 'rimraf'
describe('immutable-backups/file', async () => {
it('really lock a file', async () => {
const dir = await fs.mkdtemp(path.join(tmpdir(), 'immutable-backups-tests'))
const immutDir = path.join(dir, '.immutable')
const filePath = path.join(dir, 'test.ext')
await fs.writeFile(filePath, 'data')
assert.strictEqual(await File.isImmutable(filePath), false)
await File.makeImmutable(filePath, immutDir)
assert.strictEqual(await File.isImmutable(filePath), true)
await assert.rejects(() => fs.writeFile(filePath, 'data'))
await assert.rejects(() => fs.appendFile(filePath, 'data'))
await assert.rejects(() => fs.unlink(filePath))
await assert.rejects(() => fs.rename(filePath, filePath + 'copy'))
await File.liftImmutability(filePath, immutDir)
assert.strictEqual(await File.isImmutable(filePath), false)
await fs.writeFile(filePath, 'data')
await fs.appendFile(filePath, 'data')
await fs.unlink(filePath)
await rimraf(dir)
})
})

View File

@@ -0,0 +1,24 @@
import execa from 'execa'
import { unindexFile, indexFile } from './fileIndex.mjs'
// this work only on linux like systems
// this could work on windows : https://4sysops.com/archives/set-and-remove-the-read-only-file-attribute-with-powershell/
export async function makeImmutable(path, immutabilityCachePath) {
if (immutabilityCachePath) {
await indexFile(path, immutabilityCachePath)
}
await execa('chattr', ['+i', path])
}
export async function liftImmutability(filePath, immutabilityCachePath) {
if (immutabilityCachePath) {
await unindexFile(filePath, immutabilityCachePath)
}
await execa('chattr', ['-i', filePath])
}
export async function isImmutable(path) {
const { stdout } = await execa('lsattr', ['-d', path])
return stdout[4] === 'i'
}

View File

@@ -0,0 +1,81 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import * as FileIndex from './fileIndex.mjs'
import * as Directory from './directory.mjs'
import { tmpdir } from 'node:os'
import { rimraf } from 'rimraf'
describe('immutable-backups/fileIndex', async () => {
it('index File changes', async () => {
const dir = await fs.mkdtemp(path.join(tmpdir(), 'immutable-backups-tests'))
const immutDir = path.join(dir, '.immutable')
const filePath = path.join(dir, 'test.ext')
await fs.writeFile(filePath, 'data')
await FileIndex.indexFile(filePath, immutDir)
await fs.mkdir(path.join(immutDir, 'NOTADATE'))
await fs.writeFile(path.join(immutDir, 'NOTADATE.file'), 'content')
let nb = 0
let index, target
for await ({ index, target } of FileIndex.listOlderTargets(immutDir, 0)) {
assert.strictEqual(true, false, 'Nothing should be eligible for deletion')
}
nb = 0
for await ({ index, target } of FileIndex.listOlderTargets(immutDir, -24 * 60 * 60 * 1000)) {
assert.strictEqual(target, filePath)
await fs.unlink(index)
nb++
}
assert.strictEqual(nb, 1)
await fs.rmdir(path.join(immutDir, 'NOTADATE'))
await fs.rm(path.join(immutDir, 'NOTADATE.file'))
for await ({ index, target } of FileIndex.listOlderTargets(immutDir, -24 * 60 * 60 * 1000)) {
// should remove the empty dir
assert.strictEqual(true, false, 'Nothing should have stayed here')
}
assert.strictEqual((await fs.readdir(immutDir)).length, 0)
await rimraf(dir)
})
it('fails correctly', async () => {
const dir = await fs.mkdtemp(path.join(tmpdir(), 'immutable-backups-tests'))
const immutDir = path.join(dir, '.immutable')
await fs.mkdir(immutDir)
const placeholderFile = path.join(dir, 'test.ext')
await fs.writeFile(placeholderFile, 'data')
await FileIndex.indexFile(placeholderFile, immutDir)
const filePath = path.join(dir, 'test2.ext')
await fs.writeFile(filePath, 'data')
await FileIndex.indexFile(filePath, immutDir)
await assert.rejects(() => FileIndex.indexFile(filePath, immutDir), { code: 'EEXIST' })
await Directory.makeImmutable(immutDir)
await assert.rejects(() => FileIndex.unindexFile(filePath, immutDir), { code: 'EPERM' })
await Directory.liftImmutability(immutDir)
await rimraf(dir)
})
it('handles bomb index files', async () => {
const dir = await fs.mkdtemp(path.join(tmpdir(), 'immutable-backups-tests'))
const immutDir = path.join(dir, '.immutable')
await fs.mkdir(immutDir)
const placeholderFile = path.join(dir, 'test.ext')
await fs.writeFile(placeholderFile, 'data')
await FileIndex.indexFile(placeholderFile, immutDir)
const indexDayDir = path.join(immutDir, '1980,11-28')
await fs.mkdir(indexDayDir)
await fs.writeFile(path.join(indexDayDir, 'big'), Buffer.alloc(2 * 1024 * 1024))
assert.rejects(async () => {
let index, target
for await ({ index, target } of FileIndex.listOlderTargets(immutDir, 0)) {
// should remove the empty dir
assert.strictEqual(true, false, `Nothing should have stayed here, got ${index} ${target}`)
}
})
await rimraf(dir)
})
})

View File

@@ -0,0 +1,88 @@
import { join } from 'node:path'
import { createHash } from 'node:crypto'
import fs from 'node:fs/promises'
import { dirname } from 'path'
const MAX_INDEX_FILE_SIZE = 1024 * 1024
function sha256(content) {
return createHash('sha256').update(content).digest('hex')
}
function formatDate(date) {
return date.toISOString().split('T')[0]
}
async function computeIndexFilePath(path, immutabilityIndexPath) {
const stat = await fs.stat(path)
const date = new Date(stat.birthtimeMs)
const day = formatDate(date)
const hash = sha256(path)
return join(immutabilityIndexPath, day, hash)
}
export async function indexFile(path, immutabilityIndexPath) {
const indexFilePath = await computeIndexFilePath(path, immutabilityIndexPath)
try {
await fs.writeFile(indexFilePath, path, { flag: 'wx' })
} catch (err) {
// missing dir: make it
if (err.code === 'ENOENT') {
await fs.mkdir(dirname(indexFilePath), { recursive: true })
await fs.writeFile(indexFilePath, path)
} else {
throw err
}
}
return indexFilePath
}
export async function unindexFile(path, immutabilityIndexPath) {
try {
const cacheFileName = await computeIndexFilePath(path, immutabilityIndexPath)
await fs.unlink(cacheFileName)
} catch (err) {
if (err.code !== 'ENOENT') {
throw err
}
}
}
export async function* listOlderTargets(immutabilityCachePath, immutabilityDuration) {
// walk all dir by day until the limit day
const limitDate = new Date(Date.now() - immutabilityDuration)
const limitDay = formatDate(limitDate)
const dir = await fs.opendir(immutabilityCachePath)
for await (const dirent of dir) {
if (dirent.isFile()) {
continue
}
// ensure we have a valid date
if (isNaN(new Date(dirent.name))) {
continue
}
// recent enough to be kept
if (dirent.name >= limitDay) {
continue
}
const subDirPath = join(immutabilityCachePath, dirent.name)
const subdir = await fs.opendir(subDirPath)
let nb = 0
for await (const hashFileEntry of subdir) {
const entryFullPath = join(subDirPath, hashFileEntry.name)
const { size } = await fs.stat(entryFullPath)
if (size > MAX_INDEX_FILE_SIZE) {
throw new Error(`Index file at ${entryFullPath} is too big, ${size} bytes `)
}
const targetPath = await fs.readFile(entryFullPath, { encoding: 'utf8' })
yield {
index: entryFullPath,
target: targetPath,
}
nb++
}
// cleanup older folder
if (nb === 0) {
await fs.rmdir(subDirPath)
}
}
}

View File

@@ -0,0 +1 @@
export default path => path.match(/xo-vm-backups\/[^/]+\/[^/]+\.json$/)

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env node
import fs from 'node:fs/promises'
import * as Directory from './directory.mjs'
import { createLogger } from '@xen-orchestra/log'
import { listOlderTargets } from './fileIndex.mjs'
import cleanXoCache from './_cleanXoCache.mjs'
import loadConfig from './_loadConfig.mjs'
const { info, warn } = createLogger('xen-orchestra:immutable-backups:liftProtection')
async function liftRemoteImmutability(immutabilityCachePath, immutabilityDuration) {
for await (const { index, target } of listOlderTargets(immutabilityCachePath, immutabilityDuration)) {
await Directory.liftImmutability(target, immutabilityCachePath)
await fs.unlink(index)
await cleanXoCache(target)
}
}
async function liftImmutability(remotes) {
for (const [remoteId, { indexPath, immutabilityDuration }] of Object.entries(remotes)) {
liftRemoteImmutability(indexPath, immutabilityDuration).catch(err =>
warn('error during watchRemote', { err, remoteId, indexPath, immutabilityDuration })
)
}
}
const { liftEvery, remotes } = await loadConfig()
if (liftEvery > 0) {
info('setup watcher for immutability lifting')
setInterval(async () => {
liftImmutability(remotes)
}, liftEvery)
} else {
liftImmutability(remotes)
}

View File

@@ -0,0 +1,42 @@
{
"private": false,
"name": "@xen-orchestra/immutable-backups",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/immutable-backups",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/immutable-backups",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"bin": {
"xo-immutable-remote": "./protectRemotes.mjs",
"xo-lift-remote-immutability": "./liftProtection.mjs"
},
"license": "AGPL-3.0-or-later",
"version": "1.0.1",
"engines": {
"node": ">=14.0.0"
},
"dependencies": {
"@vates/async-each": "^1.0.0",
"@xen-orchestra/backups": "^0.44.6",
"@xen-orchestra/log": "^0.6.0",
"app-conf": "^2.3.0",
"chokidar": "^3.5.3",
"execa": "^5.0.0",
"ms": "^2.1.3",
"vhd-lib": "^4.7.0"
},
"devDependencies": {
"rimraf": "^5.0.5",
"tap": "^18.6.1"
},
"scripts": {
"postversion": "npm publish --access public",
"test-integration": "tap *.integ.mjs"
}
}

View File

@@ -0,0 +1,191 @@
#!/usr/bin/env node
import fs from 'node:fs/promises'
import * as File from './file.mjs'
import * as Directory from './directory.mjs'
import assert from 'node:assert'
import { dirname, join, sep } from 'node:path'
import { createLogger } from '@xen-orchestra/log'
import chokidar from 'chokidar'
import { indexFile } from './fileIndex.mjs'
import cleanXoCache from './_cleanXoCache.mjs'
import loadConfig from './_loadConfig.mjs'
import isInVhdDirectory from './_isInVhdDirectory.mjs'
const { debug, info, warn } = createLogger('xen-orchestra:immutable-backups:remote')
async function test(remotePath, indexPath) {
await fs.readdir(remotePath)
const testPath = join(remotePath, '.test-immut')
// cleanup
try {
await File.liftImmutability(testPath, indexPath)
await fs.unlink(testPath)
} catch (err) {}
// can create , modify and delete a file
await fs.writeFile(testPath, `test immut ${new Date()}`)
await fs.writeFile(testPath, `test immut change 1 ${new Date()}`)
await fs.unlink(testPath)
// cannot modify or delete an immutable file
await fs.writeFile(testPath, `test immut ${new Date()}`)
await File.makeImmutable(testPath, indexPath)
await assert.rejects(fs.writeFile(testPath, `test immut change 2 ${new Date()}`), { code: 'EPERM' })
await assert.rejects(fs.unlink(testPath), { code: 'EPERM' })
// can modify and delete a file after lifting immutability
await File.liftImmutability(testPath, indexPath)
await fs.writeFile(testPath, `test immut change 3 ${new Date()}`)
await fs.unlink(testPath)
}
async function handleExistingFile(root, indexPath, path) {
try {
// a vhd block directory is completly immutable
if (isInVhdDirectory(path)) {
// this will trigger 3 times per vhd blocks
const dir = join(root, dirname(path))
if (Directory.isImmutable(dir)) {
await indexFile(dir, indexPath)
}
} else {
// other files are immutable a file basis
const fullPath = join(root, path)
if (File.isImmutable(fullPath)) {
await indexFile(fullPath, indexPath)
}
}
} catch (error) {
if (error.code !== 'EEXIST') {
// there can be a symbolic link in the tree
warn('handleExistingFile', { error })
}
}
}
async function handleNewFile(root, indexPath, pendingVhds, path) {
// with awaitWriteFinish we have complete files here
// we can make them immutable
if (isInVhdDirectory(path)) {
// watching a vhd block
// wait for header/footer and BAT before making this immutable recursively
const splitted = path.split(sep)
const vmUuid = splitted[1]
const vdiUuid = splitted[4]
const uniqPath = `${vmUuid}/${vdiUuid}`
const { existing } = pendingVhds.get(uniqPath) ?? {}
if (existing === undefined) {
pendingVhds.set(uniqPath, { existing: 1, lastModified: Date.now() })
} else {
// already two of the key files,and we got the last one
if (existing === 2) {
await Directory.makeImmutable(join(root, dirname(path)), indexPath)
pendingVhds.delete(uniqPath)
} else {
// wait for the other
pendingVhds.set(uniqPath, { existing: existing + 1, lastModified: Date.now() })
}
}
} else {
const fullFilePath = join(root, path)
await File.makeImmutable(fullFilePath, indexPath)
await cleanXoCache(fullFilePath)
}
}
export async function watchRemote(remoteId, { root, immutabilityDuration, rebuildIndexOnStart = false, indexPath }) {
// create index directory
await fs.mkdir(indexPath, { recursive: true })
// test if fs and index directories are well configured
await test(root, indexPath)
// add duration and watch status in the metadata.json of the remote
const settingPath = join(root, 'immutability.json')
try {
// this file won't be made mutable by liftimmutability
await File.liftImmutability(settingPath)
} catch (error) {
// file may not exists, and it's not really a problem
info('lifting immutability on current settings', { error })
}
await fs.writeFile(
settingPath,
JSON.stringify({
since: Date.now(),
immutable: true,
duration: immutabilityDuration,
})
)
// no index path in makeImmutable(): the immutability won't be lifted
File.makeImmutable(settingPath)
// we wait for footer/header AND BAT to be written before locking a vhd directory
// this map allow us to track the vhd with partial metadata
const pendingVhds = new Map()
// cleanup pending vhd map periodically
setInterval(
() => {
pendingVhds.forEach(({ lastModified, existing }, path) => {
if (Date.now() - lastModified > 60 * 60 * 1000) {
pendingVhds.delete(path)
warn(`vhd at ${path} is incomplete since ${lastModified}`, { existing, lastModified, path })
}
})
},
60 * 60 * 1000
)
// watch the remote for any new VM metadata json file
const PATHS = [
'xo-config-backups/*/*/data',
'xo-config-backups/*/*/data.json',
'xo-config-backups/*/*/metadata.json',
'xo-pool-metadata-backups/*/metadata.json',
'xo-pool-metadata-backups/*/data',
// xo-vm-backups/<vmuuid>/
'xo-vm-backups/*/*.json',
'xo-vm-backups/*/*.xva',
'xo-vm-backups/*/*.xva.checksum',
// xo-vm-backups/<vmuuid>/vdis/<jobid>/<vdiUuid>
'xo-vm-backups/*/vdis/*/*/*.vhd', // can be an alias or a vhd file
// for vhd directory :
'xo-vm-backups/*/vdis/*/*/data/*.vhd/bat',
'xo-vm-backups/*/vdis/*/*/data/*.vhd/header',
'xo-vm-backups/*/vdis/*/*/data/*.vhd/footer',
]
let ready = false
const watcher = chokidar.watch(PATHS, {
ignored: [
/(^|[/\\])\../, // ignore dotfiles
/\.lock$/,
],
cwd: root,
recursive: false, // vhd directory can generate a lot of folder, don't let chokidar choke on this
ignoreInitial: !rebuildIndexOnStart,
depth: 7,
awaitWriteFinish: true,
})
// Add event listeners.
watcher
.on('add', async path => {
debug(`File ${path} has been added ${path.split('/').length}`)
if (ready) {
await handleNewFile(root, indexPath, pendingVhds, path)
} else {
await handleExistingFile(root, indexPath, path)
}
})
.on('error', error => warn(`Watcher error: ${error}`))
.on('ready', () => {
ready = true
info('Ready for changes')
})
}
const { remotes } = await loadConfig()
for (const [remoteId, remote] of Object.entries(remotes)) {
watchRemote(remoteId, remote).catch(err => warn('error during watchRemote', { err, remoteId, remote }))
}

View File

@@ -1,28 +0,0 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution");
module.exports = {
globals: {
XO_LITE_GIT_HEAD: true,
XO_LITE_VERSION: true,
},
root: true,
env: {
node: true,
},
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-typescript/recommended",
"@vue/eslint-config-prettier",
],
plugins: ["@limegrass/import-alias"],
rules: {
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"@limegrass/import-alias/import-alias": [
"error",
{ aliasConfigPath: require("path").join(__dirname, "tsconfig.json") },
],
},
};

View File

@@ -1,2 +0,0 @@
// Keeping this file to prevent applying the global monorepo config for now
module.exports = {};

View File

@@ -2,8 +2,51 @@
## **next**
- Fix Typescript typings errors when running `yarn type-check` command (PR [#7278](https://github.com/vatesfr/xen-orchestra/pull/7278))
- Introduce PWA Json Manifest (PR [#7291](https://github.com/vatesfr/xen-orchestra/pull/7291))
## **0.1.7** (2023-12-28)
- [VM/Action] Ability to migrate a VM from its view (PR [#7164](https://github.com/vatesfr/xen-orchestra/pull/7164))
- Ability to override host address with `master` URL query param (PR [#7187](https://github.com/vatesfr/xen-orchestra/pull/7187))
- Added tooltip on CPU provisioning warning icon (PR [#7223](https://github.com/vatesfr/xen-orchestra/pull/7223))
- Add indeterminate state on FormToggle component (PR [#7230](https://github.com/vatesfr/xen-orchestra/pull/7230))
- Add new UiStatusPanel component (PR [#7227](https://github.com/vatesfr/xen-orchestra/pull/7227))
- XOA quick deploy (PR [#7245](https://github.com/vatesfr/xen-orchestra/pull/7245))
- Fix infinite loader when no stats on pool dashboard (PR [#7236](https://github.com/vatesfr/xen-orchestra/pull/7236))
- [Tree view] Display VMs count (PR [#7185](https://github.com/vatesfr/xen-orchestra/pull/7185))
## **0.1.6** (2023-11-30)
- Explicit error if users attempt to connect from a slave host (PR [#7110](https://github.com/vatesfr/xen-orchestra/pull/7110))
- More compact UI (PR [#7159](https://github.com/vatesfr/xen-orchestra/pull/7159))
- Fix dashboard host patches list (PR [#7169](https://github.com/vatesfr/xen-orchestra/pull/7169))
- Ability to export selected VMs (PR [#7174](https://github.com/vatesfr/xen-orchestra/pull/7174))
- [VM/Action] Ability to export a VM from its view (PR [#7190](https://github.com/vatesfr/xen-orchestra/pull/7190))
## **0.1.5** (2023-11-07)
- Ability to snapshot/copy a VM from its view (PR [#7087](https://github.com/vatesfr/xen-orchestra/pull/7087))
- [Header] Replace logo with "XO LITE" (PR [#7118](https://github.com/vatesfr/xen-orchestra/pull/7118))
- New VM console toolbar + Ability to send Ctrl+Alt+Del (PR [#7088](https://github.com/vatesfr/xen-orchestra/pull/7088))
- Total overhaul of the modal system (PR [#7134](https://github.com/vatesfr/xen-orchestra/pull/7134))
## **0.1.4** (2023-10-03)
- Ability to migrate selected VMs to another host (PR [#7040](https://github.com/vatesfr/xen-orchestra/pull/7040))
- Ability to snapshot selected VMs (PR [#7021](https://github.com/vatesfr/xen-orchestra/pull/7021))
- Add Patches to Pool Dashboard (PR [#6709](https://github.com/vatesfr/xen-orchestra/pull/6709))
- Add remember me checkbox on the login page (PR [#7030](https://github.com/vatesfr/xen-orchestra/pull/7030))
## **0.1.3** (2023-09-01)
- Add Alarms to Pool Dashboard (PR [#6976](https://github.com/vatesfr/xen-orchestra/pull/6976))
## **0.1.2** (2023-07-28)
- Ability to export selected VMs as CSV file (PR [#6915](https://github.com/vatesfr/xen-orchestra/pull/6915))
- [Pool/VMs] Ability to export selected VMs as JSON file (PR [#6911](https://github.com/vatesfr/xen-orchestra/pull/6911))
- Add Tasks to Pool Dashboard (PR [#6713](https://github.com/vatesfr/xen-orchestra/pull/6713))
## **0.1.1** (2023-07-03)

View File

@@ -48,18 +48,16 @@ Note: When reading Vue official doc, don't forget to set "API Preference" toggle
```vue
<script lang="ts" setup>
import { computed, ref } from "vue";
import { computed, ref } from 'vue'
const props = defineProps<{
greetings: string;
}>();
greetings: string
}>()
const firstName = ref("");
const lastName = ref("");
const firstName = ref('')
const lastName = ref('')
const fullName = computed(
() => `${props.greetings} ${firstName.value} ${lastName.value}`
);
const fullName = computed(() => `${props.greetings} ${firstName.value} ${lastName.value}`)
</script>
```
@@ -73,9 +71,9 @@ Vue variables can be interpolated with `v-bind`.
```vue
<script lang="ts" setup>
import { ref } from "vue";
import { ref } from 'vue'
const fontSize = ref("2rem");
const fontSize = ref('2rem')
</script>
<style scoped>
@@ -105,8 +103,8 @@ Use the `busy` prop to display a loader icon.
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import { faDisplay } from '@fortawesome/free-solid-svg-icons'
</script>
```
@@ -140,21 +138,21 @@ For a `foobar` store, create a `store/foobar.store.ts` then use `defineStore('fo
#### Example
```typescript
import { computed, ref } from "vue";
import { computed, ref } from 'vue'
export const useFoobarStore = defineStore("foobar", () => {
const aStateVar = ref(0);
const otherStateVar = ref(0);
const aGetter = computed(() => aStateVar.value * 2);
const anAction = () => (otherStateVar.value += 10);
export const useFoobarStore = defineStore('foobar', () => {
const aStateVar = ref(0)
const otherStateVar = ref(0)
const aGetter = computed(() => aStateVar.value * 2)
const anAction = () => (otherStateVar.value += 10)
return {
aStateVar,
otherStateVar,
aGetter,
anAction,
};
});
}
})
```
### I18n

View File

@@ -85,9 +85,9 @@ In your `.story.vue` file, import and use the `ComponentStory` component.
</template>
<script lang="ts" setup>
import MyComponent from "@/components/MyComponent.vue";
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import { prop, event, model, slot, setting } from "@/libs/story/story-param";
import MyComponent from '@/components/MyComponent.vue'
import ComponentStory from '@/components/component-story/ComponentStory.vue'
import { prop, event, model, slot, setting } from '@/libs/story/story-param'
</script>
```
@@ -119,27 +119,27 @@ Let's take this Vue component:
<script lang="ts" setup>
withDefaults(
defineProps<{
imString: string;
imNumber: number;
imOptional?: string;
imOptionalWithDefault?: string;
modelValue?: string;
customModel?: number;
imString: string
imNumber: number
imOptional?: string
imOptionalWithDefault?: string
modelValue?: string
customModel?: number
}>(),
{ imOptionalWithDefault: "Hi World" }
);
{ imOptionalWithDefault: 'Hi World' }
)
const emit = defineEmits<{
(event: "click"): void;
(event: "clickWithArg", id: string): void;
(event: "update:modelValue", value: string): void;
(event: "update:customModel", value: number): void;
}>();
(event: 'click'): void
(event: 'clickWithArg', id: string): void
(event: 'update:modelValue', value: string): void
(event: 'update:customModel', value: number): void
}>()
const moonDistance = 384400;
const moonDistance = 384400
const handleClick = () => emit("click");
const handleClickWithArg = (id: string) => emit("clickWithArg", id);
const handleClick = () => emit('click')
const handleClickWithArg = (id: string) => emit('clickWithArg', id)
</script>
```
@@ -150,53 +150,33 @@ Here is how to document it with a Component Story:
<ComponentStory
v-slot="{ properties, settings }"
:params="[
prop('imString')
.str()
.required()
.preset('Example')
.widget()
.help('This is a required string prop'),
prop('imNumber')
.num()
.required()
.preset(42)
.widget()
.help('This is a required number prop'),
prop('imString').str().required().preset('Example').widget().help('This is a required string prop'),
prop('imNumber').num().required().preset(42).widget().help('This is a required number prop'),
prop('imOptional').str().widget().help('This is an optional string prop'),
prop('imOptionalWithDefault')
.str()
.default('Hi World')
.widget()
.default('My default value'),
model().prop((p) => p.str()),
model('customModel').prop((p) => p.num()),
prop('imOptionalWithDefault').str().default('Hi World').widget().default('My default value'),
model().prop(p => p.str()),
model('customModel').prop(p => p.num()),
event('click').help('Emitted when the user clicks the first button'),
event('clickWithArg')
.args({ id: 'string' })
.help('Emitted when the user clicks the second button'),
event('clickWithArg').args({ id: 'string' }).help('Emitted when the user clicks the second button'),
slot().help('This is the default slot'),
slot('namedSlot').help('This is a named slot'),
slot('namedScopedSlot')
.prop('moon-distance', 'number')
.help('This is a named slot'),
slot('namedScopedSlot').prop('moon-distance', 'number').help('This is a named slot'),
setting('contentExample').widget(text()).preset('Some content'),
]"
>
<MyComponent v-bind="properties">
{{ settings.contentExample }}
<template #named-slot>Named slot content</template>
<template #named-scoped-slot="{ moonDistance }">
Moon distance is {{ moonDistance }} meters.
</template>
<template #named-scoped-slot="{ moonDistance }"> Moon distance is {{ moonDistance }} meters. </template>
</MyComponent>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import MyComponent from "@/components/MyComponent.vue";
import { event, model, prop, setting, slot } from "@/libs/story/story-param";
import { text } from "@/libs/story/story-widget";
import ComponentStory from '@/components/component-story/ComponentStory.vue'
import MyComponent from '@/components/MyComponent.vue'
import { event, model, prop, setting, slot } from '@/libs/story/story-param'
import { text } from '@/libs/story/story-widget'
</script>
```

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