Compare commits

..

660 Commits

Author SHA1 Message Date
Thierry
5dba45f6cf update readme and componentstory to use new menu components 2025-01-10 10:38:40 +01:00
Thierry
b112a72f23 continue menu implementation 2025-01-10 10:08:36 +01:00
Thierry
069ed9215f feat(web-stack): start new menu implementation 2025-01-09 11:32:52 +01:00
Joris K
93a11cfb28 fix(lite): fix issue due to update dev deps (#8218) 2025-01-06 11:31:57 +01:00
Julien Fontanet
fd9c9b214a chore: update dev deps 2024-12-31 12:17:18 +01:00
Julien Fontanet
34c1cce029 fix(CHANGELOG): add 5.102.0 Highlights
Introduced by 320a61323
2024-12-30 13:56:14 +01:00
Julien Fontanet
320a61323c feat: release 5.102.0 2024-12-30 13:38:05 +01:00
Mathieu
db9c85df7a feat: technical release (#8214) 2024-12-20 17:34:32 +01:00
Mathieu
c4104b2d58 chore(CHANGELOG.unreleased.md): add missing @xen-orchestra/xapi entry (#8213) 2024-12-20 17:24:11 +01:00
Mathieu
79847a9129 feat: technical release (#8212) 2024-12-20 16:18:43 +01:00
Pierre Donias
0ec86e409c chore(CHANGELOG.unreleased.md): add missing XO 6 entry (#8211) 2024-12-20 16:04:54 +01:00
MlssFrncJrg
c41ae05275 feat(xo-server,xo-web): in host advanced tab, display mdadm status (#8190)
Fixes #7634
2024-12-20 15:59:38 +01:00
Paloma
9af2463922 feat(xo6/dashboard): add Pool Status component to dashboard (#7800) 2024-12-20 15:57:26 +01:00
Pierre Donias
22f9b3598d feat(xo-server/signin): temporary design matching XO 6 color palette (#8209) 2024-12-20 15:56:16 +01:00
MlssFrncJrg
97e7b9e5fe feat(xo-server/xo-web): in dashboard view, display more alarms (#8199) 2024-12-20 15:47:34 +01:00
Mathieu
87cfa649d3 fix(xo-server): fix 'HANDLE_INVALID' when creating VM (#8208)
Introduced by 04dd9e9
2024-12-20 15:23:03 +01:00
Sebastian CZECH
a945888e63 feat: technical release (#8207) 2024-12-20 09:34:02 +01:00
b-Nollet
4833c143de docs(backups): describe config (#8169) 2024-12-19 18:09:31 +01:00
MlssFrncJrg
8c8519f02d fix(xo-web): fix 'Pool is undefined' when creating a private network (#8188) 2024-12-19 17:02:29 +01:00
Mathieu
8ddd7b5290 feat(xo-web/VM/advanced): ability to manage XenStore entries (#8174)
See Zammad#26951
2024-12-19 16:13:10 +01:00
Mathieu
2b679c6cc1 feat(xo-server/rest): add pools status in the /dashboard endpoint (#8203) 2024-12-19 15:47:27 +01:00
Thomas Moraine
2f2a0aeed0 docs: cloudbase-init documentation (#8173)
Xen Orchestra now supports Cloudbase-init (for Windows VMs),
on top of the already-supported Cloud-init (for Linux VMs).
The Cloud-init section is therefore expanded to mention Cloudbase-init,
and we explain how to create a template for Windows VMs using Cloudbase-init.
2024-12-19 14:51:38 +01:00
OlivierFL
68168647aa fix(xo-core/UiInfo): issue with typography and add prop to handle text wrap (#8205) 2024-12-19 10:37:58 +01:00
Joris K
fd7d6b7ad7 feat(web-core): add table-pagination component (#8164) 2024-12-19 10:12:09 +01:00
Sebastian CZECH
086a42f38d feat(core): create connection status component (#8204) 2024-12-18 15:09:15 +01:00
Sebastian CZECH
d864cb57e0 feat: technical release (#8202) 2024-12-18 14:53:31 +01:00
OlivierFL
ee76c2c481 feat(xo-core): update UiLabel component to v1 (#8187) 2024-12-18 11:54:31 +01:00
Joris K
28cd54780d fix(xo-lite): fix issue with persistent scrollbar (#8191)
* Introduced by 21f0643

* change padding and add class layout to fix issue with persistent scrollbar

* reduction of the size of the icon (in the center of the nav bar), when reducing the screen, to avoid the double scrollbar
2024-12-18 09:29:20 +01:00
Florent BEAUCHAMP
e5acbe7a32 fix(backups): use callAsync instead of call for CBT enabling/disabling (#8184)
enabling and disabling CBT can take a while, and can timeout
with xapi.call().
2024-12-17 14:01:50 +01:00
Joris K
8bfd864ed8 feat(web-core): update UiInput component (#8117) 2024-12-17 11:25:34 +01:00
Mathieu
3c0a5df8e8 feat(xo-server, xo-web/patches): List missing patches for XenServer >= 8.4 (#8183) 2024-12-17 09:31:36 +01:00
Florent BEAUCHAMP
14da4fb8df VMs with this tag xo:no-health-check or xo:no-health-check=REASON will not be tested by health check (#8189) 2024-12-16 17:03:06 +01:00
Mathieu
5a933ed9ef fix(xo-server): fix 'no object with ...' during VM creation (#8195)
fix(xo-server): fix 'no object with during VM creation
2024-12-16 16:48:46 +01:00
Sebastian CZECH
56a18eba9a feat: technical release (#8196) 2024-12-16 11:02:22 +01:00
Stephane
1aef44bca5 feat(xo-web): merge backups synchronously (#8177) 2024-12-16 09:33:44 +01:00
Mathieu
04dd9e9c40 feat(xo-server/rest): ability to manage VDIs using REST API when creating VM (#8167) 2024-12-13 15:36:16 +01:00
Florent BEAUCHAMP
4a54685829 feat(xo-web): add rolling pool reboot and update to the plannable jobs (#8185) 2024-12-13 15:29:48 +01:00
Julien Fontanet
1bc0f8f245 fix(xapi/VM_destroy): use callAsync when appropriate
Related to zammad#32397
2024-12-11 17:38:29 +01:00
Julien Fontanet
02b067e1c5 fix(xapi/VM_create): fix destroy on failure 2024-12-11 17:38:29 +01:00
Joris K
e263f891ff feat(web-core): added UiDropdownButton component (#8076) 2024-12-11 13:43:20 +01:00
Stephane
7132b119c7 fix(xo-server): vdi_not_in_map error on migration (#8179)
Fix VDI_NOT_IN_MAP error on migration

XP-535
2024-12-11 11:46:32 +01:00
Sebastian CZECH
eb4b2c00ce feat(xo6): add network store (#8142) 2024-12-11 09:31:46 +01:00
Mathieu
5a88a48146 fix(xo-server): pool.installPatches correctly check for hosts permission (#8176)
Fix zammad#32199

pool.installPatches check user permissions on hosts if the pool is not passed.
2024-12-10 16:09:10 +01:00
Sebastian CZECH
c7657a5489 feat(web-core): remove aliasing from DonutChart (#8175) 2024-12-10 10:09:34 +01:00
Thierry Goettelmann
708b9b5bc8 feat(web-core): new menu management system (#8123) 2024-12-09 10:02:54 +01:00
Julien Fontanet
c08f84bf04 feat(xen-api): improve debug logs (#8155)
For `callAsync('VM.start', 'OpaqueRef:9a533a13-64bf-4755-ad6a-9b0f67d686ca', false, false)`, before:

```
xen-api DEBUG root@xcp1.company.net: Async.VM.start(...) [19ms] ==> string
xen-api DEBUG root@xcp1.company.net: task.destroy(...) [72ms] ==> string
```

After:

```
xen-api DEBUG root@xcp1.company.net: Async.VM.start started {
  callId: 'dtzkldk8t3f',
  args: [
    '* session id *',
    'OpaqueRef:9a533a13-64bf-4755-ad6a-9b0f67d686ca',
    false,
    false
  ]
}
xen-api DEBUG root@xcp1.company.net: Async.VM.start succeeded after 41ms {
  callId: 'dtzkldk8t3f',
  result: 'OpaqueRef:93a41f8e-005b-4c5b-a4b3-bf9d279f2827'
}
xen-api DEBUG root@xcp1.company.net: watching task { ref: 'OpaqueRef:93a41f8e-005b-4c5b-a4b3-bf9d279f2827' }
xen-api DEBUG root@xcp1.company.net: task succeeded { ref: 'OpaqueRef:93a41f8e-005b-4c5b-a4b3-bf9d279f2827', result: '' }
xen-api DEBUG root@xcp1.company.net: task.destroy started {
  callId: 'ngmpet66yj',
  args: [
    '* session id *',
    'OpaqueRef:93a41f8e-005b-4c5b-a4b3-bf9d279f2827'
  ]
}
xen-api DEBUG root@xcp1.company.net: task.destroy succeeded after 52ms { callId: 'ngmpet66yj', result: '' }
```
2024-12-06 18:21:36 +01:00
Julien Fontanet
d67a2ade37 fix(lite/deploy): download XVA from http://xoa.io/xva
Fixes https://xcp-ng.org/forum/post/86439

Other deploy methods (shell and vates.tech) uses this endpoint.

It should no longer be necessary to use the HTTP/1.0 special http://xoa.io:8888 endpoint.
2024-12-04 11:53:01 +01:00
Thierry Goettelmann
568e14f7ee feat(xo-stack): introduces job system (#8112)
New Job System that provides handling of asynchronous operations with built-in state management and identity-based concurrency control.

- Type-safe job definitions with typed arguments
- Concurrency control through identity tracking
- Built-in state management (`isRunning`, `canRun`, `errorMessage`)
- Argument validation
2024-12-04 08:41:28 +01:00
Thierry Goettelmann
c87d7a9eaf feat(web-core): new VtsLink component (#8000) 2024-12-04 08:40:01 +01:00
Sebastian CZECH
69347e9a4b feat(web-core): create tags list component (#8165) 2024-12-03 15:45:32 +01:00
Sebastian CZECH
bfb5ecc270 feat(web-core): add state for empty table (#8153) 2024-12-03 15:12:44 +01:00
Sebastian CZECH
6b2efe7039 feat(web-core): add no selection state component (#8170) 2024-12-03 14:34:03 +01:00
Joris K
e87334c0de feat(lite): 0.6.0 (#8172) 2024-11-29 15:15:12 +01:00
Joris K
427394f63f feat: release 5.101.0 (#8171) 2024-11-29 14:39:35 +01:00
Pierre Donias
2fda4130fe fix(web-core/VtsLayoutSidebar): remove bottom padding to match DS (#8168)
Introduced by #7589
2024-11-29 09:53:59 +01:00
Joris K
3b4e772eac feat: technical release (#8166)
* feat(vhd-lib): 4.11.2

* feat(@xen-orchestra/xapi): 7.8.0

* feat(@xen-orchestra/backups): 0.55.0

* feat(@xen-orchestra/backups-cli): 1.0.26

* feat(@xen-orchestra/immutable-backups): 1.0.14

* feat(@xen-orchestra/web-core): 0.7.0

* feat(@xen-orchestra/proxy): 0.29.6

* feat(@xen-orchestra/web): 0.5.0

* feat(xo-server): 5.164.0

* feat(xo-web): 5.163.0

* chore(CHANGELOG): update next
2024-11-28 16:21:17 +01:00
Florent BEAUCHAMP
511e2f70d1 fix(xo-web/sr): fix iso SR creation (#8159) 2024-11-28 15:12:19 +01:00
Mathieu
b41764405e feat(xo-server,xo-web/host): replace 2crsi-plugin.py -> ipmitool.py (#8144)
`2crsi-plugin` will be removed in favor of `ipmitool`. See https://github.com/xcp-ng/xcp-ng-xapi-plugins/pull/48
2024-11-28 14:36:53 +01:00
Stephane
11a801c129 feat(xo-web): ability to block/unblock migration (#8129)
Fixes https://project.vates.tech/vates-global/projects/70ab2907-1ac3-4e7d-831f-a8752c36474d/issues/b8c25c6d-40ba-4cb6-8a6c-33d887692525
2024-11-28 14:35:04 +01:00
Pierre Donias
07f8468317 feat(xo-web/backup): long-term retention settings (#8141) 2024-11-28 12:34:19 +01:00
Stephane
ba679a24c1 feat:(xo-server/host,xo-web): allow bypassing blocked migration in maintenance mode (#8149)
[host.setMaintenanceMode(true)] Allow bypassing blocked migration

in the UI

    detect the error
    display a modal indicating which VM cannot be migrated and asking if it should be bypassed
    if it should, retry the call and pass vmsToForceMigrate

in xo-server

    unblock migration for VMs in vmsToForceMigrate

XO-390
2024-11-28 11:46:12 +01:00
b-Nollet
10eafb493f fix(vhd-lib): return accurate merge size (#8156) 2024-11-28 11:38:46 +01:00
Sebastian CZECH
5219e41a64 feat(web-core): create VtsCardRowKeyValue component (#8148) 2024-11-28 11:31:39 +01:00
Florent BEAUCHAMP
fa97887d7e Feat(backups): support VTPMs content backup/restore (#8139) 2024-11-28 11:25:06 +01:00
OlivierFL
c5570c1e15 feat(xo-6): add 404 page (#8145) 2024-11-28 10:12:05 +01:00
MlssFrncJrg
d5217f92f9 feat(xo-server, xo-web): in host general tab, display 2CRSI BIOS update (#8146) 2024-11-27 14:40:51 +01:00
Mathieu
4082f12ad3 feat(xo-server): ability to manage VIFs using REST API when creating a VM (#8137) 2024-11-27 14:38:07 +01:00
Joris K
01cc733cac feat: technical release (#8163) 2024-11-27 11:01:05 +01:00
Florent BEAUCHAMP
49d5f273f2 fix(backups): update yarn.lock (#8160)
introduced by e80b39e417
2024-11-26 15:57:11 +01:00
Sebastian CZECH
904273f48b feat(xo-lite): display host console (#8136) 2024-11-26 14:48:23 +01:00
Florent BEAUCHAMP
e80b39e417 feat(backups): implement long term retention (#7999)
* feat(backups): implement long terme retention

long term retention sometimes called GFS ( Grand Father / Father / Son)
is a way to promote some backup to be kept on a long time

that way , the user can use the find the best equilibrium between
storage and security

This commit add the code mechanics to identify backup that
can be deleted safely. It is intended to use with a form
that ask the user for the number of day, week, month, and year
for which XO will keep the most recent

It extends the actual system of keeping the n most recent backup

Keep in mind that the backup promoted by week and month can be decaled
2024-11-26 14:39:39 +01:00
Sebastian CZECH
96769322a7 feat(xo-6): add state for panel to stateHero (#8124) 2024-11-26 11:27:47 +01:00
Julien Fontanet
be05b69d41 test(task): compat with Node >= 20.18.1
Introduced by ba3753ab1
2024-11-26 10:57:05 +01:00
Mathieu
a22e4a6340 fix(xo-server): support of cloudbase-init (#8154) 2024-11-26 10:01:29 +01:00
p-bo
aecc5f6aa9 fix(eslint): exclude "cs" locale for XO front projects (#8102)
Fix ESLint errors for missing `cs` locale translation keys.
2024-11-26 09:36:05 +01:00
p-bo
b2ac0d3abc feat(web-core): add Czech to i18n config (#8101) 2024-11-26 09:25:59 +01:00
p-bo
ad9a05b03e feat(web-core): add czech translation (#8100) 2024-11-26 09:24:44 +01:00
p-bo
65d98eb5a9 feat(xo-6): add czech translation (#8099) 2024-11-26 09:23:43 +01:00
p-bo
1bc3d5c1bd feat(xo-lite): add czech translation (#8098) 2024-11-26 09:22:04 +01:00
Mathieu
fa974f920c feat(web-stack): implement actions and clipboard for VM console (#8125) 2024-11-25 17:39:02 +01:00
Joris K
87b7016669 feat: technical release (#8157)
* feat(@xen-orchestra/fs): 4.3.0

* feat(@xen-orchestra/web-core): 0.5.0

* feat(@xen-orchestra/proxy): 0.29.5

* feat(@xen-orchestra/web): 0.3.2

* feat(xo-server): 5.162.0

* feat(xo-server-perf-alert): 0.6.0

* feat(xo-web): 5.162.0

* chore(CHANGELOG): update next
2024-11-25 14:58:35 +01:00
OlivierFL
21f0643c4d feat(xo-6/header): update user menu button (#8143) 2024-11-22 09:13:01 +01:00
Stephane
7445acdafe chore(xo-server-test): add app-conf (#8093)
Use app-conf to get xo-server hostname and port.
2024-11-21 17:38:42 +01:00
b-Nollet
1e98a3ed26 feat(@xen-orchestra/fs): add getSizeOnDisk function (#8138)
Split getSize in two functions and have it return a more accurate value without any ivLength magic and encryption padding
2024-11-21 15:44:05 +01:00
Florent BEAUCHAMP
a5ad395b33 fix(xo-web/New SR): better handling of ipv6 when creating a nfs iso SR (#8134) 2024-11-21 15:17:03 +01:00
Thierry Goettelmann
86c45ef3de docs(web-core): update documentation structure (#8135) 2024-11-20 15:04:53 +01:00
MlssFrncJrg
9939cee832 feat(xo-server): in proxies tab, display current version (#8104)
* feat(xo-server): in proxies tab, display current version
2024-11-18 17:07:20 +01:00
Sebastian CZECH
b120f5486c feat(xo-lite): add links to user menu (#8114) 2024-11-18 16:53:47 +01:00
Joris K
c72e6ab66e feat(web-core) add UiRadioButtonGroup component (#8096) 2024-11-18 16:36:27 +01:00
OlivierFL
7c68ff83cf feat(xo-lite/header): update user menu button (#8109) 2024-11-18 16:35:07 +01:00
OlivierFL
2a8acd2770 fix(xo-lite/stories): bad custom-event name case (#8140) 2024-11-18 16:25:19 +01:00
MlssFrncJrg
6825b24108 fix(xo-server-perf-alert): fix no writable SRs being monitored (#8113) 2024-11-18 10:15:09 +01:00
Julien Fontanet
0603b63035 chore: update dev deps 2024-11-15 19:08:37 +01:00
Stephane
99c56d282b fix(xen-orchestra/fs): nfs port (#8085)
XO-375

In "remotes", fix nfs port if specified.
The problem : port was specified in url, when it should be specified as an option.

Options already exist. It should continue to work as before.
2024-11-15 14:19:49 +01:00
OlivierFL
29b4c0a764 fix(xo-lite/dashboard): add min-width to hosts patches container (#8108)
Set a minimum width for the "Patches" and "Status" cards to align with the design system and allocate enough space for displaying data when it is being loaded.
2024-11-15 12:41:34 +01:00
Joris K
b81e83c619 fix(web-core): changing variable name due to new guidelines (#8131) 2024-11-15 11:18:47 +01:00
Mathieu
d7d4b40d66 feat(xo-server/xostor): do not install/update packages in airgap environement (#8105) 2024-11-15 09:14:49 +01:00
Thierry Goettelmann
a3340a7a7e feat(lite,core,web): control domain store, refactor context access (#8130) 2024-11-15 09:14:28 +01:00
Mathieu
91bfdf54ec fix(xo-web/host/network): do not PIF.reconfigure_ipv6 if IPv6 is an empty string (#8119)
See https://team.vates.fr/vates/pl/utda1cc3p3roiknt5y4yn3xqjo
Introduced by 1becccf
2024-11-15 09:13:20 +01:00
Sebastian CZECH
317a6587d3 feat(xo-6): add panel component (#8122) 2024-11-15 09:10:39 +01:00
Joris K
d229e2b82e feat(web-core): add UiCheckboxGroup component (#8094) 2024-11-15 09:07:40 +01:00
Joris K
36bab99a10 feat(web-core): add UiToggle component (#8084) 2024-11-15 09:03:28 +01:00
Joris K
2dafb3a1ec feat(web-core): added UiTableActions component (#8072) 2024-11-15 09:00:57 +01:00
Joris K
09b4246207 feat(web-core): added UiRadioButton component (#8067) 2024-11-15 08:56:48 +01:00
Joris K
92d504012a feat(web-core): added UiTopBottomTable component and some trad (#8055) 2024-11-15 08:54:25 +01:00
Julien Fontanet
cd6a856986 feat: release 5.100.2 2024-11-14 17:49:58 +01:00
Julien Fontanet
2f20370aec feat(xo-web): 5.161.0 2024-11-14 16:34:46 +01:00
Julien Fontanet
e5abd5c390 feat(xo-server): 5.161.0 2024-11-14 16:34:14 +01:00
Julien Fontanet
834c32f1da feat(@xen-orchestra/proxy): 0.29.4 2024-11-14 16:33:13 +01:00
Julien Fontanet
b46598f2b1 feat(@xen-orchestra/backups): 0.54.3 2024-11-14 16:32:48 +01:00
Julien Fontanet
d317e70282 feat(@xen-orchestra/xapi): 7.7.1 2024-11-14 16:30:46 +01:00
Julien Fontanet
f9c7aa92f9 feat(@xen-orchestra/log): 0.7.1 2024-11-14 16:28:42 +01:00
Julien Fontanet
fc57a7f9be feat(xo-server/api): ability to capture all logs for a call
The goal of this new feature is to help developers investigate issues by
allowing them to easily gather more information.

When enabled, all logs emitted in a given API call are stored in a file
(`${tmpdir}/xo-api-${date}-${method}.log`).

Administrators can use the special `_log` param to request it:

```sh
xo-cli vm.start id=$id _log=json:true

xo-cli vm.start id=$id _log=failure
```

Or it can be enabled for all calls to a method in xo-server's configuration:

```toml
[api.logs]
'vm.start' = 'failure'
```
2024-11-14 16:00:15 +01:00
Julien Fontanet
9abfc1509c feat(log/captureLogs): possibility to forward to fallback transport 2024-11-14 16:00:15 +01:00
Nathanaël
224c09ed57 docs: migrate from VuePress 1.4 to Docusaurus 3.6 (#8078)
Migrate docs from VuePress wich is not maintained anymore, to Docusaurus, which is maintained and also used fro XCP-ng docs.

---------

Co-authored-by: Thomas Moraine <thomas.moraine@vates.tech>
2024-11-14 10:09:28 +01:00
Julien Fontanet
10a82b07a9 fix(backups,xapi): use PV_drivers_detected instead of PV_drivers_version.major
Fixes zammad#27432
Fixes zammad#29040
2024-11-14 09:42:34 +01:00
Florent BEAUCHAMP
d6e76f4558 fix(backups): force delta chain length at zero for base backup (#8126)
from ticket 31268
probably caused by a combination of restore/clone of a VM
2024-11-14 07:44:53 +01:00
Sebastian CZECH
75a19e7ea0 feat(web-core): add UiToaster component (#8095) 2024-11-13 16:08:56 +01:00
MlssFrncJrg
05c4e8ea8a feat(xo-web/VM/Advanced): nested virtualization warning tooltip (#8107) 2024-11-13 10:11:48 +01:00
Thierry Goettelmann
ec63244ada chore(web stack): update dependencies (Vue 3.5, TS 5.6) (#8111)
- Update Vue `3.4` to `3.5`
- Update TypeScript `5.3` to `5.6` 
- Update Pinia `2.1` to `2.2`
- Update `vite.config.ts` to import paths with `fileURLToPath` and `URL` instead of `resolve`
- Update `size.util.ts` to use named imports from `human-format`
- Fix TS error in `vite.config.ts`
- Fix TS error in `story-param.ts`
- Fix TS error in `UiCardNumbers.vue`
- Fix TS error in `tree-node-base.ts`
2024-11-13 10:05:22 +01:00
Julien Fontanet
bc5182b3ca chore: format with Prettier 2024-11-13 09:39:34 +01:00
Florent BEAUCHAMP
ce16eb5ee4 fix(@xen-orchestra/xapi): use callAsync for VDI_listChangedBlocks (#8120) 2024-11-12 16:44:21 +01:00
Joris K
c7ddf33bcf feat(web-core): added UiCheckbox component (#8064) 2024-11-12 15:24:35 +01:00
Julien Fontanet
02f34c18e6 feat(log/levels): expose names, values and resolve() 2024-11-12 10:37:44 +01:00
Sebastian CZECH
86accbf912 feat(xo-6): add host console (#8103) 2024-11-12 08:43:11 +01:00
MlssFrncJrg
efee92dbdf fix(xo-web/Editable/Text): trim spaces (#8115) 2024-11-08 15:59:19 +01:00
MlssFrncJrg
28735191bc feat(xo-web): add documentation link when the host TLS key is too small (#8110) 2024-11-08 12:55:54 +01:00
Florent BEAUCHAMP
96abd49646 feat: release 5.100.1 (#8118) 2024-11-07 16:23:01 +01:00
Florent BEAUCHAMP
d8c4b60b63 feat: technical release (#8116) 2024-11-07 15:38:43 +01:00
Florent BEAUCHAMP
41bc59bdf4 fix(nbd): connect to only one host at a time per disk (#8086)
connecting to multiple host in parallel sometimes lead to a race condition in xapi-nbd while updating the sm_config property of the vdi. This lead to VDI that are not disconnected, thus VDI_IN_USE error at the next step of the backup

from : https://xcp-ng.org/forum/topic/9864/vdi-staying-connected-to-dom0-when-using-nbd-backups/46?_=1730797966321
2024-11-07 14:49:57 +01:00
Florent BEAUCHAMP
a8fd967576 fix(xo-remote-parser): encode username (#8106)
from ticket 30845
2024-11-07 14:43:57 +01:00
Julien Fontanet
9de714b5d6 feat(xo-web,xo-server): hint numeric for OTP inputs 2024-11-07 11:17:31 +01:00
Mathieu
a4ba695125 feat(web-core): new UiTextarea component (#7993) 2024-11-06 15:32:45 +01:00
Joris_K
7e4c33b2f7 feat(web-core): update the UiButton component to follow the new guidelines (#8049) 2024-11-06 11:17:00 +01:00
MlssFrncJrg
bb51de262d feat(xo-web): in health tab, hide PVS accelerator in orphan VDIs (#8039)
Fixes #7938
2024-11-06 10:52:55 +01:00
OlivierFL
4a7a2b74e0 feat(web-stack): use new guidelines for web-core components (#8069) 2024-11-05 15:33:46 +01:00
OlivierFL
804af1dc68 feat(web-stack): update normal colors to info and add brand colors (#8054) 2024-11-05 09:46:14 +01:00
Julien Fontanet
3a0155b464 fix(proxy/system.getServerVersion): fix description 2024-11-04 16:41:16 +01:00
b-Nollet
ff1189ef69 feat(lite): 0.5.0 (#8092) 2024-10-31 13:57:41 +01:00
b-Nollet
350fae90eb feat: release 5.100.0 (#8091) 2024-10-31 13:03:38 +01:00
b-Nollet
4fc97a572e feat: technical release (#8090) 2024-10-31 11:30:41 +01:00
Julien Fontanet
f077ccb09a fix(xo-server): ensure group name unicity 2024-10-31 10:49:45 +01:00
Julien Fontanet
8ffd7f993c fix(xo-server/Users): properly check email unicity (#8089)
Replaces #8088

Introduced by f2fe7731d

This commit introduce two new `Collection/Redis` protected async methods:

- `_beforeAdd(model)`: called before a new model is added
- `_beforeUpdate(model, previous)`: called before a model is updated

If the method throws, the corresponding operation is aborted.
2024-10-31 10:38:56 +01:00
b-Nollet
34bccbda56 feat: technical release (#8087) 2024-10-30 16:06:56 +01:00
Julien Fontanet
07ad2ca201 feat(backups): add debugs to file restore 2024-10-30 13:36:59 +01:00
Julien Fontanet
f1a7436a90 fix(xo-cli rest): don't alter unprefixed response 2024-10-30 13:36:59 +01:00
Stephane
7c9e1439a9 fix(xen-orchestra/fs): error creating nfs encrypted remote (#8081)
XO-386

When adding a nfs remote with encryption, the remote directory must be empty, then we create metadata.json and encryption.json on it.
If not, we fail to test these json files and the user receive an error "file not found: metadata.json" which is not understandable.

In this case, this PR return a message explaining the problem.
2024-10-30 13:14:37 +01:00
Pierre Donias
9ec7462a04 feat(netbox): synchronize description and comments (#8083) 2024-10-30 10:03:38 +01:00
Florent BEAUCHAMP
73e170e849 fix(Restore): ignore cbt_medata disk when doing differential restores (#8082)
differential restore works by reusing local data of a snapshot
linked to a delta chain on the remote
cbt_metadata don't have the disks data , thus are not eligible to
be used  for differential restores

from  https://xcp-ng.org/forum/topic/9728/problem-with-differential-restore/8
2024-10-30 09:59:13 +01:00
Pierre Donias
10150e5902 feat(xo-web/acls): ability to filter objects by tags (#8068)
See Zammad#24820
2024-10-29 15:55:08 +01:00
Julien Fontanet
30e6d4b4f1 feat(xo-web/new-vm): new cloud-init template var {index} 2024-10-29 15:16:19 +01:00
Florent BEAUCHAMP
b018e5acb3 feat(vmware): keep connection alive (#7839)
this lead to failing transfer when powering off the VM , since this is the
first api call after the export, that can take hours. The error code is a 500

We do a dummy vmware api call every 15 minutes, to ensure the session stays
up.

ticket 25980 and 29939
2024-10-29 13:45:43 +01:00
Stephane
f2fe7731d9 fix(xo-server): user can set his email to an already used email (#8062)
user can set his email to an already used email

    add a check, if email already exists, throw an error

XO-323
2024-10-29 13:41:56 +01:00
Mathieu
d89fb329ef feat(xo-server/rest-api): create_vm cloud init support (#8070) 2024-10-29 12:05:26 +01:00
Joris_K
5b34022d1d feat(web-core): add UiTitle component (#8046) 2024-10-29 11:55:26 +01:00
Julien Fontanet
297b9b2025 feat(task/{failure,success}): ensure not called during run
Introduced by ba3753ab1
2024-10-29 08:34:48 +01:00
Julien Fontanet
53266ec8f6 chore: don't auto build before running tests
This does not integrate well with Turbo and slows down the CI.
2024-10-28 17:06:44 +01:00
Julien Fontanet
163e80b6c5 feat(ci): test with Node 20 2024-10-28 16:49:59 +01:00
b-Nollet
04b3fc26fb feat: technical release (#8079) 2024-10-28 16:42:40 +01:00
Julien Fontanet
c0b61d5f67 fix(ci): correctly cache Turbo 2024-10-28 16:40:49 +01:00
Julien Fontanet
2da3721d83 chore(ci): use actions/checkout/v4 2024-10-28 16:38:23 +01:00
Julien Fontanet
0cf1d52935 chore(ci): separate steps with new lines 2024-10-28 16:37:13 +01:00
Julien Fontanet
41e847a32f chore: use native node:test instead of test package 2024-10-28 16:33:21 +01:00
Julien Fontanet
14e9059cb3 test(fs): fix broken tests 2024-10-28 15:43:14 +01:00
Mathieu
58bace558e fix(xo-server/rest-api): fix "/users/me" redirection (#8075)
fix(xo-server/rest-api): fix  redirection
2024-10-28 13:57:59 +01:00
Julien Fontanet
40fc999a3a feat(xen-api/call): add duration to error 2024-10-28 11:31:23 +01:00
Julien Fontanet
8116535b1b fix(CHANGELOG.unreleased): remove empty line
Introduced by ba3753ab1
2024-10-28 11:31:23 +01:00
Pierre Donias
95e5f50ce7 Revert "feat(xo-web/XOA update): display "Downgrade" when trial is over" (#8074)
This reverts commit c6f3b2b1ce.

Showing "Downgrade" after a trial has ended can be confusing.
2024-10-28 11:13:30 +01:00
Florent BEAUCHAMP
d1cdf6c0c5 fix(backup/incremental): clean vm before starting the transfert
especially in case of mirror backup, the cost of a backup repo in
an invalid state exceeds the cost of a retransfer
2024-10-28 11:06:46 +01:00
Florent BEAUCHAMP
bc74a0bd42 chore(backup): fix typo 2024-10-28 11:06:46 +01:00
Florent BEAUCHAMP
0eb50099af revert(backup): do not call updateuuid
commit ac8659021f

chaining must be redone since the path of the parent can change
from alias to non alias or the opposite
2024-10-28 11:06:46 +01:00
Pierre Donias
65fe1d83be feat(lite/xoa-deploy): configure custom NTP servers (#8059)
Add NTP servers to xenstore so that XO can automatically configure them on first start.
2024-10-28 11:05:24 +01:00
Mathieu
8c85a4657b feat(xo-server/rest-api): implement authentication tokens route (#8065) 2024-10-25 15:11:28 +02:00
Florent BEAUCHAMP
f236238c73 fix(backups): keep only the last snapshot across all delta schedules (#8051) 2024-10-25 11:29:57 +02:00
Joris_K
3d681f868a feat(web-core): add actions title component (#8045) 2024-10-25 11:23:18 +02:00
Julien Fontanet
c79efa0fe8 docs(xml-rpc): don't normalize or trim whitespaces when parsing XML 2024-10-24 11:21:42 +02:00
Julien Fontanet
bc5fa0f3f5 fix(xo-cli rest --json): output ndjson if response is ndjson 2024-10-24 11:21:42 +02:00
Florent BEAUCHAMP
fa9f865494 fix(Backups/cleanVm): handle missing vhd file of an alias (#8053) 2024-10-24 10:34:47 +02:00
Julien Fontanet
ba3753ab1a feat(task): add start, success & failure methods (#8061)
* feat(task): add start, success & failure methods

Allow managing the task manually instead of using `.run()` and `.runInside()`.
2024-10-22 17:08:57 +02:00
Stephane
a71e78fdbc chore(xo-server-test): cleanup and rewrite user tests (#8048) 2024-10-22 17:08:17 +02:00
Julien Fontanet
a3cb9d5619 feat(fs/cli): new method cat
```sh
xo-fs "$remote" cat "$file1" "$file2"
```
2024-10-22 17:01:47 +02:00
Julien Fontanet
cc1d492510 feat(fs/cli): ability to call a method directly from CLI
Example with `mkdir(path)`:

```sh
xo-fs "$remote" mkdir "$path"
```
2024-10-22 17:01:47 +02:00
Florent BEAUCHAMP
d0b7d305a2 fix(backups): VDI_INCOMPATIBLE_TYPE error (#8043)
See https://help.vates.tech/#knowledge_base/1/locale/en-us/answer/135
2024-10-22 15:09:50 +02:00
Julien Fontanet
c0bc294886 feat(xo-web/home/vms): ability to filter by MAC addresses
Fixes https://team.vates.fr/vates/pl/7xju4kwb6bra8nxupqxfhbiupc
2024-10-22 14:42:06 +02:00
Florent BEAUCHAMP
ac8659021f fix(backup/mirror): do not call updateuuid 2024-10-21 17:06:02 +02:00
Florent BEAUCHAMP
6756c9de99 fix(backups): ensure checkBaseVdis is called on all delta backup
before this commit checkbasevdis is not called if there are no disks
this leads to an erronous error message `checkbasevdi must be called
before updateUuidAndChain for incremental backups` instead of a
message on the missing vhd
this wille trigger  'A differential VHD must have a parent' instead
2024-10-21 17:06:02 +02:00
Florent BEAUCHAMP
56f3d9d7a9 fix(backup/mirror): homogenize differental vhd computation 2024-10-21 17:06:02 +02:00
Julien Fontanet
649add3ab3 fix(xo-server-audit): ignore host.isPubKeyTooShort
Fixes https://xcp-ng.org/forum/post/84464
2024-10-21 11:16:21 +02:00
Pierre Donias
78ae16b21e fix(xo-web/menu): handle null patches (#8060) 2024-10-18 17:30:59 +02:00
Pierre Donias
4102810be6 fix(xo-server): ability to remove blank tags (#8058)
See Zammad#30259
2024-10-18 16:51:50 +02:00
Pierre Donias
86883857d4 fix(xo-server-netbox): ignore empty tags (#8056)
See Zammad#30259
2024-10-18 16:31:19 +02:00
Julien Fontanet
79450a1c51 feat(xo-cli): new watch command 2024-10-18 15:14:23 +02:00
Pierre Donias
6495d563f5 fix(xo-web/createVm): fix typo error → err (#8052)
Introduced by #7122
2024-10-17 15:51:11 +02:00
mathieuRA
761c03e46c feat(xo-server/rest-api): implement getFromAsyncCache in the dashboard 2024-10-17 15:28:41 +02:00
mathieuRA
1eeff13de1 feat(xo-server/utils): creation of getFromAsyncCache 2024-10-17 15:28:41 +02:00
Pierre Donias
2027f8a282 fix(lite): handle IPv6 hosts (#8044)
See https://xcp-ng.org/forum/topic/9767/xo-lite-issue-8-3-ipv6

Add mandatory `[]` around IPv6 addresses when making requests.
2024-10-17 11:47:43 +02:00
Mathieu
2e8d398623 fix(xo-server,xo-web/SR/new): fix reattach button not displayed for HBA (#7986)
See zammad#26485
2024-10-16 11:15:03 +02:00
Florent BEAUCHAMP
fff3ec1f8b fix(backups): support system without cbt (XS< 7.3) (#8038) 2024-10-15 10:54:20 +02:00
Thierry Goettelmann
a7d7cf18c0 feat(web-core): new VtsLabel component (#7983) 2024-10-11 10:50:46 +02:00
Julien Fontanet
00d1792578 feat(xo-cli): 0.31.0 2024-10-10 15:20:05 +02:00
Julien Fontanet
8f6c087e3a feat(xo-cli rest patch): now behaves like post and put 2024-10-10 13:36:15 +02:00
Julien Fontanet
e886e91271 fix(xo-cli rest): correctly resolve server URLs
Previously it failed with a similar error:

```
✖ TypeError [ERR_INVALID_URL]: Invalid URL
    at new NodeError (node:internal/errors:405:5)
    at new URL (node:internal/url:676:13)
    at Object.exec (file:///usr/local/lib/node_modules/xo-cli/rest.mjs:188:21)
    at COMMANDS.<computed> (file:///usr/local/lib/node_modules/xo-cli/rest.mjs:151:29)
    at async main (file:///usr/local/lib/node_modules/xo-cli/index.mjs:434:14) {
  code: 'ERR_INVALID_URL',
  input: 'localhost'
}
```
2024-10-10 13:31:54 +02:00
Mathieu
3d054c8358 feat(xo-server/rest-api/dashboard): avoid iterating over all XAPI objects (#8008) 2024-10-09 14:38:25 +02:00
b-Nollet
a596702a96 feat: release 5.99.1 (#8036) 2024-10-04 14:45:47 +02:00
b-Nollet
bfb2ea6b17 feat: technical release (#8035) 2024-10-04 10:17:23 +02:00
Florent BEAUCHAMP
52c79caa45 fix(vdi): disconnect from dom0 succeed if disk is already destroyed 2024-10-04 09:00:50 +02:00
Florent BEAUCHAMP
8d48797639 feat(@xen-orchestra/xapi): try to disconnect vdi before retry destroy
can happens when a previous job failed with oom while
xapi-nbd kept disk attached to dom0

from https://help.vates.tech/#ticket/zoom/28709
2024-10-04 09:00:50 +02:00
Florent BEAUCHAMP
30172caa1f fix(task/combineEvents): remove ended tasks from map (#8030) 2024-10-03 14:35:10 +02:00
Julien Fontanet
a9b99a8b45 fix(xo-web/Tooltip): don't hide on changing children prop (#8031)
Due to the way React works, `props.children` always change even if it will
resolve to the same DOM node.
The implementation is therefore updated to check equality on DOM nodes directly.
2024-10-03 11:46:12 +02:00
Julien Fontanet
9321b04c5f feat(log): helper to capture logs in an async stack (#7975) 2024-10-02 15:14:32 +02:00
Julien Fontanet
6c258af43a feat(scripts/gen-deps-list.js): reject @xen-orchestra/lite 2024-10-01 11:26:29 +02:00
Stephane
0a28abaada feat(lite): 0.4.0 (#8028)
Co-authored-by: Stephane MEYER <stephane.meyer@vates.tech>
2024-09-30 18:18:18 +02:00
Stephane
a609e98c40 feat: release 5.99.0 (2024-09-30) (#8026) 2024-09-30 17:15:42 +02:00
Stephane
ac06a8ef49 feat: technical release (#8025) 2024-09-30 16:15:18 +02:00
Pierre Donias
2ea2f8040a fix(xo-web/render-xo-item/Schedule): handle undefined job/schedule name (#8024)
See https://xcp-ng.org/forum/topic/9690
Introduced by d520e53f9e
2024-09-30 14:52:40 +02:00
Julien Fontanet
f6e6e052ef feat(xo-web/tasks): ability to delete XO tasks' logs
Related to https://team.vates.fr/vates/pl/s7eb6mkpj7bzpqmwh89apu3eio
2024-09-30 11:33:52 +02:00
Julien Fontanet
748ed07241 chore(xo-web): format with Prettier 2024-09-30 11:30:00 +02:00
Stephane
e5b833dada feat: technical release (#8022) 2024-09-30 11:26:12 +02:00
Mathieu
68b1641e07 fix(xo-server/rest-api/dashboard): fix 'cannot get handler to proxy remote' (#8021) 2024-09-30 10:59:26 +02:00
Florent BEAUCHAMP
ad8cd3791b fix(xo-server): migration of vm with cbt enabled disk (#8017) 2024-09-30 09:04:33 +02:00
Julien Fontanet
030b3ad4d1 feat: move invalid response detection from xen-api to xapi
Introduced in ea5358e76

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

The detection taints the stream and makes helpers like `body.text()` unusable.
2024-09-29 17:15:05 +02:00
Stephane
cfea2b3a2b feat: technical release (#8020) 2024-09-27 16:21:46 +02:00
OlivierFL
44e33666cf feat(xo-6/dashboard): add resources overview data (#8009)
Display resources overview data on XO 6 dashboard.
2024-09-27 14:20:06 +02:00
Stephane
939ab29d04 fix(xo-server/api): private data in api call (#8019) 2024-09-27 14:04:22 +02:00
MlssFrncJrg
cdfe0b0374 feat(xo-server,xo-web): display bond mode of a network (#8010) 2024-09-27 12:37:25 +02:00
Julien Fontanet
eb39e14972 fix(xo-server/rest-api): set apiContext
This makes the REST API more closely resemblethe JSON-RPC API and addresses
several issues such as the broken _Rolling Pool Update_ pool action.

Fixes https://xcp-ng.org/forum/post/82867
2024-09-27 12:15:42 +02:00
Pierre Donias
d520e53f9e feat(xo-web/backup): backup job sequences (#8014)
See #7985
2024-09-27 12:13:55 +02:00
Florent BEAUCHAMP
f7df1c4109 feat(xo-server/api): add a schedule.runSequence method (#7985) 2024-09-27 10:30:33 +02:00
Florent BEAUCHAMP
558ea84880 feat(backups): improve health check error messages (#8016)
- waitObjectState already throw if timeout is reached
- gives more details to help user understand the issue
  - timeout reached while waiting for  ${refOrUuid} to be running
  - timeout reached while waiting for  ${refOrUuid} to report the
    driver version through the xen tools.
2024-09-27 09:11:04 +02:00
Mathieu
0e9242be51 fix(xo-server/rest-api/dashboard): fix 'no such VM <uuid>' when calculating vms protection (#8013) 2024-09-27 09:07:22 +02:00
MlssFrncJrg
943ad781cc fix(xo-server/xo-web/mirrorBackup): correct new schedule property on edited mirror backup job (#8001)
Introduced by ee0adae
2024-09-26 17:18:56 +02:00
OlivierFL
6b21f08ab0 feat(xo-6/dashboard): add VMs protection data (#8007) 2024-09-26 17:10:51 +02:00
Julien Fontanet
ea5358e76b feat(xen-api/getResource): detect invalid XAPI response 2024-09-26 15:15:00 +02:00
Julien Fontanet
742109ccba fix(CHANGELOG.unreleased): add missing entry
Introduced by b47019e0e
2024-09-26 15:14:22 +02:00
Julien Fontanet
00f2f8c103 fix(xo-cli rest get): properly parse --output 2024-09-26 15:13:07 +02:00
Julien Fontanet
291c5a430d feat(xo-cli rest): post/put accept --input $file 2024-09-26 15:13:04 +02:00
Stephane
9ad13608eb feat: technical release (#8015) 2024-09-26 14:55:46 +02:00
OlivierFL
fc3e954d3e feat(xo-6/dashboard): add S3 backup repository data (#8006) 2024-09-26 14:40:59 +02:00
b-Nollet
cc3d5f2efc feat(xo-server, xo-web): warning for too small host TLS keys (#7995) 2024-09-26 11:58:59 +02:00
Julien Fontanet
090c4ecb23 docs(airgap): fix bad copy/paste 2024-09-25 22:00:20 +02:00
Olivier Lambert
b78c7d228d docs(airgap): add the full offline process via our deploy script (#8011) 2024-09-25 21:40:15 +02:00
Mathieu
13f2afb0d6 feat(web-core): new Info component (#7997) 2024-09-25 15:32:16 +02:00
Mathieu
f651943add feat(xo-server/rest-api/dashboard): add resources overview information (#8005) 2024-09-25 15:24:22 +02:00
Julien Fontanet
c0f1d61c01 feat(PULL_REQUEST_TEMPLATE): document review process 2024-09-25 10:55:02 +02:00
Stephane M.
e65823b4a5 fix(xo-server/api): undefined on connecting iscsi lun to xoa (#8004) 2024-09-25 10:41:25 +02:00
OlivierFL
75684cbe12 feat(xo-core): update UiIcon component (#8003) 2024-09-25 10:31:59 +02:00
Mathieu
971a561879 fix(xo-server/rest-api): remove alarms info from the dashboard and messages endpoint (#7959) 2024-09-25 10:20:58 +02:00
Mathieu
d6bba81d36 feat(xo-server/rest-api/dashboard): add vmsProtection information (#7964) 2024-09-24 17:33:25 +02:00
Stephane M.
206f59575a test(xo-collection): convert from Jest to node:test (#7987)
Co-authored-by: Stephane MEYER <stephane.meyer@vates.tech>
Co-authored-by: Julien Fontanet <julien.fontanet@isonoe.net>
2024-09-24 17:04:22 +02:00
OlivierFL
dbf3d82a1b feat(xo-core): update VtsCounter component to v2 (#7994) 2024-09-24 16:00:16 +02:00
b-Nollet
d3414a94c6 feat(xo-web): display more notes on backup main page (#7977) 2024-09-24 14:31:23 +02:00
Mathieu
7e9b4447bf feat(xo-server/rest-api/dashboard): add S3 backup repositories information (#7978) 2024-09-24 12:22:25 +02:00
Pierre Donias
8a10070f4d feat(netbox): support Netbox v4.1 (#8002)
Fixes #7966
2024-09-24 11:21:19 +02:00
Stephane M.
b365e4aa25 test(defined): create tests for defined() (#7967)
Co-authored-by: Stephane MEYER <stephane.meyer@vates.tech>
Co-authored-by: Julien Fontanet <julien.fontanet@isonoe.net>
2024-09-24 10:17:31 +02:00
Julien Fontanet
28709b660e fix(eslint): accept Node 16 ES sytaxes in tests 2024-09-23 18:03:06 +02:00
Julien Fontanet
19067f7f6c feat(xo-cli rest): post/put from file support params
Params are passed in the query string instead of the body.
2024-09-23 18:03:06 +02:00
Pierre Donias
a297160432 feat(netbox): check Netbox version down to minor + allow to bypass check (#7992) 2024-09-23 14:59:51 +02:00
Stephane M.
381c83d0b3 test(parse-duration): create unit tests (#7968) 2024-09-22 20:50:42 +02:00
Julien Fontanet
7e14152473 feat(xo-server/rest-api): expose VM controllers' VDIs 2024-09-20 14:15:48 +02:00
OlivierFL
21bd7394fa fix(xo-lite/stories): add missing required props for UiButton component configs (#7996) 2024-09-20 11:07:54 +02:00
Pizzosaure
89aa7df744 fix(xo-web/backup): fix normalizedSettings[''] is undefined when editing a backup job
Introduced by 8a9a67c
2024-09-20 10:22:01 +02:00
Pizzosaure
b62e8035fc fix(xo-web/backup): fix r[''] is undefined when creating a backup job
Introduced by 8a9a67c
Fixes #7990
2024-09-20 10:22:01 +02:00
Thierry Goettelmann
02ed1bcee2 docs(web-core): component variants guidelines + composable (#7743) 2024-09-20 10:17:55 +02:00
Julien Fontanet
63a88b1525 fix(xen-api): don't normalize/trim strings in XML-RPC
This applies to both XML-RPC transport and results of XAPI tasks.

Before this commit, all whitespaces (spaces, new lines, etc.) were reduces to
a single whitespaces and leading and trailing whitespaces where removed.

This led to malformed values such as the result of `host.get_server_certificate`.
2024-09-19 16:19:58 +02:00
Thierry Goettelmann
2b1d6f21a0 fix(lite,web,web-core): lock Vue version to 3.4 + ESLint error (#7998)
Prevent Vue upgrade to 3.5 and fix ESLint error in the demo story.

- The recent upgrade of XO Lite / 6 to Vue 3.5 broke the CI due to typing errors (778362d8db)
- Temporarily blocks Vue from upgrading beyond 3.4 to avoid CI failures
- Fixes an attribute case mismatch in the demo story that was causing an ESLint error
2024-09-19 15:38:25 +02:00
Thierry Goettelmann
e13c0789e8 docs(web-core): component guidelines (#7696) 2024-09-19 11:23:45 +02:00
Julien Fontanet
778362d8db chore: refresh yarn.lock 2024-09-19 10:58:41 +02:00
Julien Fontanet
530c30fd50 fix(xo-server/rest-api): handle missing content-length
Unconditional cast gave `NaN` instead of `undefined` which broke automatic
computation when using VHD.
2024-09-18 11:12:22 +02:00
Julien Fontanet
4debc0f893 chore(backups/importIncrementalVm): remove VHD stream length computation
It's now unnecessary thanks to 57a399c64.
2024-09-18 10:47:19 +02:00
Julien Fontanet
57a399c64d feat(xapi/VDI_importContent): compute VHD stream length if missing 2024-09-18 10:45:51 +02:00
Julien Fontanet
0acd7c3530 fix(xapi/SR_importVdi): use VDI_destroy on error
It retries on `VDI_IN_USE` error contrary to `VDI.destroy`.
2024-09-18 10:18:22 +02:00
Julien Fontanet
088bd7cfd3 feat(xo-cli rest): post/put from a file 2024-09-17 17:48:40 +02:00
Julien Fontanet
b47019e0e4 feat(xo-cli rest get): displays stats when using -o $file 2024-09-17 16:02:02 +02:00
Julien Fontanet
aa490dc066 fix(xo-server/rest-api): fix broken VDI export
Introduced by 7b256ab8e

Not completely fixed by 8eae8e7f4

See zammad#27167
2024-09-17 15:38:34 +02:00
Julien Fontanet
b8388bb255 fix(backups): error message typo 2024-09-17 15:21:57 +02:00
OlivierFL
f969056d02 feat(web-stack): update components to match new colors from design system (#7989) 2024-09-17 08:47:54 +02:00
OlivierFL
5e11547439 feat(web-stack): replace components CSS color variables with new ones (#7984)
Replace legacy CSS colors variables in components with new ones created by #7973
2024-09-17 08:39:52 +02:00
MlssFrncJrg
04f3a09ab0 fix(xo-web/host): fix "an error has occurred" in host Mode column (#7954) 2024-09-16 09:46:50 +02:00
MlssFrncJrg
385ff21f4a feat(xo-web/disks): add icon for empty metadata VDI snapshots (#7970) 2024-09-16 09:44:17 +02:00
b-Nollet
40f5f48f17 fix(xo-web/backup): avoid 0 value for backup speed limit (#7980)
Backup job's speed limit (`maxExportRate`) had values `0` and `undefined`
leading to the same behaviour, but the values were both accepted, which could be
confusing. 0 values are now replaced by `undefined` when creating or editing a
backup job.
2024-09-13 09:25:35 +02:00
Julien Fontanet
74e6fa26a6 chore: refresh yarn.lock 2024-09-11 11:58:00 +02:00
Julien Fontanet
e26e1a2f90 fix(xo-server/hub/recipes/kubernetes): sort versions in selector 2024-09-11 11:53:05 +02:00
Julien Fontanet
c7970b3809 feat(scripts/gen-deps-list.js): don't upgrade lite due to dependency
XO Lite has a separate release process.
2024-09-11 10:55:45 +02:00
Julien Fontanet
4c7acc1614 feat: release 5.98.1 2024-09-10 16:57:12 +02:00
Julien Fontanet
fd18bee62f feat(xo-web): 5.155.0 2024-09-10 14:50:47 +02:00
Julien Fontanet
a9ad679933 feat(xo-server-sdn-controller): 1.0.10 2024-09-10 14:50:37 +02:00
Julien Fontanet
cd2b7a2553 feat(xo-server-perf-alert): 0.5.0 2024-09-10 14:50:15 +02:00
Julien Fontanet
b061ff63e7 feat(xo-server): 5.154.0 2024-09-10 14:49:13 +02:00
Julien Fontanet
28d1d8b473 feat(@xen-orchestra/proxy): 0.28.14 2024-09-10 14:45:06 +02:00
Julien Fontanet
8f5c4f336d feat(@xen-orchestra/backups): 0.53.1 2024-09-10 14:44:15 +02:00
Julien Fontanet
3c5ad184b5 feat(@xen-orchestra/xapi): 7.4.0 2024-09-10 14:43:12 +02:00
Mathieu
3ced19b4dc fix(xo-web/dashboard/health): alarms value can be Infinity, -Infinity or NaN (#7965)
See zammad#28645
2024-09-10 14:15:39 +02:00
Florent BEAUCHAMP
9504132585 feat(backups/CBT): retry data_destroy when error is VDI_IN USE (#7960)
Fixes #7826

Sometimes XAPI takes too long to detach the VDI,
in this case, the timeout is fixed at 4s, non modifiable
when the timeout is reached the xapi raise a VDI_IN_USE error

This commit adds a retry on XO side to give more room for XAPI
to work through this process, as XO already does it on VDI destroy
2024-09-10 12:19:13 +02:00
Pierre Donias
fc67abd25b chore(xo-web/i18n): update Japanese locale file 2024-09-10 10:49:02 +02:00
Yasushi Taki
72d0866174 feat(xo-web/i18n): add additional Japanese translations 2024-09-10 10:49:02 +02:00
Thierry Goettelmann
073b15ed07 feat(web-core): new useTable composable (#7936) 2024-09-10 10:14:37 +02:00
Thierry Goettelmann
7c31b44bb0 fix(web-core/useRouteQuery): URL not updating immediately in some cases (#7935)
Introduced by 290b5df
2024-09-10 09:11:43 +02:00
Thierry Goettelmann
e830bf0f0c fix(web-core): tooltip position does not update when content changes (#7979) 2024-09-10 09:06:24 +02:00
Thierry Goettelmann
790ba9bdd1 feat(web-core): new color scheme + naming from design system (#7973) 2024-09-10 08:35:35 +02:00
OlivierFL
6c8d41db00 feat(xo-6/dashboard): add backup issues data (#7974) 2024-09-09 15:44:15 +02:00
Thierry Goettelmann
576a3aa007 fix(web6): record can be undefined in single record context (#7976) 2024-09-09 14:48:15 +02:00
Julien Fontanet
af17f4a28a fix(xo-server-sdn-controller): don't try to install certs on ro pools 2024-09-09 10:33:18 +02:00
Florent BEAUCHAMP
2f7e0d0963 fix(v2v): computation of memory_static_max (#7971)
Introduced by ad1bf3b348

The max memory that can be assigned to a VM is not correctly passed by the v2v import
2024-09-09 09:26:53 +02:00
Julien Fontanet
01c71414ae fix(xo-server/xen-servers): don't try to mark read-only pool
This led to many warnings in the log.
2024-09-06 15:49:37 +02:00
Pierre Donias
1ca468064f fix(xo-web/host/PCI): "Enabled" → "Passthrough enabled" (#7961)
See https://team.vates.fr/vates/pl/p66inuymi3gm7cxf9tgkx3aa8c
2024-09-05 10:13:10 +02:00
MlssFrncJrg
69ffac65f0 feat(xo-server-perf-alert): don't compute RAM usage when no tools (#7886)
See https://kanban.vates.fr/b/jnfjuip4eBARBNuv9/xo-releases/jHtPoS4TKqZB7QL7Z
See #7831
2024-09-05 09:21:34 +02:00
MlssFrncJrg
f38b449624 feat(xo-server/rest-api/dashboard): add backup name and type (#7958) 2024-09-05 09:09:27 +02:00
Julien Fontanet
07cd06c245 feat(xo-server/rest-api): expose PIFs and VM controllers 2024-09-05 08:47:20 +02:00
Julien Fontanet
813ca246c4 feat(xo-web/pool/networks): searchable VLANs
See zammad#28710
2024-09-05 08:42:09 +02:00
Julien Fontanet
3b217ea975 feat(xo-web/pool/networks): sortable VLANs
See zammad#28710
2024-09-05 08:41:56 +02:00
Julien Fontanet
2151450584 chore(xo-web/pool/networks): inject default PIF directly into networks 2024-09-04 17:19:13 +02:00
Julien Fontanet
7cebe46e86 fix(log): revert incorrect change
Introduced by 974668587
2024-09-02 17:26:40 +02:00
Julien Fontanet
89b9a1f4b7 fix: revert to rimraf@5 to keep Node 18 support 2024-09-02 17:19:06 +02:00
Julien Fontanet
974668587d chore: update dev deps 2024-09-02 17:08:05 +02:00
Julien Fontanet
dfe6911aba chore: update dev deps 2024-09-02 13:49:50 +02:00
Mathieu
f1780b7b0f feat: release 5.98.0 (#7952) 2024-08-30 14:41:47 +02:00
Julien Fontanet
20c88b4e90 feat(xo-server-backup-reports): 1.4.1 2024-08-30 11:21:21 +02:00
b-Nollet
aa9c49ce6a fix(xo-server-backup-report): replacing partial symlinks by shared partials
Symlinks are not packed by npm which led to a broken package.
2024-08-30 11:19:22 +02:00
Julien Fontanet
951de95e4b chore(xo-server-backup-reports): shared partials 2024-08-30 11:19:22 +02:00
Mathieu
385b6b2fee feat: technical release (#7951) 2024-08-30 11:13:49 +02:00
mathieuRA
a976a5a3b3 fix(xo-server/rest-api/dashboard): catch error if getSchedule throw 2024-08-30 10:24:32 +02:00
mathieuRA
e8b32fef81 fix(xo-server/rest-api/dashboard): remoteInfo can be undefined 2024-08-30 10:24:32 +02:00
Julien Fontanet
edb021e40e fix(xo-server): fix mirror backup filter schema
Introduced by 259beead8
2024-08-29 17:40:07 +02:00
Mathieu
ae2e9e1a58 feat: technical release (#7948) 2024-08-29 17:33:01 +02:00
Florent BEAUCHAMP
83a8f295c7 fix(xo-server/migration): don't loop if VM failed to migrate with CBT enabled (#7940)
bug not reproducted on our lab
apply the same patch as commit d2a897c3b88d478a04fef0fb75b809cacc80b97a

The disk attached to dom0 is a xapi internal process, this commit ensure
it is not taken into account when choosing a stratrgey to disable CBT

this commit also ensure the process is not stuck in infinite loop if the
xapi fail to disable CBT
2024-08-29 16:54:28 +02:00
MlssFrncJrg
bddfb21e2d fix(xo-web/disks): error not displayed when toggling CBT (#7947)
Fixes https://kanban.vates.fr/b/jnfjuip4eBARBNuv9/xo-releases/YbkpWEJPpQgz7GTsx
2024-08-29 16:36:30 +02:00
Mathieu
cf7dbd1c36 fix(xo-server/rest-api/dashboard): exclude disabled and S3 from backupRepositoriesSize (#7945)
introduced by 4dfd77b
2024-08-29 16:29:01 +02:00
Florent BEAUCHAMP
204cb25d2b feat(backups): replication failing with snapshot attached to dom0 (#7920)
disks could still be attached to dom0 after the transfers given the caches and the asynchronous nature of the operation.

This PR ensure the cache are fully up to date before running the destroy code and also will let the XAPI handle the disk attached to dom0, as it was before the refactoring
2024-08-29 15:53:07 +02:00
Mathieu
5656ab1d48 feat(xo-web/mirror): add the possibility to select vm backups (#7941)
* feat(xo-web/mirror): add the UI to select vm backups to transfer

following 259beead81
2024-08-29 15:48:32 +02:00
b-Nollet
99ffefae10 feat(xo-web,xo-server-backup-reports): smaller backup reports option 2024-08-29 15:20:31 +02:00
b-Nollet
a78e1346f6 feat(xo-server-backup-reports): adding compact templates for Slack and XMPP 2024-08-29 15:20:31 +02:00
MlssFrncJrg
0d5f86620d fix(xo-server-perf-alert): CPU usage alert email displays NaN (#7925) 2024-08-29 15:01:40 +02:00
Florent Beauchamp
18bea75d28 refactor(backup/mirror): compute filter predicate only once 2024-08-29 14:53:43 +02:00
Florent Beauchamp
5e6bb22025 feat(backup/mirror): better full backup filtering
If the user defined a filter and the filter only match part of the full
backups, then we'll only transfer the matched backups

If the user is using incremental backup, then we'll trnsfer the full chain
if at least only one match
2024-08-29 14:53:43 +02:00
Florent Beauchamp
091da7b913 fix(backups): mirror from multiple jobs
the algorithm sorting the backup to transfer do not take into account multiple
job writing to the same remote
It can lead to additional VM being transferred
2024-08-29 14:53:43 +02:00
Florent Beauchamp
ec3fe212e1 feat: gives more information to the user 2024-08-29 14:53:43 +02:00
Florent Beauchamp
259beead81 feat(backups/mirror): filter backup to be transfered 2024-08-29 14:53:43 +02:00
Mathieu
b1ce62f23a feat(xo-server-perf-alert): add a toggle to exclude items (#7911) 2024-08-29 14:42:37 +02:00
OlivierFL
cd25873182 fix(xo-core/StackedBar): division by 0 in some cases (#7944)
Introduced by 14da422
2024-08-29 10:49:06 +02:00
OlivierFL
a7165da4a0 fix(xo-core/CardNumbers): division by 0 in some cases (#7946)
Introduced by dc7fd78
2024-08-29 10:43:11 +02:00
Julien Fontanet
95f0ef504d feat(xo-web): show copiable when activating OTP
Requested by an IRC user to make it easier to use with Bitwarden.
2024-08-29 09:25:29 +02:00
MlssFrncJrg
732cab3ac0 fix(xo-web/vm/general): fix 'an error as occured' (#7928) 2024-08-28 18:58:18 +02:00
MlssFrncJrg
fb01d20c2e fix(xo-web/new/sr): fix an error as occured when host is missing (#7931) 2024-08-28 18:42:24 +02:00
Julien Fontanet
034d4faa6a feat: release 5.95.2 2024-08-28 15:45:03 +02:00
Mathieu
beb93cf66f feat: technical release (#7943) 2024-08-28 15:03:36 +02:00
Florent BEAUCHAMP
ad1bf3b348 fix(xo-server/v2v): use the template method for static_memory_min (#7923) 2024-08-28 14:33:14 +02:00
OlivierFL
43c3a82800 fix(CHANGELOG.unreleased): add missing entries (#7942) 2024-08-28 14:05:15 +02:00
OlivierFL
7fc5b850ca feat(xo-6/tree-view): display running VMs count (#7873) 2024-08-28 12:11:58 +02:00
Julien Fontanet
916c15d8c7 chore(xo-server-backup-reports): unify templates compilation (#7939)
It will make adding new templates easier.

Co-authored-by: b-Nollet <bastien.nollet@vates.tech>
2024-08-27 15:28:59 +02:00
MlssFrncJrg
4b47bf9220 feat(xo-web/home): sort VMs by install time (#7910)
Fixes #7902
2024-08-27 15:11:19 +02:00
Julien Fontanet
877fe9a663 fix(CHANGELOG.unreleased): add missing entries
Introduced by 8eae8e7f4
2024-08-27 14:56:14 +02:00
OlivierFL
88c7c8d3ae feat(xo-core): add BackupItem component (#7934) 2024-08-26 14:52:31 +02:00
OlivierFL
23720dd05f feat(xo-core/ButtonIcon): add prop to change the click target size (#7922) 2024-08-26 13:57:43 +02:00
OlivierFL
d796e37df5 feat(xo-core): update SearchBar component's name -> QuerySearchBar (#7933) 2024-08-26 11:10:32 +02:00
Mathieu
9734410833 feat(xo-server/rest-api/dashboard): add backup issues information (#7919) 2024-08-26 10:39:11 +02:00
OlivierFL
aa7ff37b94 feat(xo-6/vm): use HeadBar component instead of custom integration (#7858) 2024-08-26 10:33:49 +02:00
OlivierFL
0eaa6a93a3 feat(xo-6/dashboard): add patches data (#7930) 2024-08-26 10:22:09 +02:00
OlivierFL
b33a84e5c7 feat(xo-6/dashboard): add backups jobs data (#7929) 2024-08-26 10:08:39 +02:00
OlivierFL
e65c5d02f3 feat(xo-6/dashboard): add storage repository data (#7927) 2024-08-26 09:00:12 +02:00
OlivierFL
dc7fd78b4a feat(xo-6/dashboard): add backup repository data (#7926) 2024-08-26 08:46:24 +02:00
Julien Fontanet
8eae8e7f46 fix(xapi/VDI_exportContent): return stream in raw mode
Introduced by 7b256ab8e

See zammad#27167
2024-08-23 10:49:25 +02:00
Julien Fontanet
da84ab1590 feat(xo-server/vif.set): returns VIF id because it might change
Fixes https://team.vates.fr/vates/pl/j1cxbejxkfdwud1qw8qwgog8oa
2024-08-22 10:40:39 +02:00
Thierry Goettelmann
290b5df27c feat(web-core): new useRouteQuery composable (#7906) 2024-08-20 17:52:58 +02:00
b-Nollet
72592a54d2 fix(backups): correctly update allowed_operations - temporary fix (#7924)
Fixes https://xcp-ng.org/forum/post/81327

`allowed_operations` are currently not updated correctly when `blocked_operations` are modified, which causes backup jobs to remove `migrate_send` and `pool_migrate` from VM's `allowed_operations`. This commit fixes that, until `allowed_operations` updates correctly again.
2024-08-20 11:11:48 +02:00
Mathieu
e5c53dfa52 feat(xo-server/rest-api/dashboard): add backupJobs info (#7908) 2024-08-19 11:20:24 +02:00
Mathieu
9d196c4211 feat(xo-server/rest-api/dashboard): add alarms info (#7914) 2024-08-19 11:06:03 +02:00
Thierry Goettelmann
e15f2a1428 feat(web-core): replace no-use-before-define ESLint rule with its TS-compliant version (#7916) 2024-08-16 16:07:55 +02:00
Thierry Goettelmann
f78a140fbb feat(web): new REST API and XO objects configuration system (#7918)
Major revamp of the REST API configuration to get a single source of truth + stronger typing + support for single objects.
2024-08-16 15:03:49 +02:00
MlssFrncJrg
79ce97b3fd fix(xo-web/import): properly filter selectors with selected pool (#7905) 2024-08-14 17:23:51 +02:00
OlivierFL
1e7d02603b fix(xo-6): rollup error when building with npm (#7891)
Add `overrides` section in XO6 `package.json` to fix
`Error: Cannot find module @rollup/rollup-linux-x64-gnu` rollup error when
building with npm.
2024-08-14 11:52:53 +02:00
MlssFrncJrg
59bf23a41a feat(xo-web/sr/disks): duplicate CBT state to storage view (#7888)
Fixes #7786
2024-08-14 11:28:09 +02:00
Thierry Goettelmann
39e34eede7 feat(web-core): add Branded TS utility type (#7917) 2024-08-14 10:02:08 +02:00
Julien Fontanet
32228e8c6a fix(xo-web/Home/VM): sort by start time move halted VMs at the end
Fixes https://team.vates.fr/vates/pl/kyxx4qgq5fn58fs7odia7zca8w
2024-08-13 11:45:47 +02:00
Julien Fontanet
7163002c51 feat(xo-web/Home): sortByFn to use a function instead of a property 2024-08-13 11:43:51 +02:00
OlivierFL
700149dabe chore(xo-core): update NoVnc typescript type (#7900)
Use type definitions from DefinitelyTyped instead of using custom declaration
2024-08-09 15:56:44 +02:00
OlivierFL
06af045dac fix(xo-core): wrong CSS lang in StackedBarWithLegend component (#7907) 2024-08-09 15:26:37 +02:00
OlivierFL
b6ccb72e43 chore(web stack): add ESLint rule to check style and script tags "lang" attribute (#7898) 2024-08-09 14:44:56 +02:00
Paloma
14da422b8b feat(xo-web): add stacked bar with legend component (#7819) 2024-08-09 14:38:46 +02:00
Mathieu
5dd305228b feat(xo-server/rest-api/dashboard): add storage repositories info (#7904)
Add storage repository info in the dashboard endpoint for the XO6 dashboard
replications and other data will be calculated in another PR as there is no trivial way to do it

```ts
{
 // ...
  "storageRepositories": {
    "size": {
      "available": number,
      "other": 0,
      "replicated": 0,
      "total": number,
      "used": number
    }
  }
}
 ```
2024-08-08 15:44:12 +02:00
Julien Fontanet
e75cccf1be feat(web-core/stores/ui): default theme is now _auto_
Introduced by 7f4f86073
2024-08-08 13:47:52 +02:00
Julien Fontanet
0e6bc063db fix(xo-server/isHyperThreadingEnabled): better XCP-ng>=8.3 support 2024-08-07 16:02:12 +02:00
Julien Fontanet
0291d42af7 fix(xo-web/home/sr): fix shared sort
It was not working at all because current system only accepts strings for `sortBy` property.
2024-08-07 15:00:40 +02:00
Julien Fontanet
656786eb86 fix(proxy/authentication): keep token synced with config 2024-08-07 14:56:10 +02:00
Mathieu
4dfd77b2d2 feat(xo-server/rest-api/dashboard): add backup repositories info (#7882) 2024-08-06 17:51:20 +02:00
Mathieu
895a551c7b fix(xo-server/rest-api/dashboard): ignore EOL hosts if getHVSupportedVersions unavailable (#7901)
Introduced by ddfdce9
2024-08-06 16:51:58 +02:00
Pierre Donias
e750a66c02 fix(xo-web/self): remove unit in CPU usage total count (#7894)
Introduced by #7353
2024-08-06 09:33:40 +02:00
Julien Fontanet
4d768252f0 feat(@vates/json-hash): 0.2.0 2024-08-05 17:56:58 +02:00
Dan Pollak
f0f5cdba72 fix(docs,xo-web): trials are now 30 days (#7896) 2024-08-05 17:38:55 +02:00
Julien Fontanet
8cc3d0cbaf feat(json-hash): use base64url instead of base64 2024-08-05 17:12:09 +02:00
Julien Fontanet
3284021964 fix(json-hash): strict mode and Node version
Introduced by 14325fe99
2024-08-05 14:43:28 +02:00
Julien Fontanet
e333332131 feat(@vates/json-hash): 0.1.0 2024-08-05 14:38:36 +02:00
Julien Fontanet
14325fe992 feat(json-hash): extract into own package 2024-08-05 14:37:33 +02:00
Mathieu
ddfdce9da5 feat(rest-api): add number of EOL hosts in the dashboard endpoint (#7895) 2024-08-05 10:14:01 +02:00
Dan Pollak
a2c62117a1 fix(xo-server,xo-web): optionnal → optional (#7897)
Signed-off-by: Dan Pollak <danpollak2@gmail.com>
2024-08-05 08:12:02 +02:00
Olivier Lambert
cb6cfc3a74 docs(airgap): improve the air gap doc and add our fully offline XOA s… (#7863)
docs(airgap): improve the air gap doc and add our fully offline XOA solution

Signed-off-by: Olivier Lambert <olivier.lambert@vates.fr>
2024-08-01 09:17:48 +02:00
Mathieu
9d61e0232d feat(xo-web): create a backup job schedule selector (#7804)
New UI component `SelectSchedule` that will be used for chaining backup jobs.
2024-07-31 17:31:42 +02:00
b-Nollet
fedab820f1 chore(xo-lite): updating package.json (#7892)
Introduced by #7890
2024-07-31 15:54:15 +02:00
OlivierFL
7eac1d1c5a fix(xo-6/css): wrong lang in components style tags (#7880)
The wrong CSS lang (`scss` instead of `postcss`) was used in some components.
2024-07-31 14:24:57 +02:00
b-Nollet
06a9ab6718 feat(lite): 0.2.7 (#7890) 2024-07-31 12:24:17 +02:00
b-Nollet
d0bd6562d0 feat: release 5.97.0 (#7889) 2024-07-31 11:21:44 +02:00
b-Nollet
7b60fab7ea feat: technical release (#7887) 2024-07-31 10:12:03 +02:00
Pierre Donias
045cae24fb fix(xo-web/VM/Stats): do not show guest tools warning when VM is offline (#7883)
Introduced by #7831
2024-07-31 09:51:41 +02:00
Olivier Lambert
bd7e82cb6b docs(supported hosts): update to add support on XS 8 and XCP-ng 8.3 (#7885) 2024-07-31 09:13:52 +02:00
b-Nollet
307179b142 feat: technical release (#7879) 2024-07-30 16:10:41 +02:00
b-Nollet
0a3c32f9d2 feat(backups): add reportWhen 'skipped and failure' (#7869)
Replace the "don't send reports for skipped backups" toggle by an option in "Report when"
2024-07-30 15:01:25 +02:00
OlivierFL
124d96f31a fix(xo-6): add comments in host and pool pages to fix Vite build (#7878)
The components `/host/[id]/index.vue` and `pool/[id]/index.vue` only use
`definePage()` macro.
The macro is removed during the build, leaving an empty script tag that causes
the build to fail.
This PR addresses this issue by adding a comment in the components to avoid
empty content in `<script>` tags at build time.
2024-07-30 14:57:27 +02:00
Pierre Donias
916418b6cf feat(lite): local XO Lite will try to load app from lite.xen-orchestra.com first (#7870) 2024-07-30 11:13:05 +02:00
MlssFrncJrg
7592734c9a feat(xo-web/VM/Stats): warning when guest tools are not installed (#7831)
Memory usage is not collected without guest tools. That means that the stats
graph shown in the VM page is pointless.
2024-07-30 10:20:41 +02:00
b-Nollet
b1088e2a5b fix(xo-web/backup): replace reportWhen raw value with message (#7876)
Replace `reportWhen`'s raw value by a message (e.g. *never* is replaced by the
`reportWhenNever` message, which is *Never*)
2024-07-30 09:49:53 +02:00
Mathieu
83715bedc6 fix(xo-web/VM/new): use new secureboot API (#7874)
Fixes #7500
2024-07-29 17:48:41 +02:00
Mathieu
e30f289073 feat(xo-server/xo-web/host): display 2CRSi informations in general tab (#7838) 2024-07-29 17:33:32 +02:00
OlivierFL
0977542dbd fix(web-core/novnc): add custom declaration file to fix type-check error (#7875)
See #7824
See 287f38cfd2/types/novnc-core
See https://github.com/DefinitelyTyped/DefinitelyTyped/pull/70022

The types definitions in DefinitelyTyped/NoVnc are outdated and don’t reflect
the changes made in the NoVnc package.
This causes an error when running `yarn type-check`.

While waiting for https://github.com/DefinitelyTyped/DefinitelyTyped/pull/70022
to be merged, a temporary workaround is to add a custom `novnc.d.ts` (moved from
`lite/src/types/novnc.d.ts`) declaration file in `web-core` to fix the
`type-check` error.
2024-07-29 16:49:37 +02:00
MlssFrncJrg
c9699c8f02 feat(xo-web/VM): display accurate Secure Boot status (#7751) 2024-07-29 14:12:27 +02:00
Thierry Goettelmann
636c88539f feat(web-core): new useItemCounter composable (#7868) 2024-07-26 13:51:01 +02:00
Paloma
333012bd29 feat(xo-web): add host status to xo6 dashboard (#7799) 2024-07-26 11:09:15 +02:00
OlivierFL
8fd516040b feat(xo-6/host): add VMs table list view (#7851) 2024-07-26 10:49:45 +02:00
Paloma
2017ec26ad feat(xo-web): add vms status for xo6 dashboard (#7795) 2024-07-26 10:33:39 +02:00
Thierry Goettelmann
72d6da38c5 feat(web-core): update heroes + add stories (#7866)
- `StateHero`, `LoadingHero` and `ComingSoonHero` can now be used inside a card by passing `type="card"` or inside a page by passing `type="page"`
- `LoadingHero` has now a `disabled?: boolean` prop and can now take a slot which will be displayed when `disabled` is `true`
- Added stories for `StateHero`, `ComingSoonHero`, `ObjectNotFoundHero` and `LoadingHero`
2024-07-26 10:01:20 +02:00
Paloma
59784315a8 feat(xo-core): add donut with legends ui component (#7829) 2024-07-26 09:00:29 +02:00
Mathieu
c5f6bf574f fix(xo-web/SR): remove newSrExistingSr modal for NFS SR (#7865)
Introduced by 659c735
See https://xcp-ng.org/forum/topic/9425
2024-07-25 17:45:50 +02:00
Jonas
991b4641ae docs(installation): add systemd service (#7848) 2024-07-25 15:53:07 +02:00
b-Nollet
0fee31b345 feat: technical release (#7864) 2024-07-25 15:35:03 +02:00
ventra007
d58ee9611f docs(backups): grammar fixes in the Concurrency section (#7859) 2024-07-25 15:08:27 +02:00
Julien Fontanet
da9d796d3f Revert "fix(xo-web/host/advanced): fix netdata link"
This reverts commit b393d72eb9.
2024-07-25 14:31:15 +02:00
Pierre Donias
e4c66bc0e8 fix(CHANGELOG): missing package to release (#7862)
Introduced by #7776
2024-07-25 14:09:29 +02:00
mathieu
d61429852d fix(xo-server-backup-reports/metadata): fix task.tasks is not iterable 2024-07-25 13:59:27 +02:00
Pizzosaure
a06e78c77c feat(xo-web/backup): add 'report recipients' when creating a metadata backup 2024-07-25 13:59:27 +02:00
Julien Fontanet
e66bdda6a1 feat(mixins/Tasks): buffer task events (#7841)
Fixes zammad#26718
Fixes zammad#26778
Fixes zammad#26865
Fixes zammad#26885
Fixes zammad#26980
Fixes zammad#27205
Fixes https://xcp-ng.org/forum/post/79281
Fixes https://xcp-ng.org/forum/post/80010
2024-07-25 12:41:41 +02:00
Mathieu
659c735b70 feat(xo-web/SR): add confirmation modal before SR creation (#7845) 2024-07-25 11:25:06 +02:00
OlivierFL
35c6248251 feat(xo-6/tree-view): improve indentation of empty items (#7861) 2024-07-25 10:46:03 +02:00
Thierry Goettelmann
4344e1567c fix(web-6/tree-view): issue with item without children (#7860)
- Remove the responsibility to exclude branches with no children from `branch.ts`
- `PoolTreeItem` and `HostTreeItem` no longer create a sublist if they have no children
2024-07-25 09:59:25 +02:00
Florent Beauchamp
2291505865 test(nbd-client): testing half succeeding multi nbd 2024-07-24 17:23:47 +02:00
Florent Beauchamp
92acd16779 feat(backups): improve error message when CBT backups aren't possible 2024-07-24 17:23:47 +02:00
Florent Beauchamp
3c5307cd64 feat(nbd-client): filter out unreachable hosts 2024-07-24 17:23:47 +02:00
Florent Beauchamp
f55ffc7d06 fix(backups): NBD backups must respect default backup network 2024-07-24 17:23:47 +02:00
Florent Beauchamp
6c118e6af5 fix(backups): incorrect assert check 2024-07-24 16:50:57 +02:00
Florent Beauchamp
d0984a7cb4 fix(backups): remove snapshot data only after the transfer is finished
* _removeUnusedSnapshots is called before and after the backup
* in some case it can delete the snapshot data before the transfer is done
2024-07-24 16:50:57 +02:00
Florent BEAUCHAMP
76336fa422 fix(xo-server): VHD is a disposable (#7856)
Introduced by 24d584e905
2024-07-24 16:44:14 +02:00
Julien Fontanet
2a9f5bcf53 fix(CHANGELOG.unreleased): add missing xen-api
Introduced by 79690fcdf
2024-07-24 16:39:31 +02:00
Julien Fontanet
9d3ec46086 feat(xo-server/self service): admins alway bypass quotas (#7855)
Fixes Zammad#27050
Fixes https://team.vates.fr/vates/pl/jb5xsh3bijf17fjokq3uczg4bo
2024-07-24 16:21:33 +02:00
Julien Fontanet
79690fcdfa feat(xen-api): cache support for call & callAsync (#7853)
* cache support for call & callAsync
* composite key is based on hashed JSON of function call arguments
2024-07-24 15:56:53 +02:00
b-Nollet
23e42c1764 feat: technical release (#7857) 2024-07-24 15:05:17 +02:00
Julien Fontanet
4b8f7146f6 feat: use new deploy page URL 2024-07-24 10:48:16 +02:00
Julien Fontanet
8172a327e4 fix(xo-web/HomeTags): strict tag filtering
Fixes https://team.vates.fr/vates/pl/776boet4qty55jkt4srabaz4hc

Exact match and respect letter case.
2024-07-23 22:28:41 +02:00
Pierre Donias
919d215b64 fix(xo-web/VM): properly identify self user to hide unauthorized actions (#7854) 2024-07-23 17:07:07 +02:00
b-Nollet
161169f295 feat(backups): option to not send reports for skipped runs (#7832) 2024-07-23 14:45:11 +02:00
b-Nollet
e91b7f8571 feat(xo-server-backup-reports): include info (#7815)
Show info of backup tasks in MJML template, for example show when NBD is used.

Currently it only displays a the message associated with the info, not its data (unlike e44933e2f7/packages/xo-web/src/xo-app/logs/backup-ng/log-alert-body.js (L131-L159)). It would be possible to display the info data, but it would probably not be done in a clean way in the current template (and I'm not sure it would be that useful).

From https://xcp-ng.org/forum/post/79833
2024-07-23 12:06:22 +02:00
OlivierFL
60291e317c feat(xo-6/pool): add hosts and VMs table list views (#7842) 2024-07-22 17:46:45 +02:00
OlivierFL
c704f156fc fix(xo-6/xo-lite): tree-view vertical line display (#7847)
Introduced by fe1f5cb

The `TreeItemLabel` component `expanded` property wasn’t handled correctly when displaying the last item of a sublist.
2024-07-22 17:42:41 +02:00
Thierry Goettelmann
9a811d7f1e feat(web-stack): upgrade Vue Router to 4.4 (#7850)
- Upgrade `vue-router` to `4.4` and `unplugin-vue-router` to `0.10`
- Disable `no-undef` ESLint rule since it's already handled by TypeScript ESLint
2024-07-22 17:01:04 +02:00
OlivierFL
a97d55c53c feat(xo-stack): add text-ellipsis utility class (#7843) 2024-07-22 16:31:44 +02:00
Julien Fontanet
8542b83f7c fix(xen-api/cli): use @xen-orchestra/log
Follows 0702454fa
2024-07-22 15:12:47 +02:00
Pierre Donias
73755c0e8c feat(lite/XoaButton): support xo-server's setting http.publicUrl (#7849)
See https://xcp-ng.org/forum/topic/9392

`xo-server` has a config setting `http.publicUrl` that allows the user to force
XO to report a custom URL in the pool's other_config (along with the
`networkInterfaces` fallback). With this change, the "Access XOA" button in
XO Lite will open that URL if it exists and fallback to `networkInterfaces`
otherwise.
2024-07-22 11:10:34 +02:00
Julien Fontanet
0702454fa8 feat(xen-api): use @xen-orchestra/log 2024-07-19 16:05:16 +02:00
Jonas
a0c3920d00 docs(users): SAML instructions for use with Google Workspace (#7846)
This shows a working example of how to connect XO SAML-plugin with Google Workspace SAML.

It's a rough example, with simple screenshots. And I've mentioned the caveat that a successfull authentication points the user to the login screen. Needing to browse into the bare URL, for access into the dahsboard. But at least it works!
2024-07-19 09:55:59 +02:00
Florent BEAUCHAMP
d4637e3335 fix(vmware-explorer): handling disks without storage (#7840)
Some VM may have disks without any storage declared. Since we
didn't find the cause, this PR ensure we don't break the VM listing.

Fixes zammad#26677
2024-07-18 16:35:06 +02:00
Jonas
6fcb838b70 feat(xo-web/i18n): add Swedish translation (#7844) 2024-07-18 10:17:30 +02:00
Julien Fontanet
065c44cc92 feat(xapi/parseDateTime): support timezones
Fixes https://team.vates.fr/vates/pl/ifht7eyhiffhtyxwywxsqwonma

`host.get_server_localtime` will include timezones in the near future.
2024-07-17 11:45:42 +02:00
Mathieu
b539b5f5e3 fix(xo-web/VM/Advanced): fix 'not enough permission' error for super admins when attaching PCIs (#7793) 2024-07-16 17:11:50 +02:00
Thierry Goettelmann
d1ce35f20a feat(web): add task store + tasks quick view (#7813) 2024-07-16 16:45:08 +02:00
Thierry Goettelmann
3f754fdc05 feat(web-core): tasks quick view (#7812) 2024-07-16 16:41:27 +02:00
Thierry Goettelmann
57d2d301ae feat(web): add polling option to xo store config builder (#7809)
This is a temporary solution. A better implementation will follow.
2024-07-16 11:12:51 +02:00
OlivierFL
d2507025a9 feat(xo-core): add HeadBar component (#7806) 2024-07-16 10:43:45 +02:00
OlivierFL
de934ec609 feat(xo-core): add CellText component (#7801) 2024-07-15 16:24:12 +02:00
OlivierFL
6894841038 feat(xo-core): add CellObject component (#7798) 2024-07-15 16:05:27 +02:00
John P. Cooper
c5e4f05d3b feat(xo-6): Initial PWA manifest for Xen Orchestra 6 (#7445)
* Update index.html

Rebased on latest commits.

* Create manifest.webmanifest

The initial PWA web manifest but rebased for latest commits.

* Update CHANGELOG.unreleased.md

Rebased on the latest commits as the conflicts were too major.

* Update CHANGELOG.unreleased.md

* Update CHANGELOG.unreleased.md

* Make URLs relative and remove changelog entry

---------

Co-authored-by: Pierre Donias <pierre.donias@gmail.com>
2024-07-15 15:28:33 +02:00
OlivierFL
dad3dc5926 feat(xo-core): add ObjectLink component (#7794) 2024-07-15 15:24:24 +02:00
Thierry Goettelmann
3fe46cae64 feat(web): fix dev token (#7779) 2024-07-15 14:58:41 +02:00
OlivierFL
14c5d2c4ec feat(xo-core): add SearchBar component (#7726) 2024-07-15 14:57:23 +02:00
Mathieu
4a4122b9a6 feat(xo-server/xo-web/VM_coalesceLeaf): implement Coalesce Leaf (#7810) 2024-07-15 11:26:34 +02:00
Dan Pollak
acffdbec8f fix(xo-server/rest-api): add dashboard endpoint to index (#7835)
Signed-off-by: Dan Pollak <danpollak2@gmail.com>
2024-07-15 11:11:28 +02:00
OlivierFL
f9220cd272 fix(xo-core/eslint/i18n): add "fa" to the list of excluded locales for missing keys checks (#7833)
Introduced by 28f94b3

Fix ESLint errors for missing `fa` locale translation keys.
2024-07-12 14:33:53 +02:00
Julien Fontanet
518b0f77bd fix(eslint): restrict require-atomic-updates to limit number of warnings 2024-07-12 12:07:40 +02:00
Mathieu
b35a5cffa6 feat(xo-server/rest-api): implement dashboard endpoint (#7823) 2024-07-11 10:30:49 +02:00
OlivierFL
bb88834fcd fix(xo-core/xo-lite): update NoVncClient import (#7824)
Introduced by e44933e

b35cf6dd12 has resulted in a change to the import path of `NoVncClient` in `web-core` and `XO Lite` console components.
2024-07-10 15:16:42 +02:00
Julien Fontanet
e492be3a67 feat(CHANGELOG.unreleased): add Security section for future releases
Related to zammad#26875
2024-07-09 18:00:27 +02:00
Julien Fontanet
a223207ff3 docs(configuration/redis): replace incorrect info
Fixes zammad#26893
2024-07-09 17:26:06 +02:00
OlivierFL
4d42f86b2c fix(xo-6/header): make XO5 link work (#7719) 2024-07-09 14:56:43 +02:00
Julien Fontanet
da3aa59781 docs(xoa): extraneous → secondary 2024-07-09 13:22:43 +02:00
Julien Fontanet
2b46e6cf30 docs(xoa): extraneous IP addresses 2024-07-09 13:12:13 +02:00
Julien Fontanet
9406d20672 docs(xoa): show CIDR syntax for xoa network static 2024-07-09 13:12:13 +02:00
Pierre Donias
0e6d3ae27a chore(xo-web/i18n): update russian locale 2024-07-09 11:01:53 +02:00
a.anushko@spbu.ru
ee3bc185da fix(xo-web/i18n/ru): fix typos, remove wrong translations, add translations 2024-07-09 11:01:53 +02:00
Jokar-xen
28f94b3888 feat(xo-web,web-core,lite/i18n): add Persian (#7775) 2024-07-09 10:27:57 +02:00
Julien Fontanet
e44933e2f7 chore: update dev deps 2024-07-08 14:41:20 +02:00
MlssFrncJrg
30dd89243f feat(XO6): create link reusable helper (#7681)
Add helper to handle external links
2024-07-08 11:25:02 +02:00
Julien Fontanet
96b7673e27 feat: release 5.96.0 2024-07-05 17:28:52 +02:00
Julien Fontanet
69a312a0c4 feat(xo-server): 5.148.2 2024-07-05 16:57:19 +02:00
Julien Fontanet
a0e6c8b115 feat(@xen-orchestra/proxy): 0.28.8 2024-07-05 16:57:14 +02:00
Julien Fontanet
dc047d47ea feat(@xen-orchestra/xapi): 7.1.1 2024-07-05 16:56:55 +02:00
Julien Fontanet
a00b24f6b8 fix(xapi/VM_snapshot): destroy snapshot VDIs instead of original ones
Fixes https://xcp-ng.org/forum/post/79769

This code, only used for running VMs, triggered `VDI_IN_USE` errors and left the
created VM snapshot with all the VDIs.
2024-07-05 16:44:58 +02:00
Julien Fontanet
7601dee7f2 fix(xapi/VM_snapshot): log original error when failing to destroy a ignored VDI 2024-07-05 16:41:42 +02:00
Julien Fontanet
1ace3cbbe8 feat(@xen-orchestra/proxy): 0.28.7 2024-07-05 11:16:32 +02:00
Julien Fontanet
8651e61210 feat(xo-server): 5.148.1 2024-07-05 10:46:58 +02:00
Julien Fontanet
6c6b6b9fa1 feat(@xen-orchestra/backups): 0.52.1 2024-07-05 10:46:49 +02:00
Julien Fontanet
6374e0d3f5 feat(@xen-orchestra/xapi): 7.1.0 2024-07-05 10:38:02 +02:00
Julien Fontanet
664d4b1af6 feat(backups/worker): forced exit after 30s 2024-07-05 10:32:08 +02:00
Julien Fontanet
0117e9dbc8 chore(mixins/Tasks): don't duplicate log deletion logic 2024-07-04 17:44:39 +02:00
Julien Fontanet
174e32173f fix(xo-server/sr.getAllUnhealthyVdiChainsLength): lodash.throttle instead of debounceWithKey
`debounceWithKey` should not be used because the API method will always be called with a different
empty object as param which will render the cache useless.

Fortunately `lodash.throttle` can be used here as `sr.getAllUnhealthyVdiChainsLength` is synchronous
and has no meaningful parameter.
2024-07-04 17:43:07 +02:00
Mathieu
0e4a300058 feat: technical release (#7808) 2024-07-04 16:42:52 +02:00
Florent Beauchamp
76b54ec579 feat(backups): expose setting to purge snapshot data
* when using CBT backup, no rolling snapshot and NBD
* this will delete the snapshot data , thus limiting the pressure on the SR
* snapshot won't be listed in the UI
* snapshot won't be usable to revert a VM
2024-07-04 16:24:25 +02:00
Florent BEAUCHAMP
7b256ab8ee feat(backups): enable Change Block Tracking Backups (#7750)
Enable CBT for all backups

* enable cbt each time a VM should be backuped by nbd
* if there are no rolling snapshot on a job =>
the snapshots data are only kept during the transfer.
Snapshot data deletion is behing a config flag 'purgeSnapshotData' (default false )

adverse effect:

* rolling back to previous XO version will make full backups
* removed read ahead in nbd , performance gain was low with concurrency
and it make code more complex
2024-07-04 16:24:25 +02:00
Florent Beauchamp
7a2bb5104c fix(backups): Cannot destructure property 'other_config' of 'undefined'
introduced by a85f44d699
2024-07-04 16:24:25 +02:00
Florent Beauchamp
56c881fd2a fix(backups): ensure vhd stream are exactly the right length 2024-07-04 16:24:25 +02:00
Florent Beauchamp
db89485efe fix(backups): fix a off by one error on delta chain length 2024-07-04 16:24:25 +02:00
Julien Fontanet
3c6a7a9575 feat(xo-server/rest-api): expose servers 2024-07-04 15:52:48 +02:00
Julien Fontanet
992d636bcd feat(xo-server/rest-api): expose a VM's VDIs 2024-07-03 15:41:59 +02:00
Mathieu
253aac4eaa feat(xo-web/SR/XOSTOR): add state column to the resource list table (#7784) 2024-06-28 16:25:56 +02:00
Mathieu
ea551a5175 chore(xo-web/SR/XOSTOR): remove unused import (#7785)
Introduced by 1ab62d4
2024-06-28 16:19:47 +02:00
OlivierFL
fe1f5cbad7 feat(xo-6/tree-view): add simple search (#7780) 2024-06-28 15:05:56 +02:00
OlivierFL
ce4c2e17af feat(xo-core): add UiInput component (#7724) 2024-06-28 14:52:29 +02:00
Mathieu
e5608680ec feat: technical release (#7787) 2024-06-28 09:49:00 +02:00
Julien Fontanet
6ee7a274d9 Revert "feat(backups): enable Change Block Tracking Backups (#7750)"
This reverts commit 2db21d3086.
2024-06-27 23:00:22 +02:00
b-Nollet
ffc93250f0 feat(xo-server): add progress to RPU and RPR XO-task (#7768) 2024-06-27 22:01:41 +02:00
b-Nollet
52b27ae884 fix(backups): fix delete first on incremental replication (#7739) 2024-06-27 21:39:20 +02:00
b-Nollet
f5cee45c02 feat(backup-reports): mjml template (#7747) 2024-06-27 21:36:59 +02:00
Julien Fontanet
818a5b7f5b fix(xapi/VM_snapshot): sets ignoreVbds to undefined
Introduced by 37a0d9da7

Fixes https://xcp-ng.org/forum/post/79317
2024-06-27 18:23:40 +02:00
Julien Fontanet
19a2f4e002 fix(xapi/VDI_export): destroy NBD task after export
Introduced by 2db21d308

Fixes https://xcp-ng.org/forum/post/79129
2024-06-27 18:20:42 +02:00
Mathieu
83920589db fix(xo-server-sdn-controller): install CA certificate on pools after plugin config update (#7755) 2024-06-27 17:25:48 +02:00
MlssFrncJrg
bc1db9960a fix(xo-web): correct link for the pool secure boot setup (#7783) 2024-06-27 16:54:56 +02:00
Julien Fontanet
5bf1aea761 fix(xo-server/test.createTask): various fixes
Initialize progress and correctly update name
2024-06-27 16:20:26 +02:00
Julien Fontanet
dcf6885dfa feat(xo-server): support [NOSNAP] tag on disks
Fixes https://xcp-ng.org/forum/post/79179
2024-06-27 15:37:56 +02:00
Julien Fontanet
37a0d9da7c feat(xapi/VM_snapshot): ignoredNoBakVdis → ignoredVdisTag
This new param is more flexible and permits the use of other tags.
2024-06-27 15:24:19 +02:00
Mathieu
abcbeb8719 feat: technical release (#7781) 2024-06-27 10:01:22 +02:00
Thierry Goettelmann
8db808a415 feat(web-stack): better layout system (#7777)
- `[core]` Created a StateHero component
  - `[core]` Created a `ComingSoonHero` component
  - `[core]` Created a `LoadingHero` component
  - `[core]` Created an `ObjectNotFound` component
- `[core;lite]` Moved `object-not-found` translation from Lite to Core
- `[core]` Updated `CoreLayout` to remove panel-related code
- `[web]` Renamed `DefaultLayout` to `AppLayout`
- `[web]` Wrap `RouterView` with `AppLayout` in `App.vue`
- `[web]` Removed all slots from `App.vue` but `default` one
- `[web]` Created `web/pages/vm/[id].vue` which handle layout around all `/vm/*` pages
2024-06-27 09:22:22 +02:00
Julien Fontanet
9383306231 fix(xo-server/rest-api): remove incorrect comma operator
Introduced by 800655a9a

Fortunately this did not have any impact.
2024-06-26 14:07:54 +02:00
Julien Fontanet
3235963cfb fix(xo-web/host/advanced): use relative path
This is required to avoid breaking reverse proxies with a preppended path.
2024-06-25 16:40:33 +02:00
Julien Fontanet
b393d72eb9 fix(xo-web/host/advanced): fix netdata link
Fixes #7702
2024-06-25 16:14:35 +02:00
Julien Fontanet
cd2942af71 chore(xo-server-netbox): unify unload handling 2024-06-25 09:03:26 +02:00
Julien Fontanet
e92cb66954 chore(xo-server-netbox/configure): use state.loaded 2024-06-25 09:03:26 +02:00
Pierre Donias
dac0b20a19 fix(xo-server-auth-ldap): do not use syntax "key=value" when key is "DN" (#7759)
See Zammad#25659
2024-06-24 17:19:29 +02:00
Julien Fontanet
0fc5d60791 feat(scripts/normalize-packages): licenses default to ISC for @vates/ 2024-06-24 17:12:17 +02:00
Julien Fontanet
2df144d43d feat(scripts/new-pkg): create a new package in the monorepo 2024-06-24 17:12:17 +02:00
Mathieu
758e3c47b8 fix(xo-web/VM/advanced): fix 'firmware not supported' for UEFI (#7767)
Introduced by 68e99a2
See https://xcp-ng.org/forum/topic/8878/uefi-firmware-not-supported
2024-06-24 14:58:23 +02:00
Mathieu
cfcc78b6d8 feat(xo-web/XOSTOR): column that links to SR (#7764) 2024-06-24 14:38:11 +02:00
Julien Fontanet
5f5580925d docs(xen-api): more CLI examples 2024-06-24 11:29:38 +02:00
Mathieu
a667e7cdbd fix(xo-web/XOSTOR): fix approximate SR capacity (#7765)
Introduced by 038d230
2024-06-24 10:53:45 +02:00
b-Nollet
a14c4bbb1c feat(xo-server/rest-api): add rolling pool reboot action (#7761) 2024-06-21 15:46:37 +02:00
Florent BEAUCHAMP
2db21d3086 feat(backups): enable Change Block Tracking Backups (#7750)
Enable CBT for all backups

* enable cbt each time a VM should be backuped by nbd
* if there are no rolling snapshot on a job => the snapshots data are only kept during the transfer. Snapshot data deletion is behing a config flag 'purgeSnapshotData' (default false )

adverse effect:

* rolling back to previous XO version will make full backups
* removed read ahead in nbd , performance gain was low with concurrency and it make code more complex
2024-06-21 11:32:37 +02:00
MlssFrncJrg
6b1036f5ce feat(xo-web): display SCSI id during creation of SR (#7742) 2024-06-21 10:59:31 +02:00
Florent BEAUCHAMP
5cc6b4fe70 feat(xo-web): add CBT to the disk tab (#7732)
show a toggle to enable CBT from the VM disk tab 

---------

Co-authored-by: Mathieu <70369997+MathieuRA@users.noreply.github.com>
2024-06-21 10:36:20 +02:00
Florent BEAUCHAMP
442dd16813 feat(xo-server): disable cbt on vdi/VM/host migration (#7756)
Change Block Tracking (CBT) is blocking VDI migration from one storage to another
The migration errror is computed at the VM level on the xapi, that mean that we need to disable CBT for any disk of the VM that use a VDI
We don't enable CBT after the migration : any client / backup software that need this will need to enable it on demand, and a migration force a full transfer
 host.evacuate don't move the vdi ,it can't generate a VDI_CBT_ENABLED error
2024-06-21 10:23:41 +02:00
Mathieu
6e000af7cb fix(xo-server/XOSTOR): remove callAsync workaround (#7723) 2024-06-21 09:50:17 +02:00
Julien Fontanet
3c13e0ab43 fix(CHANGELOG): move latest badge
Introduced by 7b8a4e720
2024-06-20 17:18:03 +02:00
Julien Fontanet
7b8a4e7209 feat: release 5.95.1 2024-06-20 17:16:26 +02:00
Pierre Donias
c63d2ab00b fix(netbox): force xo-server to use at least http-request-plus@1.0.3 (#7758)
See Zammad#24458

`xo-server-netbox` uses `response.text` which has been added in
http-request-plus@1.0.3. Therefore, the dependency version in `xo-server` needs
to be `^1.0.3` to ensure that `response.text` can be used.
2024-06-20 17:02:20 +02:00
Florent BEAUCHAMP
ba7a29eb5e docs(backups): nbd concurrency and full on migrate (#7741) 2024-06-20 15:36:23 +02:00
Paloma
304b06ca97 feat(xo-core): add card subtitle ui component (#7731) 2024-06-20 15:13:04 +02:00
Julien Fontanet
0661fbae78 feat(xo-server): 5.145.0 2024-06-20 15:08:00 +02:00
Julien Fontanet
16ae1c0d4b fix(lite/stories/card-title): fix imports order
Introduced by 9c29046a8
2024-06-20 14:38:46 +02:00
Paloma
9c29046a88 feat(xo-core): add card title component to web core (#7721) 2024-06-20 11:41:20 +02:00
Julien Fontanet
bb0fd2898e fix(xo-server/rest-api): handle collection without getObject
For instance `/backup/vms` would trigger an *Internal Error*, now it correctly returns 404.
2024-06-20 11:27:14 +02:00
Thierry Goettelmann
276d920cca feat(web-stack): add new i18n guidelines + ESLint + update and fixes (#7753)
- Add new i18n guidelines
- Remove nested keys: `"foo": { "bar": "..." }` ➡️ `"foo.bar": "..."`
  This makes code cleaner (and as a side-effect, it now allows to use `foo` + `foo.bar` keys)
- Lite/6 shared translations are no longer prefixed with `core.`
- Duplicated keys across translation files are no longer accepted.
  This will prevent accidental overwrite of missing common translations
- `fr.json` and `en.json` now required to contains the same keys
- Raw text is no longer accepted in template to prevent forgetting a translation
  Raw text is still accepted in dev-related files (stories, dev pages...)
- Remove useless `alt` on `img`
- Display of percentage now uses `$n(value, 'percent')`
- Removed legacy `lite/src/i18n.ts`
- Add missing translations
- Added visual indicator for missing translations while in dev env: `🌎❗⟨i-am-missing⟩`
2024-06-20 09:35:36 +02:00
Thierry Goettelmann
dd8ad3f274 chore(web stack): add new ESLint Vue rules #6 (#7738)
Enforces import order via the `import/order` ESLint rule
2024-06-20 09:18:15 +02:00
Florent BEAUCHAMP
7ddd579818 fix(backups): EEXIST error when restarting mirror backup (#7694)
feat(backup): remove temporary VHD before backups run

- cleanVm can only delete files when backup job is successful
- an interrupted transfer leave the temp file in place
- the temp file don't change name in a mirror backup since we transfer exactly the same file
- outputStream method use, by default, a flag that create the file if not exists and fail otherwise
    => EEXIST error

- This PR change the behaviour of cleanVM to delete temporary files on the pass before the transfer, thus removing any dot file

---------

Co-authored-by: Julien Fontanet <julien.fontanet@isonoe.net>
2024-06-20 08:24:48 +02:00
Paloma
290f510843 feat(xo-core): add card ui component (#7710)
This component can contain other components and manage their layout.
It can align elements vertically or horizontally.
2024-06-19 11:47:34 +02:00
OlivierFL
c134bbaee3 fix(lite/Dashboard): add missing translations and remove hardcoded values (#7744) 2024-06-18 09:42:27 +02:00
Thierry Goettelmann
e1dd5354a1 chore(web stack): add new ESLint Vue rules #5 (#7736) 2024-06-17 16:56:47 +02:00
Thierry Goettelmann
070258d0e3 chore(web stack): add new ESLint Vue rules #4 (#7734) 2024-06-17 16:48:06 +02:00
Julien Fontanet
f5a49b6da3 fix(xo-server/_xcpUpdate): callAsync result is already parsed
Fixes zammad#26054

Introduced by 08dc33bc9
2024-06-17 10:55:39 +02:00
Florent BEAUCHAMP
a85f44d699 Refactor(backups): move metadata to VDI (#7648)
Preparing for CBT backup that will only keep a part of the VDI snapshot, and remove the VM

* extract most of the other_config operation to a file as constants/methods
* name the constants
* set the relevant metadata values to the vm AND all its attached VDIs
* use the VDI as the base to search and update snapshots/replica/backup
2024-06-17 09:19:46 +02:00
Julien Fontanet
31461118e0 chore: update to turbo@2 2024-06-14 11:43:38 +02:00
Thierry Goettelmann
306a58e412 fix: missing routes in typed-router.d.ts + ignore it from Prettier (#7746) 2024-06-14 10:10:33 +02:00
Julien Fontanet
32448cfa1b feat(task): info, set and warning are now instance methods
Info, properties and warnings should be set on a specific/known task instead of relying on the async context.

For instance, this permits updating the progress of a task from the code on one of its subtasks.
2024-06-13 17:34:34 +02:00
Julien Fontanet
3dde723e31 feat(xo-server/api): no tasks for ignored methods
Follows 33a388193
2024-06-13 16:21:17 +02:00
Julien Fontanet
be5a523b95 feat(xo-server-transport-email): sendEmail() now accepts HTML 2024-06-13 14:56:28 +02:00
Thierry Goettelmann
ad18a860f3 chore(web stack): add new ESLint Vue rules #3 (#7729)
Add `vue/define-macros-order` ESLint rules to ensure consistency across components.

Macros will require to be defined in the following order:
1. `defineOptions`
2. `defineProps`
3. `defineEmits`
4. `defineModel`
5. `defineSlots`

Also ensures `defineExpose` to be the last statement of `<script setup>`
2024-06-13 14:06:26 +02:00
Julien Fontanet
be2efce595 fix(xo-server-transport-email): set text to markdown even if html is defined 2024-06-13 11:23:08 +02:00
Pierre Donias
4295dad3ac fix(web-core): devDependencies → dependencies + peerDependencies (#7707)
web-core is published as a library so its dependencies need to be installed
when web installs its own dependencies

- packages that are (or will be) used by lite and web are declared as
  dependencies in lite and web and as peerDependencies in web-core
- other packages that are only used in web-core are declared as dependencies in
  web-core
2024-06-13 10:42:24 +02:00
Julien Fontanet
cdf902bdf3 chore(.prettierignore): explain why Handlebar is ignored
Introduced by 3f4ca1b32
2024-06-12 15:45:00 +02:00
Thierry Goettelmann
97dd211d0f fix: prettify script must respect .prettierignore (#7745)
Specifying `--ignore-path` option overrides the default value of `[.gitignore, .prettierignore]`.
2024-06-12 15:43:54 +02:00
Paloma
0f11e4c4c4 feat(web-core): add UI separator component (#7722)
This component is a grey line (hr). When its parent is a flexbox with
`flex-direction: column`, it is horizontal, and with `flex-direction: row`,  it
is vertical.
2024-06-12 09:49:55 +02:00
Julien Fontanet
33a388193c feat(xo-server/api): pending/failed calls have an associated tasks 2024-06-11 10:36:29 +02:00
MlssFrncJrg
bed18fc954 feat: open external links in a new tab (#7695) 2024-06-11 10:20:46 +02:00
Thierry Goettelmann
1d8385e1f0 chore(web stack): add new ESLint Vue rules #2 (#7728)
Add `vue/define-emits-declaration` ESLint rule to ensure consistency in `defineEmits` syntax across the components.

It uses the new Vue 3.3+ more succinct syntax.
2024-06-11 09:12:41 +02:00
Thierry Goettelmann
11fbbf5158 chore(web stack): add new ESLint Vue rules (#7727) 2024-06-10 16:54:23 +02:00
Olivier Lambert
209dddda2b docs(roadmap): update our roadmap page (#7709) 2024-06-10 12:54:51 +02:00
Julien Fontanet
7f0984aff0 feat(xo-server/proxy.updateApplianceSettings): support for register and xoaPassword 2024-06-10 10:56:04 +02:00
Julien Fontanet
3f32f9981e chore(xo-server/updateProxyAppliance): simplify code 2024-06-10 10:56:04 +02:00
OlivierFL
faf54e18a8 feat(lite/web-core): add examples for tree-view components stories (#7532) 2024-06-10 09:56:45 +02:00
Pierre Donias
f70f4ea0e6 feat(netbox): support both content_type (Netbox <4) and object_type (Netbox 4) fields
Fixes #7645
See Zammad#24458
See Zammad#25690
See Zammad#24821
See #7733
2024-06-10 09:50:38 +02:00
Pierre Donias
c346cadf24 feat(netbox): explicit supported Netbox versions 2024-06-10 09:50:38 +02:00
Dan Pollak
00c8f37e73 docs: Storage Management (#7674) 2024-06-10 09:02:27 +02:00
Dan Pollak
ee6fa67246 docs: continous → continuous (#7466) 2024-06-07 13:10:25 +02:00
Julien Fontanet
691f36a223 fix(proxy-cli): timeout with long calls
Similar to 29d39a576
2024-06-06 16:38:55 +02:00
Julien Fontanet
3c41d612d0 feat(xo-web/proxies): refresh upgrade status every 30s 2024-06-06 13:58:09 +02:00
Julien Fontanet
0294c3dd68 fix(xo-web/proxies): fetch upgrades even if fetching licenses failed 2024-06-06 13:57:30 +02:00
Dan Pollak
6c25d9e147 docs(incremental_backups): fix XO 5.81 blogpost link (#7725) 2024-06-05 23:07:45 +02:00
Florent BEAUCHAMP
58cc1de804 fix(vmxware-explorer): vsan detection (#7717)
Fixes zammad#25110

Introduced by 2d07fdcd02
2024-06-05 16:37:35 +02:00
b-Nollet
3f4ca1b322 feat(backup-reports): rewrite with Handlebars (#7543) 2024-06-05 15:47:06 +02:00
Julien Fontanet
99e44fe139 chore(xo-web): rename proxyApplianceUpdaterState
The subscription is for a single proxy, not all of them.
2024-06-05 09:22:53 +02:00
Julien Fontanet
b87bc29f0e fix(fuse-vhd): handle readRawData failure (#7718)
The bug may leads to blocked mount.
2024-06-04 16:33:36 +02:00
Mathieu
f6672a643f feat(XO6/core): creation of the interactive UiTable (#7677) 2024-06-03 15:53:47 +02:00
OlivierFL
2a2fe93476 fix(xo-6/tree-view): show halted VMs attached to a pool (#7683)
Introduced by 796bb60
2024-06-03 15:19:00 +02:00
Paloma
93303ca7ac feat(xo-core): add stacked bar UI component (#7676) 2024-06-03 14:28:38 +02:00
Julien Fontanet
3a211524cb docs(installation): request Node LTS
XO is no longer stuck on Node 18.
2024-06-03 10:50:40 +02:00
Paloma
1bba37adb1 feat(xo-core): add card number ui component (#7666) 2024-06-03 09:34:43 +02:00
Paloma
8c4fcc43d2 feat(xo-core): add legend UI component (#7675) 2024-06-03 09:22:56 +02:00
Paloma
e20e3526a5 feat(xo-core): add Legend title UI component (#7678) 2024-06-03 09:17:14 +02:00
Paloma
17fec5876c feat(xo-core): add multi color donut chart ui component (#7661)
SVG based donut chart
2024-06-03 09:16:47 +02:00
Pierre Donias
4cf03cf0a2 feat(lite): 0.2.3 (#7715) 2024-05-31 17:51:06 +02:00
Pierre Donias
57e44734e2 feat: release 5.95.0 (#7714) 2024-05-31 17:39:15 +02:00
Pierre Donias
3075ce9232 feat: technical release (#7713) 2024-05-31 17:22:13 +02:00
Florent BEAUCHAMP
196f3969eb fix(xo-server,vmware-explorer): template is not defined (#7712) 2024-05-31 17:09:49 +02:00
Julien Fontanet
d635de0a96 fix(scripts/gen-deps-list.js): handle dev dependencies 2024-05-31 15:59:52 +02:00
Julien Fontanet
d1e3d79bf8 feat(scripts/gen-deps-list.js): add web & lite
They are applications and must be bumped if any of their deps is bumped.
2024-05-31 15:59:52 +02:00
Pierre Donias
5adec6b2ef feat: technical release (#7708) 2024-05-31 15:59:20 +02:00
MlssFrncJrg
b2b6262aea fix(xo-server,xo-web/pci): allow force reboot host after enabling/disabling PCI passthrough (#7687)
See https://xcp-ng.org/forum/topic/9043/xo-enable-pci-devices-for-pass-through-should-it-work-already-or-not-yet/14
2024-05-31 15:16:47 +02:00
Florent BEAUCHAMP
82d3ec6033 fix(xo-server-perf-alert): improve performances and reliability (#7692)
* make one query per host instead of one per object (VM or host ) per counter ( Memory/CPU)
* fix(xo-server-perf-alert): handle VM without guest tools that does not have memory used metrics
---------

Co-authored-by: b-Nollet <150058367+b-Nollet@users.noreply.github.com>
2024-05-31 14:52:34 +02:00
OlivierFL
3dd5a6ba13 feat(xo-6/vm): add console view (#7693) 2024-05-31 14:23:24 +02:00
Pierre Donias
0215ac2491 fix(yarn.lock): after adding dependency (#7706)
Introduced by 3044a42
2024-05-31 13:43:31 +02:00
Pierre Donias
3044a429cc fix(web-core): missing dependency @fontsource/poppins (#7705) 2024-05-31 11:02:32 +02:00
Florent BEAUCHAMP
53ee6988db feat(xo-server): improve error handling in v2v (#7654)
* use one defer per Vm import
*  improve error lisibility by extracting relevant values
2024-05-31 10:05:01 +02:00
b-Nollet
89950e77f4 fix(xo-server): handle unknown ref in task.other_config.applies_to (#7685) 2024-05-31 09:20:38 +02:00
Julien Fontanet
800655a9a3 fix(xo-server/rest-api): host logs are in tar+gzip
Fixes #7703
2024-05-31 09:12:30 +02:00
Florent BEAUCHAMP
59fbb3ef48 feat(v2v): select template before import (#7566)
Ask user for the base template to use when importing VM
2024-05-31 09:06:17 +02:00
Florent BEAUCHAMP
9133e32dd0 fix(backups/forkStreamUnpipe): unpause stream after unpipe (#7669)
when a backup job does delta replication + backup and the replication
fails to find the base VM, the backup job does not proceed

This commit force the stream to continue to flow after unpiping a
descendant stream
2024-05-31 08:45:33 +02:00
b-Nollet
cb6df6d155 fix(xo-server): don't use stats that were cached exactly one step ago (#7697)
Update stats instead of using cache when cache is exactly one step old. It avoids the stats to be twice the same in a row when fetching it every step.

Introduced by 8347ac6ed8
2024-05-31 08:37:40 +02:00
b-Nollet
20f398398f feat(xo-server-load-balancer): adding option to balance hosts before CPU reaches threshold 2024-05-31 08:36:43 +02:00
b-Nollet
c4d1dee6fd fix(xo-server-load-balancer): execute vCPU-prepositionning separately for each pool 2024-05-31 08:36:43 +02:00
b-Nollet
93627a794c fix(xo-server-load-balancer): don't do vCPU-prepositionning when one host is overloaded 2024-05-31 08:36:43 +02:00
b-Nollet
bfb2442df1 fix(xo-server-load-balancer): some migrations were not awaited 2024-05-31 08:36:43 +02:00
Florent BEAUCHAMP
24d584e905 fix(xo-server): dispose stream properly after v2v import (#7655) 2024-05-31 08:18:10 +02:00
MlssFrncJrg
882facd246 feat: technical release (#7704) 2024-05-30 17:41:04 +02:00
Pierre Donias
4c37be5b31 fix(web): missing prepublishOnly script (#7701) 2024-05-30 16:04:16 +02:00
Pierre Donias
5811a2ede2 chore(web-core): configure package to be published on NPM (#7700) 2024-05-30 14:50:28 +02:00
MlssFrncJrg
8fb918a64d feat: technical release (#7699) 2024-05-30 13:15:38 +02:00
MlssFrncJrg
682198df42 feat(xo-server,xo-web/pool): add migration compression (#7642) 2024-05-29 17:27:00 +02:00
Julien Fontanet
de9288b432 docs(xo-server/REST API): ova extension for VM OVA export
Introduced by acc44f0ce
2024-05-29 17:20:04 +02:00
Julien Fontanet
42d58e9dbf feat(obfuscate): new lib to obfuscate sensitive data
Code was extracted from xo-server and xen-api.
2024-05-29 16:58:54 +02:00
Julien Fontanet
d55f614540 fix(backups): handle migration blocked by a previous run
Fixes https://xcp-ng.org/forum/post/77857
Fixes zammad#25131
Fixes zammad#25443
2024-05-29 16:54:27 +02:00
Julien Fontanet
61ecc85beb feat(xapi): no longer export extractOpaqueRef()
BREAKING CHANGE

It's no longer necessary now that task results are properly parsed.
2024-05-28 11:42:57 +02:00
Julien Fontanet
08dc33bc9d feat(xen-api/watchTask): returned parsed result instead of XML-RPC
BREAKING CHANGE

Also impacts `getResource()` and `putResource()`.
2024-05-28 11:42:57 +02:00
Pierre Donias
e23f649e74 feat(xo-web/tasks): wait 10 seconds before estimating end of task (#7691)
Fixes #7689
2024-05-28 10:27:42 +02:00
OlivierFL
f0cd83e3c4 feat(xo-6/account-menu): add menu items (links to doc and support) (#7662) 2024-05-28 09:13:23 +02:00
OlivierFL
136ea23199 feat(xo-lite): add german translation (#7686)
See #7284
2024-05-28 09:11:17 +02:00
b-Nollet
73ea4fa6b4 fix(fs,xo-server): remote encryption algorithm was private (#7638)
Encryption was not showing up in remote page because the remote mixin was using an attribute that was private. See https://help.vates.tech/#ticket/zoom/24076

Introduced by 86ddb8f2d9
2024-05-27 09:18:51 +02:00
Pierre Donias
990c23b8f5 feat(xo-web/render-xo-item/Vm): yellow icon next to VM name when VM is busy (#7680)
Fixes #7593
2024-05-23 14:24:24 +02:00
Pierre Donias
5fca2f0c74 feat(lite/header): add a link to XOA when detected on the pool (#7679)
When XOA is connected to a pool, it adds information about itself on the pool's
other_config. We can use that in XO Lite to find the XOA's IP address and add a
link to open Xen Orchestra.
2024-05-23 13:32:24 +02:00
Thierry Goettelmann
5cd9e50fa3 feat(web-core/tree): tree filter composable (#7536) 2024-05-23 09:44:00 +02:00
Paloma
18a2b5b549 feat(xo-core): add status pill UI component for backup issues (#7631) 2024-05-23 09:15:30 +02:00
Thierry Goettelmann
da9b9419ae feat(web-core/tree): enhance tree library (#7534)
- Context now stores the selected/expanded/active IDs instead of the `TreeNode` to allow easy manipulation and initial hydration.
- Rename `activable` option to `selectable`
- Rename `labelClasses` to `statuses`
- Add `hasChildren` on `Branch`
- `predicate` now takes the `TreeNode` as argument instead of `TData`
- Add a `meta` option, to pass additional arbitrary data
2024-05-22 17:07:44 +02:00
Olivier Lambert
8c599b0bad docs(advanced): fix old Terraform provider links (#7673) 2024-05-22 15:47:11 +02:00
Julien Fontanet
1ded79169b chore: format with Prettier 2024-05-22 15:42:14 +02:00
Thierry Goettelmann
bea123f347 feat(lite): update stores to use new core subscribable stores (#7602)
Update XO Lite stores to use the new subscribable store system from Web Core
2024-05-22 15:08:55 +02:00
Julien Fontanet
88fb977db9 feat(xo-cli): --url allows bypassing registration
Related to zammad#25158
2024-05-22 12:11:06 +02:00
Julien Fontanet
b48e16c316 feat(xo-cli): use subcommand syntax
This change allows to properly separate options and subcommands,
which opens the door to global options.

Previous syntax is still supported for now but will show a deprecation
message.

```sh
xo-cli --register
xo-cli --createToken

xo-cli register
xo-cli create-token
```
2024-05-22 12:10:37 +02:00
Julien Fontanet
a8ffa91d41 docs(infrastructure): update xo-cli usage 2024-05-22 12:10:16 +02:00
Mathieu
6a67944112 feat(xo-web/XOA/licenses): handle new XOSTOR licenses (#7573) 2024-05-22 11:17:43 +02:00
Mathieu
4908982263 feat(lite/pool/dashboard): remove DVDs from the Storage usage card (#7670) 2024-05-17 14:28:08 +02:00
b-Nollet
6444f88662 chore(xo-server-backup-reports): remove unused legacy handler (#7665) 2024-05-16 16:16:31 +02:00
Pierre Donias
644654c7eb feat(lite/release): add CHANGELOG.md to GitHub release (#7667) 2024-05-16 15:21:00 +02:00
Thierry Goettelmann
5629bbef05 fix(xo-6): rename dynamic pages (#7668)
Fix dynamic pages name: `/:id.vue` => `/[id].vue`
2024-05-16 14:52:52 +02:00
Mathieu
a627fe31e2 fix(XO6/LogoTextOnly): fix color (#7664) 2024-05-16 11:53:54 +02:00
Julien Fontanet
2534d9ff38 chore: update deps 2024-05-16 11:16:18 +02:00
Julien Fontanet
13863beed8 chore: update dev deps 2024-05-16 10:48:44 +02:00
Thierry Goettelmann
796bb60b62 feat(web6): subscribable stores, REST API fetching, infra tree view (#7600)
- Implement subscribable store on XO 6
- Fetch data from REST API
- Display infrastructure tree view
- Updated README
- Added dev pages:
  - **index.vue**: empty page for component development
  - **token.vue**: form to register or clear a token cookie
2024-05-16 09:57:17 +02:00
b-Nollet
7882debe7a feat: release 5.94.2 (#7660) 2024-05-15 11:07:44 +02:00
b-Nollet
548a2e8141 feat: technical release (#7659) 2024-05-15 10:15:11 +02:00
Mathieu
63f89bb6af fix(xo-server/XOSTOR): license.tags can be undefined (#7657)
Co-authored-by: mathieu <mathieura@vates-mathieu.home>
2024-05-15 09:15:46 +02:00
1379 changed files with 68835 additions and 20981 deletions

View File

@@ -39,6 +39,12 @@ module.exports = {
version: '>=16',
},
],
'n/no-unsupported-features/es-builtins': [
'error',
{
version: '>=16',
},
],
'n/no-unsupported-features/es-syntax': [
'error',
{
@@ -49,16 +55,17 @@ module.exports = {
},
{
files: ['@xen-orchestra/{web-core,lite,web}/**/*.{vue,ts}'],
parser: 'vue-eslint-parser',
parserOptions: {
sourceType: 'module',
parser: '@typescript-eslint/parser',
},
plugins: ['import'],
extends: [
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:vue/vue3-recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier',
'plugin:@typescript-eslint/recommended',
'plugin:vue/vue3-essential',
],
settings: {
'import/resolver': {
@@ -73,11 +80,166 @@ module.exports = {
},
},
},
globals: {
XO_LITE_VERSION: true,
XO_LITE_GIT_HEAD: true,
},
rules: {
'no-void': 'off',
'n/no-missing-import': 'off', // using 'import' plugin instead to support TS aliases
'import/order': [
'error',
{
groups: ['builtin', 'object', 'internal', ['type', 'external']],
alphabetize: {
order: 'asc',
caseInsensitive: true,
},
pathGroups: [
{
pattern: '@/**',
group: 'internal',
},
{
pattern: '@core/**',
group: 'internal',
},
],
},
],
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 'error',
'no-void': ['error', { allowAsStatement: true }],
'n/no-missing-import': 'off', // using 'import' plugin instead, to support TS aliases
'no-redeclare': 'off', // automatically checked by the TypeScript compiler
'no-dupe-class-members': 'off', // automatically checked by the TypeScript compiler
'no-undef': 'off', // automatically checked by the TypeScript compiler
'no-use-before-define': 'off', // replaced with the rule below
'@typescript-eslint/no-use-before-define': ['error', { functions: false }],
'@typescript-eslint/no-explicit-any': 'off',
'vue/multi-word-component-names': 'off',
// Vue 3 - Strongly Recommended
'vue/attribute-hyphenation': 'error',
'vue/component-definition-name-casing': 'error',
'vue/first-attribute-linebreak': 'error',
'vue/html-closing-bracket-newline': 'off', // Conflicts with Prettier
'vue/html-closing-bracket-spacing': 'error',
'vue/html-end-tags': 'error',
'vue/html-indent': 'off', // Conflicts with Prettier
'vue/html-quotes': 'error',
'vue/html-self-closing': ['error', { html: { void: 'always', normal: 'always', component: 'always' } }],
'vue/max-attributes-per-line': 'off', // Conflicts with Prettier
'vue/multiline-html-element-content-newline': 'error',
'vue/mustache-interpolation-spacing': 'error',
'vue/no-multi-spaces': 'error',
'vue/no-spaces-around-equal-signs-in-attribute': 'error',
'vue/no-template-shadow': 'error',
'vue/prop-name-casing': 'error',
'vue/require-default-prop': 'off', // https://github.com/vuejs/eslint-plugin-vue/issues/2051
'vue/require-explicit-emits': 'error',
'vue/require-prop-types': 'error',
'vue/singleline-html-element-content-newline': 'off',
'vue/v-bind-style': 'error',
'vue/v-on-event-hyphenation': ['error', 'always', { autofix: true }],
'vue/v-on-style': 'error',
'vue/v-slot-style': 'error',
// Vue 3 - Recommended
'vue/attributes-order': 'error',
'vue/no-lone-template': 'error',
'vue/no-multiple-slot-args': 'error',
'vue/no-v-html': 'error',
'vue/this-in-template': 'error',
// Vue 3 - Uncategorized
'vue/block-lang': ['error', { script: { lang: 'ts' }, style: { lang: 'postcss' } }],
'vue/block-order': ['error', { order: ['template', 'script', 'style'] }],
'vue/block-tag-newline': ['error', { singleline: 'consistent', multiline: 'always', maxEmptyLines: 0 }],
'vue/component-api-style': ['error', ['script-setup']],
'vue/component-name-in-template-casing': ['error', 'PascalCase', { registeredComponentsOnly: false }],
'vue/custom-event-name-casing': ['error', 'camelCase'],
'vue/define-emits-declaration': ['error', 'type-literal'],
'vue/define-macros-order': [
'error',
{
order: ['defineOptions', 'defineProps', 'defineEmits', 'defineModel', 'defineSlots'],
defineExposeLast: true,
},
],
'vue/define-props-declaration': ['error', 'type-based'],
'vue/enforce-style-attribute': ['error', { allow: ['scoped', 'module'] }],
'vue/html-button-has-type': 'error',
'vue/html-comment-content-newline': ['error', { singleline: 'never', multiline: 'always' }],
'vue/html-comment-content-spacing': ['error', 'always'],
'vue/no-duplicate-attr-inheritance': 'error',
'vue/no-empty-component-block': 'error',
'vue/no-multiple-objects-in-class': 'error',
'vue/no-ref-object-reactivity-loss': 'error',
'vue/no-required-prop-with-default': 'error',
'vue/no-static-inline-styles': 'error',
'vue/no-template-target-blank': 'error',
'vue/no-undef-components': ['error', { ignorePatterns: ['RouterLink', 'RouterView', 'I18nT'] }],
'vue/no-undef-properties': 'error',
'vue/no-unused-refs': 'error',
'vue/no-use-v-else-with-v-for': 'error',
'vue/no-useless-mustaches': 'error',
'vue/no-useless-v-bind': 'error',
'vue/no-v-text': 'error',
'vue/padding-line-between-blocks': 'error',
'vue/prefer-define-options': 'error',
'vue/prefer-separate-static-class': 'error',
'vue/prefer-true-attribute-shorthand': 'error',
'vue/require-macro-variable-name': 'error',
'vue/valid-define-options': 'error',
},
},
// Specific rules for XO i18n-related files
{
files: [
'@xen-orchestra/lite/**/*.{vue,ts,json}',
'@xen-orchestra/web/**/*.{vue,ts,json}',
'@xen-orchestra/web-core/**/*.{vue,ts,json}',
],
extends: ['plugin:@intlify/vue-i18n/base'],
settings: {
'vue-i18n': {
localeDir: [
'@xen-orchestra/lite/src/locales/*.json',
'@xen-orchestra/web/src/locales/*.json',
'@xen-orchestra/web-core/lib/locales/*.json',
],
messageSyntaxVersion: '^9.9.0',
},
},
rules: {
strict: 'off',
'no-irregular-whitespace': 'off',
'@intlify/vue-i18n/no-deprecated-i18n-component': 'error',
'@intlify/vue-i18n/no-deprecated-i18n-place-attr': 'error',
'@intlify/vue-i18n/no-deprecated-i18n-places-prop': 'error',
'@intlify/vue-i18n/no-deprecated-modulo-syntax': 'error',
'@intlify/vue-i18n/no-deprecated-tc': 'error',
'@intlify/vue-i18n/no-html-messages': 'error',
'@intlify/vue-i18n/no-i18n-t-path-prop': 'error',
'@intlify/vue-i18n/no-missing-keys': 'error',
'@intlify/vue-i18n/no-raw-text': [
'error',
{
ignoreText: ['Xen Orchestra Lite', 'Xen Orchestra', 'XO Lite', 'XCP-ng', 'XO 5', '404', '⚠️'],
},
],
'@intlify/vue-i18n/no-v-html': 'error',
'@intlify/vue-i18n/valid-message-syntax': 'error',
'@intlify/vue-i18n/no-duplicate-keys-in-locale': 'error',
'@intlify/vue-i18n/no-missing-keys-in-other-locales': ['error', { ignoreLocales: ['de', 'fa', 'cs'] }],
},
},
// Specific rules for XO dev-related files
{
files: [
'@xen-orchestra/lite/src/stories/**/*',
'@xen-orchestra/lite/src/components/component-story/**/*',
'@xen-orchestra/web/src/pages/dev/**/*',
'@xen-orchestra/lite/src/views/story/**/*',
],
rules: {
'@intlify/vue-i18n/no-raw-text': 'off',
},
},
{
@@ -101,6 +263,8 @@ module.exports = {
},
],
ignorePatterns: ['@vates/fatfs/'],
parserOptions: {
ecmaVersion: 13,
sourceType: 'script',
@@ -124,7 +288,7 @@ module.exports = {
// 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',
'require-atomic-updates': ['warn', { allowProperties: true }],
strict: 'error',
},

View File

@@ -5,34 +5,46 @@ jobs:
runs-on: ubuntu-latest
steps:
# https://github.com/actions/checkout
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install packages
run: |
sudo apt-get update
sudo apt-get install -y curl qemu-utils python3-vmdkstream git libxml2-utils libfuse2 nbdkit
# https://turbo.build/repo/docs/guides/ci-vendors/github-actions#caching-with-github-actionscache
- name: Cache Turbo
# https://github.com/actions/cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: '**/node_modules/.cache/turbo'
key: ${{ runner.os }}-turbo-cache
path: '.turbo'
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
# https://github.com/actions/setup-node
- name: Setup Node environment
# https://github.com/actions/setup-node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '18'
node-version: '20'
cache: 'yarn'
- name: Install project dependencies
run: yarn
- name: Ensure packages are sorted in CHANGELOG.unreleased.md
run: ./scripts/gen-deps-list.js --check-order > /dev/null
- 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
run: sudo yarn test-integration

6
.prettierignore Normal file
View File

@@ -0,0 +1,6 @@
# Ignore Handlebars files because partials are not currently supported
# https://github.com/prettier/prettier/issues/11834
**/*.hbs
# Generated files
/@xen-orchestra/web/typed-router.d.ts

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it, beforeEach } = require('test')
const { describe, it, beforeEach } = require('node:test')
const assert = require('assert').strict
const { spy } = require('sinon')

View File

@@ -30,10 +30,9 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
"test": "node --test"
},
"devDependencies": {
"sinon": "^17.0.1",
"test": "^3.2.1"
"sinon": "^18.0.0"
}
}

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it } = require('test')
const { describe, it } = require('node:test')
const assert = require('assert')
const { coalesceCalls } = require('./')

View File

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

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it } = require('test')
const { describe, it } = require('node:test')
const assert = require('node:assert').strict
const { compose } = require('./')

View File

@@ -20,9 +20,6 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
},
"devDependencies": {
"test": "^3.2.1"
"test": "node --test"
}
}

View File

@@ -1,7 +1,7 @@
'use strict'
const assert = require('assert')
const { describe, it } = require('test')
const { describe, it } = require('node:test')
const { decorateClass, decorateWith, decorateMethodsWith, perInstance } = require('./')

View File

@@ -26,9 +26,6 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
},
"devDependencies": {
"test": "^3.2.1"
"test": "node --test"
}
}

View File

@@ -1,7 +1,7 @@
'use strict'
const assert = require('node:assert/strict')
const test = require('test')
const test = require('node:test')
const diff = require('./index.js')

View File

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

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it } = require('test')
const { describe, it } = require('node:test')
const { useFakeTimers, spy, assert } = require('sinon')
const { createDebounceResource } = require('./debounceResource')

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it } = require('test')
const { describe, it } = require('node:test')
const { spy, assert } = require('sinon')
const { deduped } = require('./deduped')

View File

@@ -14,22 +14,21 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.1.5",
"version": "0.1.6",
"engines": {
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
"test": "node --test"
},
"dependencies": {
"@vates/multi-key-map": "^0.2.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/log": "^0.7.1",
"ensure-array": "^1.0.0"
},
"devDependencies": {
"sinon": "^17.0.1",
"test": "^3.2.1"
"sinon": "^18.0.0"
}
}

3
@vates/fatfs/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.DS_Store
.vscode/
node_modules/

127
@vates/fatfs/README.md Normal file
View File

@@ -0,0 +1,127 @@
# @vates/fatfs
This is a fork of the original library [natevw/fatfs](https://github.com/natevw/fatfs).
## Why?
This fork was created to add a missing feature in the original library: the ability to create labels (createLabel).
The original library did not support this functionality, which was required to make Cloudbase-Init work on windows VMs.
See: https://github.com/natevw/fatfs/issues/30, https://github.com/natevw/fatfs/pull/31
A standalone FAT16/FAT32 implementation that takes in a block-access interface and exposes something quite similar to `require('fs')` (i.e. the node.js built-in [Filesystem API](http://nodejs.org/api/fs.html)).
## Installation
`npm install fatfs`
## Example
```js
var fatfs = require('fatfs'),
fs = fatfs.createFileSystem(exampleDriver) // see below
fs.stat('autoexec.bat', function (e, stats) {
if (e) console.error(e)
else console.log(stats)
})
// TODO: open a file and write to it or something…
```
## API
- `fs = fatfs.createFileSystem(vol, [opts], [cb])` — Simply pass in a block driver (see below) mapped to a FAT partition somewhere, and get back the API documented [here](http://nodejs.org/api/fs.html). An options dictionary can be provided, details are in the next section. You may also optionally provide a callback `cb(err)` which will be automatically registered for the on `'ready'` or `'error'` event.
- `'ready'` event — fired on `fs` when initial volume information has been determined and the API is ready to use. It is safe to call other `fs` methods before this fires **only if** you are sure the first sector will be readable and represents a valid FAT volume.
- `'error'` event — fired if initialization fails for whatever reason.
### Filesystem options
The `opts` dictionary you pass to `fatfs.createFileSystem` can contain any of the following options:
- `ro` — Enables readonly mode if `true`. It defaults to `false`, but if your volume driver does not provide a `writeSectors` method it will be overriden to `true`.
- `noatime` — The FAT filesystem can track the last access time (just a date, actually) but this means every read operation would also incur some write overhead. Defaults to `true`, meaning by default access times will **not** be updated on reads. Set this to `false` to track access times.
- `modmode` — chooses how `fs.chmod` (and the mode field from `fs.stat`–family calls) should map FAT attributes to POSIX permissions. Set to the number `0111` to map the readonly flag to the user's write bit being unset, and the archive/system/hidden flags to the user/group/other executable bits respectively. Set to the number `07000` to map the readonly flag to _all_ write bits being unset, and the archive/system/hidden flags to the sticky/setgid/setuid bits respectively. Set to `null` for readonly mapping. Defaults to `0111`.
- `umask` — any bits _set_ in this octal number will be _unset_ in the 'mode' field from `fs.stat`–family calls. It does not affect anything else. Defaults to `process.umask()`, or `0022` if that is unavailable.
- `uid` — This value will be returned as the 'uid' stat field. It does not affect anything else. Defaults to `process.getuid()`, or `0` if that is unavailable.
- `gid` — This value will be returned as the 'gid' stat field. It does not affect anything else. Defaults to `process.getgid()`, or `0` if that is unavailable.
(Note that these are similar to the options you could use with a POSIX `mount` operation.)
And that's it! The [rest of the API](http://nodejs.org/api/fs.html) (`fs.readdir`, `fs.open`, `fs.createReadStream`, `fs.appendFile`, etc.) is as documented by the node.js project.
Well, sort of…
## Caveats
### Temporary
- **BETA** **BETA** **BETA**. Seriously, this is a _brand new_, _from scratch_, _completely unproven_ filesystem implementation. It does not have full automated test coverage, and it has not been manually tested very much either. Please please please **make sure you have a backup** of any important drive/image/card you unleash this upon.
- A few methods are not quite implemented, either: `fs.rename`, `fs.unlink` and `fs.rmdir`, as well as `fs.watchFile`/`fs.unwatchFile` and `fs.watch`. These are Coming Soon™.
- There are several internal housekeeping items (redundant FAT tables, extra FAT32 information, etc.) that are not done. These do not seem to affect interop, but you may see warnings when repairing a filesystem written by this module.
- Oh, and not to scare you, but if an IO error happens while writing, the library usually just bails — bubbling an error up to your callback as if it were a hot potato. Although some attempt has been made to do separate writes in the safest order (e.g. allocating an additional file cluster, then appending data into it, and then finally updating the file's size), but this behavior has not been thoroughly audited for all operations. There's certainly no attempt to retry/cleanup/rollback if a multi-step change runs into trouble partway through.
### As-planned
Some of the differences between `fatfs` and the node.js `fs` module are "by design" for architectural simplicity and/or due to underlying FAT limitations.
- There are no `fs.*Sync` methods. (The volume driver is async, not to mention that supporting a separate \*Sync codepath would be an enormous duplication of effort of dubious value.)
- This module does [almost] no read/write caching. This should be done in your volume driver, but see notes below.
- You'll need multiple `createFileSystem` instances for multiple volumes; paths are relative to each, and don't share a namespace.
- The FAT filesystem has no concept of symlinks, and hardlinks are not really an intentional feature. You will get an ENOSYS-like error when trying to create either type of link.
## "Volume driver" API
To use 'fatfs', you must provide a driver object with the following properties/methods:
- `driver.sectorSize` — number of bytes per sector on this device
- `driver.numSectors` — count of sectors available on this media
- `driver.readSectors(i, dest, cb)` — Fill `dest` with data starting at the `i`th sector and notify `cb(e)` when finished. You may assume `dest.length` is a multiple of `driver.sectorSize`.
- `driver.writeSectors(i, data, cb)` — (optional) Write `data` starting at the `i`th sector and notify `cb(e)` when finished. You may assume `data.length` is a multiple of `driver.sectorSize`.
If you do not provide a `writeSectors` method, then `fatfs` will work in readonly mode. Pretty simple, eh? And the 'fatfs' module makes a good effort to check the parameters passed to your driver methods!
**TBD:** to facilitate proper cache handling, this module might add an optional `driver.flush(cb)` method at some point in the future.
Here's an example taken from code used to run this module's own tests:
```js
// NOTE: this assumes image at `path` has no partition table.
// If it did, you'd need to translate positions, natch…
var fs = require('fs')
exports.createDriverSync = function (path, opts) {
opts || (opts = {})
var secSize = 512,
ro = opts.readOnly || false,
fd = fs.openSync(path, ro ? 'r' : 'r+'),
s = fs.fstatSync(fd)
return {
sectorSize: secSize,
numSectors: s.size / secSize,
readSectors: function (i, dest, cb) {
if (dest.length % secSize) throw Error('Unexpected buffer length!')
fs.read(fd, dest, 0, dest.length, i * secSize, function (e, n, d) {
cb(e, d)
})
},
writeSectors: ro
? null
: function (i, data, cb) {
if (data.length % secSize) throw Error('Unexpected buffer length!')
fs.write(fd, data, 0, data.length, i * secSize, function (e) {
cb(e)
})
},
}
}
```
## License
© 2014 Nathan Vander Wilt.
Funding for this work was provided by Technical Machine, Inc.
Reuse under your choice of:
- [BSD-2-Clause](http://opensource.org/licenses/BSD-2-Clause)
- [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.html)

60
@vates/fatfs/cache.js Normal file
View File

@@ -0,0 +1,60 @@
var _ = require("./helpers.js");
exports.wrapDriver = function (volume, opts) {
opts = _.extend({
maxSectors: 2048
}, opts);
var cache = {},
advice = 'NORMAL',
secSize = volume.sectorSize;
function _freezeBuffer(b) {
var f = _.allocBuffer(b.length);
b.copy(f);
return f;
}
function addToCache(i, data) {
if (advice === 'SEQUENTIAL' || advice === 'NOREUSE') return;
data = _freezeBuffer(data);
cache[i] = data;
//if (data.length > secSize) addToCache(i+1, data.slice(secSize));
while (data.length > secSize) {
data = data.slice(secSize);
cache[++i] = data;
}
// simple highest-sectors-lose eviction policy for now
Object.keys(cache).sort().slice(opts.maxSectors).forEach(function (x) {
delete cache[x];
});
_.log(_.log.DBG, "Cache now contains:", Object.keys(cache).join(','));
}
return {
sectorSize: volume.sectorSize,
numSectors: volume.numSectors,
advice: function (val) {
if (!arguments.length) return advice;
else advice = val;
if (advice === 'SEQUENTIAL' || advice === 'NOREUSE') cache = {};
return this;
},
readSectors: function (i, dest, cb) {
// TODO: handle having partial parts of dest!
if (i in cache && dest.length === secSize) {
cache[i].copy(dest);
setImmediate(cb);
} else volume.readSectors(i, dest, function (e) {
if (e) cb(e);
else addToCache(i, dest), cb();
});
},
writeSectors: (!volume.writeSectors) ? null : function (i, data, cb) {
volume.writeSectors(i, data, function (e) {
if (e) cb(e);
else addToCache(i, data), cb();
});
}
};
};

287
@vates/fatfs/chains.js Normal file
View File

@@ -0,0 +1,287 @@
var S = require("./structs.js"),
_ = require("./helpers.js");
function _baseChain(vol) {
var chain = {};
chain.sectorSize = vol._sectorSize;
function posFromOffset(off) {
var secSize = chain.sectorSize,
offset = off % secSize,
sector = (off - offset) / secSize;
return {sector:sector, offset:offset};
}
var sectorCache = vol._makeCache();
Object.defineProperty(chain, 'cacheAdvice', {
enumerable: true,
get: function () { return sectorCache.advice(); },
set: function (v) { sectorCache.advice(v); }
});
chain._vol_readSectors = vol._readSectors.bind(vol, sectorCache);
chain._vol_writeSectors = vol._writeSectors.bind(vol, sectorCache);
// cb(error, bytesRead, buffer)
chain.readFromPosition = function (targetPos, buffer, cb) {
if (typeof targetPos === 'number') targetPos = posFromOffset(targetPos);
if (typeof buffer === 'number') buffer = _.allocBuffer(buffer);
/* NOTE: to keep our contract with the volume driver, we need to read on _full_ sector boundaries!
So we divide the read into [up to] three parts: {preface, main, trailer}
This is kind of unfortunate, but in practice should often still be reasonably efficient. */
if (targetPos.offset) chain.readSectors(targetPos.sector, _.allocBuffer(chain.sectorSize), function (e,d) {
if (e || !d) cb(e, 0, buffer);
else { // copy preface into `buffer`
var dBeg = targetPos.offset,
dEnd = dBeg + buffer.length;
d.copy(buffer, 0, dBeg, dEnd);
if (dEnd > d.length) readMain();
else cb(null, buffer.length, buffer);
}
}); else readMain();
function readMain() {
var prefaceLen = targetPos.offset && (chain.sectorSize - targetPos.offset),
trailerLen = (buffer.length - prefaceLen) % chain.sectorSize,
mainSector = (prefaceLen) ? targetPos.sector + 1 : targetPos.sector,
mainBuffer = (trailerLen) ? buffer.slice(prefaceLen, -trailerLen) : buffer.slice(prefaceLen);
if (mainBuffer.length) chain.readSectors(mainSector, mainBuffer, function (e,d) {
if (e || !d) cb(e, prefaceLen, buffer);
else if (!trailerLen) cb(null, buffer.length, buffer);
else readTrailer();
}); else readTrailer();
function readTrailer() {
var trailerSector = mainSector + (mainBuffer.length / chain.sectorSize);
chain.readSectors(trailerSector, _.allocBuffer(chain.sectorSize), function (e,d) {
if (e || !d) cb(e, buffer.length-trailerLen, buffer);
else {
d.copy(buffer, buffer.length-trailerLen, 0, trailerLen);
cb(null, buffer.length, buffer);
}
});
}
}
};
// cb(error)
chain.writeToPosition = function (targetPos, data, cb) {
_.log(_.log.DBG, "WRITING", data.length, "bytes at", targetPos, "in", this.toJSON(), data);
if (typeof targetPos === 'number') targetPos = posFromOffset(targetPos);
var prefaceBuffer = (targetPos.offset) ? data.slice(0, chain.sectorSize-targetPos.offset) : null;
if (prefaceBuffer) _modifySector(targetPos.sector, targetPos.offset, prefaceBuffer, function (e) {
if (e) cb(e);
else if (prefaceBuffer.length < data.length) writeMain();
else cb();
}); else writeMain();
function writeMain() {
var prefaceLen = (prefaceBuffer) ? prefaceBuffer.length : 0,
trailerLen = (data.length - prefaceLen) % chain.sectorSize,
mainSector = (prefaceLen) ? targetPos.sector + 1 : targetPos.sector,
mainBuffer = (trailerLen) ? data.slice(prefaceLen, -trailerLen) : data.slice(prefaceLen);
if (mainBuffer.length) chain.writeSectors(mainSector, mainBuffer, function (e) {
if (e) cb(e);
else if (!trailerLen) cb();
else writeTrailer();
}); else writeTrailer();
function writeTrailer() {
var trailerSector = mainSector + (mainBuffer.length / chain.sectorSize),
trailerBuffer = data.slice(data.length-trailerLen); // WORKAROUND: https://github.com/tessel/runtime/issues/721
_modifySector(trailerSector, 0, trailerBuffer, cb);
}
}
function _modifySector(sec, off, data, cb) {
chain.readSectors(sec, _.allocBuffer(chain.sectorSize), function (e, orig) {
if (e) return cb(e);
orig || (orig = _.allocBuffer(chain.sectorSize, 0));
data.copy(orig, off);
chain.writeSectors(sec, orig, cb);
});
}
};
return chain;
};
exports.clusterChain = function (vol, firstCluster, _parent) {
var chain = _baseChain(vol),
cache = [firstCluster];
chain.firstCluster = firstCluster;
function _cacheIsComplete() {
return cache[cache.length-1] === 'eof';
}
function extendCacheToInclude(i, cb) { // NOTE: may `cb()` before returning!
if (i < cache.length) cb(null, cache[i]);
else if (_cacheIsComplete()) cb(null, 'eof');
else vol.fetchFromFAT(cache[cache.length-1], function (e,d) {
if (e) cb(e);
else if (typeof d === 'string' && d !== 'eof') cb(S.err.IO());
else {
cache.push(d);
extendCacheToInclude(i, cb);
}
});
}
function expandChainToLength(clusterCount, cb) {
if (!_cacheIsComplete()) throw Error("Must be called only when cache is complete!");
else cache.pop(); // remove 'eof' entry until finished
function addCluster(clustersNeeded, lastCluster) {
if (!clustersNeeded) cache.push('eof'), cb();
else vol.allocateInFAT(lastCluster, function (e, newCluster) {
if (e) cb(e);
else vol.storeToFAT(lastCluster, newCluster, function (e) {
if (e) return cb(e);
cache.push(newCluster);
addCluster(clustersNeeded-1, newCluster);
});
});
}
addCluster(clusterCount - cache.length, cache[cache.length - 1]);
}
function shrinkChainToLength(clusterCount, cb) {
if (!_cacheIsComplete()) throw Error("Must be called only when cache is complete!");
else cache.pop(); // remove 'eof' entry until finished
function removeClusters(count, cb) {
if (!count) cache.push('eof'), cb();
else vol.storeToFAT(cache.pop(), 'free', function (e) {
if (e) cb(e);
else removeClusters(count - 1, cb);
});
}
// NOTE: for now, we don't remove the firstCluster ourselves; we should though!
if (clusterCount) removeClusters(cache.length - clusterCount, cb);
else removeClusters(cache.length - 1, cb);
}
// [{firstSector,numSectors},{firstSector,numSectors},…]
function determineSectorGroups(sectorIdx, numSectors, alloc, cb) {
var sectorOffset = sectorIdx % vol._sectorsPerCluster,
clusterIdx = (sectorIdx - sectorOffset) / vol._sectorsPerCluster,
numClusters = Math.ceil((numSectors + sectorOffset) / vol._sectorsPerCluster),
chainLength = clusterIdx + numClusters;
extendCacheToInclude(chainLength-1, function (e,c) {
if (e) cb(e);
else if (c === 'eof' && alloc) expandChainToLength(chainLength, function (e) {
if (e) cb(e);
else _determineSectorGroups();
});
else _determineSectorGroups();
});
function _determineSectorGroups() {
// …now we have a complete cache
var groups = [],
_group = null;
for (var i = clusterIdx; i < chainLength; ++i) {
var c = (i < cache.length) ? cache[i] : 'eof';
if (c === 'eof') break;
else if (_group && c !== _group._nextCluster) {
groups.push(_group);
_group = null;
}
if (!_group) _group = {
_nextCluster: c+1,
firstSector: vol._firstSectorOfCluster(c) + sectorOffset,
numSectors: vol._sectorsPerCluster - sectorOffset
}; else {
_group._nextCluster += 1;
_group.numSectors += vol._sectorsPerCluster;
}
sectorOffset = 0; // only first group is offset
}
if (_group) groups.push(_group);
cb(null, groups, i === chainLength);
}
}
chain.readSectors = function (i, dest, cb) {
var groupOffset = 0, groupsPending;
determineSectorGroups(i, dest.length / chain.sectorSize, false, function (e, groups, complete) {
if (e) cb(e);
else if (!complete) groupsPending = -1, _pastEOF(cb);
else if ((groupsPending = groups.length)) groups.forEach(function (group) {
var groupLength = group.numSectors * chain.sectorSize,
groupBuffer = dest.slice(groupOffset, groupOffset += groupLength);
chain._vol_readSectors(group.firstSector, groupBuffer, function (e,d) {
if (e && groupsPending !== -1) groupsPending = -1, cb(e);
else if (--groupsPending === 0) cb(null, dest);
});
});
else cb(null, dest); // 0-length destination case
});
};
// TODO: does this handle NOSPC condition?
chain.writeSectors = function (i, data, cb) {
var groupOffset = 0, groupsPending;
determineSectorGroups(i, data.length / chain.sectorSize, true, function (e, groups) {
if (e) cb(e);
else if ((groupsPending = groups.length)) groups.forEach(function (group) {
var groupLength = group.numSectors * chain.sectorSize,
groupBuffer = data.slice(groupOffset, groupOffset += groupLength);
chain._vol_writeSectors(group.firstSector, groupBuffer, function (e) {
if (e && groupsPending !== -1) groupsPending = -1, cb(e);
else if (--groupsPending === 0) cb();
});
});
else cb(); // 0-length data case
});
};
chain.truncate = function (numSectors, cb) {
extendCacheToInclude(Infinity, function (e,c) {
if (e) return cb(e);
var currentLength = cache.length-1,
clustersNeeded = Math.ceil(numSectors / vol._sectorsPerCluster);
if (clustersNeeded < currentLength) shrinkChainToLength(clustersNeeded, cb);
else if (clustersNeeded > currentLength) expandChainToLength(clustersNeeded, cb);
else cb();
});
};
chain.toJSON = function () {
return {firstCluster:firstCluster};
};
return chain;
};
exports.sectorChain = function (vol, firstSector, numSectors) {
var chain = _baseChain(vol);
chain.firstSector = firstSector;
chain.numSectors = numSectors;
chain.readSectors = function (i, dest, cb) {
if (i < numSectors) chain._vol_readSectors(firstSector+i, dest, cb);
else _pastEOF(cb);
};
chain.writeSectors = function (i, data, cb) {
if (i < numSectors) chain._vol_writeSectors(firstSector+i, data, cb);
else _.delayedCall(cb, S.err.NOSPC());
};
chain.truncate = function (i, cb) {
_.delayedCall(cb, S.err.INVAL());
};
chain.toJSON = function () {
return {firstSector:firstSector, numSectors:numSectors};
};
return chain;
};
// NOTE: used with mixed feelings, broken out to mark uses
function _pastEOF(cb) { _.delayedCall(cb, null, null); }

372
@vates/fatfs/dir.js Normal file
View File

@@ -0,0 +1,372 @@
var S = require("./structs.js"),
_ = require("./helpers.js");
var dir = exports;
dir.iterator = function (dirChain, opts) {
opts || (opts = {});
var cache = {buffer:null, n: null};
function getSectorBuffer(n, cb) {
if (cache.n === n) cb(null, cache.buffer);
else cache.n = cache.buffer = null, dirChain.readSectors(n, _.allocBuffer(dirChain.sectorSize), function (e,d) {
if (e) cb(e);
else if (!d) return cb(null, null);
else {
cache.n = n;
cache.buffer = d;
getSectorBuffer(n, cb);
}
});
}
var secIdx = 0,
off = {bytes:0},
long = null;
function getNextEntry(cb) {
if (off.bytes >= dirChain.sectorSize) {
secIdx += 1;
off.bytes -= dirChain.sectorSize;
}
var entryPos = {chain:dirChain, sector:secIdx, offset:off.bytes};
getSectorBuffer(secIdx, function (e, buf) {
if (e) return cb(S.err.IO());
else if (!buf) return cb(null, null, entryPos);
var entryIdx = off.bytes,
signalByte = buf[entryIdx];
if (signalByte === S.entryDoneFlag) return cb(null, null, entryPos);
else if (signalByte === S.entryFreeFlag) {
off.bytes += S.dirEntry.size;
long = null;
if (opts.includeFree) return cb(null, {_free:true,_pos:entryPos}, entryPos);
else return getNextEntry(cb); // usually just skip these
}
var attrByte = buf[entryIdx+S.dirEntry.fields.Attr.offset],
entryType = (attrByte === S.longDirFlag) ? S.longDirEntry : S.dirEntry_simple;
var entry = entryType.valueFromBytes(buf, off);
entry._pos = entryPos;
_.log(_.log.DBG, "entry:", entry, secIdx, entryIdx);
if (entryType === S.longDirEntry) {
var firstEntry;
if (entry.Ord & S.lastLongFlag) {
firstEntry = true;
entry.Ord &= ~S.lastLongFlag;
long = {
name: -1,
sum: entry.Chksum,
_rem: entry.Ord-1,
_arr: []
}
}
if (firstEntry || long && entry.Chksum === long.sum && entry.Ord === long._rem--) {
var S_lde_f = S.longDirEntry.fields,
namepart = entry.Name1;
if (entry.Name1.length === S_lde_f.Name1.size/2) {
namepart += entry.Name2;
if (entry.Name2.length === S_lde_f.Name2.size/2) {
namepart += entry.Name3;
}
}
long._arr.push(namepart);
if (!long._rem) {
long.name = long._arr.reverse().join('');
delete long._arr;
delete long._rem;
}
} else long = null;
} else if ((attrByte & 0x08) === 0) { // NOTE: checks `!entry.Attr.volume_id`
var bestName = null;
if (long && long.name) {
var pos = entryIdx + S.dirEntry.fields['Name'].offset,
sum = _.checksumName(buf, pos);
if (sum === long.sum) bestName = long.name;
}
if (!bestName) {
if (signalByte === S.entryIsE5Flag) entry.Name.filename = '\u00E5'+entry.Name.filename.slice(1);
var nam = entry.Name.filename.replace(/ +$/, ''),
ext = entry.Name.extension.replace(/ +$/, '');
// TODO: lowercase bits http://en.wikipedia.org/wiki/8.3_filename#Compatibility
// via NTRes, bits 0x08 and 0x10 http://www.fdos.org/kernel/fatplus.txt.1
bestName = (ext) ? nam+'.'+ext : nam;
}
entry._name = bestName;
// OPTIMIZATION: avoid processing any fields for non-matching entries
// TODO: we could make this automatic via getters, but…?
var _entryBuffer = buf.slice(off.bytes-S.dirEntry.size, off.bytes);
entry._full = function () {
var _entry = S.dirEntry.valueFromBytes(_entryBuffer);
_.extend(entry, _entry);
entry._size = entry.FileSize;
entry._firstCluster = (entry.FstClusHI << 16) + entry.FstClusLO;
return entry;
};
long = null;
return cb(null, entry, entryPos);
} else long = null;
getNextEntry(cb);
});
}
function iter(cb) {
getNextEntry(cb);
return iter; // TODO: previous value can't be re-used, so why make caller re-assign?
}
return iter;
};
function _updateEntry(vol, entry, newStats) {
if ('size' in newStats) entry._size = entry.FileSize = newStats.size;
if ('_touch' in newStats) newStats.archive = newStats.atime = newStats.mtime = true;
if ('archive' in newStats) entry.Attr.archive = true; // TODO: also via newStats.mode?
var _now;
function applyDate(d, prefix, timeToo, tenthToo) {
if (d === true) d = _now || (_now = new Date());
entry[prefix+'Date'] = {year:d.getFullYear()-1980, month:d.getMonth()+1, day:d.getDate()};
if (timeToo) {
entry[prefix+'Time'] = {hours:d.getHours(), minutes:d.getMinutes(), seconds_2:d.getSeconds()>>>1};
if (tenthToo) {
var msec = (d.getSeconds() % 2)*1000 + d.getMilliseconds();
entry[prefix+'TimeTenth'] = Math.floor(msec / 100);
}
}
}
if ('ctime' in newStats) applyDate(newStats.ctime, 'Crt', true, true);
if ('mtime' in newStats) applyDate(newStats.mtime, 'Wrt', true);
if ('atime' in newStats) applyDate(newStats.atime, 'LstAcc');
if ('mode' in newStats) {
entry.Attr.directory = (newStats.mode & S._I.FDIR) ? true : false;
entry.Attr.volume_id = (newStats.mode & S._I.FREG) ? false : true;
if (vol.opts.modmode === 0111) {
entry.Attr.archive = (newStats.mode & S._I.XUSR) ? true : false;
entry.Attr.system = (newStats.mode & S._I.XGRP) ? true : false;
entry.Attr.hidden = (newStats.mode & S._I.XOTH) ? true : false;
entry.Attr.readonly = (newStats.mode & S._I.WUSR) ? false : true;
} else if (vol.opts.modmode === 07000) {
entry.Attr.archive = (newStats.mode & S._I.SVTX) ? true : false;
entry.Attr.system = (newStats.mode & S._I.SGID) ? true : false;
entry.Attr.hidden = (newStats.mode & S._I.SUID) ? true : false;
entry.Attr.readonly = (
newStats.mode & S._I.WUSR ||
newStats.mode & S._I.WGRP ||
newStats.mode & S._I.WOTH
) ? false : true;
}
}
if ('firstCluster' in newStats) {
entry.FstClusLO = newStats.firstCluster & 0xFFFF;
entry.FstClusHI = newStats.firstCluster >>> 16;
entry._firstCluster = newStats.firstCluster;
}
return entry;
}
dir.makeStat = function (vol, entry) {
var stats = {}; // TODO: return an actual `instanceof fs.Stat` somehow?
stats.isFile = function () {
return (!entry.Attr.volume_id && !entry.Attr.directory);
};
stats.isDirectory = function () {
return entry.Attr.directory;
};
stats.isBlockDevice = function () { return false; }
stats.isCharacterDevice = function () { return false; }
stats.isSymbolicLink = function () { return false; }
stats.isFIFO = function () { return false; }
stats.isSocket = function () { return false; }
stats.size = entry.FileSize;
stats.blksize = vol._sectorsPerCluster*vol._sectorSize;
stats.blocks = Math.ceil(stats.size / stats.blksize) || 1;
stats.nlink = 1;
stats.mode = S._I.RUSR | S._I.RGRP | S._I.ROTH;
if (!entry.Attr.readonly) stats.mode |= S._I.WUSR | S._I.WGRP | S._I.WOTH;
if (entry.Attr.directory) stats.mode |= S._I.FDIR;
else if (!entry.Attr.volume_id) stats.mode |= S._I.FREG;
// NOTE: discussion at https://github.com/natevw/fatfs/issues/7
if (vol.opts.modmode === 0111) {
// expose using executable bits, like Samba
if (entry.Attr.archive) stats.mode |= S._I.XUSR;
if (entry.Attr.system) stats.mode |= S._I.XGRP;
if (entry.Attr.hidden) stats.mode |= S._I.XOTH;
} else if (vol.opts.modmode === 07000) {
// expose using setXid/sticky bits, like MKS
if (entry.Attr.archive) stats.mode |= S._I.SVTX;
if (entry.Attr.system) stats.mode |= S._I.SGID;
if (entry.Attr.hidden) stats.mode |= S._I.SUID;
}
stats.mode &= ~vol.opts.umask;
stats.uid = vol.opts.uid;
stats.gid = vol.opts.gid;
function extractDate(prefix) {
var date = entry[prefix+'Date'],
time = entry[prefix+'Time'] || {hours:0, minutes:0, seconds_2:0},
secs = time.seconds_2 * 2,
sect = entry[prefix+'TimeTenth'] || 0;
if (sect > 100) {
secs += 1;
sect -= 100;
}
return new Date(date.year+1980, date.month-1, date.day, time.hours, time.minutes, secs, sect*100);
}
stats.atime = extractDate('LstAcc');
stats.mtime = extractDate('Wrt');
stats.ctime = extractDate('Crt');
entry = { // keep immutable copy (with only the fields we need)
Attr: _.extend({},entry.Attr),
};
return stats;
};
dir.init = function (vol, dirInfo, cb) {
var dirChain = dirInfo.chain,
isRootDir = ('numSectors' in dirChain), // HACK: all others would be a clusterChain
initialCluster = _.allocBuffer(dirChain.sectorSize*vol._sectorsPerCluster),
entriesOffset = {bytes:0};
initialCluster.fill(0);
function writeEntry(name, clusterNum) {
while (name.length < 8) name += " ";
S.dirEntry.bytesFromValue(_updateEntry(vol, {
Name: {filename:name, extension:" "},
Attr: {directory:true}
}, {firstCluster:clusterNum, _touch:true,ctime:true}), initialCluster, entriesOffset);
}
if (!isRootDir) {
writeEntry(".", dirChain.firstCluster);
writeEntry("..", dirInfo.parent.chain.firstCluster);
};
dirChain.writeToPosition(0, initialCluster, cb);
};
dir.addFile = function (vol, dirChain, entryInfo, opts, cb) {
if (typeof opts === 'function') {
cb = opts;
opts = {};
}
var name = entryInfo.name,
entries = [], mainEntry;
entries.push(mainEntry = {
Name: _.shortname(name),
Attr: {directory:opts.dir||false},
_name: name
});
if (1 || mainEntry.Name._lossy) { // HACK: always write long names until `._lossy` is more useful!
var workaroundTessel427 = ('\uFFFF'.length !== 1);
if (workaroundTessel427) throw Error("Your JS runtime does not have proper Unicode string support. (If Tessel, is your firmware up-to-date?)");
// name entries should be 0x0000-terminated and 0xFFFF-filled
var S_lde_f = S.longDirEntry.fields,
ENTRY_CHUNK_LEN = (S_lde_f.Name1.size + S_lde_f.Name2.size + S_lde_f.Name3.size)/2,
partialLen = name.length % ENTRY_CHUNK_LEN,
paddingNeeded = partialLen && (ENTRY_CHUNK_LEN - partialLen);
if (paddingNeeded--) name += '\u0000';
while (paddingNeeded-- > 0) name += '\uFFFF';
// now fill in as many entries as it takes
var off = 0,
ord = 1;
while (off < name.length) entries.push({
Ord: ord++,
Name1: name.slice(off, off+=S_lde_f.Name1.size/2),
Attr_raw: S.longDirFlag,
Chksum: null,
Name2: name.slice(off, off+=S_lde_f.Name2.size/2),
Name3: name.slice(off, off+=S_lde_f.Name3.size/2)
});
entries[entries.length - 1].Ord |= S.lastLongFlag;
}
if (entryInfo.tail) {
var name = mainEntry.Name.filename,
suffix = '~'+entryInfo.tail,
endIdx = name.indexOf(' '),
sufIdx = (~endIdx) ? Math.min(endIdx, name.length-suffix.length) : name.length-suffix.length;
if (sufIdx < 0) return cb(S.err.NAMETOOLONG()); // TODO: would EXIST be more correct?
mainEntry.Name.filename = name.slice(0,sufIdx)+suffix+name.slice(sufIdx+suffix.length);
_.log(_.log.DBG, "Shortname amended to:", mainEntry.Name);
}
vol.allocateInFAT(dirChain.firstCluster || 2, function (e,fileCluster) {
if (e) return cb(e);
var nameBuf = S.dirEntry.fields['Name'].bytesFromValue(mainEntry.Name),
nameSum = _.checksumName(nameBuf);
// TODO: finalize initial properties… (via `opts.mode` instead?)
_updateEntry(vol, mainEntry, {firstCluster:fileCluster, size:0, ctime:true,_touch:true});
mainEntry._pos = _.adjustedPos(vol, entryInfo.target, S.dirEntry.size*(entries.length-1));
entries.slice(1).forEach(function (entry) {
entry.Chksum = nameSum;
});
entries.reverse();
if (entryInfo.lastEntry) entries.push({});
var entriesData = _.allocBuffer(S.dirEntry.size*entries.length),
dataOffset = {bytes:0};
entries.forEach(function (entry) {
var entryType = ('Ord' in entry) ? S.longDirEntry : S.dirEntry;
entryType.bytesFromValue(entry, entriesData, dataOffset);
});
_.log(_.log.DBG, "Writing", entriesData.length, "byte directory entry", mainEntry, "into", dirChain.toJSON(), "at", entryInfo.target);
dirChain.writeToPosition(entryInfo.target, entriesData, function (e) {
// TODO: if we get error, what/should we clean up?
if (e) cb(e);
else cb(null, mainEntry, vol.chainForCluster(fileCluster, dirChain));
});
});
};
dir.findInDirectory = function (vol, dirChain, name, opts, cb) {
var matchName = name.toUpperCase(),
tailName = (opts.prepareForCreate) ? _.shortname(name) : null,
maxTail = 0;
function processNext(next) {
next = next(function (e, d, entryPos) {
if (e) cb(e);
else if (!d) cb(S.err.NOENT(), {tail:maxTail, target:entryPos, lastEntry:true});
else if (d._free) processNext(next); // TODO: look for long enough reusable run
else if (d._name.toUpperCase() === matchName) return cb(null, d._full());
else if (!opts.prepareForCreate) processNext(next);
else {
var dNum = 1,
dName = d.Name.filename,
dTail = dName.match(/(.*)~(\d+)/);
if (dTail) {
dNum = +dTail[2];
dName = dTail[1];
}
if (tailName.extension === d.Name.extension &&
tailName.filename.indexOf(dName) === 0)
{
maxTail = Math.max(dNum+1, maxTail);
}
processNext(next);
}
});
}
processNext(dir.iterator(dirChain, {includeFree:(0 && opts.prepareForCreate)}));
};
dir.updateEntry = function (vol, entry, newStats, cb) {
if (!entry._pos || !entry._pos.chain) throw Error("Entry source unknown!");
var entryPos = entry._pos,
newEntry = _updateEntry(vol, entry, newStats),
data = S.dirEntry.bytesFromValue(newEntry);
_.log(_.log.DBG, "UPDATING ENTRY", newStats, newEntry, entryPos, data);
// TODO: if write fails, then entry becomes corrupt!
entryPos.chain.writeToPosition(entryPos, data, cb);
};

206
@vates/fatfs/helpers.js Normal file
View File

@@ -0,0 +1,206 @@
var S = require("./structs.js"),
_xok = require('xok');
// ponyfills for older node.js
exports.allocBuffer = Buffer.alloc || function (len, val) {
var b = Buffer(len);
if (arguments.length > 1) b.fill(val);
return b;
};
exports.bufferFrom = Buffer.from || function (arg0, arg1) {
return (arguments.length > 1) ? Buffer(arg0, arg1) : Buffer(arg0);
};
// flag for WORKAROUND: https://github.com/tessel/beta/issues/380
exports.workaroundTessel380 = !Buffer.from && function () {
var b = Buffer([0]),
s = b.slice(0);
return ((s[0] = 0xFF) !== b[0]);
}();
// WORKAROUND: https://github.com/tessel/beta/issues/433
var oldslice;
if (!Buffer.alloc && Buffer(5).slice(10).length < 0) oldslice = Buffer.prototype.slice, Buffer.prototype.slice = function (s, e) {
if (s > this.length) s = this.length;
// ~WORKAROUND: https://github.com/tessel/beta/issues/434
return (arguments.length > 1) ? oldslice.call(this, s, e) : oldslice.call(this, s);
}
exports.absoluteSteps = function (path) {
var steps = [];
path.split('/').forEach(function (str) {
// NOTE: these should actually be fine, just wasteful…
if (str === '..') steps.pop();
else if (str && str !== '.') steps.push(str);
});
return steps.map(exports.longname);
};
exports.absolutePath = function (path) {
return '/'+exports.absoluteSteps(path).join('/');
};
exports.parseFlags = function (flags) {
// read, write, append, create, truncate, exclusive
var info, _dir; // NOTE: there might be more clever ways to "parse", but…
if (flags[0] === '\\') {
// internal flag used internally to `fs.open` directories without `S.err.ISDIR()`
flags = flags.slice(1);
_dir = true;
}
switch (flags) {
case 'r': info = {read:true, write:false, create:false}; break;
case 'r+': info = {read:true, write:true, create:false}; break;
case 'rs': info = {read:true, write:false, create:false, sync:true}; break;
case 'rs+': info = {read:true, write:true, create:false, sync:true}; break;
case 'w': info = {read:false, write:true, create:true, truncate:true}; break;
case 'wx': info = {read:false, write:true, create:true, exclusive:true}; break;
case 'w+': info = {read:true, write:true, create:true, truncate:true}; break;
case 'wx+': info = {read:true, write:true, create:true, exclusive:true}; break;
case 'a': info = {read:false, write:true, create:true, append:true}; break;
case 'ax': info = {read:false, write:true, create:true, append:true, exclusive:true}; break;
case 'a+': info = {read:true, write:true, create:true, append:true}; break;
case 'ax+': info = {read:true, write:true, create:true, append:true, exclusive:true}; break;
default: throw Error("Uknown mode: "+flags); // TODO: throw as `S.err.INVAL`
}
if (info.sync) throw Error("Mode not implemented."); // TODO: what would this require of us?
if (_dir) info._openDir = true;
return info;
};
// TODO: these are great candidates for special test coverage!
var _snInvalid = /[^A-Z0-9$%'-_@~`!(){}^#&.]/g; // NOTE: '.' is not valid but we split it away
exports.shortname = function (name) {
var lossy = false;
// TODO: support preservation of case for otherwise non-lossy name!
name = name.toUpperCase().replace(/ /g, '').replace(/^\.+/, '');
name = name.replace(_snInvalid, function () {
lossy = true;
return '_';
});
var parts = name.split('.'),
basis3 = parts.pop(),
basis8 = parts.join('');
if (!parts.length) {
basis8 = basis3;
basis3 = ' ';
}
if (basis8.length > 8) {
basis8 = basis8.slice(0,8);
// NOTE: technically, spec's "lossy conversion" flag is NOT set by excess length.
// But since lossy conversion and truncated names both need a numeric tail…
lossy = true;
} else while (basis8.length < 8) basis8 += ' ';
if (basis3.length > 3) {
basis3 = basis3.slice(0,3);
lossy = true;
} else while (basis3.length < 3) basis3 += ' ';
return {filename:basis8, extension:basis3, _lossy:lossy};
return {basis:[basis8,basis3], lossy:lossy};
};
//shortname("autoexec.bat") => {basis:['AUTOEXEC','BAT'],lossy:false}
//shortname("autoexecutable.batch") => {basis:['AUTOEXEC','BAT'],lossy:true}
// TODO: OS X stores `shortname("._.Trashes")` as ['~1', 'TRA'] — should we?
var _lnInvalid = /[^a-zA-Z0-9$%'-_@~`!(){}^#&.+,;=[\] ]/g;
exports.longname = function (name) {
name = name.trim().replace(/\.+$/, '').replace(_lnInvalid, function (c) {
if (c.length > 1) throw Error("Internal problem: unexpected match length!");
if (c.charCodeAt(0) > 127) return c;
else throw Error("Invalid character "+JSON.stringify(c)+" in name.");
lossy = true;
return '_';
});
if (name.length > 255) throw Error("Name is too long.");
return name;
};
function nameChkSum(sum, c) {
return ((sum & 1) ? 0x80 : 0) + (sum >>> 1) + c & 0xFF;
}
// WORKAROUND: https://github.com/tessel/beta/issues/335
function reduceBuffer(buf, start, end, fn, res) {
// NOTE: does not handle missing `res` like Array.prototype.reduce would
for (var i = start; i < end; ++i) {
res = fn(res, buf[i]);
}
return res;
}
exports.checksumName = function (buf,off) {
off || (off = 0);
var len = S.dirEntry.fields['Name'].size;
return reduceBuffer(buf, off, off+len, nameChkSum, 0);
};
/* comparing C rounding trick from FAT spec with Math.ceil
function tryBoth(d) {
var a = ((D.RootEntCnt * 32) + (D.BytsPerSec - 1)) / D.BytsPerSec >>> 0,
b = Math.ceil((D.RootEntCnt * 32) / D.BytsPerSec);
if (b !== a) console.log("try", b, a, (b === a) ? '' : '*');
return (b === a);
}
// BytsPerSec — "may take on only the following values: 512, 1024, 2048 or 4096"
[512, 1024, 2048, 4096].forEach(function (bps) {
// RootEntCnt — "a count that when multiplied by 32 results in an even multiple of BPB_BytsPerSec"
for (var evenMultiplier = 0; evenMultiplier < 1024*1024*16; evenMultiplier += 2) {
var rec = (bps * evenMultiplier) / 32;
tryBoth({RootEntCnt:rec, BytsPerSec:bps});
}
});
*/
exports.fmtHex = function (n, ff) {
return (1+ff+n).toString(16).slice(1);
};
exports.delayedCall = function (fn) {
if (!fn) throw Error("No function provided!"); // debug aid
var ctx = this,
args = Array.prototype.slice.call(arguments, 1);
setImmediate(function () {
fn.apply(ctx, args);
});
};
exports.adjustedPos = function (vol, pos, bytes) {
var _pos = {
chain: pos.chain,
sector: pos.sector,
offset: pos.offset + bytes
}, secSize = vol._sectorSize;
while (_pos.offset >= secSize) {
_pos.sector += 1;
_pos.offset -= secSize;
}
return _pos;
};
exports.extend = _xok;
var _prevDbg = Date.now(),
_thresh = 50;
function log(level) {
if (level < log.level) return;
var now = Date.now(),
diff = now - _prevDbg;
arguments[0] = ((diff < _thresh) ? " " : '') + diff.toFixed(0) + "ms";
console.log.apply(console, arguments);
_prevDbg = now;
}
log.DBG = -4;
log.INFO = -3;
log.WARN = -2;
log.ERR = -1;
log.level = log.WARN;
exports.log = log;

View File

@@ -0,0 +1,25 @@
var fs = require('fs');
exports.createDriverSync = function (path, opts) {
opts || (opts = {});
var secSize = 512,
ro = opts.readOnly || false,
fd = fs.openSync(path, (ro) ? 'r' : 'r+'),
s = fs.fstatSync(fd);
return {
sectorSize: secSize,
numSectors: s.size / secSize,
readSectors: function (i, dest, cb) {
fs.read(fd, dest, 0, dest.length, i*secSize, function (e,n,d) {
cb(e,d);
});
},
writeSectors: (ro) ? null : function (i, data, cb) {
fs.write(fd, data, 0, data.length, i*secSize, function (e) {
cb(e);
});
}
};
};

771
@vates/fatfs/index.js Normal file
View File

@@ -0,0 +1,771 @@
var events = require('events'),
streams = require('stream'),
fifolock = require('fifolock'),
S = require('./structs.js'),
_ = require('./helpers.js')
//_.log.level = _.log.DBG;
exports.createFileSystem = function (volume, opts, cb) {
if (typeof opts === 'function') {
cb = opts
opts = null
}
opts = _.extend(
{
// c.f. https://www.kernel.org/doc/Documentation/filesystems/vfat.txt
ro: false,
noatime: true,
modmode: 0111, // or `07000`
umask: 'umask' in process ? process.umask() : 0022,
uid: 'getuid' in process ? process.getuid() : 0,
gid: 'getgid' in process ? process.getgid() : 0,
},
opts
)
if (!volume.writeSectors) opts.ro = true
if (opts.ro) opts.noatime = true // natch
var fs = new events.EventEmitter(),
vol = null,
dir = require('./dir.js'),
c = require('./chains.js'),
q = fifolock()
var GROUP =
_.log.level < _.log.INFO
? q.TRANSACTION_WRAPPER.bind({
postAcquire: function (proceed) {
_.log(_.log.DBG, '=== Starting GROUP ===')
proceed()
},
preRelease: function (finish) {
_.log(_.log.DBG, '=== Finishing GROUP ===')
finish()
},
})
: q.TRANSACTION_WRAPPER
q.acquire(function (unlock) {
// because of this, callers can start before 'ready'
var d = _.allocBuffer(volume.sectorSize)
volume.readSectors(0, d, function (e) {
if (e) fs.emit('error', e)
else {
try {
init(d)
} catch (e) {
fs.emit('error', e)
unlock()
return
}
fs.emit('ready')
}
unlock()
})
})
if (cb) fs.on('error', cb).on('ready', cb.bind(null, null))
function init(bootSector) {
vol = require('./vol.js').init(volume, opts, bootSector)
fs._dirIterator = dir.iterator.bind(dir)
var entryInfoByPath = {},
baseEntry = {
_refs: 0,
_record: function () {
entryInfoByPath[this.path] = this
if (this.parent) this.parent.retain()
return this
},
retain: function () {
if (!this._refs) this._record()
this._refs += 1
return this
},
release: function () {
this._refs -= 1
if (!this._refs) this._rescind()
},
_rescind: function () {
if (this.parent) this.parent.release()
delete entryInfoByPath[this.path]
},
}
fs._createSharedEntry = function (path, entry, chain, parent) {
return _.extend(Object.create(baseEntry), {
_refs: 0, // WORKAROUND: https://github.com/tessel/beta/issues/455
path: path,
entry: entry,
chain: chain,
parent: parent,
}).retain()
}
fs._createSharedEntry('/', { Attr: { directory: true } }, vol.rootDirectoryChain)
fs._sharedEntryForSteps = function (steps, opts, cb) {
// NOTE: may `cb` before returning!
var path = steps.join('/') || '/',
name = steps.pop(), // n.b.
info = entryInfoByPath[path]
if (info) cb(null, info.retain())
else
fs._sharedEntryForSteps(steps, {}, function (e, parentInfo) {
// n.b. `steps` don't include `name`
if (e) cb(e)
else if (!parentInfo.entry.Attr.directory) cb(S.err.NOTDIR())
else
dir.findInDirectory(vol, parentInfo.chain, name, opts, function (e, entry) {
if (e && !opts.prepareForCreate) cb(e)
else if (e) cb(e, { missingChild: _.extend(entry, { name: name }), parent: parentInfo })
else cb(null, fs._createSharedEntry(path, entry, vol.chainForCluster(entry._firstCluster), parentInfo))
})
})
}
fs._updateEntry = dir.updateEntry.bind(dir, vol)
fs._makeStat = dir.makeStat.bind(dir, vol)
fs._addFile = dir.addFile.bind(dir, vol)
fs._initDir = dir.init.bind(dir, vol)
}
/**** ---- CORE API ---- ****/
// NOTE: we really don't share namespace, but avoid first three anyway…
var fileDescriptors = [null, null, null]
fs.open = function (path, flags, mode, cb, _n_) {
if (typeof mode === 'function') {
_n_ = cb
cb = mode
mode = 0666
}
cb = GROUP(
cb,
function () {
var _fd = { flags: null, entry: null, chain: null, pos: 0 },
f = _.parseFlags(flags)
if (vol.opts.ro && (f.write || f.create || f.truncate)) return _.delayedCall(cb, S.err.ROFS())
else _fd.flags = f
fs._sharedEntryForSteps(_.absoluteSteps(path), { prepareForCreate: f.create }, function (e, info) {
if (e && !(e.code === 'NOENT' && f.create && info)) cb(e)
else if (e)
fs._addFile(info.parent.chain, info.missingChild, { dir: f._openDir }, function (e, newEntry, newChain) {
if (e) cb(e)
else finish(fs._createSharedEntry(_.absolutePath(path), newEntry, newChain, info.parent))
})
else if (info && f.exclusive) cb(S.err.EXIST())
else if (info.entry.Attr.directory && !f._openDir) cb(S.err.ISDIR())
else if (f.write && info.entry.Attr.readonly) cb(S.err.ACCES())
else finish(info)
function finish(fileInfo) {
var fd = fileDescriptors.push(_fd) - 1
_fd.info = fileInfo
_fd.entry = fileInfo.entry
_fd.chain = fileInfo.chain
if (f.append) _fd.pos = _fd.entry._size
if (f._openDir) _fd.chain.cacheAdvice = 'WILLNEED'
if (f.truncate && _fd.entry._size)
fs.ftruncate(
fd,
0,
function (e) {
cb(e, fd)
},
'_nested_'
)
else _.delayedCall(cb, null, fd) // (delay in case fs._sharedEntryForSteps all cached!)
}
})
},
_n_ === '_nested_'
)
}
fs.fstat = function (fd, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.read) _.delayedCall(cb, S.err.BADF())
else _.delayedCall(cb, null, fs._makeStat(_fd.entry))
},
_n_ === '_nested_'
)
}
fs.futimes = function (fd, atime, mtime, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) _.delayedCall(cb, S.err.BADF())
// NOTE: ctime would get touched on POSIX; but we map that to create time!
else fs._updateEntry(_fd.entry, { atime: atime || true, mtime: mtime || true }, cb)
},
_n_ === '_nested_'
)
}
fs.fchmod = function (fd, mode, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) _.delayedCall(cb, S.err.BADF())
else {
mode &= S._I._chmoddable
if (_fd.entry.Attr.directory) mode |= S._I.FDIR
else if (!_fd.entry.Attr.volume_id) mode |= S._I.FREG
fs._updateEntry(_fd.entry, { mode: mode }, cb)
}
},
_n_ === '_nested_'
)
}
fs.read = function (fd, buf, off, len, pos, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.read) return _.delayedCall(cb, S.err.BADF())
var _pos = pos === null ? _fd.pos : pos,
_len = Math.min(len, _fd.entry._size - _pos),
_buf = buf.slice(off, off + _len)
_fd.chain.readFromPosition(_pos, _buf, function (e, bytes, slice) {
if (_.workaroundTessel380) _buf.copy(buf, off) // WORKAROUND: https://github.com/tessel/beta/issues/380
_fd.pos = _pos + bytes
if (e || vol.opts.noatime) finish(e)
else fs._updateEntry(_fd.entry, { atime: true }, finish)
function finish(e) {
cb(e, bytes, buf)
}
})
},
_n_ === '_nested_'
)
}
fs._readdir = function (fd, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.read) return _.delayedCall(cb, S.err.BADF())
var entryNames = [],
getNextEntry = fs._dirIterator(_fd.chain)
function processNext() {
getNextEntry(function (e, d) {
if (e) cb(e)
else if (!d && !entryNames.length)
cb(null, entryNames) // WORKAROUND: https://github.com/tessel/beta/issues/435
else if (!d)
cb(null, entryNames.sort()) // NOTE: sort not required, but… [simplifies tests for starters!]
else {
if (d._name !== '.' && d._name !== '..') entryNames.push(d._name)
processNext()
}
})
}
processNext()
},
_n_ === '_nested_'
)
}
fs._mkdir = function (fd, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) _.delayedCall(cb, S.err.BADF())
else fs._initDir(_fd.info, cb)
},
_n_ === '_nested_'
)
}
fs.write = function (fd, buf, off, len, pos, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) return _.delayedCall(cb, S.err.BADF())
var _pos = pos === null || _fd.flags.append ? _fd.pos : pos,
_buf = buf.slice(off, off + len)
if (_pos > _fd.entry._size) {
// TODO: handle huge jumps by zeroing clusters individually?
var padLen = _pos - _fd.entry._size,
padBuf = _.allocBuffer(padLen + _buf.length)
padBuf.fill(0x00, 0, padLen)
_buf.copy(padBuf, padLen)
_pos = _fd.entry._size
_buf = padBuf
}
_fd.chain.writeToPosition(_pos, _buf, function (e) {
_fd.pos = _pos + len
var newSize = Math.max(_fd.entry._size, _fd.pos),
newInfo = { size: newSize, _touch: true }
fs._updateEntry(_fd.entry, newInfo, function (ee) {
cb(e || ee, len, buf)
})
})
},
_n_ === '_nested_'
)
}
fs.ftruncate = function (fd, len, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) return _.delayedCall(cb, S.err.BADF())
var newStats = { size: len, _touch: true }
// NOTE: we order operations for best state in case of only partial success
if (len === _fd.entry._size) _.delayedCall(cb)
else if (len < _fd.entry._size)
fs._updateEntry(_fd.entry, newStats, function (e) {
if (e) cb(e)
else _fd.chain.truncate(Math.ceil(len / _fd.chain.sectorSize), cb)
})
// TODO: handle huge file expansions without as much memory pressure
else
_fd.chain.writeToPosition(_fd.entry._size, _.allocBuffer(len - _fd.entry._size, 0x00), function (e) {
if (e) cb(e)
else fs._updateEntry(_fd.entry, newStats, cb)
})
},
_n_ === '_nested_'
)
}
// 'NORMAL', 'SEQUENTIAL', 'RANDOM', 'WILLNEED', 'DONTNEED', 'NOREUSE'
fs._fadviseSync = function (fd, off, len, advice) {
if (off !== 0 || len !== 0) throw Error('Cache advise can currently be given only for whole file!')
var _fd = fileDescriptors[fd]
if (!_fd) throw S.err.BADF()
else _fd.chain.cacheAdvice = advice
}
fs.fsync = function (fd, cb) {
// NOTE: we'll need to flush write cache here once we have one…
var _fd = fileDescriptors[fd]
if (!_fd) _.delayedCall(cb, S.err.BADF())
else _.delayedCall(cb)
}
fs.close = function (fd, cb) {
var _fd = fileDescriptors[fd]
if (!_fd) _.delayedCall(cb, S.err.BADF())
else setTimeout(_fd.info.release.bind(_fd.info), 500), _.delayedCall(cb, (fileDescriptors[fd] = null))
}
/* STREAM WRAPPERS */
var workaroundTessel436
try {
new require('stream').Readable({ encoding: 'utf8' })
new streams.Readable({ encoding: 'utf8' })
} catch (e) {
workaroundTessel436 = true
}
function _createStream(StreamType, path, opts) {
// [NOT REALLY A] WORKAROUND: https://github.com/tessel/beta/issues/436
if (workaroundTessel436 && 'encoding' in opts) {
console.warn('Tessel does not currently support encoding option for Readable streams, discarding!')
delete opts.encoding
}
var fd = opts.fd !== null ? opts.fd : '_opening_',
pos = opts.start,
stream = new StreamType(opts)
if (fd === '_opening_')
fs.open(path, opts.flags, opts.mode, function (e, _fd) {
if (e) {
fd = '_open_error_'
stream.emit('error', e)
} else {
fd = _fd
fs._fadviseSync(fd, 0, 0, 'SEQUENTIAL')
stream.emit('open', fd)
}
})
function autoClose(tombstone) {
// NOTE: assumes caller will clear `fd`
if (opts.autoClose)
fs.close(fd, function (e) {
if (e) stream.emit('error', e)
else stream.emit('close')
})
fd = tombstone
}
if (StreamType === streams.Readable) {
stream._read = function (n) {
var buf
// TODO: optimize to fetch at least a full sector regardless of `n`…
n = Math.min(n, opts.end - pos)
if (fd === '_opening_')
stream.once('open', function () {
stream._read(n)
})
else if (pos > opts.end) stream.push(null)
else if (n > 0)
(buf = _.allocBuffer(n)),
fs.read(fd, buf, 0, n, pos, function (e, n, d) {
if (e) {
autoClose('_read_error_')
stream.emit('error', e)
} else stream.push(n ? d.slice(0, n) : null)
}),
(pos += n)
else stream.push(null)
}
stream.once('end', function () {
autoClose('_ended_')
})
} else if (StreamType === streams.Writable) {
stream.bytesWritten = 0
stream._write = function (data, _enc, cb) {
if (fd === '_opening_')
stream.once('open', function () {
stream._write(data, null, cb)
})
else
fs.write(fd, data, 0, data.length, pos, function (e, n) {
if (e) {
autoClose('_write_error_')
cb(e)
} else {
stream.bytesWritten += n
cb()
}
}),
(pos += data.length)
}
stream.once('finish', function () {
autoClose('_finished_')
})
}
return stream
}
fs.createReadStream = function (path, opts) {
return _createStream(
streams.Readable,
path,
_.extend(
{
start: 0,
end: Infinity,
flags: 'r',
mode: 0666,
encoding: null,
fd: null, // ??? see https://github.com/joyent/node/issues/7708
autoClose: true,
},
opts
)
)
}
fs.createWriteStream = function (path, opts) {
return _createStream(
streams.Writable,
path,
_.extend(
{
start: 0,
flags: 'w',
mode: 0666,
//encoding: null, // see https://github.com/joyent/node/issues/7710
fd: null, // ??? see https://github.com/joyent/node/issues/7708
autoClose: true,
},
opts,
{ decodeStrings: true, objectMode: false }
)
)
}
/* PATH WRAPPERS (albeit the only public interface for some folder operations) */
function _fdOperation(path, opts, fn, cb) {
cb = GROUP(cb, function () {
opts.advice || (opts.advice = 'NORMAL')
fs.open(
path,
opts.flag,
function (e, fd) {
if (e) cb(e)
else
fs._fadviseSync(fd, 0, 0, opts.advice),
fn(fd, function () {
var ctx = this,
args = arguments
fs.close(
fd,
function (closeErr) {
cb.apply(ctx, args)
},
'_nested_'
)
})
},
'_nested_'
)
})
}
fs.stat = fs.lstat = function (path, cb) {
_fdOperation(
path,
{ flag: 'r' },
function (fd, cb) {
fs.fstat(fd, cb, '_nested_')
},
cb
)
}
fs.exists = function (path, cb) {
fs.stat(path, function (err) {
cb(err ? false : true)
})
}
fs.readFile = function (path, opts, cb) {
if (typeof opts === 'function') {
cb = opts
opts = {}
}
opts.flag || (opts.flag = 'r')
opts.advice || (opts.advice = 'NOREUSE')
_fdOperation(
path,
opts,
function (fd, cb) {
fs.fstat(
fd,
function (e, stat) {
if (e) return cb(e)
else {
var buffer = _.allocBuffer(stat.size)
fs.read(
fd,
buffer,
0,
buffer.length,
null,
function (e) {
if (e) cb(e)
else cb(null, opts.encoding ? buffer.toString(opts.encoding) : buffer)
},
'_nested_'
)
}
},
'_nested_'
)
},
cb
)
}
fs.writeFile = function (path, data, opts, cb) {
if (typeof opts === 'function') {
cb = opts
opts = {}
}
opts.flag || (opts.flag = 'w')
opts.advice || (opts.advice = 'NOREUSE')
_fdOperation(
path,
opts,
function (fd, cb) {
if (typeof data === 'string') data = _.bufferFrom(data, opts.encoding || 'utf8')
fs.write(
fd,
data,
0,
data.length,
null,
function (e) {
cb(e)
},
'_nested_'
)
},
cb
)
}
fs.appendFile = function (path, data, opts, cb) {
if (typeof opts === 'function') {
cb = opts
opts = {}
}
opts.flag || (opts.flag = 'a')
fs.writeFile(path, data, opts, cb)
}
fs.truncate = function (path, len, cb) {
_fdOperation(
path,
{ flag: 'r+' },
function (fd, cb) {
fs.ftruncate(fd, len, cb, '_nested_')
},
cb
)
}
fs.readdir = function (path, cb) {
_fdOperation(
path,
{ flag: '\\r' },
function (fd, cb) {
fs._readdir(fd, cb, '_nested_')
},
cb
)
}
fs.mkdir = function (path, mode, cb) {
if (typeof mode === 'function') {
cb = mode
mode = 0777
}
_fdOperation(
path,
{ flag: '\\wx' },
function (fd, cb) {
fs._mkdir(fd, cb, '_nested_')
},
cb
)
}
fs.utimes = function (path, atime, mtime, cb) {
_fdOperation(
path,
{ flag: 'r+' },
function (fd, cb) {
fs.futimes(fd, atime, mtime, cb, '_nested_')
},
cb
)
}
fs.chmod = fs.lchmod = function (path, mode, cb) {
_fdOperation(
path,
{ flag: '\\r+' },
function (fd, cb) {
fs.fchmod(fd, mode, cb, '_nested_')
},
cb
)
}
fs.chown = fs.lchown = function (path, uid, gid, cb) {
_fdOperation(
path,
{ flag: '\\r+' },
function (fd, cb) {
fs.fchown(fd, uid, gid, cb, '_nested_')
},
cb
)
}
/* STUBS */
fs.link = function (src, dst, cb) {
// NOTE: theoretically we _could_ do hard links [with untracked `stat.nlink` count…]
_.delayedCall(cb, S.err.NOSYS())
}
fs.symlink = function (src, dst, type, cb) {
if (typeof type === 'function') {
cb = type
type = null
}
_.delayedCall(cb, S.err.NOSYS())
}
fs.readlink = function (path, cb) {
_fdOperation(
path,
{ flag: '\\r' },
function (fd, cb) {
// the named file is *never* a symbolic link…
// NOTE: we still use _fdOperation for catching e.g. NOENT/NOTDIR errors…
cb(S.err.INVAL())
},
cb
)
}
fs.realpath = function (path, cache, cb) {
if (typeof cache === 'function') {
cb = cache
cache = null
}
if (cache)
_.delayedCall(cb, S.err.NOSYS()) // TODO: what would be involved here?
else
_fdOperation(
path,
{ flag: '\\r' },
function (fd, cb) {
cb(null, _.absolutePath(path))
},
cb
)
}
fs.fchown = function (fd, uid, gid, cb, _n_) {
cb = GROUP(
cb,
function () {
var _fd = fileDescriptors[fd]
if (!_fd || !_fd.flags.write) _.delayedCall(cb, S.err.BADF())
else _.delayedCall(cb, S.err.NOSYS())
},
_n_ === '_nested_'
)
}
// See https://github.com/natevw/fatfs/pull/31/files
fs.createLabel = function (name, cb) {
fs.open(name, 'a', (e, _fd) => {
if (e) {
cb(e)
} else {
let fd = fileDescriptors[_fd]
fd.entry.Attr.volume_id = true
fd.entry.FstClusLO = 0
fd.entry.FstClusHI = 0
fs._updateEntry(fd.entry, {}, cb)
}
})
}
return fs
}

36
@vates/fatfs/make_sample.sh Executable file
View File

@@ -0,0 +1,36 @@
#! /bin/sh
# NOTE: this only works on OS X
# TODO: Linux could use mkfs stuff without attach?
# ./make_sample.sh /tmp/fat16.img FAT16
FILE=$1
: ${FILE:?must be provided as first argument}
TYPE=$2
: ${TYPE:?must be provided as second argument}
SIZE=$3 # in MB
case "$TYPE" in
"FAT12") : ${SIZE:=1} # <128
;;
"FAT16") : ${SIZE:=5} # >4
;;
"FAT32") : ${SIZE:=33} # >32
;;
*) : ${SIZE:=1} # e.g. ExFAT
esac
echo Making $TYPE of $SIZE MB at $FILE
if [[ "$TYPE" = FAT* ]]
then
TYPE="MS-DOS $TYPE"
fi
dd if=/dev/zero of=$FILE bs=1048576 count=$SIZE
DEV=`hdiutil attach $FILE -nomount`
diskutil eraseVolume "$TYPE" "FATFS TEST" $DEV
hdiutil detach $DEV

32
@vates/fatfs/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"private": false,
"name": "@vates/fatfs",
"version": "0.11.0",
"description": "fs implementation on top of raw FAT16/FAT32 block source",
"main": "index.js",
"scripts": {
"postversion": "npm publish --access public"
},
"repository": {
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra"
},
"keywords": [
"filesystem",
"fs",
"fat",
"fat32",
"fat16"
],
"author": "Vates SAS",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/vatesfr/xen-orchestra/issues"
},
"homepage": "https://github.com/vatesfr/xen-orchestra",
"dependencies": {
"fifolock": "^1.0.0",
"struct-fu": "^1.2.1",
"xok": "^1.0.0"
}
}

255
@vates/fatfs/structs.js Normal file
View File

@@ -0,0 +1,255 @@
// see http://staff.washington.edu/dittrich/misc/fatgen103.pdf
// and http://www.cse.scu.edu/~tschwarz/COEN252_09/Lectures/FAT.html
var _ = require('struct-fu'),
__ = require("./helpers.js");
var bootBase = _.struct([
_.byte('jmpBoot', 3),
_.char('OEMName', 8),
_.uint16le('BytsPerSec'),
_.uint8('SecPerClus'),
_.uint16le('ResvdSecCnt'), // Rsvd in table, but Resvd in calcs…
_.uint8('NumFATs'),
_.uint16le('RootEntCnt'),
_.uint16le('TotSec16'),
_.uint8('Media'),
_.uint16le('FATSz16'),
_.uint16le('SecPerTrk'),
_.uint16le('NumHeads'),
_.uint32le('HiddSec'),
_.uint32le('TotSec32')
]);
var bootInfo = _.struct([
_.uint8('DrvNum'),
_.uint8('Reserved1'),
_.uint8('BootSig'),
_.uint32le('VolID'),
_.char('VolLab', 11),
_.char('FilSysType', 8)
]);
exports.boot16 = _.struct([
bootBase,
bootInfo
]);
exports.boot32 = _.struct([
bootBase,
_.uint32le('FATSz32'),
_.struct('ExtFlags', [
_.ubit('NumActiveFAT', 4),
_.ubit('_reserved1', 3),
_.bool('MirroredFAT'),
_.ubit('_reserved2', 8)
].reverse()),
_.struct('FSVer', [
_.uint8('Major'),
_.uint8('Minor')
]),
_.uint32le('RootClus'),
_.uint16le('FSInfo'),
_.uint16le('BkBootSec'),
_.byte('Reserved', 12),
bootInfo
]);
var _time = _.struct([
_.ubit('hours',5),
_.ubit('minutes',6),
_.ubit('seconds_2',5)
]), time = {
valueFromBytes: function (buf, off) {
off || (off = {bytes:0});
var _buf = __.bufferFrom([buf[off.bytes+1], buf[off.bytes+0]]),
val = _time.valueFromBytes(_buf);
off.bytes += this.size;
return val;
},
bytesFromValue: function (val, buf, off) {
val || (val = {hours:0, minutes:0, seconds_2:0});
buf || (buf = __.allocBuffer(this.size));
off || (off = {bytes:0});
var _buf = _time.bytesFromValue(val);
buf[off.bytes+1] = _buf[0];
buf[off.bytes+0] = _buf[1];
off.bytes += this.size;
return buf;
},
size: _time.size
};
var _date = _.struct([
_.ubit('year',7),
_.ubit('month',4),
_.ubit('day',5)
]), date = {
valueFromBytes: function (buf, off) {
off || (off = {bytes:0});
var _buf = __.bufferFrom([buf[off.bytes+1], buf[off.bytes+0]]),
val = _date.valueFromBytes(_buf);
off.bytes += this.size;
return val;
},
bytesFromValue: function (val, buf, off) {
val || (val = {year:0, month:0, day:0});
buf || (buf = __.allocBuffer(this.size));
off || (off = {bytes:0});
var _buf = _date.bytesFromValue(val);
buf[off.bytes+1] = _buf[0];
buf[off.bytes+0] = _buf[1];
off.bytes += this.size;
return buf;
},
size: _date.size
};
exports.dirEntry = _.struct([
_.struct('Name', [
_.char('filename',8),
_.char('extension',3)
]),
_.struct('Attr', [
_.bool('readonly'),
_.bool('hidden'),
_.bool('system'),
_.bool('volume_id'),
_.bool('directory'),
_.bool('archive'),
_.ubit('reserved', 2)
].reverse()),
_.byte('NTRes', 1),
_.uint8('CrtTimeTenth'),
_.struct('CrtTime', [time]),
_.struct('CrtDate', [date]),
_.struct('LstAccDate', [date]),
_.uint16le('FstClusHI'),
_.struct('WrtTime', [time]),
_.struct('WrtDate', [date]),
_.uint16le('FstClusLO'),
_.uint32le('FileSize')
]);
exports.entryDoneFlag = 0x00;
exports.entryFreeFlag = 0xE5;
exports.entryIsE5Flag = 0x05;
exports.dirEntry_simple = _.struct([
_.struct('Name', [
_.char('filename',8),
_.char('extension',3)
]),
_.padTo(exports.dirEntry.size)
/*
_.uint8('Attr_raw'),
_.byte('NTRes', 1),
_.byte('Crt_raw', 1+2+2),
_.byte('Lst_raw', 2),
_.uint16le('FstClusHI'),
_.byte('Wrt_raw', 2+2),
_.uint16le('FstClusLO'),
_.uint32le('FileSize')
*/
]);
exports.lastLongFlag = 0x40;
exports.longDirFlag = 0x0F;
exports.longDirEntry = _.struct([
_.uint8('Ord'),
_.char16le('Name1', 10),
_.uint8('Attr_raw'),
_.uint8('Type'),
_.uint8('Chksum'),
_.char16le('Name2', 12),
_.uint16le('FstClusLO'),
_.char16le('Name3', 4)
]);
if (exports.longDirEntry.size !== exports.dirEntry.size) throw Error("Structs ain't right!");
exports.fatField = {
'fat12': _.struct('Status', [
_.ubit('field0bc', 8),
_.ubit('field1c', 4),
_.ubit('field0a', 4),
_.ubit('field1ab', 8),
]),
'fat16': _.uint16le('Status'),
'fat32': _.uint32le('Status') // more properly this 4 bits reserved + uint28le
};
exports.fatPrefix = {
'fat12': 0xF00,
'fat16': 0xFF00,
'fat32': 0x0FFFFF00
};
exports.fatStat = {
free: 0x00,
_undef: 0x01,
rsvMin: 0xF0,
bad: 0xF7,
eofMin: 0xF8,
eof: 0xFF
};
exports._I = {
RUSR: 0400,
WUSR: 0200,
XUSR: 0100,
RGRP: 0040,
WGRP: 0020,
XGRP: 0010,
ROTH: 0004,
WOTH: 0002,
XOTH: 0001,
SUID: 04000,
SGID: 02000,
SVTX: 01000,
FDIR: 040000,
FREG: 0100000,
};
exports._I.RWXU = exports._I.RUSR | exports._I.WUSR | exports._I.XUSR;
exports._I.RWXG = exports._I.RGRP | exports._I.WGRP | exports._I.XGRP;
exports._I.RWXO = exports._I.ROTH | exports._I.WOTH | exports._I.XOTH;
exports._I._sss = exports._I.SUID | exports._I.SGID | exports._I.SVTX;
exports._I._chmoddable = exports._I.RWXU | exports._I.RWXG | exports._I.RWXO | exports._I._sss;
var _errors = {
IO: "Input/output error",
NOENT: "No such file or directory",
INVAL: "Invalid argument",
EXIST: "File exists",
NAMETOOLONG: "Filename too long",
NOSPC: "No space left on device",
NOSYS: "Function not supported",
ROFS: "ROFLCopter file system",
NOTDIR: "Not a directory",
BADF: "Bad file descriptor",
EXIST: "File exists",
ISDIR: "Is a directory",
ACCES: "Permission denied",
NOSYS: "Function not implemented",
_TODO: "Not implemented yet!"
};
exports.err = {};
Object.keys(_errors).forEach(function (sym) {
var msg = _errors[sym];
exports.err[sym] = function () {
var e = new Error(msg);
e.code = sym;
return e;
};
});

338
@vates/fatfs/test.js Normal file
View File

@@ -0,0 +1,338 @@
// can be called from CLI with image type, absolute path to an image, or, required as a module
var _ = require("./helpers.js");
var type = process.argv[2];
if (module.parent) exports.startTests = startTests;
else if (!type) throw "Usage: node test [FAT12|FAT16|FAT32|ExFAT|…|/path/to/image]";
else if (type[0] === '/') testWithImage(type);
else {
var uniq = Math.random().toString(36).slice(2),
IMG = require('os').tmpdir()+"fatfs-test-"+uniq+".img";
require('child_process').exec("./make_sample.sh "+JSON.stringify(IMG)+" "+JSON.stringify(type), function (e,out,err) {
if (e) throw e;
console.warn(err.toString());
//console.log(out.toString());
testWithImage(IMG);
//console.log("open", IMG);
//return;
require('fs').unlink(IMG, function (e) {
if (e) console.warn("Error cleaning up test image", e);
});
});
}
function testWithImage(imagePath) {
var vol = require("./img_volume.js").createDriverSync(imagePath);
startTests(vol);
}
function startTests(vol, waitTime) {
var fatfs = require("./"),
fs = fatfs.createFileSystem(vol, {umask:0020, uid:99, gid:42});
waitTime || (waitTime = 0.5e3);
[
'mkdir','readdir',
//'rename','unlink','rmdir',
'close','open','fsync',
'ftruncate','truncate',
'write','read','readFile','writeFile', 'appendFile',
'chown','lchown','fchown',
'chmod','lchmod', 'fchmod',
'utimes','futimes',
'stat','lstat','fstat','exists',
'link','symlink','readlink','realpath',
//'watchFile','unwatchFile','watch'
].forEach(function (method) { assert(method in fs, "fs."+method+" has implementation."); });
var BASE_DIR = "/fat_test-"+Math.random().toFixed(20).slice(2),
FILENAME = "Simple File.txt",
TEXTDATA = "Hello world!";
var isReady = false;
fs.on('ready', function () {
assert(isReady = true, "Driver is ready.");
}).on('error', function (e) {
assert(e, "If fs driver fires 'error' event, it should include error object.");
assert(false, "…but driver should not error when initializing in our case.");
});
setTimeout(function () {
assert(isReady, "Driver fired ready event in timely fashion.");
}, waitTime);
fs.readdir("/", function (e,files) {
assert(isReady, "Method completed after 'ready' event.");
assert(!e, "No error reading root directory.");
assert(Array.isArray(files), "Got a list of files: "+files);
});
fs.mkdir(BASE_DIR, function (e) {
assert(!e, "No error from fs.mkdir");
fs.readdir(BASE_DIR, function (e,arr) {
assert(!e, "No error from fs.readdir");
assert(arr.length === 0 , "No files in BASE_DIR yet.");
});
var file = [BASE_DIR,FILENAME].join('/');
fs.writeFile(file, TEXTDATA, function (e) {
assert(!e, "No error from fs.writeFile");
startStreamTests();
fs.realpath(file, function (e,path) {
assert(!e, "No error from basic fs.realpath call.");
assert(path === file, "We already had the real path.");
});
fs.realpath([BASE_DIR,".","garbage",".","..",FILENAME].join('/'), function (e,path) {
assert(!e, "No error from fluffy fs.realpath call.");
assert(path === file, "Fixed fluffy path matches normal one.");
});
fs.realpath([BASE_DIR,"non","existent","path"].join('/'), function (e) {
assert(e, "Expected error calling fs.realpath on non-existent file.");
});
fs.readdir(BASE_DIR, function (e, arr) {
assert(!e, "Still no error from fs.readdir");
assert(arr.length === 2, "Test directory contains two files."); // (ours + startStreamTests's)
assert(arr[0] === FILENAME, "Filename is correct.");
fs.stat(file, function (e,d) {
assert(!e, "No error from fs.stat");
assert(d.isFile() === true, "Result is a file…");
assert(d.isDirectory() === false, "…and not a directory.");
assert(d.size === Buffer.byteLength(TEXTDATA), "Size matches length of content written.");
});
fs.exists(file, function (bool) {
assert(bool === true, "File exists.");
});
fs.readFile(file, {encoding:'utf8'}, function (e, d) {
assert(!e, "No error from fs.readFile");
assert(d === TEXTDATA, "Data matches what was written.");
});
// now, overwrite the same file and make sure that goes well too
fs.writeFile(file, _.bufferFrom([0x42]), function (e) {
assert(!e, "Still no error from fs.writeFile");
fs.readdir(BASE_DIR, function (e, arr) {
assert(!e, "No error from fs.readdir");
assert(arr.length === 2, "Test directory still contains two files.");
assert(arr[0] === FILENAME, "Filename still correct.");
fs.stat(file, function (e,d) {
assert(!e, "Still no error from fs.stat");
assert(d.isFile() === true, "Result is still a file…");
assert(d.isDirectory() === false, "…and not a directory.");
assert(d.size === 1, "Size matches length of now-truncated content.");
});
fs.readFile(file, function (e, d) {
assert(!e, "Still no error from fs.readFile");
assert(Buffer.isBuffer(d), "Result without encoding is a buffer.");
assert(d.length === 1, "Buffer is correct size.");
assert(d[0] === 0x42, "Buffer content is correct.");
fs.truncate(file, 1025, function (e) {
assert(!e, "No error from fs.truncate (extending)");
fs.readFile(file, function (e, d) {
assert(!e, "Still no error from fs.readFile after extension");
assert(d.length === 1025, "Read after extension is correct size.");
assert(d[0] === 0x42, "First byte is still correct.");
var allZeroes = true;
for (var i = 1, len = d.length; i < len; ++i) if (d[i] !== 0) allZeroes = false;
assert(allZeroes, "Extended portion of file is zero-filled.");
fs.truncate(file, 3, function (e) {
assert(!e, "No error from fs.truncate (shortening)");
fs.readFile(file, function (e, d) {
assert(!e, "Still no error from fs.readFile after shortening.");
assert(d.length === 3, "Read after shortening is correct size.");
assert(d[0] === 0x42, "First byte is still correct.");
assert(d[1] === 0x00, "Second byte is still correct.");
assert(d[2] === 0x00, "Third byte is still correct.");
proceedWithMoreTests();
});
});
});
});
});
});
});
});
});
function proceedWithMoreTests() {
var fd;
fs.open(file, 'r', function (e, _fd) {
assert(!e, "No error from fs.open.");
fd = _fd;
});
var was = "\u0042\u0000\u0000",
str = "abc";
fs.appendFile(file, str, function (e) {
assert(!e, "No error from fs.appendFile.");
assert(fd, "File descriptor opened before appendFile called.");
var buf = _.allocBuffer(str.length);
fs.read(fd, buf, 0, buf.length, was.length, function (e,n,d) {
assert(!e, "No error from fs.read after append.");
assert(n === str.length, "All appended data was readable.");
assert(d === buf, "Buffer returned from fs.read matched what was passed in.");
assert(d.toString() === str, "Correct data found where expected.");
});
});
fs.readFile(file, {encoding:'ascii'}, function (e,d) {
assert(!e, "No error from fs.appendFile.");
assert(d.length === 6, "Read is correct size after append.");
assert(d === was+str, "Read string matches what was written and then appended.");
fs.open(file, 'a', function (e, fd2) {
assert(!e, "No error from second open of file.");
var str2 = "zyx",
buf2 = _.allocBuffer(str2.length+2);
buf2.write(str2, 1);
fs.write(fd2, buf2, 1, buf2.length-2, was.length, function (e,n,d) {
assert(!e, "No error from appending fs.write.");
assert(n === buf2.length-2, "Wrote proper amount from buffer.");
assert(d === buf2, "Returned original buffer.");
buf2.fill(0);
buf2[0] = 0xFF;
fs.read(fd, buf2, 1, buf2.length-1, was.length, function (e,n,d) {
assert(!e, "No error from twice-appended read.");
assert(n === buf2.length-1, "Read proper amount into buffer.");
assert(buf2[0] === 0xFF, "Read left first byte in buffer properly alone.");
assert(d.slice(1).toString() === (str+str2).slice(0, buf2.length-1), "Data was appended, not written at position.");
});
});
});
});
var F = [BASE_DIR,"Manually inspect from time to time, please!.txt"].join('/'),
S = 512,
N = 16,
b = _.allocBuffer(S*N);
for (var i = 0; i < N; ++i) b.slice(S*i, S*i+S).fill(i.toString(16).charCodeAt(0));
fs.writeFile(F, b, function (e) {
assert(!e, "No error from fs.writeFile with counting blocks.");
fs.readFile(F, function (e,d) {
assert(!e, "No error from fs.readFile with counting blocks.");
assert(d.length === b.length, "Readback is correct size");
var matched = true;
for (var i = 0; i < S*N; ++i) if (b[i] !== d[i]) matched = false;
assert(matched, "Readback matches write byte-for-byte");
});
});
fs.stat(F, function (e,d) {
assert(!e, "No error from fs.stat on counting blocks file.");
assert(d.atime instanceof Date && !isNaN(d.atime.getTime()), "Access time is a valid date.");
assert(d.mtime instanceof Date && !isNaN(d.mtime.getTime()), "Modify time is a valid date.");
assert(d.ctime instanceof Date && !isNaN(d.ctime.getTime()), "Change^WCreate time is a valid date.");
var tf = d.ctime.getTime(),
ct = Date.now();
assert(tf - 2*waitTime < ct && ct < tf + 2*waitTime, "Create time is within ± two `waitTime`s of now.");
fs.utimes(F, new Date(2009, 7-1, 2), null, function (e) {
assert(!e, "No error from fs.utimes.");
fs.stat(F, function (e,d2) {
assert(!e, "No fs.stat error after touching timestamps.");
assert(+d.ctime === +d2.ctime, "Create time not changed by fs.utimes");
assert(d2.atime.toString().indexOf("Jul 02 2009 00:00:00") === 4, "Access time set correctly");
var tf = d2.mtime.getTime(),
ct = Date.now();
assert(tf-2e3 < ct && ct < tf+2e3+waitTime, "Modify time is within ± a few seconds of now.");
// NOTE: due to serialization, this can check results of the `fs.chmod` below, too!
assert(!(d2.mode & 0100), "Archive bit is now unset.");
assert(!(d2.mode & 0222), "Writable perms are unset.");
assert(d2.mode & 0100000, "Regular file bit is set.");
});
});
assert(d.uid === 99, "Desired UID applied.");
assert(d.gid === 42, "Desired GID applied.");
assert(d.mode & 0100, "Archive bit is set.");
assert(d.mode & 0200, "Writable perm is set for user.");
assert((d.mode & 0022) === 0002, "Writable perm is only masked out for group.");
fs.chmod(F, 0422, function (e) {
assert(!e, "No error from fs.chmod.");
});
});
fs.chown(F, 99, 256, function (e) {
assert(e && e.code === 'NOSYS', "Expected error from fs.fchown.");
});
}
function startStreamTests() {
var file2 = [BASE_DIR,FILENAME+"2"].join('/'),
outStream = fs.createWriteStream(file2);
var outStreamOpened = false;
outStream.on('open', function (fd) {
outStreamOpened = true;
assert(typeof fd === 'number', "Got file descriptor on fs.createWriteStream open.");
}).on('error', function (e) {
assert(e, "If fs.createWriteStream fires 'error' event, it should include error object.");
assert(false, "But, fs.createWriteStream should not error during these tests.");
});
setTimeout(function () {
assert(outStreamOpened, "outStream fired 'open' event in a timely fashion.");
}, waitTime);
var TEXT_MOD = TEXTDATA.toLowerCase()+"\n",
NUM_REPS = (waitTime <= 1e3) ? 1024 : 16;
outStream.write(TEXT_MOD, 'utf16le');
outStream.write("Ο καλύτερος χρόνος να φυτευτεί ένα \ud83c\udf31 είναι δέκα έτη πριν.", 'utf16le');
outStream.write("La vez del segundo mejor ahora está.\n", 'utf16le');
for (var i = 0; i < NUM_REPS; ++i) outStream.write("123456789\n", 'ascii');
outStream.write("JavaScript how do they work\n", 'utf16le');
outStream.write("The end, almost.\n", 'utf16le');
outStream.end(TEXTDATA, 'utf16le');
var outStreamFinished = false;
outStream.on('finish', function () {
outStreamFinished = true;
var inStream = fs.createReadStream(file2, {start:NUM_REPS*10, encoding:'utf16le', autoClose:false}),
gotData = false, gotEOF = false, inStreamFD = null;
inStream.on('open', function (fd) {
assert(typeof fd === 'number', "Got file descriptor on fs.createReadStream open.");
inStreamFD = fd;
});
inStream.on('data', function (d) {
gotData = true;
assert(typeof d === 'string', "Data returned as string.");
assert(d.slice(d.length-TEXTDATA.length) === TEXTDATA, "End of file matches what was written.");
});
inStream.on('end', function () {
gotEOF = true;
var len = Buffer.byteLength(TEXT_MOD, 'utf16le'),
buf = _.allocBuffer(len);
fs.fsync(inStreamFD, function (e) {
assert(!e, "No error from proper fsync.");
});
fs.fsync('garbage', function (e) {
assert(e, "Expected error from garbage fsync.");
});
fs.read(inStreamFD, buf, 0, len, 0, function (e,n,d) {
assert(!e, "No error reading from beginning of inStream's file descriptor.");
assert(n === len, "Read complete buffer at beginning of inStream's fd.");
assert(d.toString('utf16le') === TEXT_MOD, "Data matches at beginning of inStream's fd.");
fs.close(inStreamFD, function (e) {
assert(!e, "No error closing inStream's fd.");
});
});
});
setTimeout(function () {
assert(gotData, "inStream fired 'data' event in a timely fashion.");
setTimeout(function () {
assert(gotEOF, "inStream fired 'eof' event in a timely fashion.");
}, waitTime);
}, waitTime);
});
setTimeout(function () {
assert(outStreamFinished, "outStream fired 'finish' event in a timely fashion.");
}, 2*waitTime);
}
});
}
function assert(b,msg) { if (!msg) console.warn("no msg", Error().stack); if (!b) throw Error("Assertion failure. "+msg); else console.log(msg); }

165
@vates/fatfs/vol.js Normal file
View File

@@ -0,0 +1,165 @@
var S = require("./structs.js"),
c = require("./chains.js"),
$ = require("./cache.js"),
_ = require("./helpers.js");
exports.init = function (volume, opts, bootSector) {
if (bootSector[510] !== 0x55 || bootSector[511] !== 0xAA) throw Error("Invalid volume signature!");
var isFAT16 = bootSector.readUInt16LE(S.boot16.fields['FATSz16'].offset),
bootStruct = (isFAT16) ? S.boot16 : S.boot32,
BS = bootStruct.valueFromBytes(bootSector);
_.log(_.log.DBG, "Boot sector info:", BS);
bootSector = null; // allow GC
if (!BS.BytsPerSec) throw Error("This looks like an ExFAT volume! (unsupported)");
else if (BS.BytsPerSec !== volume.sectorSize) throw Error("Sector size mismatch with FAT table.");
var FATSz = (isFAT16) ? BS.FATSz16 : BS.FATSz32,
rootDirSectors = Math.ceil((BS.RootEntCnt * 32) / BS.BytsPerSec),
firstDataSector = BS.ResvdSecCnt + (BS.NumFATs * FATSz) + rootDirSectors,
totSec = (BS.TotSec16) ? BS.TotSec16 : BS.TotSec32,
dataSec = totSec - firstDataSector,
countofClusters = Math.floor(dataSec / BS.SecPerClus);
// avoid corrupting sectors from other partitions or whatnot
if (totSec > volume.numSectors) throw Error("Volume size mismatch!");
var fatType;
if (countofClusters < 4085) {
fatType = 'fat12';
} else if (countofClusters < 65525) {
fatType = 'fat16';
} else {
fatType = 'fat32';
}
_.log(_.log.DBG, "rootDirSectors", rootDirSectors, "firstDataSector", firstDataSector, "countofClusters", countofClusters, "=>", fatType);
var vol = {};
vol.opts = opts;
vol._sectorSize = BS.BytsPerSec;
vol._sectorsPerCluster = BS.SecPerClus;
vol._firstSectorOfCluster = function (n) {
return firstDataSector + (n-2)*vol._sectorsPerCluster;
};
vol._makeCache = function () {
return $.wrapDriver(volume);
};
vol._readSectors = function (cache, secNum, dest, cb) {
if (typeof dest === 'function') {
cb = dest;
dest = _.allocBuffer(vol._sectorSize);
}
_.log(_.log.DBG, "vol._readSectors", secNum, dest.length);
if (secNum < volume.numSectors) cache.readSectors(secNum, dest, function (e) { cb(e, dest); });
else throw Error("Invalid sector number!");
};
vol._writeSectors = function (cache, secNum, data, cb) {
_.log(_.log.DBG, "vol._writeSectors", secNum, data.length);
// NOTE: these are internal assertions, public API will get proper `S.err`s
if (data.length % volume.sectorSize) throw Error("Buffer length not a multiple of sector size");
else if (opts.ro) throw Error("Read-only filesystem");
else if (secNum < volume.numSectors) cache.writeSectors(secNum, data, cb);
else throw Error("Invalid sector number!");
};
function fatInfoForCluster(n) {
var entryStruct = S.fatField[fatType],
FATOffset = (fatType === 'fat12') ? Math.floor(n/2) * entryStruct.size : n * entryStruct.size,
SecNum = BS.ResvdSecCnt + Math.floor(FATOffset / BS.BytsPerSec);
EntOffset = FATOffset % BS.BytsPerSec;
return {sector:SecNum-BS.ResvdSecCnt, offset:EntOffset, struct:entryStruct};
}
// TODO: all this FAT manipulation is crazy inefficient! needs read caching *and* write caching
// …the best place for cache might be in `volume` handler, though. add a `flush` method to that spec?
// TODO: how should we handle redundant FATs? mirror every write? just ignore completely? copy-on-eject?
var fatChain = c.sectorChain(vol, BS.ResvdSecCnt, FATSz);
fatChain.cacheAdvice = 'RANDOM';
vol.fetchFromFAT = function (clusterNum, cb) {
var info = fatInfoForCluster(clusterNum);
fatChain.readFromPosition(info, info.struct.size, function (e,n,d) {
if (e) return cb(e);
var status = info.struct.valueFromBytes(d), prefix;
if (fatType === 'fat12') {
if (clusterNum % 2) {
status = (status.field1ab << 4) + status.field1c;
} else {
status = (status.field0a << 8) + status.field0bc;
}
}
else if (fatType === 'fat32') {
status &= 0x0FFFFFFF;
}
var prefix = S.fatPrefix[fatType];
if (status === S.fatStat.free) cb(null, 'free');
else if (status === S.fatStat._undef) cb(null, '-invalid-');
else if (status > prefix+S.fatStat.eofMin) cb(null, 'eof');
else if (status === prefix+S.fatStat.bad) cb(null, 'bad');
else if (status > prefix+S.fatStat.rsvMin) cb(null, 'reserved');
else cb(null, status);
});
};
vol.storeToFAT = function (clusterNum, status, cb) {
if (typeof status === 'string') {
status = S.fatStat[status];
status += S.fatPrefix[fatType];
}
var info = fatInfoForCluster(clusterNum);
// TODO: technically fat32 needs to *preserve* the high 4 bits
if (fatType === 'fat12') fatChain.readFromPosition(info, info.struct.size, function (e,n,d) {
var value = info.struct.valueFromBytes(d);
if (clusterNum % 2) {
value.field1ab = status >>> 4;
value.field1c = status & 0x0F;
} else {
value.field0a = status >>> 8;
value.field0bc = status & 0xFF;
}
var entry = info.struct.bytesFromValue(value);
fatChain.writeToPosition(info, entry, cb);
}); else {
var entry = info.struct.bytesFromValue(status);
fatChain.writeToPosition(info, entry, cb);
}
};
vol.allocateInFAT = function (hint, cb) {
if (typeof hint === 'function') {
cb = hint;
hint = 2; // TODO: cache a better starting point?
}
function searchForFreeCluster(num, cb) {
if (num < countofClusters) vol.fetchFromFAT(num, function (e, status) {
if (e) cb(e);
else if (status === 'free') cb(null, num);
else searchForFreeCluster(num+1, cb);
}); else cb(S.err.NOSPC()); // TODO: try searching backwards from hint…
}
searchForFreeCluster(hint, function (e, clusterNum) {
if (e) cb(e);
else vol.storeToFAT(clusterNum, 'eof', cb.bind(null,null,clusterNum));
});
};
vol.rootDirectoryChain = (isFAT16) ?
c.sectorChain(vol, firstDataSector - rootDirSectors, rootDirSectors) :
c.clusterChain(vol, BS.RootClus);
vol.rootDirectoryChain.cacheAdvice = 'WILLNEED';
vol.chainForCluster = c.clusterChain.bind(c, vol);
vol.chainFromJSON = function (d) {
return ('numSectors' in d) ?
c.sectorChain(vol, d.firstSector, d.numSectors) :
c.clusterChain(vol, d.firstCluster);
};
return vol;
}

View File

@@ -1,8 +1,11 @@
import LRU from 'lru-cache'
import Fuse from 'fuse-native'
import { createLogger } from '@xen-orchestra/log'
import { VhdSynthetic } from 'vhd-lib'
import { Disposable, fromCallback } from 'promise-toolbox'
const { warn } = createLogger('vates:fuse-vhd')
// build a s stat object from https://github.com/fuse-friends/fuse-native/blob/master/test/fixtures/stat.js
const stat = st => ({
mtime: st.mtime || new Date(),
@@ -52,7 +55,10 @@ export const mount = Disposable.factory(async function* mount(handler, diskPath,
},
read(path, fd, buf, len, pos, cb) {
if (path === '/vhd0') {
return vhd.readRawData(pos, len, cache, buf).then(cb)
return vhd.readRawData(pos, len, cache, buf).then(cb, error => {
warn('read error', { path, len, pos, error })
cb(Fuse.EIO)
})
}
throw new Error(`read file ${path} not exists`)
},

View File

@@ -1,6 +1,6 @@
{
"name": "@vates/fuse-vhd",
"version": "2.1.0",
"version": "2.1.2",
"license": "ISC",
"private": false,
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/fuse-vhd",
@@ -19,11 +19,12 @@
},
"main": "./index.mjs",
"dependencies": {
"@xen-orchestra/fs": "^4.1.6",
"@xen-orchestra/fs": "^4.3.0",
"@xen-orchestra/log": "^0.7.1",
"fuse-native": "^2.2.6",
"lru-cache": "^7.14.0",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^4.9.2"
"vhd-lib": "^4.11.2"
},
"bin": {
"xo-fuse-vhd": "./cli.mjs"

View File

@@ -0,0 +1,18 @@
The use for this librariy is to create a composite key from a JSON value, ignoring objects identity and properties order.
The hash algorithm used is intended to be fast and with low collisions and is not guaranteed to be be secure.
The hash is encoded using [Base64 URL](https://en.wikipedia.org/wiki/Base64#URL_applications) to make it easy to use in URLs and filenames.
```js
import { jsonHash } from '@vates/json-hash'
console.log(jsonHash('foo'))
// → "siEyldVkkW-JpqQkVVZ8h8P0gPzXocFeIg8X1xaaeQs"
// order of properties is ignored
console.log(jsonHash({ foo: 0, bar: 1 }))
// → "JckoRSMIBjNlgEWIXhgpBOuyLQYqABZqvf1ccb3BPg0"
console.log(jsonHash({ bar: 1, foo: 0 }))
// → "JckoRSMIBjNlgEWIXhgpBOuyLQYqABZqvf1ccb3BPg0"
```

1
@vates/json-hash/.npmignore Symbolic link
View File

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

View File

@@ -0,0 +1,51 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/json-hash
[![Package Version](https://badgen.net/npm/v/@vates/json-hash)](https://npmjs.org/package/@vates/json-hash) ![License](https://badgen.net/npm/license/@vates/json-hash) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/json-hash)](https://bundlephobia.com/result?p=@vates/json-hash) [![Node compatibility](https://badgen.net/npm/node/@vates/json-hash)](https://npmjs.org/package/@vates/json-hash)
> Compute a stable hash from a JSON-ifiable value
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/json-hash):
```sh
npm install --save @vates/json-hash
```
## Usage
The use for this librariy is to create a composite key from a JSON value, ignoring objects identity and properties order.
The hash algorithm used is intended to be fast and with low collisions and is not guaranteed to be be secure.
The hash is encoded using [Base64 URL](https://en.wikipedia.org/wiki/Base64#URL_applications) to make it easy to use in URLs and filenames.
```js
import { jsonHash } from '@vates/json-hash'
console.log(jsonHash('foo'))
// → "siEyldVkkW-JpqQkVVZ8h8P0gPzXocFeIg8X1xaaeQs"
// order of properties is ignored
console.log(jsonHash({ foo: 0, bar: 1 }))
// → "JckoRSMIBjNlgEWIXhgpBOuyLQYqABZqvf1ccb3BPg0"
console.log(jsonHash({ bar: 1, foo: 0 }))
// → "JckoRSMIBjNlgEWIXhgpBOuyLQYqABZqvf1ccb3BPg0"
```
## 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)

38
@vates/json-hash/index.js Normal file
View File

@@ -0,0 +1,38 @@
'use strict'
const { createHash } = require('node:crypto')
function updateJsonHash(value, hash) {
if (value !== null && typeof value === 'object') {
if (Array.isArray(value)) {
hash.update('[')
for (const item of value) {
updateJsonHash(item, hash)
// trailing is not a big deal because it does not need to be valid JSON
hash.update(',')
}
hash.update(']')
} else {
hash.update('{')
for (const key of Object.keys(value).sort()) {
updateJsonHash(key, hash)
hash.update(':')
updateJsonHash(value[key], hash)
// trailing is not a big deal because it does not need to be valid JSON
hash.update(',')
}
hash.update('}')
}
} else {
hash.update(JSON.stringify(value))
}
}
exports.jsonHash = function jsonHash(value) {
// this hash does not need to be secure, it just needs to be fast and with low collisions
const hash = createHash('sha256')
updateJsonHash(value, hash)
return hash.digest('base64url')
}

View File

@@ -0,0 +1,21 @@
'use strict'
const { jsonHash } = require('@vates/json-hash')
const assert = require('node:assert/strict')
const test = require('node:test')
const cases = [
[null, 'dCNOmK_nSY-12vHzasLXiswzlGT5UHA7jAGYkvmCuQs'],
[42, 'c0dctApWjo2ooEXO0RATfhWfiQrE2og7axfcZRs6gEk'],
['foo', 'siEyldVkkW-JpqQkVVZ8h8P0gPzXocFeIg8X1xaaeQs'],
[[], 'T1PNoYwrqgwDVLtfmj7L5e0Sq02OEbqHPC8RFhICuUU'],
[{}, 'RBNvo1WzZ4oRRq0W9-hknpT7T8If536DEMBg9hyq_4o'],
[{ a: 1, b: 2 }, 'LMjVxyj2vtMZ3W1cZ2cJrQij5zn7FBOlPiyBB38OmFE'],
[{ b: 2, a: 1 }, 'LMjVxyj2vtMZ3W1cZ2cJrQij5zn7FBOlPiyBB38OmFE'],
]
for (const [value, hash] of cases) {
test(JSON.stringify(value), function () {
assert.equal(jsonHash(value), hash)
})
}

View File

@@ -0,0 +1,30 @@
{
"private": false,
"name": "@vates/json-hash",
"description": "Compute a stable hash from a JSON-ifiable value",
"keywords": [
"hash",
"json",
"stable"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/json-hash",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/json-hash",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.2.0",
"engines": {
"node": ">=14.18.0"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --test"
}
}

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it } = require('test')
const { describe, it } = require('node:test')
const assert = require('node:assert')
const { MultiKeyMap } = require('./')

View File

@@ -24,9 +24,6 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
},
"devDependencies": {
"test": "^3.2.1"
"test": "node --test"
}
}

View File

@@ -34,7 +34,6 @@ export default class NbdClient {
#exportSize
#waitBeforeReconnect
#readAhead
#readBlockRetries
#reconnectRetry
#connectTimeout
@@ -55,7 +54,6 @@ export default class NbdClient {
connectTimeout = 6e4,
messageTimeout = 6e4,
waitBeforeReconnect = 1e3,
readAhead = 10,
readBlockRetries = 5,
reconnectRetry = 5,
} = {}
@@ -65,7 +63,6 @@ export default class NbdClient {
this.#exportName = exportname
this.#serverCert = cert
this.#waitBeforeReconnect = waitBeforeReconnect
this.#readAhead = readAhead
this.#readBlockRetries = readBlockRetries
this.#reconnectRetry = reconnectRetry
this.#connectTimeout = connectTimeout

View File

@@ -6,7 +6,10 @@ import { createLogger } from '@xen-orchestra/log'
const { warn } = createLogger('vates:nbd-client:multi')
export default class MultiNbdClient {
#clients = []
#nbdConcurrency
#options
#readAhead
#settings
get exportSize() {
return this.#clients[0].exportSize
@@ -14,36 +17,63 @@ export default class MultiNbdClient {
constructor(settings, { nbdConcurrency = 8, readAhead = 16, ...options } = {}) {
this.#readAhead = readAhead
this.#options = options
this.#nbdConcurrency = nbdConcurrency
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) })
)
}
this.#settings = settings
}
/**
*
* open nbdConcurrency connections to NBD servers
* it must obtain at least one connection to succeed
* it tries to spread connections on multiple host
*/
async connect() {
const connectedClients = []
for (const clientId in this.#clients) {
const client = this.#clients[clientId]
const candidates = [...this.#settings]
const baseOptions = this.#options
const _connect = async () => {
if (candidates.length === 0) {
return
}
// a little bit of randomization to spread the load
const nbdInfo = candidates[Math.floor(Math.random() * candidates.length)]
const client = new NbdClient(nbdInfo, {
...baseOptions,
readAhead: Math.ceil(this.#readAhead / this.#nbdConcurrency),
})
try {
await client.connect()
connectedClients.push(client)
this.#clients.push(client)
} catch (err) {
client.disconnect().catch(() => {})
// do not hammer unreachable hosts, once failed, remove from the list
const candidateIndex = candidates.findIndex(({ address }) => address === nbdInfo.address)
if (candidateIndex >= 0) {
// this candidate may have already been deleted by another parallel promise
candidates.splice(candidateIndex, 1)
}
warn(`can't connect to one nbd client`, { err })
// retry with another candidate (if available)
return _connect()
}
}
if (connectedClients.length === 0) {
// don't connect in parallel since this can lead to race condition
// on distributed systems ( like the NBD server of the XAPI)
for (let i = 0; i < this.#nbdConcurrency; i++) {
await _connect()
}
if (this.#clients.length === 0) {
throw new Error(`Fail to connect to any Nbd client`)
}
if (connectedClients.length < this.#clients.length) {
if (this.#clients.length < this.#nbdConcurrency) {
warn(
`incomplete connection by multi Nbd, only ${connectedClients.length} over ${this.#clients.length} expected clients`
`incomplete connection by multi Nbd, only ${this.#clients.length} over ${this.#nbdConcurrency} expected clients`
)
this.#clients = connectedClients
}
}

View File

@@ -13,7 +13,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "3.0.1",
"version": "3.1.2",
"engines": {
"node": ">=14.0"
},
@@ -22,9 +22,9 @@
"@vates/async-each": "^1.0.0",
"@vates/read-chunk": "^1.2.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/log": "^0.7.1",
"promise-toolbox": "^0.21.0",
"xen-api": "^3.0.1"
"xen-api": "^4.7.0"
},
"devDependencies": {
"tap": "^18.7.0",

View File

@@ -78,15 +78,14 @@ async function killNbdKit() {
)
}
test('it works with unsecured network', async tap => {
test('it works secured network', async tap => {
const path = await createTempFile(FILE_SIZE)
let nbdServer = await spawnNbdKit(path)
const client = new MultiNbdClient(
{
address: '127.0.0.1',
exportname: 'MY_SECRET_EXPORT',
cert: `-----BEGIN CERTIFICATE-----
const connectionSettings = {
address: '127.0.0.1',
exportname: 'MY_SECRET_EXPORT',
cert: `-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUeHpQ0IeD6BmP2zgsv3LV3J4BI/EwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzA1MTcxMzU1MzBaFw0yNDA1
@@ -108,12 +107,18 @@ CYu1Xn/FVPx1HoRgWc7E8wFhDcA/P3SJtfIQWHB9FzSaBflKGR4t8WCE2eE8+cTB
57ABhfYpMlZ4aHjuN1bL
-----END CERTIFICATE-----
`,
},
{
nbdConcurrency: 1,
readAhead: 2,
}
)
}
const invalid = {
address: '500.500.500.500',
port: 0,
exportname: 'nop',
}
// it should work even with some broken servers
const nbdInfos = [connectionSettings, connectionSettings, invalid]
const client = new MultiNbdClient(nbdInfos, {
nbdConcurrency: 4,
readAhead: 2,
})
await client.connect()
tap.equal(client.exportSize, BigInt(FILE_SIZE))
@@ -156,3 +161,18 @@ CYu1Xn/FVPx1HoRgWc7E8wFhDcA/P3SJtfIQWHB9FzSaBflKGR4t8WCE2eE8+cTB
nbdServer.kill()
await fs.unlink(path)
})
test('fails if server does not answer', async () => {
const client = new MultiNbdClient(
{
address: '500.500.500.500',
port: 0,
exportname: 'nop',
},
{
nbdConcurrency: 1,
readAhead: 2,
}
)
await assert.rejects(client.connect())
})

View File

@@ -17,7 +17,20 @@ import https from 'node:https'
import util from 'util'
import soap from 'soap'
import Cookie from 'soap-cookie' // required for session persistence
import { createLogger } from '@xen-orchestra/log'
const { warn } = createLogger('xo:node-vsphere-soap:client')
class VmwareError extends Error {
constructor(rawError) {
super(rawError.body)
const matches = rawError.body?.match(/<faultstring[^>]*>(.*)<\/faultstring>/im)
this.message = matches?.[1] ?? rawError.message
this.name = 'VmwareError'
this.stack = rawError.stack
// not putting the cause since the error failed to be stringified correctly down the road
warn(rawError)
}
}
// Client class
// inherits from EventEmitter
// possible events: connect, error, ready
@@ -140,8 +153,9 @@ Client.prototype._connect = function () {
self.clientopts,
function (err, client) {
if (err) {
self.emit('error', err)
throw err
const vErr = new VmwareError(err)
self.emit('error', vErr)
throw vErr
}
self.client = client // save client for later use
@@ -150,6 +164,7 @@ Client.prototype._connect = function () {
.runCommand('RetrieveServiceContent', { _this: 'ServiceInstance' })
.once('result', function (result, raw, soapHeader) {
if (!result.returnval) {
// this is a domain/server error return it as is
self.status = 'disconnected'
self.emit('error', raw)
return
@@ -208,7 +223,7 @@ Client.prototype._close = function () {
function _soapErrorHandler(self, emitter, command, args, err) {
err = err || { body: 'general error' }
if (err.body.match(/session is not authenticated/)) {
if (err.body?.match(/session is not authenticated/)) {
self.status = 'disconnected'
process.removeAllListeners('beforeExit')
@@ -220,16 +235,19 @@ function _soapErrorHandler(self, emitter, command, args, err) {
emitter.emit('result', result, raw, soapHeader)
})
.once('error', function (err) {
emitter.emit('error', err.body)
throw err
const vErr = new VmwareError(err)
emitter.emit('error', vErr)
throw vErr
})
} else {
emitter.emit('error', err.body)
throw err
const vErr = new VmwareError(err)
emitter.emit('error', vErr)
throw vErr
}
} else {
emitter.emit('error', err.body)
throw err
const vErr = new VmwareError(err)
emitter.emit('error', vErr)
throw vErr
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vates/node-vsphere-soap",
"version": "2.0.1",
"version": "2.1.1",
"description": "interface to vSphere SOAP/WSDL from node for interfacing with vCenter or ESXi, forked from node-vsphere-soap",
"main": "lib/client.mjs",
"author": "reedog117",
@@ -11,13 +11,11 @@
},
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/log": "^0.7.1",
"axios": "^1.4.0",
"soap": "^1.0.0",
"soap-cookie": "^0.10.1"
},
"devDependencies": {
"test": "^3.3.0"
},
"keywords": [
"vsphere",
"vcenter",

View File

@@ -5,7 +5,7 @@
*/
import assert from 'assert'
import { describe, it } from 'test'
import { describe, it } from 'node:test'
import * as vc from '../lib/client.mjs'

View File

@@ -0,0 +1,46 @@
### `replace(value, replacement)`
> Recursively replace sensitive entries with a replacement value.
```js
import { replace } from '@vates/obfuscate'
replace({ username: 'foo', password: 'bar' }, 'obfuscated')
// → { username: 'foo', password: 'obfuscated'}
```
### `obfuscate(value)`
> Recursively replace sensitive entries with a special tag.
```js
import { obfuscate } from '@vates/obfuscate'
obfuscate({ username: 'foo', password: 'bar' })
// → {
// username: 'foo',
// password: 'obfuscated-q3oi6d9X8uenGvdLnHk2',
// }
```
### `merge(newValue, oldValue)`
> Replace entries obfuscated by `obfuscate()` in the new value by entries from the old one.
>
> This is especially useful to give an obfuscated value to a client, let them modify it, and then merge modified and unmodified values.
```js
import { obfuscate, merge } from '@vates/obfuscate'
const original = { username: 'foo', password: 'bar', token: 'baz' }
// compute an obfuscated value to send to a client
const obfuscated = obfuscate(original)
// let the client update any entries
obfuscated.token = 'qux'
// merge the updated value with the original one
merge(obfuscated, original)
// → { username: 'foo', password: 'bar', token: 'qux' }
```

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

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

View File

@@ -0,0 +1,77 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/obfuscate
[![Package Version](https://badgen.net/npm/v/@vates/obfuscate)](https://npmjs.org/package/@vates/obfuscate) ![License](https://badgen.net/npm/license/@vates/obfuscate) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/obfuscate)](https://bundlephobia.com/result?p=@vates/obfuscate) [![Node compatibility](https://badgen.net/npm/node/@vates/obfuscate)](https://npmjs.org/package/@vates/obfuscate)
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/obfuscate):
```sh
npm install --save @vates/obfuscate
```
## Usage
### `replace(value, replacement)`
> Recursively replace sensitive entries with a replacement value.
```js
import { replace } from '@vates/obfuscate'
replace({ username: 'foo', password: 'bar' }, 'obfuscated')
// → { username: 'foo', password: 'obfuscated'}
```
### `obfuscate(value)`
> Recursively replace sensitive entries with a special tag.
```js
import { obfuscate } from '@vates/obfuscate'
obfuscate({ username: 'foo', password: 'bar' })
// → {
// username: 'foo',
// password: 'obfuscated-q3oi6d9X8uenGvdLnHk2',
// }
```
### `merge(newValue, oldValue)`
> Replace entries obfuscated by `obfuscate()` in the new value by entries from the old one.
>
> This is especially useful to give an obfuscated value to a client, let them modify it, and then merge modified and unmodified values.
```js
import { obfuscate, merge } from '@vates/obfuscate'
const original = { username: 'foo', password: 'bar', token: 'baz' }
// compute an obfuscated value to send to a client
const obfuscated = obfuscate(original)
// let the client update any entries
obfuscated.token = 'qux'
// merge the updated value with the original one
merge(obfuscated, original)
// → { username: 'foo', password: 'bar', token: 'qux' }
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,9 +1,18 @@
import mapValues from 'lodash/mapValues.js'
'use strict'
function mapValues(object, iteratee) {
const result = {}
for (const key of Object.keys(object)) {
result[key] = iteratee(object[key], key, object)
}
return result
}
// this random value is used to obfuscate real data
const OBFUSCATED_VALUE = 'obfuscated-q3oi6d9X8uenGvdLnHk2'
exports.OBFUSCATED_VALUE = OBFUSCATED_VALUE
export const merge = (newValue, oldValue) => {
function merge(newValue, oldValue) {
if (newValue === OBFUSCATED_VALUE) {
return oldValue
}
@@ -23,14 +32,16 @@ export const merge = (newValue, oldValue) => {
const iteratee = (v, k) => merge(v, oldValue[k])
return isArray ? newValue.map(iteratee) : mapValues(newValue, iteratee)
}
exports.merge = merge
export const obfuscate = value => replace(value, OBFUSCATED_VALUE)
const obfuscate = value => replace(value, OBFUSCATED_VALUE)
exports.obfuscate = obfuscate
const SENSITIVE_PARAMS = ['token', 'passphrase', /password/i, 'encryptionKey']
const isSensitiveParam = name =>
SENSITIVE_PARAMS.some(pattern => (typeof pattern === 'string' ? pattern === name : pattern.test(name)))
export function replace(value, replacement) {
function replace(value, replacement) {
function helper(value, name) {
if (typeof value === 'string' && isSensitiveParam(name)) {
return replacement
@@ -45,3 +56,4 @@ export function replace(value, replacement) {
return helper(value)
}
exports.replace = replace

View File

@@ -0,0 +1,23 @@
{
"private": false,
"name": "@vates/obfuscate",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/obfuscate",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/obfuscate",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.1.0",
"engines": {
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public"
}
}

View File

@@ -1,5 +1,5 @@
import { strict as assert } from 'node:assert'
import test from 'test'
import test from 'node:test'
import {
generateHotp,

View File

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

View File

@@ -6,9 +6,11 @@ exports.parseDuration = value => {
if (typeof value === 'number') {
return value
}
const duration = ms(value)
if (duration === undefined) {
throw new TypeError(`not a valid duration: ${value}`)
if (typeof value === 'string' && value !== '') {
const duration = ms(value)
if (duration !== undefined) {
return duration
}
}
return duration
throw new TypeError(`not a valid duration: ${value}`)
}

View File

@@ -0,0 +1,22 @@
import { describe, it } from 'node:test'
import { parseDuration } from '@vates/parse-duration'
import assert from 'node:assert/strict'
describe('parseDuration()', () => {
it('should parse string', () => {
const input = '2 days'
const expected = 172800000
assert.strictEqual(parseDuration(input), expected)
})
it('should return its input if already a number', () => {
const input = 172800000
assert.strictEqual(parseDuration(input), input)
})
for (const input of [undefined, '', 'invalid duration']) {
it('should throw an error for ' + input, () => {
assert.throws(() => parseDuration(input), { message: `not a valid duration: ${input}` })
})
}
})

View File

@@ -27,6 +27,7 @@
"ms": "^2.1.2"
},
"scripts": {
"postversion": "npm publish --access public"
"postversion": "npm publish --access public",
"test": "node --test"
}
}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it } = require('test')
const { describe, it } = require('node:test')
const assert = require('node:assert').strict
const { Readable } = require('stream')

View File

@@ -25,15 +25,12 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
"test": "node --test"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"devDependencies": {
"test": "^3.2.1"
},
"dependencies": {
"isutf8": "^4.0.0"
}

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it } = require('test')
const { describe, it } = require('node:test')
const assert = require('node:assert').strict
const { Readable } = require('stream')

View File

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

View File

@@ -49,6 +49,15 @@ const result = await task.runInside(fn)
// if fn rejects, the task will be marked as failed
// if fn resolves, the task will be marked as succeeded
const result = await task.run(fn)
// manually starts the task
task.start()
// manually finishes the task in success state
task.success(result)
// manually finishes the task in failure state
task.failure(error)
```
Inside a task:

View File

@@ -65,6 +65,15 @@ const result = await task.runInside(fn)
// if fn rejects, the task will be marked as failed
// if fn resolves, the task will be marked as succeeded
const result = await task.run(fn)
// manually starts the task
task.start()
// manually finishes the task in success state
task.success(result)
// manually finishes the task in failure state
task.failure(error)
```
Inside a task:

View File

@@ -44,6 +44,8 @@ exports.makeOnProgress = function ({ onRootTaskEnd = noop, onRootTaskStart = noo
} else if (type === 'property') {
;(taskLog.properties ?? (taskLog.properties = { __proto__: null }))[event.name] = event.value
} else if (type === 'end') {
taskLogs.delete(id)
taskLog.end = event.timestamp
taskLog.result = event.result
taskLog.status = event.status

View File

@@ -1,7 +1,7 @@
'use strict'
const assert = require('node:assert').strict
const { describe, it } = require('test')
const { describe, it } = require('node:test')
const { makeOnProgress } = require('./combineEvents.js')
const { Task } = require('./index.js')

View File

@@ -3,6 +3,10 @@
const assert = require('node:assert').strict
const { AsyncLocalStorage } = require('node:async_hooks')
function alreadyEnded() {
throw new Error('task has already ended')
}
// define a read-only, non-enumerable, non-configurable property
function define(object, property, value) {
Object.defineProperty(object, property, { value })
@@ -21,7 +25,7 @@ const asyncStorage = global[asyncStorageKey] ?? (global[asyncStorageKey] = new A
const getTask = () => asyncStorage.getStore()
exports.Task = class Task {
class Task {
static get abortSignal() {
const task = getTask()
if (task !== undefined) {
@@ -29,13 +33,6 @@ exports.Task = class Task {
}
}
static info(message, data) {
const task = getTask()
if (task !== undefined) {
task.#emit('info', { data, message })
}
}
static run(...args) {
let opts = args[0]
let fn
@@ -57,20 +54,6 @@ exports.Task = class Task {
return new Task(opts).run(() => fn.apply(thisArg, args))
}
static set(name, value) {
const task = getTask()
if (task !== undefined) {
task.#emit('property', { name, value })
}
}
static warning(message, data) {
const task = getTask()
if (task !== undefined) {
task.#emit('warning', { data, message })
}
}
static wrap(opts, fn) {
// compatibility with @decorateWith
if (typeof fn !== 'function') {
@@ -127,9 +110,7 @@ exports.Task = class Task {
this.#emit('abortionRequested', { reason: signal.reason })
if (!this.#running) {
const status = FAILURE
this.#status = status
this.#emit('end', { result: signal.reason, status })
this.#end(FAILURE, signal.reason)
}
}
})
@@ -140,25 +121,41 @@ exports.Task = class Task {
}
#emit(type, data) {
assert.equal(this.#startData, undefined, 'task has not started yet')
data.id = this.id
data.timestamp = Date.now()
data.type = type
this.#onProgress(data)
}
#end(status, result) {
assert.equal(this.#status, PENDING)
assert.equal(this.#running, false)
this.#emit('end', { status, result })
this.#onProgress = alreadyEnded
this.#status = status
}
failure(error) {
this.#end(FAILURE, error)
}
info(message, data) {
this.#emit('info', { data, message })
}
#maybeStart() {
const startData = this.#startData
if (startData !== undefined) {
this.#startData = undefined
this.#emit('start', startData)
if (this.#startData !== undefined) {
this.start()
}
}
async run(fn) {
const result = await this.runInside(fn)
if (this.status === PENDING) {
this.#status = SUCCESS
this.#emit('end', { status: SUCCESS, result })
this.#end(SUCCESS, result)
}
return result
}
@@ -172,18 +169,40 @@ exports.Task = class Task {
this.#maybeStart()
try {
const result = await asyncStorage.run(this, fn)
const result = await asyncStorage.run(this, fn, this)
this.#running = false
return result
} catch (result) {
const status = FAILURE
this.#status = status
this.#emit('end', { status, result })
this.#running = false
this.#end(FAILURE, result)
throw result
}
}
set(name, value) {
assert.equal(this.status, PENDING)
this.#emit('property', { name, value })
}
start() {
const startData = this.#startData
assert.notEqual(startData, undefined, 'task has already started')
this.#startData = undefined
this.#emit('start', startData)
}
success(result) {
this.#end(SUCCESS, result)
}
warning(message, data) {
assert.equal(this.status, PENDING)
this.#emit('warning', { data, message })
}
wrap(fn) {
const task = this
return function taskRun() {
@@ -198,3 +217,15 @@ exports.Task = class Task {
}
}
}
exports.Task = Task
// setup static aliases
for (const name of ['info', 'set', 'warning']) {
const method = Task.prototype[name]
Task[name] = function () {
const task = getTask()
if (task !== undefined) {
return method.apply(task, arguments)
}
}
}

View File

@@ -1,7 +1,7 @@
'use strict'
const assert = require('node:assert').strict
const { describe, it } = require('test')
const { describe, it } = require('node:test')
const { makeOnProgress } = require('./combineEvents.js')
const { Task } = require('./index.js')
@@ -145,6 +145,43 @@ describe('Task', function () {
})
})
describe('#failure()', function () {
const error = new Error()
it('throws if the task has not started yet', function () {
const task = createTask()
assert.throws(() => task.failure(error), { message: /^task has not started yet\b/ })
})
it('throws if the task is running', function () {
const task = createTask()
task.runInside(noop)
assert.throws(() => task.failure(error), { code: 'ERR_ASSERTION' })
})
it('throws if the task has already finished', function () {
const task = createTask()
task.start()
task.success()
assert.throws(() => task.failure(error), { code: 'ERR_ASSERTION' })
})
it('finishes the task and mark it as failed', function () {
const task = createTask()
task.start()
task.failure(error)
assertEvent(task, {
status: 'failure',
result: error,
type: 'end',
})
})
})
describe('.info()', function () {
it('does nothing when run outside a task', function () {
Task.info('foo')
@@ -219,6 +256,84 @@ describe('Task', function () {
})
})
describe('#set()', function () {
const name = 'progress'
const value = 10
it('throws when the task is not started', function () {
const task = createTask()
assert.throws(() => task.set(name, value), { message: /^task has not started yet\b/ })
})
it(`emits an property message`, async function () {
const task = createTask()
await task.run(async () => {
await Task.run(() => {
task.set(name, value)
assertEvent(task, {
name,
type: 'property',
value,
})
})
})
})
})
describe('#start', function () {
it('starts the task', function () {
const task = createTask()
task.start()
assertEvent(task, { type: 'start' })
})
it('throws when the task has already started', function () {
const task = createTask()
task.start()
assert.throws(() => task.start(), { message: 'task has already started' })
})
})
describe('#success()', function () {
const result = {}
it('throws if the task has not started yet', function () {
const task = createTask()
assert.throws(() => task.success(result), { message: /^task has not started yet\b/ })
})
it('throws if the task is running', function () {
const task = createTask()
task.runInside(noop)
assert.throws(() => task.success(result), { code: 'ERR_ASSERTION' })
})
it('throws if the task has already finished', function () {
const task = createTask()
task.start()
task.success()
assert.throws(() => task.success(result), { code: 'ERR_ASSERTION' })
})
it('finishes the task and mark it as successful', function () {
const task = createTask()
task.start()
task.success(result)
assertEvent(task, {
status: 'success',
result,
type: 'end',
})
})
})
describe('.warning()', function () {
it('does nothing when run outside a task', function () {
Task.warning('foo')
@@ -237,6 +352,30 @@ describe('Task', function () {
})
})
for (const type of ['info', 'warning']) {
describe(`#${type}()`, function () {
it('throws when the task is not started', function () {
const task = createTask()
assert.throws(() => task[type]('foo'), { message: /^task has not started yet\b/ })
})
it(`emits an ${type} message`, async function () {
const task = createTask()
await task.run(async () => {
await Task.run(() => {
task[type]('foo')
assertEvent(task, {
data: undefined,
message: 'foo',
type,
})
})
})
})
})
}
describe('#id', function () {
it('can be set', function () {
const task = createTask()

View File

@@ -13,16 +13,13 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.3.0",
"version": "0.6.0",
"engines": {
"node": ">=14"
},
"devDependencies": {
"test": "^3.3.0"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
"test": "node --test"
},
"exports": {
".": "./index.js",

View File

@@ -121,7 +121,8 @@ const xml = `
</methodResponse>
`
const tree = parseXml(xml)
// do not normalize or trim whitespaces as it will break string values
const tree = parseXml(xml, { normalize: false, trim: false })
// it can now be passed to the XML-RPC parser
xmlRpcParser.parse(tree)

View File

@@ -139,7 +139,8 @@ const xml = `
</methodResponse>
`
const tree = parseXml(xml)
// do not normalize or trim whitespaces as it will break string values
const tree = parseXml(xml, { normalize: false, trim: false })
// it can now be passed to the XML-RPC parser
xmlRpcParser.parse(tree)

View File

@@ -19,8 +19,7 @@
"node": ">=12"
},
"devDependencies": {
"@vates/xml": "^2.0.0",
"test": "^3.3.0"
"@vates/xml": "^2.0.0"
},
"exports": {
"./formatter": "./formatter.js",
@@ -28,6 +27,6 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
"test": "node --test"
}
}

View File

@@ -6,7 +6,7 @@ const { parseXml } = require('@vates/xml/parse')
const { readFileSync } = require('node:fs')
const { xmlRpcFormatter } = require('@vates/xml-rpc/formatter')
const assert = require('node:assert/strict')
const test = require('test')
const test = require('node:test')
const methodCall = require('./methodCall.js')

View File

@@ -5,7 +5,7 @@ const { parseXml } = require('@vates/xml/parse')
const { readFileSync } = require('node:fs')
const { xmlRpcParser } = require('@vates/xml-rpc/parser')
const assert = require('node:assert/strict')
const test = require('test')
const test = require('node:test')
const methodCall = require('./methodCall.js')

View File

@@ -21,15 +21,12 @@
"dependencies": {
"sax": "^1.3.0"
},
"devDependencies": {
"test": "^3.3.0"
},
"exports": {
"./format": "./format.js",
"./parse": "./parse.js"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
"test": "node --test"
}
}

View File

@@ -4,7 +4,7 @@ const { formatXml } = require('@vates/xml/format')
const { join } = require('node:path')
const { readFileSync } = require('node:fs')
const assert = require('node:assert/strict')
const test = require('test')
const test = require('node:test')
test('format()', function () {
const xml = String(readFileSync(join(__dirname, 'data.xml')))

View File

@@ -4,7 +4,7 @@ const { parseXml } = require('@vates/xml/parse')
const { readFileSync } = require('node:fs')
const { join } = require('node:path')
const assert = require('node:assert/strict')
const test = require('test')
const test = require('node:test')
test('parse()', function () {
const xml = String(readFileSync(join(__dirname, 'data.xml')))

View File

@@ -1,6 +1,6 @@
'use strict'
const { describe, it } = require('test')
const { describe, it } = require('node:test')
const assert = require('assert').strict
const sinon = require('sinon')

View File

@@ -32,10 +32,9 @@
},
"scripts": {
"postversion": "npm publish",
"test": "node--test"
"test": "node --test"
},
"devDependencies": {
"sinon": "^17.0.1",
"test": "^3.2.1"
"sinon": "^18.0.0"
}
}

View File

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

View File

@@ -7,17 +7,17 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.3.0",
"version": "0.3.1",
"engines": {
"node": ">=14"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
"test": "node --test"
},
"dependencies": {
"@vates/decorate-with": "^2.1.0",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/log": "^0.7.1",
"golike-defer": "^0.5.1",
"object-hash": "^2.0.1"
},
@@ -26,8 +26,5 @@
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"devDependencies": {
"test": "^3.3.0"
}
}

View File

@@ -7,8 +7,8 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.48.1",
"@xen-orchestra/fs": "^4.1.6",
"@xen-orchestra/backups": "^0.57.0",
"@xen-orchestra/fs": "^4.3.0",
"filenamify": "^6.0.0",
"getopts": "^2.2.5",
"lodash": "^4.17.15",
@@ -27,7 +27,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "1.0.18",
"version": "1.0.28",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -0,0 +1,10 @@
## Changing the default backup configuration
This library receives a backup configuration from `xo-server` or `xo-proxy`. This configuration is split between multiple properties:
- `config` contains values related to the general backup configuration
- `config.defaultSettings`, `config.vm.defaultSettings` and `config.metadata.defaultSettings` contain values related to backup jobs default settings:
- `config.vm.defaultSettings` is related to VM backup jobs (see [DEFAULT_XAPI_VM_SETTINGS](https://github.com/vatesfr/xen-orchestra/blob/568e14f7eec7c3ddf685d324ad4733a3ef577995/%40xen-orchestra/backups/_runners/VmsRemote.mjs#L15))
- `config.metadata.defaultSettings` is related to metadata backup jobs (see [DEFAULT_METADATA_SETTINGS](https://github.com/vatesfr/xen-orchestra/blob/568e14f7eec7c3ddf685d324ad4733a3ef577995/%40xen-orchestra/backups/_runners/Metadata.mjs#L12))
- `config.defaultSettings` is related to all backup jobs (see [DEFAULT_SETTINGS](https://github.com/vatesfr/xen-orchestra/blob/568e14f7eec7c3ddf685d324ad4733a3ef577995/%40xen-orchestra/backups/_runners/_Abstract.mjs#L7))
- in case of duplicate value, `config.vm.defaultSettings` and `config.metadata.defaultSettings` will prevail over `config.defaultSettings`, and a defined job setting will prevail over any `defaultSettings`

View File

@@ -49,26 +49,25 @@ export class HealthCheckVmBackup {
}
// wait for the 'Running' event to be really stored in local xapi object cache
restoredVm = await xapi.waitObjectState(restoredVm.$ref, vm => vm.power_state === 'Running', {
timeout: remainingTimeout,
timeoutMessage: refOrUuid =>
`local xapi did not get Running state for VM ${refOrUuid} after ${timeout / 1000} second`,
})
const running = new Date()
remainingTimeout -= running - started
if (remainingTimeout < 0) {
throw new Error(`local xapi did not get Running state for VM ${restoredId} after ${timeout / 1000} second`)
}
// wait for the guest tool version to be defined
await xapi.waitObjectState(restoredVm.guest_metrics, gm => gm?.PV_drivers_version?.major !== undefined, {
// wait for the guest tools to be detected
await xapi.waitObjectState(restoredVm.guest_metrics, gm => gm?.PV_drivers_detected, {
timeout: remainingTimeout,
timeoutMessage: refOrUuid =>
`timeout reached while waiting for ${refOrUuid} to report the driver version through the Xen tools. Please check or update the Xen tools.`,
})
const guestToolsReady = new Date()
remainingTimeout -= guestToolsReady - running
if (remainingTimeout < 0) {
throw new Error(`local xapi did not get he guest tools check ${restoredId} after ${timeout / 1000} second`)
}
if (waitForScript) {
const startedRestoredVm = await xapi.waitObjectState(
@@ -79,19 +78,10 @@ export class HealthCheckVmBackup {
vm.xenstore_data['vm-data/xo-backup-health-check'] === 'failure'),
{
timeout: remainingTimeout,
timeoutMessage: refOrUuid =>
`timeout reached while waiting for ${refOrUuid} to report the startup script execution.`,
}
)
const scriptOk = new Date()
remainingTimeout -= scriptOk - guestToolsReady
if (remainingTimeout < 0) {
throw new Error(
`Backup health check script did not update vm-data/xo-backup-health-check of ${restoredId} after ${
timeout / 1000
} second, got ${
startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check']
} instead of 'success' or 'failure'`
)
}
if (startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check'] !== 'success') {
const message = startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check-error']

View File

@@ -58,7 +58,7 @@ export class ImportVmBackup {
async _reuseNearestSnapshot($defer, ignoredVdis) {
const metadata = this._metadata
const { mapVdisSrs } = this._importIncrementalVmSettings
const { vbds, vhds, vifs, vm, vmSnapshot } = metadata
const { vbds, vhds, vifs, vm, vmSnapshot, vtpms } = metadata
const streams = {}
const metdataDir = dirname(metadata._filename)
const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
@@ -79,8 +79,13 @@ export class ImportVmBackup {
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 })
debug('handling snapshot', { snapshot })
if (snapshot.type === 'cbt_metadata') {
// disk without data can't be used as a base
debug('cbt metadata snapshot, skip')
continue
}
// take only the first snapshot
if (snapshotCandidate && snapshotCandidate.snapshot_time < snapshot.snapshot_time) {
debug('already got a better candidate')
@@ -186,6 +191,7 @@ export class ImportVmBackup {
version: '1.0.0',
vifs,
vm: { ...vm, suspend_VDI: vmSnapshot.suspend_VDI },
vtpms,
}
}

View File

@@ -12,6 +12,19 @@ Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/backu
npm install --save @xen-orchestra/backups
```
## Usage
## Changing the default backup configuration
This library receives a backup configuration from `xo-server` or `xo-proxy`. This configuration is split between multiple properties:
- `config` contains values related to the general backup configuration
- `config.defaultSettings`, `config.vm.defaultSettings` and `config.metadata.defaultSettings` contain values related to backup jobs default settings:
- `config.vm.defaultSettings` is related to VM backup jobs (see [DEFAULT_XAPI_VM_SETTINGS](https://github.com/vatesfr/xen-orchestra/blob/568e14f7eec7c3ddf685d324ad4733a3ef577995/%40xen-orchestra/backups/_runners/VmsRemote.mjs#L15))
- `config.metadata.defaultSettings` is related to metadata backup jobs (see [DEFAULT_METADATA_SETTINGS](https://github.com/vatesfr/xen-orchestra/blob/568e14f7eec7c3ddf685d324ad4733a3ef577995/%40xen-orchestra/backups/_runners/Metadata.mjs#L12))
- `config.defaultSettings` is related to all backup jobs (see [DEFAULT_SETTINGS](https://github.com/vatesfr/xen-orchestra/blob/568e14f7eec7c3ddf685d324ad4733a3ef577995/%40xen-orchestra/backups/_runners/_Abstract.mjs#L7))
- in case of duplicate value, `config.vm.defaultSettings` and `config.metadata.defaultSettings` will prevail over `config.defaultSettings`, and a defined job setting will prevail over any `defaultSettings`
## Contributions
Contributions are _very_ welcomed, either on the documentation or on

View File

@@ -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 reduce from 'lodash/reduce.js'
import tar from 'tar'
import zlib from 'zlib'
@@ -106,10 +107,13 @@ export class RemoteAdapter {
async *_getLvmLogicalVolumes(devicePath, pvId, vgName) {
yield this._getLvmPhysicalVolume(devicePath, pvId && (await this._findPartition(devicePath, pvId)))
debug('activate LVM volume group', { vgName })
await fromCallback(execFile, 'vgchange', ['-ay', vgName])
try {
debug('get LVM volume group name and path', { vgName })
yield lvs(['lv_name', 'lv_path'], vgName)
} finally {
debug('deactivate LVM volume group', { vgName })
await fromCallback(execFile, 'vgchange', ['-an', vgName])
}
}
@@ -120,15 +124,22 @@ export class RemoteAdapter {
args.push('-o', partition.start * 512, '--sizelimit', partition.size)
}
args.push('--show', '-f', devicePath)
debug('attach loop device', { devicePath, partition })
const path = (await fromCallback(execFile, 'losetup', args)).trim()
try {
debug('list LVM physical volume', { path })
await fromCallback(execFile, 'pvscan', ['--cache', path])
yield path
} finally {
try {
const vgNames = await pvs('vg_name', path)
debug('deactivate LVM volume groups', { vgNames })
await fromCallback(execFile, 'vgchange', ['-an', ...vgNames])
} finally {
debug('detach loop device', { path })
await fromCallback(execFile, 'losetup', ['-d', path])
}
}
@@ -149,6 +160,7 @@ export class RemoteAdapter {
const path = yield getTmpDir()
const mount = options => {
debug('mount device', { devicePath, mountPath: path })
return fromCallback(execFile, 'mount', [
`--options=${options.join(',')}`,
`--source=${devicePath}`,
@@ -166,6 +178,7 @@ export class RemoteAdapter {
try {
yield path
} finally {
debug('umount device', { devicePath, mountPath: path })
await fromCallback(execFile, 'umount', ['--lazy', path])
}
}
@@ -335,6 +348,8 @@ export class RemoteAdapter {
const diskPath = handler.getFilePath('/' + diskId)
const mountDir = yield getTmpDir()
debug('mount VHD (vhdimount)', { diskPath, mountPath: mountDir })
await fromCallback(execFile, 'vhdimount', [diskPath, mountDir])
try {
let max = 0
@@ -356,6 +371,7 @@ export class RemoteAdapter {
yield `${mountDir}/${maxEntry}`
} finally {
debug('umount VHD (fusermount)', { diskPath, mountPath: mountDir })
await fromCallback(execFile, 'fusermount', ['-uz', mountDir])
}
}
@@ -728,7 +744,7 @@ export class RemoteAdapter {
async readIncrementalVmBackup(metadata, ignoredVdis, { useChain = true } = {}) {
const handler = this._handler
const { vbds, vhds, vifs, vm, vmSnapshot } = metadata
const { vbds, vhds, vifs, vm, vmSnapshot, vtpms } = metadata
const dir = dirname(metadata._filename)
const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
@@ -744,6 +760,7 @@ export class RemoteAdapter {
version: '1.0.0',
vifs,
vm: { ...vm, suspend_VDI: vmSnapshot.suspend_VDI },
vtpms,
}
}
@@ -826,6 +843,29 @@ export class RemoteAdapter {
}
return metadata
}
#computeTotalBackupSizeRecursively(backups) {
return reduce(
backups,
(prev, backup) => {
const _backup = Array.isArray(backup) ? this.#computeTotalBackupSizeRecursively(backup) : backup
return {
onDisk: prev.onDisk + (_backup.onDisk ?? _backup.size),
}
},
{ onDisk: 0 }
)
}
async getTotalVmBackupSize() {
return this.#computeTotalBackupSizeRecursively(await this.listAllVmBackups())
}
async getTotalBackupSize() {
const vmBackupSize = await this.getTotalVmBackupSize()
// @TODO: add `getTotalXoBackupSize` and `getTotalPoolBackupSize` once `size` is implemented by fs
return vmBackupSize
}
}
Object.assign(RemoteAdapter.prototype, {

View File

@@ -23,7 +23,7 @@ createCachedLookup().patchGlobal()
const logger = createLogger('xo:backups:worker')
catchGlobalErrors(logger)
const { debug, info } = logger
const { debug, info, warn } = logger
class BackupWorker {
#config
@@ -204,6 +204,13 @@ process.on('message', async message => {
info('backup has ended')
await ignoreErrors.call(backupWorker.debounceResource.flushAll())
process.disconnect()
setTimeout(() => {
warn('worker process did not exit automatically, forcing...')
// eslint-disable-next-line n/no-process-exit
process.exit()
}, 30e3).unref()
}
}
})

View File

@@ -1,4 +1,4 @@
import test from 'test'
import test from 'node:test'
import { strict as assert } from 'node:assert'
import tmp from 'tmp'

View File

@@ -3,7 +3,7 @@ import sum from 'lodash/sum.js'
import { asyncMap } from '@xen-orchestra/async-map'
import { Constants, openVhd, VhdAbstract, VhdFile } from 'vhd-lib'
import { isVhdAlias, resolveVhdAlias } from 'vhd-lib/aliases.js'
import { dirname, resolve } from 'node:path'
import { basename, dirname, resolve } from 'node:path'
import { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } from './_backupType.mjs'
import { limitConcurrency } from 'limit-concurrency-decorator'
import { mergeVhdChain } from 'vhd-lib/merge.js'
@@ -121,7 +121,19 @@ export async function checkAliases(
) {
const aliasFound = []
for (const alias of aliasPaths) {
const target = await resolveVhdAlias(handler, alias)
let target
try {
target = await resolveVhdAlias(handler, alias)
} catch (err) {
if (err.code === 'ENOENT') {
logWarn('missing target of alias', { alias })
if (remove) {
logInfo('removing alias and non VHD target', { alias, target })
await handler.unlink(target)
await handler.unlink(alias)
}
}
}
if (!isVhdFile(target)) {
logWarn('alias references non VHD target', { alias, target })
@@ -179,7 +191,8 @@ export async function cleanVm(
vmDir,
{
fixMetadata,
remove,
remove = false,
removeTmp = remove,
merge,
mergeBlockConcurrency,
mergeLimiter = defaultMergeLimiter,
@@ -200,6 +213,10 @@ export async function cleanVm(
// remove broken VHDs
await asyncMap(vhds, async path => {
if (removeTmp && basename(path)[0] === '.') {
logInfo('deleting temporary VHD', { path })
return VhdAbstract.unlink(handler, path)
}
try {
await Disposable.use(openVhd(handler, path, { checkSecondFooter: !interruptedVhds.has(path) }), vhd => {
if (vhd.footer.diskType === DISK_TYPES.DIFFERENCING) {

View File

@@ -1,4 +1,136 @@
// returns all entries but the last retention-th
export function getOldEntries(retention, entries) {
return entries === undefined ? [] : retention > 0 ? entries.slice(0, -retention) : entries
import moment from 'moment-timezone'
import assert from 'node:assert'
function instantiateTimezonedDateCreator(timezone) {
return date => {
const transformed = timezone ? moment.tz(date, timezone) : moment(date)
assert.ok(transformed.isValid(), `date ${date} , timezone ${timezone} is invalid`)
return transformed
}
}
const LTR_DEFINITIONS = {
daily: {
makeDateFormatter: ({ firstHourOfTheDay = 0, dateCreator } = {}) => {
return date => {
const copy = dateCreator(date)
copy.hour(copy.hour() - firstHourOfTheDay)
return copy.format('YYYY-MM-DD')
}
},
},
weekly: {
makeDateFormatter: ({ firstDayOfWeek = 0 /* relative to timezone week start */, dateCreator } = {}) => {
return date => {
const copy = dateCreator(date)
copy.date(date.date() - firstDayOfWeek)
// warning, the year in term of week may different from YYYY
// since the computation of the first week of a year is timezone dependant
return copy.format('gggg-WW')
}
},
ancestor: 'daily',
},
monthly: {
makeDateFormatter: ({ firstDayOfMonth = 0, dateCreator } = {}) => {
return date => {
const copy = dateCreator(date)
copy.date(copy.date() - firstDayOfMonth)
return copy.format('YYYY-MM')
}
},
ancestor: 'weekly',
},
yearly: {
makeDateFormatter: ({ firstDayOfYear = 0, dateCreator } = {}) => {
return date => {
const copy = dateCreator(date)
copy.date(copy.date() - firstDayOfYear)
return copy.format('YYYY')
}
},
ancestor: 'monthly',
},
}
// returns all entries but the last retention-th
/**
* return the entries too old to be kept
* if multiple entries are i the same time bucket : keep only the most recent one
* if an entry is valid in any of the bucket OR the minRetentionCount : keep it
* if a bucket is completly empty : it does not count as one, thus it may extend the retention
* @returns Array<Backup>
*/
export function getOldEntries(minRetentionCount, entries, { longTermRetention = {}, timezone } = {}) {
assert.strictEqual(
typeof minRetentionCount,
'number',
`minRetentionCount must be a number, got ${JSON.stringify(minRetentionCount)}`
)
assert.strictEqual(
minRetentionCount >= 0,
true,
`minRetentionCount must be a positive number, got ${JSON.stringify(minRetentionCount)}`
)
const dateBuckets = {}
const dateCreator = instantiateTimezonedDateCreator(timezone)
// only check buckets that have a retention set
for (const [duration, { retention, settings }] of Object.entries(longTermRetention)) {
assert.notStrictEqual(LTR_DEFINITIONS[duration], undefined, `Retention of type ${duration} is not defined`)
assert.notStrictEqual(
timezone,
undefined,
`timezone must defined for ltr, got ${JSON.stringify({ minRetentionCount, longTermRetention, timezone })}`
)
assert.strictEqual(
typeof retention,
'number',
`retention of type ${duration} must be a number, got ${JSON.stringify(retention)}`
)
assert.strictEqual(
retention > 0,
true,
`retention of type ${duration} must be a positive number, got ${JSON.stringify(retention)}`
)
dateBuckets[duration] = {
remaining: retention,
lastMatchingBucket: null,
formatter: LTR_DEFINITIONS[duration].makeDateFormatter({ ...settings, dateCreator }),
}
}
const nb = entries.length
const oldEntries = []
for (let i = nb - 1; i >= 0; i--) {
const entry = entries[i]
const entryDate = dateCreator(entry.timestamp)
let shouldBeKept = false
for (const [duration, { remaining, lastMatchingBucket, formatter }] of Object.entries(dateBuckets)) {
if (remaining === 0) {
continue
}
const bucket = formatter(entryDate)
if (lastMatchingBucket !== bucket) {
if (lastMatchingBucket !== null) {
assert.strictEqual(
lastMatchingBucket > bucket,
true,
`entries must be sorted in asc order ${lastMatchingBucket} ${bucket}`
)
}
shouldBeKept = true
dateBuckets[duration].remaining -= 1
dateBuckets[duration].lastMatchingBucket = bucket
}
}
if (i >= nb - minRetentionCount) {
shouldBeKept = true
}
if (!shouldBeKept) {
oldEntries.push(entry)
}
}
// we expect the entries to be in the right order
return oldEntries.reverse()
}

View File

@@ -0,0 +1,282 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { getOldEntries } from './_getOldEntries.mjs'
describe('_getOldEntries() should succeed', () => {
const tests = [
{
args: [
1,
[
{ timestamp: 1, id: 1 },
{ timestamp: 3, id: 2 },
{ timestamp: 2, id: 3 },
],
],
expectedIds: [1, 2],
testLabel: 'should handle number based retention ',
},
{
args: [
0,
[
{ timestamp: +new Date('2024-09-01 00:01:00'), id: 1 }, // too old
{ timestamp: +new Date('2024-09-01 00:00:00'), id: 2 }, // too old
{ timestamp: +new Date('2024-09-02 00:09:00'), id: 3 }, // too old in same day
{ timestamp: +new Date('2024-09-02 00:10:00'), id: 4 },
{ timestamp: +new Date('2024-09-03 00:09:00'), id: 5 },
{ timestamp: +new Date('2024-09-04 00:09:00'), id: 6 }, // too old in same day
{ timestamp: +new Date('2024-09-04 00:10:00'), id: 7 },
],
{
longTermRetention: {
daily: { retention: 3 },
},
timezone: 'Europe/Paris',
},
],
expectedIds: [1, 2, 3, 6],
testLabel: 'should handle day based retention ',
},
{
args: [
0,
[
{ timestamp: +new Date('2024-09-01 00:01:00'), id: 1 }, // week n-3 too old
{ timestamp: +new Date('2024-09-02 00:00:00'), id: 2 }, // week n-3 too old
{ timestamp: +new Date('2024-09-03 00:09:00'), id: 3 }, // week n-2
{ timestamp: +new Date('2024-09-04 00:09:00'), id: 4 }, // week n-2
{ timestamp: +new Date('2024-09-05 00:09:00'), id: 5 }, // week n-2
{ timestamp: +new Date('2024-09-06 00:09:00'), id: 6 }, // week n-2
{ timestamp: +new Date('2024-09-07 00:09:00'), id: 7 }, // week n-2, most recent kept
{ timestamp: +new Date('2024-09-09 00:09:00'), id: 8 }, // week n-1, too old
{ timestamp: +new Date('2024-09-15 00:09:00'), id: 9 }, // week n-1 kept
{ timestamp: +new Date('2024-09-22 00:09:00'), id: 10 }, // week n kept
],
{
longTermRetention: {
weekly: { retention: 3 },
},
timezone: 'Europe/Paris',
},
],
expectedIds: [1, 2, 3, 4, 5, 6, 8],
testLabel: 'should handle week based retention ',
},
{
args: [
0,
[
{ timestamp: +new Date('2024-06-22 00:09:00'), id: 1 }, // too old
{ timestamp: +new Date('2024-07-31 00:09:00'), id: 2 }, // first of july
{ timestamp: +new Date('2024-08-01 00:09:00'), id: 3 }, // older of august
{ timestamp: +new Date('2024-08-05 00:09:00'), id: 4 }, // older of august
{ timestamp: +new Date('2024-08-07 00:09:00'), id: 5 }, // most recent of august
{ timestamp: +new Date('2024-09-09 00:09:00'), id: 6 }, // older of september
{ timestamp: +new Date('2024-09-15 00:09:00'), id: 7 }, // older of september
{ timestamp: +new Date('2024-09-22 00:09:00'), id: 8 }, // most recent of september
],
{
longTermRetention: {
monthly: { retention: 3 },
},
timezone: 'Europe/Paris',
},
],
expectedIds: [1, 3, 4, 6, 7],
testLabel: 'should handle month based retention',
},
{
args: [
0,
[
{ timestamp: +new Date('2023-05-18 00:09:00'), id: 1 }, // too old
{ timestamp: +new Date('2024-06-15 00:09:00'), id: 2 }, // too old in same year
{ timestamp: +new Date('2024-07-04 00:09:00'), id: 3 }, // too old
{ timestamp: +new Date('2024-08-12 00:09:00'), id: 4 }, // too old
{ timestamp: +new Date('2024-09-05 00:09:00'), id: 5 }, // too old
{ timestamp: +new Date('2024-10-02 00:09:00'), id: 6 }, // new month,
{ timestamp: +new Date('2024-11-01 00:09:00'), id: 7 }, // new month , week reached retention
{ timestamp: +new Date('2024-12-17 00:09:00'), id: 8 }, // new week
{ timestamp: +new Date('2024-12-24 00:09:00'), id: 9 }, // new week/month / year daily reach retention
{ timestamp: +new Date('2025-01-01 00:09:00'), id: 10 }, // same day/week/month/year
{ timestamp: +new Date('2025-01-01 00:10:00'), id: 11 }, // new day / week / month
{ timestamp: +new Date('2025-12-31 00:09:00'), id: 12 }, // same day/week/month/year
{ timestamp: +new Date('2025-12-31 00:09:00'), id: 13 }, // new month /year
],
{
longTermRetention: {
daily: { retention: 2 },
weekly: { retention: 4 },
monthly: { retention: 5 },
yearly: { retention: 2 },
},
timezone: 'Europe/Paris', // use a time zone here because week definition is timezone bound
},
],
expectedIds: [1, 2, 3, 4, 5, 10, 12],
testLabel: 'complete test ',
},
{
args: [
0,
[
{ timestamp: +new Date('2024-09-01 00:01:00'), id: 1 }, // thrid day too old
{ timestamp: +new Date('2024-09-01 00:10:00'), id: 2 }, // thrid day
{ timestamp: +new Date('2024-09-01 00:20:00'), id: 3 }, // second day, too old
{ timestamp: +new Date('2024-09-02 00:22:00'), id: 4 }, // second day too old
{ timestamp: +new Date('2024-09-03 00:20:00'), id: 5 }, // second day in NZ
{ timestamp: +new Date('2024-09-04 00:09:00'), id: 6 }, // same day in NZ
{ timestamp: +new Date('2024-09-04 00:10:00'), id: 7 }, // most recent
],
{
longTermRetention: {
daily: { retention: 3 },
},
timezone: 'Pacific/Auckland', // GMT +6
},
],
expectedIds: [1, 2, 3, 6],
testLabel: 'should handle timezone ',
},
]
for (const { args, expectedIds, testLabel } of tests) {
it(testLabel, () => {
const oldEntries = getOldEntries.apply(null, args)
assert.strictEqual(
oldEntries.length,
expectedIds.length,
`different length , ${JSON.stringify({ oldEntries, expectedIds })}`
)
for (let i = 0; i < expectedIds.length; i++) {
assert.strictEqual(
oldEntries[i].id,
expectedIds[i],
`different id , ${JSON.stringify({ i, expectedIds, oldEntries })}`
)
}
})
}
})
describe('_getOldEntries() should fail when called incorrectly', () => {
const tests = [
{
args: [
-1,
[
{ timestamp: 1, id: 1 },
{ timestamp: 3, id: 2 },
{ timestamp: 2, id: 3 },
],
],
testLabel: 'negative min retention ',
},
{
args: [
{ not: 'a number' },
[
{ timestamp: 1, id: 1 },
{ timestamp: 3, id: 2 },
{ timestamp: 2, id: 3 },
],
],
testLabel: 'non number min retention ',
},
{
args: [
4,
[
{ timestamp: 1, id: 1 },
{ timestamp: 2, id: 2 },
{ timestamp: 3, id: 3 },
],
{
longTermRetention: {
invalidType: { retention: 3 },
},
timezone: 'Europe/Paris',
},
],
testLabel: 'invalid type ltr ',
},
{
args: [
3,
[
{ timestamp: 1, id: 1 },
{ timestamp: 3, id: 2 },
{ timestamp: 2, id: 3 },
],
{
longTermRetention: {
daily: { retention: 0 },
},
timezone: 'Europe/Paris',
},
],
testLabel: 'zero retention ltr ',
},
{
args: [
1,
[
{ timestamp: 1, id: 1 },
{ timestamp: 2, id: 2 },
{ timestamp: 3, id: 3 },
],
{
longTermRetention: {
daily: { retention: 'not a number' },
},
timezone: 'Europe/Paris',
},
],
testLabel: 'no timezone ltr ',
},
{
args: [
5,
[
{ timestamp: +new Date('2024-09-01'), id: 1 },
{ timestamp: +new Date('2024-09-03'), id: 3 },
{ timestamp: +new Date('2024-09-02'), id: 2 },
],
{
longTermRetention: {
daily: { retention: 5 },
},
timezone: 'Europe/Paris',
},
],
testLabel: 'unsorted entries ',
},
{
args: [
5,
[
{ timestamp: 'notadate', id: 1 },
{ timestamp: 'still not a date, but after', id: 3 },
],
{
longTermRetention: {
daily: { retention: 5 },
},
timezone: 'Europe/Paris',
},
],
testLabel: 'broken date ',
},
]
for (const { args, testLabel } of tests) {
it(testLabel, () => {
assert.throws(() => getOldEntries.apply(null, args), { code: 'ERR_ASSERTION' })
})
}
})

View File

@@ -1,16 +1,23 @@
import Disposable from 'promise-toolbox/Disposable'
import { createLogger } from '@xen-orchestra/log'
import { join } from 'node:path'
import { mkdir, rmdir } from 'node:fs/promises'
import { tmpdir } from 'os'
const { debug } = createLogger('xo:backups:getTmpDir')
const MAX_ATTEMPTS = 3
export async function getTmpDir() {
for (let i = 0; true; ++i) {
const path = join(tmpdir(), Math.random().toString(36).slice(2))
try {
debug('creating directory', { path })
await mkdir(path)
return new Disposable(() => rmdir(path), path)
return new Disposable(() => {
debug('removing directory', { path })
return rmdir(path)
}, path)
} catch (error) {
if (i === MAX_ATTEMPTS) {
throw error

View File

@@ -1,52 +1,23 @@
import groupBy from 'lodash/groupBy.js'
import ignoreErrors from 'promise-toolbox/ignoreErrors'
import omit from 'lodash/omit.js'
import { asyncMap } from '@xen-orchestra/async-map'
import { CancelToken } from 'promise-toolbox'
import { compareVersions } from 'compare-versions'
import { createVhdStreamWithLength } from 'vhd-lib'
import { defer } from 'golike-defer'
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'
import { BASE_DELTA_VDI, COPY_OF, VM_UUID } from './_otherConfig.mjs'
const ensureArray = value => (value === undefined ? [] : Array.isArray(value) ? value : [value])
export async function exportIncrementalVm(
vm,
baseVm,
{
cancelToken = CancelToken.none,
// Sets of UUIDs of VDIs that must be exported as full.
fullVdisRequired = new Set(),
disableBaseTags = false,
nbdConcurrency = 1,
preferNbd,
} = {}
baseVdis = {},
{ cancelToken = CancelToken.none, nbdConcurrency = 1, preferNbd } = {}
) {
// refs of VM's VDIs → base's VDIs.
const baseVdis = {}
baseVm &&
baseVm.$VBDs.forEach(vbd => {
let vdi, snapshotOf
if ((vdi = vbd.$VDI) && (snapshotOf = vdi.$snapshot_of) && !fullVdisRequired.has(snapshotOf.uuid)) {
baseVdis[vdi.snapshot_of] = vdi
}
})
const streams = {}
const vdis = {}
@@ -67,25 +38,46 @@ export async function exportIncrementalVm(
}
// Look for a snapshot of this vdi in the base VM.
const baseVdi = baseVdis[vdi.snapshot_of]
const baseVdi = baseVdis[vdi.$snapshot_of.uuid]
vdis[vdiRef] = {
...vdi,
other_config: {
...vdi.other_config,
[TAG_BASE_DELTA]: baseVdi && !disableBaseTags ? baseVdi.uuid : undefined,
[BASE_DELTA_VDI]: baseVdi?.uuid,
[VM_UUID]:
vm.$snapshot_of?.uuid ?? // vm is a snapshot
vm.uuid, // vm is a not snapshot
},
$snapshot_of$uuid: vdi.$snapshot_of?.uuid,
$SR$uuid: vdi.$SR.uuid,
}
streams[`${vdiRef}.vhd`] = await vdi.$exportContent({
baseRef: baseVdi?.$ref,
cancelToken,
format: 'vhd',
nbdConcurrency,
preferNbd,
})
try {
streams[`${vdiRef}.vhd`] = await vdi.$exportContent({
baseRef: baseVdi?.$ref,
cancelToken,
format: 'vhd',
nbdConcurrency,
preferNbd,
})
} catch (err) {
if (err.code === 'VDI_CANT_DO_DELTA') {
// fall back to a base
Task.info(`Can't do delta, will try to get a full stream`, { vdi })
streams[`${vdiRef}.vhd`] = await vdi.$exportContent({
cancelToken,
format: 'vhd',
nbdConcurrency,
preferNbd,
})
// only warn if the fall back succeed
Task.warning(`Can't do delta with this vdi, transfer will be a full`, {
vdi,
})
} else {
throw err
}
}
})
const suspendVdi = vm.$suspend_VDI
@@ -112,6 +104,18 @@ export async function exportIncrementalVm(
}
})
const vtpms = await Promise.all(
vm.$VTPMs.map(async vtpm => {
let content
try {
content = await vm.$xapi.call('VTPM.get_contents', vtpm.$ref)
} catch (err) {
console.error(err)
}
return content
})
)
return Object.defineProperty(
{
version: '1.1.0',
@@ -120,14 +124,8 @@ export async function exportIncrementalVm(
vifs,
vm: {
...vm,
other_config:
baseVm && !disableBaseTags
? {
...vm.other_config,
[TAG_BASE_DELTA]: baseVm.uuid,
}
: omit(vm.other_config, TAG_BASE_DELTA),
},
vtpms,
},
'streams',
{
@@ -205,7 +203,7 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
newVdi = await xapi.getRecord('VDI', await vdi.baseVdi.$clone())
$defer.onFailure(() => newVdi.$destroy())
await newVdi.update_other_config(TAG_COPY_SRC, vdi.uuid)
await newVdi.update_other_config(COPY_OF, vdi.uuid)
if (vdi.virtual_size > newVdi.virtual_size) {
await newVdi.$callAsync('resize', vdi.virtual_size)
}
@@ -259,9 +257,6 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
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)
@@ -299,6 +294,12 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
}
}),
])
// recreate VTPMs
await Promise.all(
(incrementalVm.vtpms ?? []).map(async contents => {
await xapi.VTPM_create({ VM: vmRef, contents })
})
)
await Promise.all([
incrementalVm.vm.ha_always_run && xapi.setField('VM', vmRef, 'ha_always_run', true),

View File

@@ -0,0 +1,165 @@
import { formatDateTime } from '@xen-orchestra/xapi'
import assert from 'node:assert/strict'
// in `other_config` of an incrementally replicated VM or VDI
// contains the UUID of the object used as a base for an incremental export
// used to search for the replica of the base before applying a incremental replication
export const BASE_DELTA_VDI = 'xo:base_delta_vdi'
// 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 REPLICATED_TO_SR_UUID = 'xo:backup:sr'
// in other_config of VDIs of an incrementally replicated VM
// contains the UUID of the source exported object (snapshot or VM)
export const COPY_OF = 'xo:copy_of'
export const DATETIME = 'xo:backup:datetime'
export const JOB_ID = 'xo:backup:job'
export const SCHEDULE_ID = 'xo:backup:schedule'
// contains the number of delta in a chain, stored as a string
export const DELTA_CHAIN_LENGTH = 'xo:backup:deltaChainLength'
// contains the string true if this vdi has been exported successfully
export const EXPORTED_SUCCESSFULLY = 'xo:backup:exported'
// the VM ( not the snapshot) uuid
export const VM_UUID = 'xo:backup:vm'
async function listVdiRefs(xapi, vmRef) {
return xapi.VM_getDisks(vmRef)
}
async function applyToVmAndVdis(xapi, vmRef, fn) {
const vdiRefs = await listVdiRefs(xapi, vmRef)
return Promise.all([fn('VM', vmRef), ...vdiRefs.map(vdiRef => fn('VDI', vdiRef))])
}
async function getDeltaChainLength(xapi, type, ref) {
const otherConfig = await xapi.getField(type, ref, 'other_config')
return Number(otherConfig[DELTA_CHAIN_LENGTH] ?? 0)
}
/**
* set the delta chain lenght ( number of delta since last base backup) to a VM and its associated VDIs
*
* @param {Xapi} xapi
* @param {String} vmRef
* @param {Number} length
* @returns {Promise}
*/
export async function setVmDeltaChainLength(xapi, vmRef, length) {
return applyToVmAndVdis(xapi, vmRef, async (type, ref) => {
await xapi.setFieldEntry(type, ref, 'other_config', DELTA_CHAIN_LENGTH, String(length))
})
}
/**
* Compute the delta chain length of a VM and its associated VDIs
* if there is a discrependcy, use, the highest value
* @param {Xapi} xapi
* @param {String} vmRef
* @returns {Promise}
*/
export async function getVmDeltaChainLength(xapi, vmRef) {
const lengths = await applyToVmAndVdis(xapi, vmRef, async (type, ref) => getDeltaChainLength(xapi, type, ref))
return Math.max(...lengths)
}
/**
*
* Reset the other_config field of a VM and its VDIs
*
* @param {Xapi} xapi
* @param {String} vmRef
* @returns {Promise}
*/
export function resetVmOtherConfig(xapi, vmRef) {
return applyToVmAndVdis(xapi, vmRef, (type, ref) => {
return xapi.setFieldEntries(type, ref, 'other_config', {
[DATETIME]: null,
[DELTA_CHAIN_LENGTH]: null,
[EXPORTED_SUCCESSFULLY]: null,
[JOB_ID]: null,
[SCHEDULE_ID]: null,
[VM_UUID]: null,
// REPLICATED_TO_SR_UUID is not reset since we can replicate a replication
})
})
}
/**
*
* used to ensure compatibiliy with the previous snapshots that were having the config stored only into VM
*
* @param {Xapi} xapi
* @param {String} vmRef
* @returns {Promise}
*/
export async function populateVdisOtherConfig(xapi, vmRef) {
const otherConfig = await xapi.getField('VM', vmRef, 'other_config')
const {
[DATETIME]: datetime,
[DELTA_CHAIN_LENGTH]: chainLength,
[EXPORTED_SUCCESSFULLY]: successfully,
[JOB_ID]: jobId,
[REPLICATED_TO_SR_UUID]: replicatedTo,
[SCHEDULE_ID]: scheduleId,
[VM_UUID]: vmUuid,
} = otherConfig
return applyToVmAndVdis(xapi, vmRef, (type, ref) =>
xapi.setFieldEntries(type, ref, 'other_config', {
[DATETIME]: datetime,
[DELTA_CHAIN_LENGTH]: chainLength,
[EXPORTED_SUCCESSFULLY]: successfully,
[JOB_ID]: jobId,
[REPLICATED_TO_SR_UUID]: replicatedTo,
[SCHEDULE_ID]: scheduleId,
[VM_UUID]: vmUuid,
})
)
}
/**
*
* set the other_config key related to a backup of a VM and its associated VDIs
*
* @param {Xapi} xapi
* @param {String} vmRef
* @param {*} settings
* @returns {PRomise}
*/
export async function setVmOtherConfig(xapi, vmRef, { timestamp, jobId, scheduleId, vmUuid, srUuid = null, ...other }) {
assert.notEqual(timestamp, undefined)
assert.notEqual(jobId, undefined)
assert.notEqual(scheduleId, undefined)
assert.notEqual(vmUuid, undefined)
// srUuid is nullish for backup
assert.equal(Object.keys(other).length, 0)
return applyToVmAndVdis(xapi, vmRef, (type, ref) =>
xapi.setFieldEntries(type, ref, 'other_config', {
[REPLICATED_TO_SR_UUID]: srUuid,
[DATETIME]: formatDateTime(timestamp),
[JOB_ID]: jobId,
[SCHEDULE_ID]: scheduleId,
[VM_UUID]: vmUuid,
})
)
}
/**
*
* mark the export of he VM and its VDIs as successfull
*
* @param {Xapi} xapi
* @param {String} vmRef
* @returns {Promise}
*/
export async function markExportSuccessfull(xapi, vmRef) {
return applyToVmAndVdis(xapi, vmRef, (type, ref) =>
xapi.setFieldEntry(type, ref, 'other_config', EXPORTED_SUCCESSFULLY, 'true')
)
}

View File

@@ -15,6 +15,7 @@ const noop = Function.prototype
const DEFAULT_XAPI_VM_SETTINGS = {
bypassVdiChainsCheck: false,
cbtDestroySnapshotData: false,
checkpointSnapshot: false,
concurrency: 2,
copyRetention: 0,

View File

@@ -29,6 +29,10 @@ export function forkStreamUnpipe(source) {
if (source.forks === 0) {
debug('no more forks, destroying original stream')
source.destroy(new Error('no more consumers for this stream'))
} else {
// a combination of stream.unpipe, onReadable and onData may stall stream here
// force it to flow again since we're piping it
source.resume()
}
})
return fork

View File

@@ -2,39 +2,39 @@ import { AbstractRemote } from './_AbstractRemote.mjs'
import { FullRemoteWriter } from '../_writers/FullRemoteWriter.mjs'
import { forkStreamUnpipe } from '../_forkStreamUnpipe.mjs'
import { watchStreamSize } from '../../_watchStreamSize.mjs'
import { Task } from '../../Task.mjs'
export const FullRemote = class FullRemoteVmBackupRunner extends AbstractRemote {
_getRemoteWriter() {
return FullRemoteWriter
}
_filterTransferList(transferList) {
return transferList.filter(this._filterPredicate)
}
async _run() {
const transferList = await this._computeTransferList(({ mode }) => mode === 'full')
if (transferList.length > 0) {
for (const metadata of transferList) {
const stream = await this._sourceRemoteAdapter.readFullVmBackup(metadata)
const sizeContainer = watchStreamSize(stream)
for (const metadata of transferList) {
const stream = await this._sourceRemoteAdapter.readFullVmBackup(metadata)
const sizeContainer = watchStreamSize(stream)
// @todo shouldn't transfer backup if it will be deleted by retention policy (higher retention on source than destination)
await this._callWriters(
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,
sizeContainer,
}),
'writer.run()'
)
// for healthcheck
this._tags = metadata.vm.tags
}
} else {
Task.info('No new data to upload for this VM')
// @todo shouldn't transfer backup if it will be deleted by retention policy (higher retention on source than destination)
await this._callWriters(
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,
sizeContainer,
}),
'writer.run()'
)
// for healthcheck
this._tags = metadata.vm.tags
}
}
}

View File

@@ -1,3 +1,5 @@
import { createLogger } from '@xen-orchestra/log'
import { asyncEach } from '@vates/async-each'
import assert from 'node:assert'
import * as UUID from 'uuid'
@@ -7,15 +9,25 @@ import mapValues from 'lodash/mapValues.js'
import { AbstractRemote } from './_AbstractRemote.mjs'
import { forkDeltaExport } from './_forkDeltaExport.mjs'
import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs'
import { Task } from '../../Task.mjs'
import { Disposable } from 'promise-toolbox'
import { openVhd } from 'vhd-lib'
import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
const { warn } = createLogger('xo:backups:Incrementalremote')
class IncrementalRemoteVmBackupRunner extends AbstractRemote {
_getRemoteWriter() {
return IncrementalRemoteWriter
}
// we'll transfer the full list if at least one backup should be transfered
// to ensure we don't cut the delta chain
_filterTransferList(transferList) {
if (transferList.some(vmBackupMetadata => this._filterPredicate(vmBackupMetadata))) {
return transferList
}
return []
}
async _selectBaseVm(metadata) {
// for each disk , get the parent
const baseUuidToSrcVdi = new Map()
@@ -37,11 +49,7 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
})
const presentBaseVdis = new Map(baseUuidToSrcVdi)
await this._callWriters(
writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis),
'writer.checkBaseVdis()',
false
)
await this._callWriters(writer => writer.checkBaseVdis(presentBaseVdis), 'writer.checkBaseVdis()', false)
// check if the parent vdi are present in all the remotes
baseUuidToSrcVdi.forEach((srcVdiUuid, baseUuid) => {
if (!presentBaseVdis.has(baseUuid)) {
@@ -53,50 +61,58 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
async _run() {
const transferList = await this._computeTransferList(({ mode }) => mode === 'delta')
if (transferList.length > 0) {
for (const metadata of transferList) {
assert.strictEqual(metadata.mode, 'delta')
await this._selectBaseVm(metadata)
await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
const incrementalExport = await this._sourceRemoteAdapter.readIncrementalVmBackup(metadata, undefined, {
useChain: false,
for (const metadata of transferList) {
assert.strictEqual(metadata.mode, 'delta')
const incrementalExport = await this._sourceRemoteAdapter.readIncrementalVmBackup(metadata, undefined, {
useChain: false,
})
// don't trust metadata too much
// recompute if it's a base backup
// recompute if disks are differencing or not
const isVhdDifferencing = {}
await asyncEach(Object.entries(incrementalExport.streams), async ([key, stream]) => {
isVhdDifferencing[key] = await isVhdDifferencingDisk(stream)
})
const hasDifferencingDisk = Object.values(isVhdDifferencing).includes(true)
if (metadata.isBase === hasDifferencingDisk) {
warn(`Metadata isBase and real disk value are different`, {
metadataIsBase: metadata.isBase,
diskIsBase: !hasDifferencingDisk,
isVhdDifferencing,
})
const isVhdDifferencing = {}
await asyncEach(Object.entries(incrementalExport.streams), async ([key, stream]) => {
isVhdDifferencing[key] = await isVhdDifferencingDisk(stream)
})
incrementalExport.streams = mapValues(incrementalExport.streams, this._throttleStream)
await this._callWriters(
writer =>
writer.transfer({
deltaExport: forkDeltaExport(incrementalExport),
isVhdDifferencing,
timestamp: metadata.timestamp,
vm: metadata.vm,
vmSnapshot: metadata.vmSnapshot,
}),
'writer.transfer()'
)
// this will update parent name with the needed alias
await this._callWriters(
writer =>
writer.updateUuidAndChain({
isVhdDifferencing,
timestamp: metadata.timestamp,
vdis: incrementalExport.vdis,
}),
'writer.updateUuidAndChain()'
)
await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
// for healthcheck
this._tags = metadata.vm.tags
}
} else {
Task.info('No new data to upload for this VM')
metadata.isBase = !hasDifferencingDisk
metadata.isVhdDifferencing = isVhdDifferencing
await this._selectBaseVm(metadata)
await this._callWriters(writer => writer.prepare({ isBase: metadata.isBase }), 'writer.prepare()')
incrementalExport.streams = mapValues(incrementalExport.streams, this._throttleStream)
await this._callWriters(
writer =>
writer.transfer({
deltaExport: forkDeltaExport(incrementalExport),
isVhdDifferencing,
timestamp: metadata.timestamp,
vm: metadata.vm,
vmSnapshot: metadata.vmSnapshot,
}),
'writer.transfer()'
)
// this will update parent name with the needed alias
await this._callWriters(
writer =>
writer.updateUuidAndChain({
isVhdDifferencing,
timestamp: metadata.timestamp,
vdis: incrementalExport.vdis,
}),
'writer.updateUuidAndChain()'
)
await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
// for healthcheck
this._tags = metadata.vm.tags
}
}
}

View File

@@ -1,8 +1,6 @@
import { asyncEach } from '@vates/async-each'
import { asyncMap } from '@xen-orchestra/async-map'
import { createLogger } from '@xen-orchestra/log'
import { pipeline } from 'node:stream'
import findLast from 'lodash/findLast.js'
import isVhdDifferencingDisk from 'vhd-lib/isVhdDifferencingDisk.js'
import keyBy from 'lodash/keyBy.js'
import mapValues from 'lodash/mapValues.js'
@@ -15,9 +13,15 @@ import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs
import { IncrementalXapiWriter } from '../_writers/IncrementalXapiWriter.mjs'
import { Task } from '../../Task.mjs'
import { watchStreamSize } from '../../_watchStreamSize.mjs'
import {
DATETIME,
DELTA_CHAIN_LENGTH,
EXPORTED_SUCCESSFULLY,
setVmDeltaChainLength,
markExportSuccessfull,
} from '../../_otherConfig.mjs'
const { debug } = createLogger('xo:backups:IncrementalXapiVmBackup')
const noop = Function.prototype
export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXapi {
@@ -30,17 +34,14 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
}
async _copy() {
const baseVm = this._baseVm
const baseVdis = this._baseVdis
const vm = this._vm
const exportedVm = this._exportedVm
const fullVdisRequired = this._fullVdisRequired
const isFull = fullVdisRequired === undefined || fullVdisRequired.size !== 0
const isFull = Object.values(baseVdis).length === 0 || Object.values(baseVdis).some(_ => !_)
await this._callWriters(writer => writer.prepare({ isFull }), 'writer.prepare()')
const deltaExport = await exportIncrementalVm(exportedVm, baseVm, {
fullVdisRequired,
const deltaExport = await exportIncrementalVm(exportedVm, baseVdis, {
nbdConcurrency: this._settings.nbdConcurrency,
preferNbd: this._settings.preferNbd,
})
@@ -90,18 +91,15 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
'writer.updateUuidAndChain()'
)
this._baseVm = exportedVm
if (baseVm !== undefined) {
await exportedVm.update_other_config(
'xo:backup:deltaChainLength',
String(+(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1)
)
if (isFull) {
await setVmDeltaChainLength(this._xapi, exportedVm.$ref, 0)
} else {
await setVmDeltaChainLength(this._xapi, exportedVm.$ref, (this._deltaChainLength ?? 0) + 1)
}
// not the case if offlineBackup
if (exportedVm.is_a_snapshot) {
await exportedVm.update_other_config('xo:backup:exported', 'true')
await markExportSuccessfull(this._xapi, exportedVm.$ref)
}
const size = Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0)
@@ -119,43 +117,67 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
async _selectBaseVm() {
const xapi = this._xapi
let baseVm = findLast(this._jobSnapshots, _ => 'xo:backup:exported' in _.other_config)
if (baseVm === undefined) {
debug('no base VM found')
return
// filter _jobSnapshotVdis to have only the last successfully exported vdi
// compute delta chain length
// fill baseUuidToSrcVdi with this data
const exportedVdis = this._jobSnapshotVdis.filter(
_ => EXPORTED_SUCCESSFULLY in _.other_config && DATETIME in _.other_config
)
let lastSuccessfullBackup
let lastExportedVdis = []
for (const exportedVdi of exportedVdis) {
if (lastSuccessfullBackup === undefined || lastSuccessfullBackup < exportedVdi.other_config[DATETIME]) {
lastExportedVdis = []
lastSuccessfullBackup = exportedVdi.other_config[DATETIME]
}
if (lastSuccessfullBackup === exportedVdi.other_config[DATETIME]) {
lastExportedVdis.push(exportedVdi)
}
}
this._baseVdis = {}
if (lastExportedVdis.length === 0) {
debug('no base VDIS found', {
jobLength: this._jobSnapshotVdis.length,
lastSuccessfullBackup,
exportedLength: exportedVdis.length,
})
return
}
const deltaChainLength = Math.max(
...lastExportedVdis.map(({ other_config }) => Number(other_config[DELTA_CHAIN_LENGTH] ?? 0))
)
const fullInterval = this._settings.fullInterval
const deltaChainLength = +(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1
if (!(fullInterval === 0 || fullInterval > deltaChainLength)) {
debug('not using base VM becaust fullInterval reached')
if (fullInterval !== 0 && fullInterval <= deltaChainLength + 1) {
debug('not using base VM because fullInterval reached', {
fullInterval,
deltaChainLength,
eq: fullInterval < deltaChainLength + 1,
dc1: deltaChainLength + 1,
})
return
}
const srcVdis = keyBy(await xapi.getRecords('VDI', await this._vm.$getDisks()), '$ref')
// resolve full record
baseVm = await xapi.getRecord('VM', baseVm.$ref)
const baseUuidToSrcVdi = new Map()
await asyncMap(await baseVm.$getDisks(), async baseRef => {
const [baseUuid, snapshotOf] = await Promise.all([
xapi.getField('VDI', baseRef, 'uuid'),
xapi.getField('VDI', baseRef, 'snapshot_of'),
])
const baseUuidToSrcVdiUuid = new Map()
for (const lastExportedVdi of lastExportedVdis) {
const baseUuid = lastExportedVdi.uuid
const snapshotOf = lastExportedVdi.snapshot_of
const srcVdi = srcVdis[snapshotOf]
if (srcVdi !== undefined) {
baseUuidToSrcVdi.set(baseUuid, srcVdi.uuid)
baseUuidToSrcVdiUuid.set(baseUuid, srcVdi.uuid)
} else {
debug('ignore snapshot VDI because no longer present on VM', {
vdi: baseUuid,
})
}
})
}
const presentBaseVdis = new Map(baseUuidToSrcVdi)
const presentBaseVdis = new Map(baseUuidToSrcVdiUuid)
await this._callWriters(
writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis, baseVm),
writer => presentBaseVdis.size !== 0 && writer.checkBaseVdis(presentBaseVdis),
'writer.checkBaseVdis()',
false
)
@@ -164,24 +186,22 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
debug('no base VM found')
return
}
const fullVdisRequired = new Set()
baseUuidToSrcVdi.forEach((srcVdiUuid, baseUuid) => {
baseUuidToSrcVdiUuid.forEach((srcVdiUuid, baseUuid) => {
if (presentBaseVdis.has(baseUuid)) {
debug('found base VDI', {
base: baseUuid,
vdi: srcVdiUuid,
})
this._baseVdis[srcVdiUuid] = lastExportedVdis.find(vdi => vdi.uuid === baseUuid)
} else {
debug('missing base VDI', {
base: baseUuid,
vdi: srcVdiUuid,
})
fullVdisRequired.add(srcVdiUuid)
// this will force a full for this vdi
this._baseVdis[srcVdiUuid] = undefined
}
})
this._baseVm = baseVm
this._fullVdisRequired = fullVdisRequired
this._deltaChainLength = deltaChainLength
}
}

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