Compare commits

..

212 Commits

Author SHA1 Message Date
Mathieu
ae087a6539 feat(xo-lite/ObjectStatus): use ProgressCircle component (#6207) 2022-04-28 09:59:59 +02:00
Mathieu
4db93f8ced feat(lite/ProgressCircle): creation of the component (#6128) 2022-04-26 17:18:41 +02:00
Rajaa.BARHTAOUI
e5c737cba7 feat(lite/pool): dashboard Status card (#6112) 2022-04-21 16:22:49 +02:00
Rajaa.BARHTAOUI
9f0f38ef94 feat(lite): add Tabs component (#6096) 2022-04-08 10:13:26 +02:00
Rajaa.BARHTAOUI
d76996b1d5 feat(lite/tree): auto-reveal active VM (#6088) 2022-04-07 15:43:48 +02:00
Rajaa.BARHTAOUI
3b77897692 feat(lite): update PanelHeader to match mockup (#6111) 2022-03-23 16:57:10 +01:00
Mathieu
d4ed555abd feat(lite/console): handle case where host sends self-signed certificate (#6104) 2022-02-18 15:36:18 +01:00
Mathieu
97d77c0aa5 fix(lite/Select): fix controlled input with undefined value (#6106)
Fix `MUI: The 'value' prop must be an array when using the 'Select' component with 'multiple'.` in case we toggle the `multiple` prop `false -> true` with a controlled input initialized with undefined value.
2022-02-17 15:45:17 +01:00
Rajaa.BARHTAOUI
a9ad0ec455 feat(lite): change pool icon (#6110) 2022-02-03 09:34:25 +01:00
Rajaa.BARHTAOUI
78ec008c26 feat(lite/Icon): possibility to use colors of theme's palette (#6114) 2022-02-02 14:35:56 +01:00
Pierre Donias
2d71bef5d8 chore(lite): handle asset relative paths in prod and dev environment (#6109)
- Make asset URLs relative
- Add a base tag in production to make all the URLs relative to
  lite.xen-orchestra.com (change made directly on the server)
2022-01-31 11:18:12 +01:00
Mathieu
3ec7c61987 fix(lite/tree): navigate with keyboard into tree-view (#6069) 2022-01-27 11:27:22 +01:00
Mathieu
526c2001d3 fix(lite/console): console cropped on window vertical resize (#6077)
Introduced by f3d4e40c6d
2022-01-20 14:11:38 +01:00
Florent BEAUCHAMP
f3d4e40c6d feat(xo-lite): implement Title bar component (#5960) 2021-12-15 17:07:56 +01:00
Mathieu
ac8f93fb0e feat(lite/ActionButton): creation of the ActionButton component (#6021) 2021-12-09 16:34:37 +01:00
Rajaa.BARHTAOUI
d2fbc1b573 fix(lite/Tree,TreeView): fix type errors (#6011) 2021-12-09 10:56:40 +01:00
Rajaa.BARHTAOUI
c5670a047f feat(lite): sort hosts by name_label (#6046) 2021-12-08 16:45:48 +01:00
Mathieu
e9472889f2 feat(xo-lite/Modal): creation of the Modal component (#5775) 2021-12-02 10:07:00 +01:00
Rajaa.BARHTAOUI
9bec4b571c fix(lite/tree): clicking next to VM name should work (#6005) 2021-11-30 16:18:53 +01:00
Rajaa.BARHTAOUI
b56cc96e37 feat(lite): sort VMs by name_label (#5989) 2021-11-30 15:41:55 +01:00
Rajaa.BARHTAOUI
011164f16c feat(lite/tree): highlight selected node (#5939)
Inspired by https://mui.com/components/tree-view/#contentcomponent-prop
2021-11-16 17:05:53 +01:00
Mathieu
b9a9471408 feat(xo-lite): creation of Select component (#5878) 2021-11-10 10:33:33 +01:00
Florent BEAUCHAMP
9abd1429a2 feat(lint-staged): apply validation rules to ts and tsx files (#5985)
See https://github.com/vatesfr/xen-orchestra/pull/5960#discussion_r738332567
2021-11-08 15:05:07 +01:00
Mathieu
7f656973de feat(xo-lite/Input): creation of Input component (#5975) 2021-11-05 16:30:56 +01:00
Mathieu
5e0766fcb1 feat(xo-lite/Button): replace styled component to mui-button (#5964) 2021-11-05 16:22:20 +01:00
Mathieu
2dc5c0e161 fix(lite): console scaling (#5933) 2021-10-15 17:34:55 +02:00
Pierre Donias
d0730d05fd chore(lite): update xen-api (#5945) 2021-10-12 15:52:03 +02:00
Pierre Donias
8fe3a439fc chore(lite): uninstall @material-ui/core (#5928)
Use @mui/material instead
2021-10-04 14:27:05 +02:00
Pierre Donias
12c7113662 fix(lite): use absolute assets URLs 2021-09-30 17:29:05 +02:00
Pierre Donias
36be46b073 feat(lite): add UI template and prepare for initial release (#5922) 2021-09-30 15:03:28 +02:00
Rajaa.BARHTAOUI
25ef579df5 feat(lite): Tree view (#5804) 2021-09-30 15:02:59 +02:00
Mathieu
cbbb07d389 feat(lite): list pool updates (#5794) 2021-09-30 15:02:58 +02:00
Pierre Donias
96df84c9d8 fix(lite): fix credentials error type (#5845) 2021-09-30 15:02:58 +02:00
Pierre Donias
17c4b5cbe7 feat(lite): show version in UI (#5844) 2021-09-30 15:02:58 +02:00
Mathieu
cf642cd720 feat(xo-lite/Pool): display IP, DNS, gateway from management PIF (#5771) 2021-09-30 15:02:58 +02:00
Mathieu
047f3a9b4c feat(xo-lite): use styled-components for console component (#5827) 2021-09-30 15:02:58 +02:00
Mathieu
b0f85e0380 feat(xo-lite/Console): handle disconnection and halted VMs (#5728) 2021-09-30 15:02:58 +02:00
Mathieu
7aa518b43c feat(xo-lite): wrapper for FormattedMessage (#5803) 2021-09-30 15:02:58 +02:00
Pierre Donias
d187d6aeeb chore(lite): allow explicit any 2021-09-30 15:02:58 +02:00
Pierre Donias
289dce3876 chore(lite/types): handle async computed 2021-09-30 15:02:58 +02:00
Pierre Donias
930afea1a1 feat(lite): signin page (#5787) 2021-09-30 15:02:58 +02:00
Mathieu
3801fa9134 feat(lite/Console): add CtrlAltDel button (#5722) 2021-09-30 15:02:58 +02:00
Julien Fontanet
ae211046b8 fix(lite): don't let Babel transpile import/export 2021-09-30 15:02:58 +02:00
Julien Fontanet
87ce9ff63a fix(lite): blacklist dns module
It's used by `xen-api` but should be fine as long as `reverseHostIpAddresses` is not enable.
2021-09-30 15:02:58 +02:00
Pierre Donias
131c6321be chore(xo-lite): fix config and xen-api 2021-09-30 15:02:58 +02:00
Pierre Donias
6abcce498f feat(xo-lite): style guide (#5764) 2021-09-30 15:02:21 +02:00
Pierre Donias
9c38f5b327 feat(xo-lite): styled-components 2021-09-30 15:02:00 +02:00
Pierre Donias
14720d4cbf chore(xo-lite): move all dependencies to devDependencies 2021-09-30 15:01:38 +02:00
Mathieu
940ef2845d feat(xo-lite/Console): ability to scale VM console (#5703) 2021-09-30 15:01:38 +02:00
Pierre Donias
e3dbb7a6c2 fix(xo-lite/novnc): remove types 2021-09-30 15:01:38 +02:00
Pierre Donias
8cba6ebb20 fix(xo-lite/novnc): use @types/novnc-core
See https://github.com/DefinitelyTyped/DefinitelyTyped/pull/18602
2021-09-30 15:01:37 +02:00
Pierre Donias
a1b322f5be chore(xo-lite): update xen-api 2021-09-30 15:01:37 +02:00
Pierre Donias
07ff19c4b8 feat(xo-lite): Reaclette types 2021-09-30 15:01:06 +02:00
Pierre Donias
3a0af4e7e0 fix(xo-lite/console): get console URL from XAPI 2021-09-30 15:01:06 +02:00
Pierre Donias
dbb3f74ab0 feat(xo-lite): initial commit 2021-09-30 15:01:06 +02:00
Pierre Donias
0eaac8fd7a feat: technical release (#5924) 2021-09-30 11:17:45 +02:00
Julien Fontanet
06c71154b9 fix(xen-api/_setHostAddressInUrl): pass params in array
Introduced in fb21e4d58
2021-09-30 10:32:12 +02:00
Julien Fontanet
0e8f314dd6 fix(xo-web/new-vm): don't send default networkConfig (#5923)
Fixes #5918
2021-09-30 09:37:12 +02:00
Florent BEAUCHAMP
f53ec8968b feat(xo-web/SortedTable): move filter and pagination to top (#5914) 2021-09-29 17:35:46 +02:00
Mathieu
919d118f21 feat(xo-web/health): filter duplicated MAC addresses by running VMs (#5917)
See xoa-support#4054
2021-09-24 17:25:42 +02:00
Mathieu
216b759df1 feat(xo-web/health): hide CR VMs duplicated MAC addresses (#5916)
See xoa-support#4054
2021-09-24 15:52:34 +02:00
Julien Fontanet
01450db71e fix(proxy/backup.run): clear error on license issue
Fixes https://xcp-ng.org/forum/topic/4901/backups-silently-fail-with-invalid-xo-proxy-license
2021-09-24 13:15:32 +02:00
Julien Fontanet
ed987e1610 fix(proxy/api/ndJsonStream): send JSON-RPC error if whole iteration failed
See https://xcp-ng.org/forum/topic/4901/backups-silently-fail-with-invalid-xo-proxy-license
2021-09-24 13:15:24 +02:00
Florent BEAUCHAMP
2773591e1f feat(xo-web): add go back to ActionButton and use it when saving a backup (#5913)
See xoa-support#2149
2021-09-24 11:38:37 +02:00
Pierre Donias
a995276d1e fix(xo-server-netbox): better handle missing uuid custom field (#5909)
Fixes #5905
See #5806
See #5834
See xoa-support#3812

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

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

Introduced by ea10df8a92

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

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

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

Introduced by 030477454

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

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

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

* grammar edits for anti-affinity

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

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

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

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

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

Introduced by a73acedc4d

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

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

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

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

Better import of CommonJS modules.
2021-05-26 16:07:34 +02:00
Julien Fontanet
9f1a300d2a fix(backups): properly close streams are destroyed in case of failure
Fixes xoa-support#3753
2021-05-26 14:39:56 +02:00
250 changed files with 10085 additions and 3195 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,17 +18,6 @@ const wrapCall = (fn, arg, thisArg) => {
* @returns {Promise<Item[]>}
*/
exports.asyncMap = function asyncMap(iterable, mapFn, thisArg = iterable) {
let onError
if (onError !== undefined) {
const original = mapFn
mapFn = async function () {
try {
return await original.apply(this, arguments)
} catch (error) {
return onError.call(this, error, ...arguments)
}
}
}
return Promise.all(Array.from(iterable, mapFn, thisArg))
}

View File

@@ -9,7 +9,7 @@
},
"version": "0.2.0",
"engines": {
"node": ">=8.10"
"node": ">=10"
},
"main": "dist/",
"scripts": {
@@ -30,9 +30,8 @@
"rimraf": "^3.0.0"
},
"dependencies": {
"@vates/decorate-with": "^0.0.1",
"@xen-orchestra/log": "^0.2.0",
"core-js": "^3.6.4",
"@vates/decorate-with": "^0.1.0",
"@xen-orchestra/log": "^0.3.0",
"golike-defer": "^0.5.1",
"object-hash": "^2.0.1"
},

View File

@@ -1,6 +1,3 @@
// see https://github.com/babel/babel/issues/8450
import 'core-js/features/symbol/async-iterator'
import assert from 'assert'
import hash from 'object-hash'
import { createLogger } from '@xen-orchestra/log'

View File

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

View File

@@ -56,14 +56,22 @@ module.exports = function (pkg, configs = {}) {
}),
presets: Object.keys(presets).map(preset => [preset, presets[preset]]),
targets: (() => {
const targets = {}
if (pkg.browserslist !== undefined) {
targets.browsers = pkg.browserslist
}
let node = (pkg.engines || {}).node
if (node !== undefined) {
const trimChars = '^=>~'
while (trimChars.includes(node[0])) {
node = node.slice(1)
}
targets.node = node
}
return { browsers: pkg.browserslist, node }
return targets
})(),
}
}

View File

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

View File

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

View File

@@ -7,8 +7,8 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.11.0",
"@xen-orchestra/fs": "^0.17.0",
"@xen-orchestra/backups": "^0.13.0",
"@xen-orchestra/fs": "^0.18.0",
"filenamify": "^4.1.0",
"getopts": "^2.2.5",
"lodash": "^4.17.15",

View File

@@ -543,40 +543,6 @@ class RemoteAdapter {
async readVmBackupMetadata(path) {
return Object.defineProperty(JSON.parse(await this._handler.readFile(path)), '_filename', { value: path })
}
async writeFullVmBackup({ jobId, mode, scheduleId, timestamp, vm, vmSnapshot, xva }, sizeContainer, stream) {
const basename = formatFilenameDate(timestamp)
const dataBasename = basename + '.xva'
const dataFilename = backupDir + '/' + dataBasename
const metadataFilename = `${backupDir}/${basename}.json`
const metadata = {
jobId: job.id,
mode: job.mode,
scheduleId,
timestamp,
version: '2.0.0',
vm,
vmSnapshot: this._backup.exportedVm,
xva: './' + dataBasename,
}
const { deleteFirst } = settings
if (deleteFirst) {
await deleteOldBackups()
}
await adapter.outputStream(stream, dataFilename, {
validator: tmpPath => {
if (handler._getFilePath !== undefined) {
return isValidXva(handler._getFilePath('/' + tmpPath))
}
},
})
metadata.size = sizeContainer.size
await handler.outputFile(metadataFilename, JSON.stringify(metadata))
}
}
Object.assign(RemoteAdapter.prototype, {

View File

@@ -1,11 +1,8 @@
const CancelToken = require('promise-toolbox/CancelToken.js')
const Zone = require('node-zone')
const logAfterEnd = function (log) {
const error = new Error('task has already ended:' + this.id)
error.result = log.result
error.log = log
throw error
const logAfterEnd = () => {
throw new Error('task has already ended')
}
const noop = Function.prototype
@@ -47,19 +44,11 @@ class Task {
}
}
get id() {
return this.#id
}
#cancelToken
#id = Math.random().toString(36).slice(2)
#onLog
#zone
get id() {
return this.#id
}
constructor({ name, data, onLog }) {
let parentCancelToken, parentId
if (onLog === undefined) {
@@ -111,8 +100,6 @@ class Task {
run(fn, last = false) {
return this.#zone.run(() => {
try {
this.#cancelToken.throwIfRequested()
const result = fn()
let then
if (result != null && typeof (then = result.then) === 'function') {

View File

@@ -1,5 +1,4 @@
const assert = require('assert')
// const asyncFn = require('promise-toolbox/asyncFn')
const findLast = require('lodash/findLast.js')
const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
const keyBy = require('lodash/keyBy.js')
@@ -104,9 +103,21 @@ exports.VmBackup = class VmBackup {
// calls fn for each function, warns of any errors, and throws only if there are no writers left
async _callWriters(fn, warnMessage, parallel = true) {
const writers = this._writers
if (writers.size === 0) {
const n = writers.size
if (n === 0) {
return
}
if (n === 1) {
const [writer] = writers
try {
await fn(writer)
} catch (error) {
writers.delete(writer)
throw error
}
return
}
await (parallel ? asyncMap : asyncEach)(writers, async function (writer) {
try {
await fn(writer)
@@ -144,7 +155,6 @@ exports.VmBackup = class VmBackup {
const doSnapshot =
this._isDelta || (!settings.offlineBackup && vm.power_state === 'Running') || settings.snapshotRetention !== 0
console.log({ doSnapshot })
if (doSnapshot) {
await Task.run({ name: 'snapshot' }, async () => {
if (!settings.bypassVdiChainsCheck) {
@@ -183,7 +193,6 @@ exports.VmBackup = class VmBackup {
await this._callWriters(writer => writer.prepare({ isFull }), 'writer.prepare()')
const deltaExport = await exportDeltaVm(exportedVm, baseVm, {
cancelToken: Task.cancelToken,
fullVdisRequired,
})
const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))
@@ -229,7 +238,6 @@ exports.VmBackup = class VmBackup {
async _copyFull() {
const { compression } = this.job
const stream = await this._xapi.VM_export(this.exportedVm.$ref, {
cancelToken: Task.cancelToken,
compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
useSnapshot: false,
})
@@ -334,22 +342,10 @@ exports.VmBackup = class VmBackup {
this._baseVm = baseVm
this._fullVdisRequired = fullVdisRequired
Task.info('base data', {
vm: baseVm.uuid,
fullVdisRequired: Array.from(fullVdisRequired),
})
}
run = defer(this.run)
async run($defer) {
this.exportedVm = this.vm
this.timestamp = Date.now()
const doSnapshot = this._isDelta || vm.power_state === 'Running' || settings.snapshotRetention !== 0
if (!this._isDelta) {
}
const settings = this._settings
assert(
!settings.offlineBackup || settings.snapshotRetention === 0,
@@ -396,6 +392,3 @@ exports.VmBackup = class VmBackup {
}
}
}
// const { prototype } = exports.VmBackup
// prototype.run = asyncFn.cancelable(prototype.run)

View File

@@ -1,4 +1,5 @@
const assert = require('assert')
const sum = require('lodash/sum')
const { asyncMap } = require('@xen-orchestra/async-map')
const { default: Vhd, mergeVhd } = require('vhd-lib')
const { dirname, resolve } = require('path')
@@ -113,7 +114,7 @@ const listVhds = async (handler, vmDir) => {
return { vhds, interruptedVhds }
}
exports.cleanVm = async function cleanVm(vmDir, { remove, merge, onLog = noop }) {
exports.cleanVm = async function cleanVm(vmDir, { fixMetadata, remove, merge, onLog = noop }) {
const handler = this._handler
const vhds = new Set()
@@ -219,11 +220,16 @@ exports.cleanVm = async function cleanVm(vmDir, { remove, merge, onLog = noop })
await asyncMap(jsons, async json => {
const metadata = JSON.parse(await handler.readFile(json))
const { mode } = metadata
let size
if (mode === 'full') {
const linkedXva = resolve('/', vmDir, metadata.xva)
if (xvas.has(linkedXva)) {
unusedXvas.delete(linkedXva)
size = await handler.getSize(linkedXva).catch(error => {
onLog(`failed to get size of ${json}`, { error })
})
} else {
onLog(`the XVA linked to the metadata ${json} is missing`)
if (remove) {
@@ -241,6 +247,10 @@ exports.cleanVm = async function cleanVm(vmDir, { remove, merge, onLog = noop })
// possible (existing disks) even if one disk is missing
if (linkedVhds.every(_ => vhds.has(_))) {
linkedVhds.forEach(_ => unusedVhds.delete(_))
size = await asyncMap(linkedVhds, vhd => handler.getSize(vhd)).then(sum, error => {
onLog(`failed to get size of ${json}`, { error })
})
} else {
onLog(`Some VHDs linked to the metadata ${json} are missing`)
if (remove) {
@@ -249,6 +259,22 @@ exports.cleanVm = async function cleanVm(vmDir, { remove, merge, onLog = noop })
}
}
}
const metadataSize = metadata.size
if (size !== undefined && metadataSize !== size) {
onLog(`incorrect size in metadata: ${metadataSize ?? 'none'} instead of ${size}`)
// don't update if the the stored size is greater than found files,
// it can indicates a problem
if (fixMetadata && (metadataSize === undefined || metadataSize < size)) {
try {
metadata.size = size
await handler.writeFile(json, JSON.stringify(metadata), { flags: 'w' })
} catch (error) {
onLog(`failed to update size in backup metadata ${json}`, { error })
}
}
}
})
// TODO: parallelize by vm/job/vdi

View File

@@ -202,6 +202,7 @@ exports.importDeltaVm = defer(async function importDeltaVm(
blocked_operations: {
...vmRecord.blocked_operations,
start: 'Importing…',
start_on: 'Importing…',
},
ha_always_run: false,
is_a_template: false,
@@ -305,9 +306,6 @@ exports.importDeltaVm = defer(async function importDeltaVm(
}
}),
// Wait for VDI export tasks (if any) termination.
Promise.all(Object.values(streams).map(stream => stream.task)),
// Create VIFs.
asyncMap(Object.values(deltaVm.vifs), vif => {
let network = vif.$network$uuid && xapi.getObjectByUuid(vif.$network$uuid, undefined)

View File

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

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.11.0",
"version": "0.13.0",
"engines": {
"node": ">=14.6"
},
@@ -20,25 +20,25 @@
"@vates/disposable": "^0.1.1",
"@vates/parse-duration": "^0.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^0.17.0",
"@xen-orchestra/log": "^0.2.0",
"@xen-orchestra/fs": "^0.18.0",
"@xen-orchestra/log": "^0.3.0",
"@xen-orchestra/template": "^0.1.0",
"compare-versions": "^3.6.0",
"d3-time-format": "^3.0.0",
"end-of-stream": "^1.4.4",
"fs-extra": "^9.0.0",
"fs-extra": "^10.0.0",
"golike-defer": "^0.5.1",
"limit-concurrency-decorator": "^0.5.0",
"lodash": "^4.17.20",
"node-zone": "^0.4.0",
"parse-pairs": "^1.1.0",
"pump": "^3.0.0",
"promise-toolbox": "^0.19.2",
"vhd-lib": "^1.0.0",
"pump": "^3.0.0",
"vhd-lib": "^1.2.0",
"yazl": "^2.5.1"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^0.6.2"
"@xen-orchestra/xapi": "^0.7.0"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -128,7 +128,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
})
}
async transfer({ timestamp, deltaExport, sizeContainers }) {
async _transfer({ timestamp, deltaExport, sizeContainers }) {
const adapter = this._adapter
const backup = this._backup

View File

@@ -77,7 +77,7 @@ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinRepli
return asyncMapSettled(this._oldEntries, vm => vm.$destroy())
}
async transfer({ timestamp, deltaExport, sizeContainers }) {
async _transfer({ timestamp, deltaExport, sizeContainers }) {
const sr = this._sr
const { job, scheduleId, vm } = this._backup
@@ -106,9 +106,11 @@ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinRepli
targetVm.ha_restart_priority !== '' &&
Promise.all([targetVm.set_ha_restart_priority(''), targetVm.add_tags('HA disabled')]),
targetVm.set_name_label(`${vm.name_label} - ${job.name} - (${formatFilenameDate(timestamp)})`),
targetVm.update_blocked_operations(
'start',
'Start operation for this vm is blocked, clone it if you want to use it.'
asyncMap(['start', 'start_on'], op =>
targetVm.update_blocked_operations(
op,
'Start operation for this vm is blocked, clone it if you want to use it.'
)
),
targetVm.update_other_config({
'xo:backup:sr': srUuid,

View File

@@ -25,7 +25,7 @@ exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(Abst
)
}
async run({ timestamp, sizeContainer, stream }) {
async _run({ timestamp, sizeContainer, stream }) {
const backup = this._backup
const settings = this._settings

View File

@@ -1,5 +1,5 @@
const ignoreErrors = require('promise-toolbox/ignoreErrors.js')
const { asyncMapSettled } = require('@xen-orchestra/async-map')
const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
const { formatDateTime } = require('@xen-orchestra/xapi')
const { formatFilenameDate } = require('../_filenameDate.js')
@@ -29,7 +29,7 @@ exports.FullReplicationWriter = class FullReplicationWriter extends MixinReplica
)
}
async run({ timestamp, sizeContainer, stream }) {
async _run({ timestamp, sizeContainer, stream }) {
const sr = this._sr
const settings = this._settings
const { job, scheduleId, vm } = this._backup
@@ -64,9 +64,11 @@ exports.FullReplicationWriter = class FullReplicationWriter extends MixinReplica
const targetVm = await xapi.getRecord('VM', targetVmRef)
await Promise.all([
targetVm.update_blocked_operations(
'start',
'Start operation for this vm is blocked, clone it if you want to use it.'
asyncMap(['start', 'start_on'], op =>
targetVm.update_blocked_operations(
op,
'Start operation for this vm is blocked, clone it if you want to use it.'
)
),
targetVm.update_other_config({
'xo:backup:sr': srUuid,

View File

@@ -13,7 +13,14 @@ exports.AbstractDeltaWriter = class AbstractDeltaWriter extends AbstractWriter {
throw new Error('Not implemented')
}
transfer({ timestamp, deltaExport, sizeContainers }) {
throw new Error('Not implemented')
async transfer({ timestamp, deltaExport, sizeContainers }) {
try {
return await this._transfer({ timestamp, deltaExport, sizeContainers })
} finally {
// ensure all streams are properly closed
for (const stream of Object.values(deltaExport.streams)) {
stream.destroy()
}
}
}
}

View File

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

View File

@@ -16,7 +16,7 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
_cleanVm(options) {
return this._adapter
.cleanVm(getVmBackupDir(this._backup.vm.uuid), { ...options, onLog: warn, lock: false })
.cleanVm(getVmBackupDir(this._backup.vm.uuid), { ...options, fixMetadata: true, onLog: warn, lock: false })
.catch(warn)
}

View File

@@ -77,7 +77,11 @@ ${cliName} v${pkg.version}
'xo:backup:sr': tgtSr.uuid,
'xo:copy_of': srcSnapshotUuid,
}),
tgtVm.update_blocked_operations('start', 'Start operation for this vm is blocked, clone it if you want to use it.'),
Promise.all(
['start', 'start_on'].map(op =>
tgtVm.update_blocked_operations(op, 'Start operation for this vm is blocked, clone it if you want to use it.')
)
),
Promise.all(
userDevices.map(userDevice => {
const srcDisk = srcDisks[userDevice]

View File

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

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/defined",
"version": "0.0.0",
"version": "0.0.1",
"license": "ISC",
"description": "Utilities to help handling (possibly) undefined values",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/defined",

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/emit-async",
"version": "0.0.0",
"version": "0.1.0",
"license": "ISC",
"description": "Emit an event for async listeners to settle",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/emit-async",

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/fs",
"version": "0.17.0",
"version": "0.18.0",
"license": "AGPL-3.0-or-later",
"description": "The File System for Xen Orchestra backups.",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",
@@ -23,9 +23,9 @@
"@vates/coalesce-calls": "^0.1.0",
"@xen-orchestra/async-map": "^0.1.2",
"aws-sdk": "^2.686.0",
"decorator-synchronized": "^0.5.0",
"decorator-synchronized": "^0.6.0",
"execa": "^5.0.0",
"fs-extra": "^9.0.0",
"fs-extra": "^10.0.0",
"get-stream": "^6.0.0",
"limit-concurrency-decorator": "^0.5.0",
"lodash": "^4.17.4",
@@ -45,7 +45,7 @@
"async-iterator-to-stream": "^1.1.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^7.0.2",
"dotenv": "^8.0.0",
"dotenv": "^10.0.0",
"rimraf": "^3.0.0"
},
"scripts": {

View File

@@ -1,13 +1,13 @@
import asyncMapSettled from '@xen-orchestra/async-map/legacy'
import getStream from 'get-stream'
import path, { basename } from 'path'
import synchronized from 'decorator-synchronized'
import { coalesceCalls } from '@vates/coalesce-calls'
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
import { limitConcurrency } from 'limit-concurrency-decorator'
import { parse } from 'xo-remote-parser'
import { pipeline } from 'stream'
import { randomBytes } from 'crypto'
import { synchronized } from 'decorator-synchronized'
import normalizePath from './_normalizePath'
import { createChecksumStream, validChecksumOfReadStream } from './checksum'

View File

@@ -27,3 +27,12 @@ export const getHandler = (remote, ...rest) => {
}
return new Handler(remote, ...rest)
}
export const getSyncedHandler = async (...opts) => {
const handler = getHandler(...opts)
await handler.sync()
return {
dispose: () => handler.forget(),
value: handler,
}
}

View File

@@ -0,0 +1,6 @@
module.exports = require('../../@xen-orchestra/babel-config')(require('./package.json'), {
'@babel/preset-env': {
exclude: ['@babel/plugin-proposal-dynamic-import', '@babel/plugin-transform-regenerator'],
modules: false,
},
})

View File

@@ -0,0 +1,32 @@
module.exports = {
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
parserOptions: {
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
ecmaFeatures: {
jsx: true, // Allows for the parsing of JSX
},
},
settings: {
react: {
version: '17',
},
},
extends: [
'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from @typescript-eslint/eslint-plugin
],
rules: {
'eslint-comments/disable-enable-pair': 'off',
// Necessary to pass empty Effects/State to Reaclette
'@typescript-eslint/no-empty-interface': 'off',
// https://github.com/typescript-eslint/typescript-eslint/issues/1071
'@typescript-eslint/no-explicit-any': 'off',
// https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md#how-to-use
'@typescript-eslint/no-use-before-define': ['error'],
'no-use-before-define': 'off',
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }],
'@typescript-eslint/ban-ts-comment': 'off',
},
}

24
@xen-orchestra/lite/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.eslintcache
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -0,0 +1,71 @@
{
"name": "xo-lite",
"version": "0.1.0",
"devDependencies": {
"@babel/core": "^7.13.1",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.0",
"@babel/plugin-proposal-optional-chaining": "^7.13.0",
"@babel/preset-env": "^7.13.5",
"@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.13.0",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@fortawesome/fontawesome-svg-core": "^1.2.34",
"@fortawesome/free-solid-svg-icons": "^5.15.2",
"@fortawesome/react-fontawesome": "^0.1.14",
"@mui/icons-material": "^5.0.0",
"@mui/lab": "^5.0.0-alpha.48",
"@mui/material": "^5.0.1",
"@novnc/novnc": "^1.2.0",
"@types/immutable": "^3.8.7",
"@types/js-cookie": "^2.2.6",
"@types/lodash": "^4.14.175",
"@types/node": "^14.14.21",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-helmet": "^6.1.5",
"@types/react-intl": "^3.0.0",
"@types/react-router-dom": "^5.1.7",
"@types/react-syntax-highlighter": "^13.5.0",
"@types/styled-components": "^5.1.9",
"@types/webpack-env": "^1.16.0",
"@typescript-eslint/eslint-plugin": "^4.16.1",
"@typescript-eslint/parser": "^4.16.1",
"babel-loader": "^8.2.2",
"classnames": "^2.3.1",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^10.2.0",
"eslint": "^7.21.0",
"eslint-plugin-react": "^7.22.0",
"html-webpack-plugin": "^5.2.0",
"human-format": "^0.11.0",
"immutable": "^4.0.0-rc.12",
"iterable-backoff": "^0.1.0",
"json-rpc-protocol": "^0.13.1",
"lodash": "^4.17.21",
"node-polyfill-webpack-plugin": "^1.0.3",
"process": "^0.11.10",
"promise-toolbox": "^0.16.0",
"reaclette": "^0.10.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-helmet": "^6.1.0",
"react-intl": "^5.10.16",
"react-router-dom": "^5.2.0",
"react-syntax-highlighter": "^15.4.3",
"styled-components": "^5.2.1",
"typescript": "^4.3.1",
"webpack": "^5.24.2",
"webpack-cli": "^4.9.1",
"xen-api": "^0.34.3"
},
"resolutions": {
"styled-components": "^5"
},
"scripts": {
"build": "cross-env NODE_ENV=production webpack",
"start": "cross-env NODE_ENV=development webpack serve",
"start:open": "npm run start -- --open"
},
"browserslist": "> 0.5%, last 2 versions, Firefox ESR, not dead"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Xen Orchestra Lite" />
<title>XO Lite</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -0,0 +1,90 @@
import React from 'react'
import styled from 'styled-components'
import { Switch, Route, RouteComponentProps } from 'react-router-dom'
import { withState } from 'reaclette'
import { withRouter } from 'react-router'
import Pool from './Pool'
import TabConsole from './TabConsole'
import TreeView from './TreeView'
import { ObjectsByType } from '../libs/xapi'
const Container = styled.div`
display: flex;
overflow: hidden;
`
const LeftPanel = styled.div`
background: #f5f5f5;
min-width: 15em;
overflow-y: scroll;
width: 20%;
`
// FIXME: temporary work-around while investigating flew-grow issue:
// `overflow: hidden` forces the console to shrink to the max available width
// even when the tree component takes more than 20% of the width due to
// `min-width`
const MainPanel = styled.div`
overflow: hidden;
width: 80%;
`
interface ParentState {
objectsByType: ObjectsByType
pool?: string
}
interface State {
selectedObject?: string
selectedVm?: string
}
// For compatibility with 'withRouter'
interface Props extends RouteComponentProps {}
interface ParentEffects {}
interface Effects {
initialize: () => void
}
interface Computed {}
const selectedNodesToArray = (nodes: Array<string> | string | undefined) =>
nodes === undefined ? undefined : Array.isArray(nodes) ? nodes : [nodes]
const Infrastructure = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
initialState: props => ({
selectedVm: props.location.pathname.split('/')[3],
}),
computed: {
selectedObject: (state, props) =>
props.location.pathname.startsWith('/infrastructure/pool') ? state.pool : state.selectedVm,
},
},
({ state: { pool, selectedObject } }) => (
<Container>
<LeftPanel>
<TreeView defaultSelectedNodes={selectedNodesToArray(selectedObject)} />
</LeftPanel>
<MainPanel>
<Switch>
<Route exact path={`/infrastructure/pool/${pool}/dashboard`}>
<Pool id={pool} />
</Route>
<Route
path='/infrastructure/vms/:id/console'
render={({
match: {
params: { id },
},
}) => <TabConsole key={id} vmId={id} />}
/>
</Switch>
</MainPanel>
</Container>
)
)
export default withRouter(Infrastructure)

View File

@@ -0,0 +1,120 @@
import Grid from '@mui/material/Grid'
import React from 'react'
import styled from 'styled-components'
import Typography from '@mui/material/Typography'
import { withState } from 'reaclette'
import Icon from '../../../components/Icon'
import IntlMessage from '../../../components/IntlMessage'
import ProgressCircle from '../../../components/ProgressCircle'
interface ParentState {}
interface State {}
interface Props {
nActive?: number
nTotal?: number
type: 'host' | 'VM'
}
interface ParentEffects {}
interface Effects {}
interface Computed {
nInactive?: number
}
const DEFAULT_CAPTION_STYLE = { textTransform: 'uppercase', mt: 2 }
const TYPOGRAPHY_SX = { mb: 2 }
const ObjectStatusContainer = styled.div`
display: flex;
overflow: hidden;
flex-direction: row;
align-content: space-between;
margin-bottom: 1em;
`
const CircularProgressPanel = styled.div`
margin-left: 2em;
`
const GridPanel = styled.div`
margin-left: 2em;
width: 100%;
height: 100%;
`
// TODO: Add a loading page when data is not loaded as it is in the model(figma).
// FIXME: replace the hard-coded colors with the theme colors.
const ObjectStatus = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
computed: {
nInactive: (state, { nTotal = 0, nActive = 0 }) => nTotal - nActive,
},
},
({ state: { nInactive }, nActive = 0, nTotal = 0, type }) => {
if (nTotal === 0) {
return (
<span>
<IntlMessage id={type === 'VM' ? 'noVms' : 'noHosts'} />
</span>
)
}
return (
<ObjectStatusContainer>
<CircularProgressPanel>
<ProgressCircle max={nTotal} value={nActive} />
</CircularProgressPanel>
<GridPanel>
<Grid container>
<Grid item xs={12}>
<Typography sx={TYPOGRAPHY_SX} variant='h5' component='div'>
<IntlMessage id={type === 'VM' ? 'vms' : 'hosts'} />
</Typography>
</Grid>
<Grid item xs={10}>
<Icon icon='circle' htmlColor='#00BA34' />
&nbsp;
<Typography variant='body2' component='span'>
<IntlMessage id='active' />
</Typography>
</Grid>
<Grid item xs={2}>
<Typography variant='body2' component='div'>
{nActive}
</Typography>
</Grid>
<Grid item xs={10}>
<Icon icon='circle' htmlColor='#E8E8E8' />
&nbsp;
<Typography variant='body2' component='span'>
<IntlMessage id='inactive' />
</Typography>
</Grid>
<Grid item xs={2}>
<Typography variant='body2' component='div'>
{nInactive}
</Typography>
</Grid>
<Grid item xs={10}>
<Typography variant='caption' component='div' sx={DEFAULT_CAPTION_STYLE}>
<IntlMessage id='total' />
</Typography>
</Grid>
<Grid item xs={2}>
<Typography variant='caption' component='div' sx={DEFAULT_CAPTION_STYLE}>
{nTotal}
</Typography>
</Grid>
</Grid>
</GridPanel>
</ObjectStatusContainer>
)
}
)
export default ObjectStatus

View File

@@ -0,0 +1,79 @@
import Divider from '@mui/material/Divider'
import React from 'react'
import styled from 'styled-components'
import Typography from '@mui/material/Typography'
import { withState } from 'reaclette'
import ObjectStatus from './ObjectStatus'
import IntlMessage from '../../../components/IntlMessage'
import { Host, ObjectsByType, Vm } from '../../../libs/xapi'
interface ParentState {
objectsByType?: ObjectsByType
}
interface State {
hosts?: Map<string, Host>
nRunningHosts?: number
nRunningVms?: number
vms?: Map<string, Vm>
}
interface Props {}
interface ParentEffects {}
interface Effects {}
interface Computed {}
const DEFAULT_STYLE = { m: 2 }
const Container = styled.div`
display: flex;
overflow: hidden;
flex-direction: row;
align-content: space-between;
gap: 1.25em;
background: '#E8E8E8';
`
const Panel = styled.div`
background: #ffffff;
border-radius: 0.5em;
box-shadow: 0px 1px 1px 0px #00000014, 0px 2px 1px 0px #0000000f, 0px 1px 3px 0px #0000001a;
margin: 0.5em;
`
const getHostPowerState = (host: Host) => {
const { $metrics } = host
return $metrics ? ($metrics.live ? 'Running' : 'Halted') : 'Unknown'
}
const Dashboard = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
computed: {
hosts: state => state.objectsByType?.get('host'),
vms: state =>
state.objectsByType
?.get('VM')
?.filter((vm: Vm) => !vm.is_control_domain && !vm.is_a_snapshot && !vm.is_a_template),
nRunningHosts: state => (state.hosts?.filter((host: Host) => getHostPowerState(host) === 'Running')).size,
nRunningVms: state => (state.vms?.filter((vm: Vm) => vm.power_state === 'Running')).size,
},
},
({ state: { hosts, nRunningHosts, nRunningVms, vms } }) => (
<Container>
<Panel>
<Typography variant='h4' component='div' sx={DEFAULT_STYLE}>
<IntlMessage id='status' />
</Typography>
<ObjectStatus nActive={nRunningHosts} nTotal={hosts?.size} type='host' />
<Divider variant='middle' sx={DEFAULT_STYLE} />
<ObjectStatus nActive={nRunningVms} nTotal={vms?.size} type='VM' />
</Panel>
</Container>
)
)
export default Dashboard

View File

@@ -0,0 +1,46 @@
import React from 'react'
import { withState } from 'reaclette'
import Dashboard from './dashboard'
import Icon from '../../components/Icon'
import PanelHeader from '../../components/PanelHeader'
import { ObjectsByType, Pool as PoolType } from '../../libs/xapi'
interface ParentState {
objectsByType: ObjectsByType
}
interface State {}
interface Props {
id: string
}
interface ParentEffects {}
interface Effects {}
interface Computed {
pool?: PoolType
}
// TODO: add tabs when https://github.com/vatesfr/xen-orchestra/pull/6096 is merged.
const Pool = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
computed: {
pool: (state, props) => state.objectsByType?.get('pool')?.get(props.id),
},
},
({ state: { pool } }) => (
<>
<PanelHeader>
<span>
<Icon icon='warehouse' color='primary' /> {pool?.name_label}
</span>
</PanelHeader>
<Dashboard />
</>
)
)
export default Pool

View File

@@ -0,0 +1,65 @@
import React from 'react'
import { Map } from 'immutable'
import { withState } from 'reaclette'
import IntlMessage from '../../components/IntlMessage'
import Table, { Column } from '../../components/Table'
import { ObjectsByType, Pif } from '../../libs/xapi'
interface ParentState {
objectsByType: ObjectsByType
objectsFetched: boolean
}
interface State {}
interface Props {
poolId: string
}
interface ParentEffects {}
interface Effects {}
interface Computed {
managementPifs?: Pif[]
pifs?: Map<string, Pif>
}
const COLUMNS: Column<Pif>[] = [
{
header: <IntlMessage id='device' />,
render: pif => pif.device,
},
{
header: <IntlMessage id='dns' />,
render: pif => pif.DNS,
},
{
header: <IntlMessage id='gateway' />,
render: pif => pif.gateway,
},
{
header: <IntlMessage id='ip' />,
render: pif => pif.IP,
},
]
const PoolNetworks = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
computed: {
managementPifs: state =>
state.pifs
?.filter(pif => pif.management)
.map(pif => ({ ...pif, id: pif.$id }))
.valueSeq()
.toArray(),
pifs: state => state.objectsByType.get('PIF'),
},
},
({ state }) => (
<Table collection={state.managementPifs} columns={COLUMNS} placeholder={<IntlMessage id='noManagementPifs' />} />
)
)
export default PoolNetworks

View File

@@ -0,0 +1,89 @@
import React from 'react'
import humanFormat from 'human-format'
import { withState } from 'reaclette'
import IntlMessage from '../../components/IntlMessage'
import Table, { Column } from '../../components/Table'
import XapiConnection, { ObjectsByType, PoolUpdate } from '../../libs/xapi'
const COLUMN: Column<PoolUpdate>[] = [
{
header: <IntlMessage id='name' />,
render: update => update.name,
},
{
header: <IntlMessage id='description' />,
render: update => update.description,
},
{
header: <IntlMessage id='version' />,
render: update => update.version,
},
{
header: <IntlMessage id='release' />,
render: update => update.release,
},
{
header: <IntlMessage id='size' />,
render: update => humanFormat.bytes(update.size),
},
]
interface ParentState {
objectsByType: ObjectsByType
objectsFetched: boolean
xapi: XapiConnection
}
interface State {}
interface Props {
hostRef: string
}
interface ParentEffects {}
interface Effects {}
interface Computed {
availableUpdates?: PoolUpdate[] | JSX.Element
}
const PoolUpdates = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
computed: {
availableUpdates: async function (state, { hostRef }) {
try {
const stringifiedPoolUpdates = (await state.xapi.call(
'host.call_plugin',
hostRef,
'updater.py',
'check_update',
{}
)) as string
return JSON.parse(stringifiedPoolUpdates)
} catch (err) {
console.error(err)
return <IntlMessage id='errorOccurred' />
}
},
},
},
({ state: { availableUpdates } }) =>
availableUpdates !== undefined ? (
Array.isArray(availableUpdates) ? (
<>
{availableUpdates.length !== 0 && (
<IntlMessage id='availableUpdates' values={{ nUpdates: availableUpdates.length }} />
)}
<Table collection={availableUpdates} columns={COLUMN} placeholder={<IntlMessage id='noUpdatesAvailable' />} />
</>
) : (
availableUpdates
)
) : (
<IntlMessage id='loading' />
)
)
export default PoolUpdates

View File

@@ -0,0 +1,53 @@
import React from 'react'
import { Map } from 'immutable'
import { withState } from 'reaclette'
import PoolNetworks from './PoolNetworks'
import PoolUpdates from './PoolUpdates'
import IntlMessage from '../../components/IntlMessage'
import { Host, ObjectsByType, Pool } from '../../libs/xapi'
interface ParentState {
objectsFetched: boolean
}
interface State {
objectsByType: ObjectsByType
}
interface Props {}
interface ParentEffects {}
interface Effects {}
interface Computed {
hosts?: Map<string, Host>
pool?: Pool
}
const PoolTab = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
computed: {
hosts: state => (state.objectsFetched ? state.objectsByType?.get('host') : undefined),
pool: state => (state.objectsFetched ? state.objectsByType?.get('pool')?.first() : undefined),
},
},
({ state }) =>
state.pool !== undefined ? (
<>
<PoolNetworks poolId={state.pool.$id} />
{state.hosts?.valueSeq().map(host => (
<div key={host.$id}>
<p>{host.name_label}</p>
<PoolUpdates hostRef={host.$ref} />
</div>
))}
</>
) : (
<IntlMessage id='loading' />
)
)
export default PoolTab

View File

@@ -0,0 +1,110 @@
import React from 'react'
import styled from 'styled-components'
import { withState } from 'reaclette'
import Button from '../../components/Button'
import Checkbox from '../../components/Checkbox'
import Input from '../../components/Input'
import IntlMessage from '../../components/IntlMessage'
interface ParentState {
error: string
}
interface State {
password: string
rememberMe: boolean
}
interface Props {}
interface ParentEffects {
connectToXapi: (password: string, rememberMe: boolean) => void
}
interface Effects {
setRememberMe: (event: React.ChangeEvent<HTMLInputElement>) => void
setPassword: (event: React.ChangeEvent<HTMLInputElement>) => void
submit: (event: React.MouseEvent<HTMLButtonElement>) => void
}
interface Computed {}
const Wrapper = styled.div`
height: 100vh;
display: flex;
`
const Form = styled.form`
width: 20em;
margin: auto;
text-align: center;
`
const Fieldset = styled.fieldset`
border: 0;
padding-left: 0;
padding-right: 0;
`
const RememberMe = styled(Fieldset)`
text-align: start;
vertical-align: baseline;
`
const Error = styled.p`
color: #a33;
`
const Signin = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
initialState: () => ({
password: '',
rememberMe: false,
}),
effects: {
setRememberMe: function ({ currentTarget: { checked: rememberMe } }) {
this.state.rememberMe = rememberMe
},
setPassword: function ({ currentTarget: { value: password } }) {
this.state.password = password
},
submit: function () {
this.effects.connectToXapi(this.state.password, this.state.rememberMe)
},
},
},
({ effects, state }) => (
<Wrapper>
<Form onSubmit={e => e.preventDefault()}>
<img src='logo.png' />
<h1>Xen Orchestra Lite</h1>
<Fieldset>
<Input disabled label={<IntlMessage id='login' />} value='root' />
</Fieldset>
<Fieldset>
<Input
autoFocus
label={<IntlMessage id='password' />}
onChange={effects.setPassword}
type='password'
value={state.password}
/>
</Fieldset>
<RememberMe>
<label>
<Checkbox onChange={effects.setRememberMe} checked={state.rememberMe} />
&nbsp;
<IntlMessage id='rememberMe' />
</label>
</RememberMe>
<Error>{state.error}</Error>
<Button type='submit' onClick={effects.submit}>
<IntlMessage id='connect' />
</Button>
</Form>
</Wrapper>
)
)
export default Signin

View File

@@ -0,0 +1,300 @@
// https://mui.com/components/material-icons/
import AccountCircleIcon from '@mui/icons-material/AccountCircle'
import DeleteIcon from '@mui/icons-material/Delete'
import React from 'react'
import styled from 'styled-components'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { materialDark as codeStyle } from 'react-syntax-highlighter/dist/esm/styles/prism'
import { toNumber } from 'lodash'
import { SelectChangeEvent } from '@mui/material'
import { withState } from 'reaclette'
import ActionButton from '../../components/ActionButton'
import Button from '../../components/Button'
import Checkbox from '../../components/Checkbox'
import Icon from '../../components/Icon'
import Input from '../../components/Input'
import ProgressCircle from '../../components/ProgressCircle'
import Select from '../../components/Select'
import Tabs from '../../components/Tabs'
import { alert, confirm } from '../../components/Modal'
interface ParentState {}
interface State {
progressBarValue: number
value: unknown
}
interface Props {}
interface ParentEffects {}
interface Effects {
onChangeProgressBarValue: (e: React.ChangeEvent<HTMLInputElement>) => void
onChangeSelect: (e: SelectChangeEvent<unknown>) => void
sayHello: () => void
sendPromise: (data: Record<string, unknown>) => Promise<void>
showAlertModal: () => void
showConfirmModal: () => void
}
interface Computed {}
const Page = styled.div`
margin: 30px;
`
const Container = styled.div`
display: flex;
column-gap: 10px;
`
const Render = styled.div`
flex: 1;
padding: 20px;
border: solid 1px gray;
border-radius: 3px;
`
const Code = styled(SyntaxHighlighter).attrs(() => ({
language: 'jsx',
style: codeStyle,
}))`
flex: 1;
border-radius: 3px;
margin: 0 !important;
`
const App = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
initialState: () => ({
progressBarValue: 100,
value: '',
}),
effects: {
onChangeProgressBarValue: function (e) {
this.state.progressBarValue = toNumber(e.target.value)
},
onChangeSelect: function (e) {
this.state.value = e.target.value
},
sayHello: () => alert('hello'),
sendPromise: data =>
new Promise(resolve => {
setTimeout(() => {
resolve()
window.alert(data.foo)
}, 1000)
}),
showAlertModal: () => alert({ message: 'This is an alert modal', title: 'Alert modal', icon: 'info' }),
showConfirmModal: () =>
confirm({
message: 'This is a confirm modal test',
title: 'Confirm modal',
icon: 'download',
}),
},
},
({ effects, state }) => (
<Page>
<h2>ActionButton</h2>
<Container>
<Render>
<ActionButton data-foo='forwarded data props' onClick={effects.sendPromise}>
Send promise
</ActionButton>
</Render>
<Code>
{`<ActionButton data-foo='forwarded data props' onClick={effects.sendPromise}>
Send promise
</ActionButton>`}
</Code>
</Container>
<h2>Button</h2>
<Container>
<Render>
<Button color='primary' onClick={effects.sayHello} startIcon={<AccountCircleIcon />}>
Primary
</Button>
<Button color='secondary' endIcon={<DeleteIcon />} onClick={effects.sayHello}>
Secondary
</Button>
<Button color='success' onClick={effects.sayHello}>
Success
</Button>
<Button color='warning' onClick={effects.sayHello}>
Warning
</Button>
<Button color='error' onClick={effects.sayHello}>
Error
</Button>
<Button color='info' onClick={effects.sayHello}>
Info
</Button>
</Render>
<Code>{`<Button color='primary' onClick={doSomething} startIcon={<AccountCircleIcon />}>
Primary
</Button>
<Button color='secondary' endIcon={<DeleteIcon />} onClick={doSomething}>
Secondary
</Button>
<Button color='success' onClick={doSomething}>
Success
</Button>
<Button color='warning' onClick={doSomething}>
Warning
</Button>
<Button color='error' onClick={doSomething}>
Error
</Button>
<Button color='info' onClick={doSomething}>
Info
</Button>`}</Code>
</Container>
<h2>Icon</h2>
<Container>
<Render>
<Icon icon='truck' htmlColor='#0085FF' />
<Icon icon='truck' color='primary' size='2x' />
</Render>
<Code>{`// https://fontawesome.com/icons
<Icon icon='truck' htmlColor='#0085FF'/>
<Icon icon='truck' color='primary' size='2x' />`}</Code>
</Container>
<h2>Input</h2>
<Container>
<Render>
<Input label='Input' />
<Checkbox />
</Render>
<Code>{`<TextInput label='Input' />
<Checkbox />`}</Code>
</Container>
<h2>Modal</h2>
<Container>
<Render>
<Button
color='primary'
onClick={effects.showAlertModal}
sx={{
marginBottom: 1,
}}
>
Alert
</Button>
<Button color='primary' onClick={effects.showConfirmModal}>
Confirm
</Button>
</Render>
<Code>{`<Button
color='primary'
onClick={() =>
alert({
message: 'This is an alert modal',
title: 'Alert modal',
icon: 'info'
})
}
>
Alert
</Button>
<Button
color='primary'
onClick={async () => {
try {
await confirm({
message: 'This is a confirm modal',
title: 'Confirm modal',
icon: 'download',
})
// The modal has been confirmed
} catch (reason) { // "cancel"
// The modal has been closed
}
}}
>
Confirm
</Button>`}</Code>
</Container>
<h2>ProgressCircle</h2>
<Container>
<Render>
<div style={{ display: 'flex', justifyContent: 'space-around', flexWrap: 'wrap' }}>
<div>
<ProgressCircle max={200} value={state.progressBarValue} />
</div>
<div>
<ProgressCircle max={200} showLabel={false} size={150} value={state.progressBarValue} />
</div>
</div>
<input
defaultValue={state.progressBarValue}
max='200'
min='0'
onChange={effects.onChangeProgressBarValue}
step='1'
style={{
display: 'block',
margin: '10px auto',
}}
type='range'
/>
</Render>
<Code>
{`<ProgressCircle max={200} value={state.progressBarValue} />
<ProgressCircle max={200} showLabel={false} size={150} value={state.progressBarValue} />`}
</Code>
</Container>
<h2>Select</h2>
<Container>
<Render>
<Select
onChange={effects.onChangeSelect}
options={[
{ name: 'Bar', value: 1 },
{ name: 'Foo', value: 2 },
]}
value={state.value}
valueRenderer='value'
/>
</Render>
<Code>
{`<Select
onChange={handleChange}
optionRenderer={item => item.name}
options={[
{ name: 'Bar', value: 1 },
{ name: 'Foo', value: 2 },
]}
value={state.value}
valueRenderer='value'
/>`}
</Code>
</Container>
<h2>Tabs</h2>
<Container>
<Render>
<Tabs
tabs={[
{ component: 'Hello BAR!', label: 'BAR', pathname: '/styleguide' },
{ label: 'FOO', pathname: '/styleguide/foo' },
]}
useUrl
/>
</Render>
<Code>
{`<Tabs
tabs={[
{ component: 'Hello BAR!', label: 'BAR', pathname: '/styleguide' },
{ label: 'FOO', pathname: '/styleguide/foo' },
]}
useUrl
/>`}
</Code>
</Container>
</Page>
)
)
export default App

View File

@@ -0,0 +1,102 @@
import React from 'react'
import { withState } from 'reaclette'
import Console from '../components/Console'
import IntlMessage, { translate } from '../components/IntlMessage'
import { ObjectsByType, Vm } from '../libs/xapi'
import PanelHeader from '../components/PanelHeader'
interface ParentState {
objectsByType: ObjectsByType
}
interface State {
consoleScale: number
sendCtrlAltDel?: () => void
}
interface Props {
vmId: string
}
interface ParentEffects {}
interface Effects {
scaleConsole: React.ChangeEventHandler<HTMLInputElement>
setCtrlAltDel: (sendCtrlAltDel: State['sendCtrlAltDel']) => void
showNotImplemented: () => void
}
interface Computed {
vm?: Vm
}
const TabConsole = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
initialState: () => ({
// Value in percent
consoleScale: 100,
sendCtrlAltDel: undefined,
}),
effects: {
scaleConsole: function (e) {
this.state.consoleScale = +e.currentTarget.value
// With "scaleViewport", the canvas occupies all available space of its
// container. But when the size of the container is changed, the canvas
// size isn't updated
// Issue https://github.com/novnc/noVNC/issues/1364
// PR https://github.com/novnc/noVNC/pull/1365
window.dispatchEvent(new UIEvent('resize'))
},
setCtrlAltDel: function (sendCtrlAltDel) {
this.state.sendCtrlAltDel = sendCtrlAltDel
},
showNotImplemented: function () {
alert('Not Implemented')
},
},
computed: {
vm: (state, { vmId }) => state.objectsByType.get('VM')?.get(vmId),
},
},
({ effects, state, vmId }) => (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<PanelHeader
actions={[
{
key: 'start',
icon: 'play',
color: 'primary',
title: translate({ id: 'vmStartLabel' }),
variant: 'contained',
onClick: effects.showNotImplemented,
},
]}
>
{state.vm?.name_label ?? 'loading'}{' '}
</PanelHeader>
{/* Hide scaling and Ctrl+Alt+Del button temporarily */}
{/* <RangeInput max={100} min={1} onChange={effects.scaleConsole} step={1} value={state.consoleScale} />
{state.sendCtrlAltDel !== undefined && (
<Button onClick={state.sendCtrlAltDel}>
<IntlMessage id='ctrlAltDel' />
</Button>
)} */}
{state.vm?.power_state !== 'Running' ? (
<p>
<IntlMessage id='consoleNotAvailable' />
</p>
) : (
<div style={{ flex: 1, overflow: 'hidden' }}>
<div style={{ height: '100%', width: '100%' }}>
<Console vmId={vmId} scale={state.consoleScale} setCtrlAltDel={effects.setCtrlAltDel} />
</div>
</div>
)}
</div>
)
)
export default TabConsole

View File

@@ -0,0 +1,131 @@
import React from 'react'
import { Collection, Map } from 'immutable'
import { withState } from 'reaclette'
import Icon from '../components/Icon'
import IntlMessage from '../components/IntlMessage'
import Tree, { ItemType } from '../components/Tree'
import { Host, ObjectsByType, Pool, Vm } from '../libs/xapi'
interface ParentState {
objectsByType: ObjectsByType
}
interface State {}
interface Props {
defaultSelectedNodes?: Array<string>
}
interface ParentEffects {}
interface Effects {}
interface Computed {
collection?: Array<ItemType>
hostsByPool?: Collection.Keyed<string, Collection<string, Host>>
pools?: Map<string, Pool>
vms?: Map<string, Vm>
vmsByContainerRef?: Collection.Keyed<string, Collection<string, Vm>>
}
const getHostPowerState = (host: Host) => {
const { $metrics } = host
return $metrics ? ($metrics.live ? 'Running' : 'Halted') : 'Unknown'
}
const getIconColor = (obj: Host | Vm) => {
const powerState = obj.power_state ?? getHostPowerState(obj as Host)
return powerState === 'Running' ? '#198754' : powerState === 'Halted' ? '#dc3545' : '#6c757d'
}
const TreeView = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
computed: {
collection: state => {
if (state.pools === undefined) {
return
}
const collection: ItemType[] = []
state.pools.valueSeq().forEach((pool: Pool) => {
const hosts = state.hostsByPool
?.get(pool.$id)
?.valueSeq()
.sortBy(host => host.name_label)
.map((host: Host) => ({
children: state.vmsByContainerRef
?.get(host.$ref)
?.valueSeq()
.sortBy(vm => vm.name_label)
.map((vm: Vm) => ({
id: vm.$id,
label: (
<span>
<Icon icon='desktop' htmlColor={getIconColor(vm)} /> {vm.name_label}
</span>
),
to: `/infrastructure/vms/${vm.$id}/console`,
tooltip: <IntlMessage id={vm.power_state.toLowerCase()} />,
}))
.toArray(),
id: host.$id,
label: (
<span>
<Icon icon='server' htmlColor={getIconColor(host)} /> {host.name_label}
</span>
),
tooltip: <IntlMessage id={getHostPowerState(host).toLowerCase()} />,
}))
.toArray()
const haltedVms = state.vmsByContainerRef
?.get(pool.$ref)
?.valueSeq()
.sortBy((vm: Vm) => vm.name_label)
.map((vm: Vm) => ({
id: vm.$id,
label: (
<span>
<Icon icon='desktop' htmlColor={getIconColor(vm)} /> {vm.name_label}
</span>
),
to: `/infrastructure/vms/${vm.$id}/console`,
tooltip: <IntlMessage id='halted' />,
}))
.toArray()
collection.push({
children: (hosts ?? []).concat(haltedVms ?? []),
id: pool.$id,
label: (
<span>
<Icon icon='warehouse' color='primary' /> {pool.name_label}
</span>
),
to: `/infrastructure/pool/${pool.$id}/dashboard`,
})
})
return collection
},
hostsByPool: state => state.objectsByType?.get('host')?.groupBy((host: Host) => host.$pool.$id),
pools: state => state.objectsByType?.get('pool'),
vms: state =>
state.objectsByType
?.get('VM')
?.filter((vm: Vm) => !vm.is_control_domain && !vm.is_a_snapshot && !vm.is_a_template),
vmsByContainerRef: state =>
state.vms?.groupBy(({ power_state: powerState, resident_on: host, $pool }: Vm) =>
powerState === 'Running' || powerState === 'Paused' ? host : $pool.$ref
),
},
},
({ state, defaultSelectedNodes }) =>
state.collection === undefined ? null : (
<div style={{ padding: '10px' }}>
<Tree collection={state.collection} defaultSelectedNodes={defaultSelectedNodes} />
</div>
)
)
export default TreeView

View File

@@ -0,0 +1,506 @@
// import Badge from '@mui/material/Badge'
import Box from '@mui/material/Box'
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'
import Container from '@mui/material/Container'
import Cookies from 'js-cookie'
import CssBaseline from '@mui/material/CssBaseline'
import Divider from '@mui/material/Divider'
import IconButton from '@mui/material/IconButton'
import List from '@mui/material/List'
import ListItem from '@mui/material/ListItem'
import ListItemIcon from '@mui/material/ListItemIcon'
import ListItemButton from '@mui/material/ListItemButton'
import ListItemText from '@mui/material/ListItemText'
import MenuIcon from '@mui/icons-material/Menu'
import MuiAppBar, { AppBarProps as MuiAppBarProps } from '@mui/material/AppBar'
import MuiDrawer from '@mui/material/Drawer'
import React from 'react'
import styledComponent from 'styled-components'
import Toolbar from '@mui/material/Toolbar'
import Typography from '@mui/material/Typography'
import { HashRouter as Router, Switch, Redirect, Route } from 'react-router-dom'
import { IntlProvider } from 'react-intl'
import { Map } from 'immutable'
import { styled, createTheme, ThemeProvider } from '@mui/material/styles'
import { withState } from 'reaclette'
// import Button from '../components/Button'
import Icon from '../components/Icon'
import Infrastructure from './Infrastructure'
import IntlMessage from '../components/IntlMessage'
import Link from '../components/Link'
import messagesEn from '../lang/en.json'
import Modal from '../components/Modal'
import PoolTab from './PoolTab'
import Signin from './Signin/index'
import StyleGuide from './StyleGuide/index'
import TabConsole from './TabConsole'
import XapiConnection, { ObjectsByType, Pool, Vm } from '../libs/xapi'
const drawerWidth = 240
const redirectPaths = ['/', '/infrastructure']
interface AppBarProps extends MuiAppBarProps {
open?: boolean
}
// -----------------------------------------------------------------------------
// Provided by this template: https://github.com/mui-org/material-ui/tree/next/docs/src/pages/getting-started/templates/dashboard
const AppBar = styled(MuiAppBar, {
shouldForwardProp: prop => prop !== 'open',
})<AppBarProps>(({ theme, open }) => ({
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
...(open && {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
}),
}))
const Drawer = styled(MuiDrawer, { shouldForwardProp: prop => prop !== 'open' })(({ theme, open }) => ({
'& .MuiDrawer-paper': {
position: 'relative',
whiteSpace: 'nowrap',
width: drawerWidth,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
boxSizing: 'border-box',
...(!open && {
overflowX: 'hidden',
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
width: theme.spacing(7),
[theme.breakpoints.up('sm')]: {
width: theme.spacing(9),
},
}),
},
}))
const MainListItems = (): JSX.Element => (
<div>
<ListItemButton component='a' href='#infrastructure'>
<ListItemIcon>
<Icon icon='project-diagram' />
</ListItemIcon>
<ListItemText primary={<IntlMessage id='infrastructure' />} />
</ListItemButton>
<ListItemButton component='a' href='#about'>
<ListItemIcon>
<Icon icon='info-circle' />
</ListItemIcon>
<ListItemText primary='About' />
</ListItemButton>
</div>
)
interface SecondaryListItemsParentState {}
interface SecondaryListItemsState {}
interface SecondaryListItemsProps {}
interface SecondaryListItemsParentEffects {}
interface SecondaryListItemsEffects {
disconnect: () => void
}
interface SecondaryListItemsComputed {}
const ICON_STYLE = { fontSize: '1.5em' }
const SecondaryListItems = withState<
SecondaryListItemsState,
SecondaryListItemsProps,
SecondaryListItemsEffects,
SecondaryListItemsComputed,
SecondaryListItemsParentState,
SecondaryListItemsParentEffects
>({}, ({ effects }) => (
<div>
<ListItem button onClick={() => effects.disconnect()}>
<ListItemIcon style={ICON_STYLE}>
<Icon icon='sign-out-alt' />
</ListItemIcon>
<ListItemText primary={<IntlMessage id='disconnect' />} />
</ListItem>
</div>
))
// -----------------------------------------------------------------------------
// Default bootstrap 4 colors
// https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss#L67-L74
const mdTheme = createTheme({
background: {
primary: {
dark: '#111111',
light: '#FFFFFF',
},
},
palette: {
error: {
main: '#dc3545',
},
info: {
main: '#17a2b8',
},
primary: {
dark: '#168FFF',
light: '#0085FF',
main: '#007bff',
},
secondary: {
main: '#6c757d',
},
success: {
main: '#28a745',
},
warning: {
main: '#ffc107',
},
},
components: {
MuiTab: {
styleOverrides: {
root: {
color: '#E8E8E8',
fontStyle: 'medium',
fontSize: '1.25em',
textAlign: 'center',
},
},
},
},
typography: {
fontFamily: 'inter',
h1: {
fontWeight: 500,
fontSize: '3em',
fontStyle: 'medium',
lineHeight: '3.75em',
},
h2: {
fontWeight: 500,
fontSize: '2.25em',
fontStyle: 'medium',
},
h3: {
fontWeight: 500,
fontSize: '1.5em',
fontStyle: 'medium',
lineHeight: '2em',
},
h4: {
fontWeight: 500,
fontSize: '1.25em',
fontStyle: 'medium',
lineHeight: '1.75em',
},
h5: {
fontWeight: 500,
fontSize: '1em',
fontStyle: 'medium',
lineHeight: '1.50em',
},
h6: {
fontWeight: 500,
fontSize: '0.8em',
fontStyle: 'medium',
lineHeight: '1.25em',
},
caption: {
// styleName: Caps / Caps 1 - 14 Semi Bold
fontSize: '0.9em',
fontStyle: 'normal',
fontWeight: 600,
lineHeight: '1.25em',
verticalAlign: 'top',
letterSpacing: '0.04em',
textAlign: 'left',
},
body2: {
// styleName: Paragraph / P2 - 16
fontSize: '1em',
fontStyle: 'normal',
fontWeight: 400,
lineHeight: '1.5em',
letterSpacing: '0em',
textAlign: 'left',
},
},
})
const FullPage = styledComponent.div`
height: 100vh;
display: flex;
flex-direction: column;
`
interface ParentState {
objectsByType: ObjectsByType
xapi: XapiConnection
}
interface State {
connected: boolean
drawerOpen: boolean
error: React.ReactNode
xapiHostname: string
}
interface Props {}
interface ParentEffects {}
interface Effects {
connectToXapi: (password: string, rememberMe: boolean) => void
disconnect: () => void
toggleDrawer: () => void
}
interface Computed {
objectsFetched: boolean
pool?: Pool
url: string
vms?: Map<string, Vm>
}
const App = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
initialState: () => ({
connected: Cookies.get('sessionId') !== undefined,
drawerOpen: false,
error: '',
objectsByType: undefined,
xapi: undefined,
xapiHostname: process.env.XAPI_HOST || window.location.host,
}),
effects: {
initialize: async function () {
const xapi = (this.state.xapi = new XapiConnection())
xapi.on('connected', () => {
this.state.connected = true
})
xapi.on('disconnected', () => {
this.state.connected = false
})
xapi.on('objects', (objectsByType: ObjectsByType) => {
this.state.objectsByType = objectsByType
})
try {
await xapi.reattachSession(this.state.url)
} catch (err) {
if (err?.code !== 'SESSION_INVALID') {
throw err
}
console.log('Session ID is invalid. Asking for credentials.')
}
},
toggleDrawer: function () {
this.state.drawerOpen = !this.state.drawerOpen
},
connectToXapi: async function (password, rememberMe = false) {
try {
await this.state.xapi.connect({
url: this.state.url,
user: 'root',
password,
rememberMe,
})
} catch (err) {
if (err?.code !== 'SESSION_AUTHENTICATION_FAILED') {
throw err
}
this.state.error = <IntlMessage id='badCredentials' />
}
},
disconnect: async function () {
await this.state.xapi.disconnect()
this.state.connected = false
},
},
computed: {
objectsFetched: state => state.objectsByType !== undefined,
pool: state => (state.objectsFetched ? state.objectsByType?.get('pool')?.keySeq().first() : undefined),
vms: state =>
state.objectsFetched
? state.objectsByType
?.get('VM')
?.filter((vm: Vm) => !vm.is_control_domain && !vm.is_a_snapshot && !vm.is_a_template)
: undefined,
url: state => `${window.location.protocol}//${state.xapiHostname}`,
},
},
({ effects, state }) => (
<IntlProvider messages={messagesEn} locale='en'>
{/* Provided by this template: https://github.com/mui-org/material-ui/tree/next/docs/src/pages/getting-started/templates/dashboard */}
<ThemeProvider theme={mdTheme}>
<Modal />
{!state.connected ? (
<Signin />
) : !state.objectsFetched ? (
<IntlMessage id='loading' />
) : (
<>
<Router>
<Switch>
<Route exact path={redirectPaths}>
<Redirect to={`/infrastructure/pool/${state.pool.$id}/dashboard`} />
</Route>
<Route exact path='/vm-list'>
{state.vms !== undefined && (
<>
<p>There are {state.vms.size} VMs!</p>
<ul>
{state.vms.valueSeq().map((vm: Vm) => (
<li key={vm.$id}>
<Link to={vm.$id}>
{vm.name_label} - {vm.name_description} ({vm.power_state})
</Link>
</li>
))}
</ul>
</>
)}
</Route>
<Route exact path='/styleguide'>
<StyleGuide />
</Route>
<Route exact path='/styleguide/foo'>
<StyleGuide />
</Route>
<Route exact path='/pool'>
<PoolTab />
</Route>
<Route path='/'>
<Box sx={{ display: 'flex' }}>
<CssBaseline />
<AppBar position='absolute' open={state.drawerOpen}>
<Toolbar
sx={{
pr: '24px', // keep right padding when drawer closed
}}
>
<IconButton
edge='start'
color='inherit'
aria-label='open drawer'
onClick={effects.toggleDrawer}
sx={{
marginRight: '36px',
...(state.drawerOpen && { display: 'none' }),
}}
>
<MenuIcon />
</IconButton>
<Typography component='h1' variant='h6' color='inherit' noWrap sx={{ flexGrow: 1 }}>
<Switch>
<Route path='/infrastructure'>
<IntlMessage id='infrastructure' />
</Route>
<Route path='/about'>
<IntlMessage id='about' />
</Route>
<Route>
<IntlMessage id='notFound' />
</Route>
</Switch>
</Typography>
{/* <IconButton color='inherit'>
<Badge badgeContent={4} color='secondary'>
<NotificationsIcon />
</Badge>
</IconButton> */}
</Toolbar>
</AppBar>
<Drawer variant='permanent' open={state.drawerOpen}>
<Toolbar
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
px: [1],
}}
>
<IconButton onClick={effects.toggleDrawer}>
<ChevronLeftIcon />
</IconButton>
</Toolbar>
<Divider />
<List>
<MainListItems />
</List>
<Divider />
<List>
<SecondaryListItems />
</List>
</Drawer>
<Box
component='main'
sx={{
backgroundColor: theme =>
theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900],
flexGrow: 1,
height: '100vh',
overflow: 'auto',
}}
>
<Switch>
<Route path='/infrastructure'>
<FullPage>
<Toolbar />
<Infrastructure />
</FullPage>
</Route>
<Route path='/about'>
<Toolbar />
<Container maxWidth='lg' sx={{ mt: 4, mb: 4 }}>
<p>
Check out{' '}
<Link to='https://xen-orchestra.com/blog/xen-orchestra-lite/'>Xen Orchestra Lite</Link>{' '}
dev blog.
</p>
<p>
<IntlMessage id='versionValue' values={{ version: process.env.NPM_VERSION }} />
</p>
</Container>
</Route>
<Route>
<Toolbar />
<IntlMessage id='pageNotFound' />
</Route>
</Switch>
</Box>
</Box>
</Route>
</Switch>
</Router>
</>
)}
</ThemeProvider>
</IntlProvider>
)
)
export default App

View File

@@ -0,0 +1,57 @@
import React from 'react'
import LoadingButton, { LoadingButtonProps } from '@mui/lab/LoadingButton'
import { withState } from 'reaclette'
interface ParentState {}
interface State {
isLoading: boolean
}
// Omit the `onClick` props to rewrite its own one.
interface Props extends Omit<LoadingButtonProps, 'onClick'> {
onClick: (data: Record<string, unknown>) => Promise<void>
// to pass props with the following pattern: "data-something"
[key: string]: unknown
}
interface ParentEffects {}
interface Effects {
_onClick: React.MouseEventHandler<HTMLButtonElement>
}
interface Computed {}
const ActionButton = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
initialState: () => ({ isLoading: false }),
effects: {
_onClick: function () {
this.state.isLoading = true
const data: Record<string, unknown> = {}
Object.keys(this.props).forEach(key => {
if (key.startsWith('data-')) {
data[key.slice(5)] = this.props[key]
}
})
return this.props.onClick(data).finally(() => (this.state.isLoading = false))
},
},
},
({ children, color = 'secondary', effects, onClick, resetState, state, variant = 'contained', ...props }) => (
<LoadingButton
color={color}
disabled={state.isLoading}
fullWidth
loading={state.isLoading}
onClick={effects._onClick}
variant={variant}
{...props}
>
{children}
</LoadingButton>
)
)
export default ActionButton

View File

@@ -0,0 +1,26 @@
import React from 'react'
import { Button as MuiButton, ButtonProps } from '@mui/material'
import { withState } from 'reaclette'
interface ParentState {}
interface State {}
interface Props extends ButtonProps {}
interface ParentEffects {}
interface Effects {}
interface Computed {}
const Button = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{},
({ children, color = 'secondary', effects, resetState, state, variant = 'contained', ...props }) => (
<MuiButton color={color} fullWidth variant={variant} {...props}>
{children}
</MuiButton>
)
)
export default Button

View File

@@ -0,0 +1,22 @@
import React from 'react'
import { CheckboxProps, Checkbox as MuiCheckbox } from '@mui/material'
import { withState } from 'reaclette'
interface ParentState {}
interface State {}
interface Props extends CheckboxProps {}
interface ParentEffects {}
interface Effects {}
interface Computed {}
const Checkbox = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{},
({ effects, resetState, state, ...props }) => <MuiCheckbox {...props} />
)
export default Checkbox

View File

@@ -0,0 +1,193 @@
import React from 'react'
import RFB from '@novnc/novnc/lib/rfb'
import styled from 'styled-components'
import { fibonacci } from 'iterable-backoff'
import { withState } from 'reaclette'
import IntlMessage from './IntlMessage'
import { confirm } from './Modal'
import XapiConnection, { ObjectsByType, Vm } from '../libs/xapi'
interface ParentState {
objectsByType: ObjectsByType
xapi: XapiConnection
}
interface State {
// Type error with HTMLDivElement.
// See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451
container: React.RefObject<any>
// See https://github.com/vatesfr/xen-orchestra/pull/5722#discussion_r619296074
rfb: any
rfbConnected: boolean
timeout?: NodeJS.Timeout
tryToReconnect: boolean
url?: URL
}
interface Props {
scale: number
setCtrlAltDel: (sendCtrlAltDel: Effects['sendCtrlAltDel']) => void
vmId: string
}
interface ParentEffects {}
interface Effects {
_connect: () => Promise<void>
_handleConnect: () => void
_handleDisconnect: () => Promise<void>
sendCtrlAltDel: () => void
}
interface Computed {}
interface PropsStyledConsole {
scale: number
visible: boolean
}
enum Protocols {
http = 'http:',
https = 'https:',
ws = 'ws:',
wss = 'wss:',
}
const StyledConsole = styled.div<PropsStyledConsole>`
height: ${props => props.scale}%;
margin: auto;
visibility: ${props => (props.visible ? 'visible' : 'hidden')};
width: ${props => props.scale}%;
`
// https://github.com/novnc/noVNC/blob/master/docs/API.md
const Console = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
initialState: () => ({
container: React.createRef(),
rfb: undefined,
rfbConnected: false,
timeout: undefined,
tryToReconnect: true,
url: undefined,
}),
effects: {
initialize: function () {
this.effects._connect()
},
_handleConnect: function () {
this.state.rfbConnected = true
},
_handleDisconnect: async function () {
this.state.rfbConnected = false
const {
state: { objectsByType, url },
effects: { _connect },
} = this
const { protocol } = window.location
if (protocol === Protocols.https) {
try {
await fetch(`${protocol}//${url?.host}`)
} catch (error) {
console.error(error)
try {
await confirm({
icon: 'exclamation-triangle',
message: (
<a href={`${protocol}//${url?.host}`} rel='noopener noreferrer' target='_blank'>
<IntlMessage
id='unreachableHost'
values={{
name: objectsByType.get('host')?.find(host => host.address === url?.host)?.name_label,
}}
/>
</a>
),
title: <IntlMessage id='connectionError' />,
})
} catch {
this.state.tryToReconnect = false
}
}
}
if (this.state.tryToReconnect) {
_connect()
}
},
_connect: async function () {
const { vmId } = this.props
const { objectsByType, rfb, xapi } = this.state
let lastError: unknown
// 8 tries mean 54s
for (const delay of fibonacci().toMs().take(8)) {
try {
const consoles = (objectsByType.get('VM')?.get(vmId) as Vm)?.$consoles.filter(
vmConsole => vmConsole.protocol === 'rfb'
)
if (rfb !== undefined) {
rfb.removeEventListener('connect', this.effects._handleConnect)
rfb.removeEventListener('disconnect', this.effects._handleDisconnect)
}
if (consoles === undefined || consoles.length === 0) {
throw new Error('Could not find VM console')
}
if (xapi.sessionId === undefined) {
throw new Error('Not connected to XAPI')
}
this.state.url = new URL(consoles[0].location)
this.state.url.protocol = window.location.protocol === Protocols.https ? Protocols.wss : Protocols.ws
this.state.url.searchParams.set('session_id', xapi.sessionId)
this.state.rfb = new RFB(this.state.container.current, this.state.url, {
wsProtocols: ['binary'],
})
this.state.rfb.addEventListener('connect', this.effects._handleConnect)
this.state.rfb.addEventListener('disconnect', this.effects._handleDisconnect)
this.state.rfb.scaleViewport = true
this.props.setCtrlAltDel(this.effects.sendCtrlAltDel)
return
} catch (error) {
lastError = error
await new Promise(resolve => (this.state.timeout = setTimeout(resolve, delay)))
}
}
throw lastError
},
finalize: function () {
const { rfb, timeout } = this.state
rfb.removeEventListener('connect', this.effects._handleConnect)
rfb.removeEventListener('disconnect', this.effects._handleDisconnect)
if (timeout !== undefined) {
clearTimeout(timeout)
}
},
sendCtrlAltDel: async function () {
await confirm({
message: <IntlMessage id='confirmCtrlAltDel' />,
title: <IntlMessage id='ctrlAltDel' />,
})
this.state.rfb.sendCtrlAltDel()
},
},
},
({ scale, state }) => (
<>
{state.rfb !== undefined && !state.rfbConnected && (
<p>
<IntlMessage id={state.tryToReconnect ? 'reconnectionAttempt' : 'hostUnreachable'} />
</p>
)}
<StyledConsole ref={state.container} scale={scale} visible={state.rfbConnected} />
</>
)
)
export default Console

View File

@@ -0,0 +1,30 @@
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconName as _IconName, library, SizeProp } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'
import { useTheme } from '@mui/material/styles'
library.add(fas)
const Icon = ({
color,
htmlColor,
icon,
size,
}: {
color?: 'inherit' | 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning'
htmlColor?: string
icon: _IconName
size?: SizeProp
}): JSX.Element => {
const { palette } = useTheme()
return (
<FontAwesomeIcon
icon={icon}
size={size}
color={htmlColor ?? (color !== undefined ? palette[color][palette.mode] : undefined)}
/>
)
}
export default Icon
export type IconName = _IconName

View File

@@ -0,0 +1,26 @@
import React from 'react'
import { TextField, TextFieldProps } from '@mui/material'
import { withState } from 'reaclette'
interface ParentState {}
interface State {}
// An interface can only extend an object type or intersection
// of object types with statically known members.
type Props = _Props & TextFieldProps
interface _Props {}
interface ParentEffects {}
interface Effects {}
interface Computed {}
const Input = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{},
({ effects, resetState, state, ...props }) => <TextField fullWidth {...props} />
)
export default Input

View File

@@ -0,0 +1,21 @@
import React, { ElementType, ReactElement, ReactNode } from 'react'
import { FormattedMessage, MessageDescriptor, useIntl } from 'react-intl'
import intlMessage from '../lang/en.json'
// Extends FormattedMessage not working: "FormattedMessage refers to a value, but is being used as a type here"
// https://stackoverflow.com/questions/62059408/reactjs-and-typescript-refers-to-a-value-but-is-being-used-as-a-type-here-ts
// InstanceType<typeof FormattedMessage> not working: "Type [...] does not satisfy the constraint abstract new (...args: any) => any."
// See https://formatjs.io/docs/react-intl/components/#formattedmessage
interface Props extends MessageDescriptor {
children?: (chunks: ReactElement) => ReactElement
id?: keyof typeof intlMessage
tagName?: ElementType
values?: Record<string, ReactNode>
}
const IntlMessage = (props: Props): JSX.Element => <FormattedMessage {...props} />
export function translate(message: MessageDescriptor){
return useIntl().formatMessage(message)
}
export default React.memo(IntlMessage)

View File

@@ -0,0 +1,38 @@
import MaterialLink from '@mui/material/Link'
import React from 'react'
import { Link as RouterLink } from 'react-router-dom'
import { withState } from 'reaclette'
interface ParentState {}
interface State {}
interface Props {
children: React.ReactNode
decorated?: boolean
to?: string
}
interface ParentEffects {}
interface Effects {}
interface Computed {}
const UNDECORATED_LINK = { textDecoration: 'none', color: 'inherit' }
const Link = withState<State, Props, Effects, Computed, ParentState, ParentEffects>({}, ({ to, decorated = true, children }) =>
to === undefined ? (
<>{children}</>
) : to.startsWith('http') ? (
<MaterialLink style={decorated ? undefined : UNDECORATED_LINK} target='_blank' rel='noopener noreferrer' href={to}>
{children}
</MaterialLink>
) : (
<RouterLink style={decorated ? undefined : UNDECORATED_LINK} component={MaterialLink} to={to}>
{children}
</RouterLink>
)
)
export default Link

View File

@@ -0,0 +1,152 @@
import React from 'react'
import { ButtonProps, Dialog, DialogContent, DialogContentText, DialogActions, DialogTitle } from '@mui/material'
import { withState } from 'reaclette'
import Button from './Button'
import Icon, { IconName } from './Icon'
import IntlMessage from './IntlMessage'
type ModalButton = {
color?: ButtonProps['color']
label: string | React.ReactNode
reason?: unknown
value?: unknown
}
interface GeneralParamsModal {
icon: IconName
message: string | React.ReactNode
title: string | React.ReactNode
}
interface ModalParams extends GeneralParamsModal {
buttonList: ModalButton[]
}
let instance: EffectContext<State, Props, Effects, Computed, ParentState, ParentEffects> | undefined
const modal = ({ buttonList, icon, message, title }: ModalParams) =>
new Promise((resolve, reject) => {
if (instance === undefined) {
throw new Error('No modal instance')
}
instance.state.buttonList = buttonList
instance.state.icon = icon
instance.state.message = message
instance.state.onReject = reject
instance.state.onSuccess = resolve
instance.state.showModal = true
instance.state.title = title
})
export const alert = (params: GeneralParamsModal): Promise<unknown> => {
const buttonList: ModalButton[] = [
{
label: <IntlMessage id='ok' />,
color: 'primary',
value: 'success',
},
]
return modal({ ...params, buttonList })
}
export const confirm = (params: GeneralParamsModal): Promise<unknown> => {
const buttonList: ModalButton[] = [
{
label: <IntlMessage id='confirm' />,
value: 'confirm',
color: 'success',
},
{
label: <IntlMessage id='cancel' />,
color: 'secondary',
reason: 'cancel',
},
]
return modal({ ...params, buttonList })
}
interface ParentState {}
interface State {
buttonList?: ModalButton[]
icon?: IconName
message?: string | React.ReactNode
onReject?: (reason: unknown) => void
onSuccess?: (value: unknown) => void
showModal: boolean
title?: string | React.ReactNode
}
interface Props {}
interface ParentEffects {}
interface Effects {
closeModal: () => void
reject: (reason: unknown) => void
}
interface Computed {}
const Modal = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
initialState: () => ({
buttonList: undefined,
icon: undefined,
message: undefined,
onReject: undefined,
onSuccess: undefined,
showModal: false,
title: undefined,
}),
effects: {
initialize: function () {
if (instance !== undefined) {
throw new Error('Modal is a singelton')
}
instance = this
},
closeModal: function () {
this.state.showModal = false
},
reject: function (reason) {
this.state.onReject?.(reason)
this.effects.closeModal()
},
},
},
({ effects, state }) => {
const { closeModal, reject } = effects
const { buttonList, icon, message, onReject, onSuccess, showModal, title } = state
return showModal ? (
<Dialog open={showModal} onClose={reject}>
<DialogTitle>
{icon !== undefined && <Icon icon={icon} />} {title}
</DialogTitle>
<DialogContent>
<DialogContentText>{message}</DialogContentText>
</DialogContent>
<DialogActions>
{buttonList?.map(({ label, reason, value, ...props }, index) => {
const onClick = () => {
if (value !== undefined) {
onSuccess?.(value)
} else {
onReject?.(reason)
}
closeModal()
}
return (
<Button key={index} onClick={onClick} {...props}>
{label}
</Button>
)
})}
</DialogActions>
</Dialog>
) : null
}
)
export default Modal

View File

@@ -0,0 +1,63 @@
import React from 'react'
import { withState } from 'reaclette'
import Icon, { IconName } from './Icon'
import Button, { ButtonProps } from '@mui/material/Button'
import ButtonGroup, { ButtonGroupClassKey } from '@mui/material/ButtonGroup'
import Stack from '@mui/material/Stack'
import Typography, { TypographyClassKey } from '@mui/material/Typography'
import { Theme } from '@mui/material/styles'
interface ParentState {}
interface State {}
interface Action extends ButtonProps {
icon: IconName
}
interface ParentEffects {}
interface Effects {}
interface Computed {}
const DEFAULT_TITLE_STYLE = { marginLeft: '0.5em', flex: 1, fontSize: '250%' }
const DEFAULT_BUTTONGROUP_STYLE = { margin: '0.5em', flex: 0 }
const DEFAULT_STACK_STYLE = {
backgroundColor: (theme: Theme) => {
const { background, palette } = theme
return palette.mode === 'light' ? background.primary.light : background.primary.dark
},
paddingTop: '1em',
}
interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
// Accepts an array of Actions. An action accepts all the props of a Button + an icon
actions?: Array<Action>
// the props passed to the title, accepts all the keys of Typography
titleProps?: TypographyClassKey
// the props passed to the button group, accepts all the keys of a ButtonGroup
buttonGroupProps?: ButtonGroupClassKey
}
const PanelHeader = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{},
({ actions = [], titleProps = {}, buttonGroupProps = {}, children = null }) => (
<Stack direction='row' justifyContent='space-between' alignItems='center' sx={DEFAULT_STACK_STYLE}>
<Typography variant='h2' sx={DEFAULT_TITLE_STYLE} {...titleProps}>
{children}
</Typography>
<ButtonGroup sx={DEFAULT_BUTTONGROUP_STYLE} {...buttonGroupProps}>
{(actions as Array<Action>)?.map(({ icon, ...actionProps }) => (
<Button {...actionProps} key={actionProps.key}>
<Icon icon={icon} />
</Button>
))}
</ButtonGroup>
</Stack>
)
)
export default PanelHeader

View File

@@ -0,0 +1,87 @@
import Box from '@mui/material/Box'
import React from 'react'
import CircularProgress, { CircularProgressProps } from '@mui/material/CircularProgress'
import { styled } from '@mui/material/styles'
import { Typography } from '@mui/material'
import { withState } from 'reaclette'
const BackgroundBox = styled(Box)({
position: 'absolute',
})
const BackgroundCircle = styled(CircularProgress)({
color: '#e3dede',
})
const Container = styled(Box)({
display: 'inline-flex',
position: 'relative',
})
const StyledLabel = styled(Typography)(({ color, theme: { palette } }) => ({
color: (palette[(color as string) ?? 'primary'] ?? palette.primary).main,
textAlign: 'center',
}))
const LabelBox = styled(Box)({
alignItems: 'center',
bottom: 0,
display: 'flex',
height: '80%',
justifyContent: 'center',
left: 0,
margin: 'auto',
overflow: 'hidden',
position: 'absolute',
right: 0,
top: 0,
width: '80%',
})
interface ParentState {}
interface State {}
interface Props {
color?: CircularProgressProps['color']
label?: string
max?: number
showLabel?: boolean
size?: number
value: number
}
interface ParentEffects {}
interface Effects {}
interface Computed {
label: string
progress: number
}
const ProgressCircle = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
computed: {
label: ({ progress }, { label }) => label ?? `${progress}%`,
progress: (_, { max = 100, value }) => Math.round((value / max) * 100),
},
},
({ color = 'success', showLabel = true, size = 100, state: { label, progress } }) => (
<Container>
<BackgroundBox>
<BackgroundCircle variant='determinate' value={100} size={size} />
</BackgroundBox>
<CircularProgress aria-label={label} color={color} size={size} value={progress} variant='determinate' />
{showLabel && (
<LabelBox>
<StyledLabel variant='h5' color={color}>
{label}
</StyledLabel>
</LabelBox>
)}
</Container>
)
)
export default ProgressCircle

View File

@@ -0,0 +1,7 @@
import React from 'react'
type Props = Omit<React.ComponentPropsWithoutRef<'input'>, 'type'>
const RangeInput = React.memo((props: Props) => <input {...props} type='range' />)
export default RangeInput

View File

@@ -0,0 +1,97 @@
import FormControl from '@mui/material/FormControl'
import MenuItem from '@mui/material/MenuItem'
import React from 'react'
import SelectMaterialUi, { SelectProps } from '@mui/material/Select'
import { iteratee } from 'lodash'
import { SelectChangeEvent } from '@mui/material'
import { withState } from 'reaclette'
import IntlMessage from './IntlMessage'
type AdditionalProps = Record<string, any>
interface ParentState {}
interface State {}
interface Props extends SelectProps {
additionalProps?: AdditionalProps
onChange: (e: SelectChangeEvent<unknown>) => void
optionRenderer?: string | { (item: any): number | string }
options: any[] | undefined
value: any
valueRenderer?: string | { (item: any): number | string }
}
interface ParentEffects {}
interface Effects {}
interface Computed {
renderOption: (item: any, additionalProps?: AdditionalProps) => React.ReactNode
renderValue: (item: any, additionalProps?: AdditionalProps) => number | string
options?: JSX.Element[]
}
const Select = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
computed: {
// @ts-ignore
renderOption: (_, { optionRenderer }) => iteratee(optionRenderer),
// @ts-ignore
renderValue: (_, { valueRenderer }) => iteratee(valueRenderer),
options: (state, { additionalProps, options, optionRenderer, valueRenderer }) =>
options?.map(item => {
const label =
optionRenderer === undefined
? item.name ?? item.label ?? item.name_label
: state.renderOption(item, additionalProps)
const value =
valueRenderer === undefined ? item.value ?? item.id ?? item.$id : state.renderValue(item, additionalProps)
if (value === undefined) {
console.error('Computed value is undefined')
}
return (
<MenuItem key={value} value={value}>
{label}
</MenuItem>
)
}),
},
},
({
additionalProps,
displayEmpty = true,
effects,
multiple,
options,
required,
resetState,
state,
value,
...props
}) => (
<FormControl>
<SelectMaterialUi
multiple={multiple}
required={required}
displayEmpty={displayEmpty}
value={value ?? (multiple ? [] : '')}
{...props}
>
{!multiple && (
<MenuItem value=''>
<em>
<IntlMessage id='none' />
</em>
</MenuItem>
)}
{state.options}
</SelectMaterialUi>
</FormControl>
)
)
export default Select

View File

@@ -0,0 +1,73 @@
import React from 'react'
import styled from 'styled-components'
import { withState } from 'reaclette'
import IntlMessage from './IntlMessage'
export type Column<Type> = {
header: React.ReactNode
id?: string
render: { (item: Type): React.ReactNode }
}
type Item = {
id?: string
[key: string]: any
}
interface ParentState {}
interface State {}
interface Props {
collection: Item[] | undefined
columns: Column<any>[]
placeholder?: JSX.Element
}
interface ParentEffects {}
interface Effects {}
interface Computed {}
const StyledTable = styled.table`
border: 1px solid #333;
td {
border: 1px solid #333;
}
thead {
background-color: #333;
color: #fff;
}
`
const Table = withState<State, Props, Effects, Computed, ParentState, ParentEffects>({}, ({ collection, columns, placeholder }) =>
collection !== undefined ? (
collection.length !== 0 ? (
<StyledTable>
<thead>
<tr>
{columns.map((col, index) => (
<td key={col.id ?? index}>{col.header}</td>
))}
</tr>
</thead>
<tbody>
{collection.map((item, index) => (
<tr key={item.id ?? index}>
{columns.map((col, index) => (
<td key={col.id ?? index}>{col.render(item)}</td>
))}
</tr>
))}
</tbody>
</StyledTable>
) : (
placeholder ?? <IntlMessage id='noData' />
)
) : (
<IntlMessage id='loading' />
)
)
export default Table

View File

@@ -0,0 +1,114 @@
import Box from '@mui/material/Box'
import React from 'react'
import Tab from '@mui/material/Tab'
import TabContext from '@mui/lab/TabContext'
import TabList from '@mui/lab/TabList'
import TabPanel from '@mui/lab/TabPanel'
import Typography from '@mui/material/Typography'
import { RouteComponentProps } from 'react-router-dom'
import { withState } from 'reaclette'
import { withRouter } from 'react-router'
import IntlMessage from '../components/IntlMessage'
const BOX_STYLE = { borderBottom: 1, borderColor: 'divider', marginTop: '0.5em' }
interface ParentState {}
interface State {
value: string
}
interface Tab {
component?: React.ReactNode
disabled?: boolean
label: React.ReactNode
}
interface UrlTab extends Tab {
pathname: string
value?: any
}
interface NoUrlTab extends Tab {
value: any
}
// For compatibility with 'withRouter'
interface Props extends RouteComponentProps {
indicatorColor?: 'primary' | 'secondary'
textColor?: 'inherit' | 'primary' | 'secondary'
// tabs = [
// {
// component: <span>BAR</span>,
// pathname: '/path',
// label: (
// <span>
// <Icon icon='cloud' /> {labelA}
// </span>
// ),
// },
// ]
tabs: Array<NoUrlTab | UrlTab>
useUrl?: boolean
value?: any
}
interface ParentEffects {}
interface Effects {
onChange: (event: React.SyntheticEvent, value: string) => void
}
interface Computed {}
// TODO: improve view as done in the model(figma).
const pageUnderConstruction = (
<div style={{ color: '#0085FF', textAlign: 'center' }}>
<Typography variant='h2'>
<IntlMessage id='xoLiteUnderConstruction' />
</Typography>
<Typography variant='h3'>
<IntlMessage id='newFeaturesUnderConstruction' />
</Typography>
</div>
)
const Tabs = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
initialState: ({ location: { pathname }, tabs, useUrl = false, value }) => ({
value: (useUrl && pathname) || (value ?? tabs[0].value ?? tabs[0].pathname),
}),
effects: {
onChange: function (_, value) {
if (this.props.useUrl) {
const { history, tabs } = this.props
history.push(tabs.find(tab => (tab.value ?? tab.pathname) === value).pathname)
}
this.state.value = value
},
},
},
({ effects, state: { value }, indicatorColor, textColor, tabs }) => (
<TabContext value={value}>
<Box sx={BOX_STYLE}>
<TabList indicatorColor={indicatorColor} onChange={effects.onChange} textColor={textColor}>
{tabs.map((tab: UrlTab | NoUrlTab) => {
const value = tab.value ?? tab.pathname
return <Tab disabled={tab.disabled} key={value} label={tab.label} value={value} />
})}
</TabList>
</Box>
{tabs.map((tab: UrlTab | NoUrlTab) => {
const value = tab.value ?? tab.pathname
return (
<TabPanel key={value} value={value}>
{tab.component === undefined ? pageUnderConstruction : tab.component}
</TabPanel>
)
})}
</TabContext>
)
)
export default withRouter(Tabs)

View File

@@ -0,0 +1,196 @@
import classNames from 'classnames'
import React, { useEffect } from 'react'
import Tooltip from '@mui/material/Tooltip'
import TreeView from '@mui/lab/TreeView'
import TreeItem, { useTreeItem, TreeItemContentProps } from '@mui/lab/TreeItem'
import { withState } from 'reaclette'
import { useHistory } from 'react-router-dom'
import Icon from '../components/Icon'
interface ParentState {}
interface State {
expandedNodes?: Array<string>
selectedNodes?: Array<string>
}
export interface ItemType {
children?: Array<ItemType>
id: string
label: React.ReactElement
to?: string
tooltip?: React.ReactNode
}
interface Props {
// collection = [
// {
// id: 'idA',
// label: (
// <span>
// <Icon icon='warehouse' /> {labelA}
// </span>
// ),
// to: '/routeA',
// children: [
// {
// id: 'ida',
// label: label: (
// <span>
// <Icon icon='server' /> {labela}
// </span>
// ),
// },
// ],
// },
// {
// id: 'idB',
// label: (
// <span>
// <Icon icon='warehouse' /> {labelB}
// </span>
// ),
// to: '/routeB',
// tooltip: <IntlMessage id='tooltipB' />
// }
// ]
collection: Array<ItemType>
defaultSelectedNodes?: Array<string>
}
interface CustomContentProps extends TreeItemContentProps {
defaultSelectedNode?: string
to?: string
}
interface ParentEffects {}
interface Effects {
setExpandedNodeIds: (event: React.SyntheticEvent, nodeIds: Array<string>) => void
setSelectedNodeIds: (event: React.SyntheticEvent, nodeIds: Array<string>) => void
}
interface Computed {
defaultSelectedNode?: string
}
// Inspired by https://mui.com/components/tree-view/#contentcomponent-prop.
const CustomContent = React.forwardRef(function CustomContent(props: CustomContentProps, ref) {
const { classes, className, defaultSelectedNode, expansionIcon, label, nodeId, to } = props
const { focused, handleExpansion, handleSelection, selected } = useTreeItem(nodeId)
const history = useHistory()
useEffect(() => {
// There can only be one node selected at once for now.
// Auto-revealing more than one node in the tree would require a different implementation.
if (defaultSelectedNode === nodeId) {
ref?.current?.scrollIntoView()
}
}, [])
useEffect(() => {
if (selected) {
to !== undefined && history.push(to)
}
}, [selected])
const handleExpansionClick = (event: React.SyntheticEvent) => {
event.stopPropagation()
handleExpansion(event)
}
return (
<span
className={classNames(className, { [classes.focused]: focused, [classes.selected]: selected })}
onClick={handleSelection}
ref={ref}
>
<span className={classes.iconContainer} onClick={handleExpansionClick}>
{expansionIcon}
</span>
<span className={classes.label}>{label}</span>
</span>
)
})
const renderItem = ({ children, id, label, to, tooltip }: ItemType, defaultSelectedNode?: string) => {
return (
<TreeItem
ContentComponent={CustomContent}
// FIXME: ContentProps should only be React.HTMLAttributes<HTMLElement> or undefined, it doesn't support other type.
// when https://github.com/mui-org/material-ui/issues/28668 is fixed, remove 'as CustomContentProps'.
ContentProps={{ defaultSelectedNode, to } as CustomContentProps}
label={tooltip ? <Tooltip title={tooltip}>{label}</Tooltip> : label}
key={id}
nodeId={id}
>
{Array.isArray(children) ? children.map(item => renderItem(item, defaultSelectedNode)) : null}
</TreeItem>
)
}
const Tree = withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
{
initialState: ({ collection, defaultSelectedNodes }) => {
if (defaultSelectedNodes === undefined) {
return {
expandedNodes: [collection[0].id],
selectedNodes: [],
}
}
// expandedNodes should contain all nodes up to the defaultSelectedNodes.
const expandedNodes = new Set<string>()
const pathToNode = new Set<string>()
const addExpandedNode = (collection: Array<ItemType> | undefined) => {
if (collection === undefined) {
return
}
for (const node of collection) {
if (defaultSelectedNodes.includes(node.id)) {
for (const nodeId of pathToNode) {
expandedNodes.add(nodeId)
}
}
pathToNode.add(node.id)
addExpandedNode(node.children)
pathToNode.delete(node.id)
}
}
addExpandedNode(collection)
return { expandedNodes: Array.from(expandedNodes), selectedNodes: defaultSelectedNodes }
},
effects: {
setExpandedNodeIds: function (_, nodeIds) {
this.state.expandedNodes = nodeIds
},
setSelectedNodeIds: function (_, nodeIds) {
this.state.selectedNodes = [nodeIds[0]]
},
},
computed: {
defaultSelectedNode: (_, { defaultSelectedNodes }) =>
defaultSelectedNodes !== undefined ? defaultSelectedNodes[0] : undefined,
},
},
({ effects, state: { defaultSelectedNode, expandedNodes, selectedNodes }, collection }) => (
<TreeView
defaultCollapseIcon={<Icon icon='chevron-up' />}
defaultExpanded={[collection[0].id]}
defaultExpandIcon={<Icon icon='chevron-down' />}
expanded={expandedNodes}
multiSelect
onNodeSelect={effects.setSelectedNodeIds}
onNodeToggle={effects.setExpandedNodeIds}
selected={selectedNodes}
>
{collection.map(item => renderItem(item, defaultSelectedNode))}
</TreeView>
)
)
export default Tree

View File

@@ -0,0 +1,26 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { Helmet } from 'react-helmet'
import { createGlobalStyle } from 'styled-components'
import App from './App/index'
const GlobalStyle = createGlobalStyle`
body {
margin: 0;
font-family: Arial, Verdana, Helvetica, Ubuntu, sans-serif;
box-sizing: border-box;
color: #212529;
}
`
ReactDOM.render(
<React.StrictMode>
<Helmet>
<link rel='shortcut icon' href='favicon.ico' />
</Helmet>
<GlobalStyle />
<App />
</React.StrictMode>,
document.getElementById('root')
)

View File

@@ -0,0 +1,55 @@
{
"about": "About",
"active": "Active",
"availableUpdates": "{nUpdates, number} available update{nUpdates, plural, one {} other {s}}",
"badCredentials": "Bad credentials",
"cancel": "Cancel",
"confirm": "Confirm",
"confirmCtrlAltDel": "Send Ctrl+Alt+Del to VM?",
"connect": "Connect",
"connectionError": "Connection error",
"consoleNotAvailable": "Console is only available for running VMs",
"ctrlAltDel": "Ctrl+Alt+Del",
"description": "Description",
"device": "Device",
"disconnect": "Disconnect",
"dns": "DNS",
"errorOccurred": "An error has occurred.",
"gateway": "Gateway",
"halted": "Halted",
"hosts": "Hosts",
"hostUnreachable": "Host unreachable",
"inactive": "Inactive",
"infrastructure": "Infrastructure",
"ip": "IP",
"loading": "Loading…",
"login": "Login",
"name": "Name",
"newFeaturesUnderConstruction": "New features are coming soon!",
"noHosts": "No hosts",
"noData": "No data",
"noImplemented": "Not implemented",
"noManagementPifs": "No management PIFs found",
"none": "None",
"noVms": "No VMs",
"notFound": "Not Found",
"pageNotFound": "This page doesn't exist.",
"xoLiteUnderConstruction": "XO Lite is under construction",
"noUpdatesAvailable": "No updates available",
"ok": "OK",
"password": "Password",
"paused": "Paused",
"reconnectionAttempt": "Trying to reconnect…",
"release": "Release",
"rememberMe": "Remember me",
"running": "Running",
"size": "Size",
"status": "Status",
"suspended": "Suspended",
"total": "Total",
"unreachableHost": "Click here to make sure your host ({name}) is reachable. You may have to allow self-signed SSL certificates in your browser.",
"vms": "VMs",
"version": "Version",
"versionValue": "Version {version}",
"vmStartLabel": "Start"
}

View File

@@ -0,0 +1,4 @@
{
"connect": "Connexion",
"vmStartLabel": "Démarrer"
}

View File

@@ -0,0 +1,205 @@
import Cookies from 'js-cookie'
import { EventEmitter } from 'events'
import { Map } from 'immutable'
import { Xapi } from 'xen-api'
export interface XapiObject {
$pool: Pool
$ref: string
$type: keyof types
$id: string
}
// Dictionary of XAPI types and their corresponding TypeScript types
interface types {
PIF: Pif
pool: Pool
VM: Vm
host: Host
}
// XAPI types ---
export interface Pif extends XapiObject {
device: string
DNS: string
gateway: string
IP: string
management: boolean
network: string
}
export interface Pool extends XapiObject {
name_label: string
}
export interface PoolUpdate {
changelog: {
author: string
date: Date
description: string
}
description: string
license: string
name: string
release: string
size: number
url: string
version: string
}
export interface Vm extends XapiObject {
$consoles: Array<{ protocol: string; location: string }>
is_a_snapshot: boolean
is_a_template: boolean
is_control_domain: boolean
name_description: string
name_label: string
power_state: string
resident_on: string
}
interface HostMetrics {
live: boolean
}
export interface Host extends XapiObject {
$metrics: HostMetrics
address: string
name_label: string
power_state: string
}
// --------
export interface ObjectsByType extends Map<string, Map<string, XapiObject>> {
get<NSV, T extends keyof types>(key: T, notSetValue: NSV): Map<string, types[T]> | NSV
get<T extends keyof types>(key: T): Map<string, types[T]> | undefined
}
export default class XapiConnection extends EventEmitter {
areObjectsFetched: Promise<void>
connected: boolean
objectsByType: ObjectsByType
sessionId?: string
_resolveObjectsFetched!: () => void
_xapi?: {
objects: EventEmitter & {
all: { [id: string]: XapiObject }
}
connect(): Promise<void>
disconnect(): Promise<void>
call: (method: string, ...args: unknown[]) => Promise<unknown>
_objectsFetched: Promise<void>
}
constructor() {
super()
this.objectsByType = Map() as ObjectsByType
this.connected = false
this.areObjectsFetched = new Promise(resolve => {
this._resolveObjectsFetched = resolve
})
}
async reattachSession(url: string): Promise<void> {
const sessionId = Cookies.get('sessionId')
if (sessionId === undefined) {
return
}
return this.connect({ url, sessionId })
}
async connect({
url,
user = 'root',
password,
sessionId,
rememberMe = Cookies.get('rememberMe') === 'true',
}: {
url: string
user?: string
password?: string
sessionId?: string
rememberMe?: boolean
}): Promise<void> {
const xapi = (this._xapi = new Xapi({
auth: { user, password, sessionId },
url,
watchEvents: true,
readonly: false,
}))
const updateObjects = (objects: { [id: string]: XapiObject }) => {
try {
this.objectsByType = this.objectsByType.withMutations(objectsByType => {
Object.entries(objects).forEach(([id, object]) => {
if (object === undefined) {
// Remove
objectsByType.forEach((objects, type) => {
objectsByType.set(type, objects.remove(id))
})
} else {
// Add or update
const { $type } = object
objectsByType.set($type, objectsByType.get($type, Map<string, XapiObject>()).set(id, object))
}
})
})
this.emit('objects', this.objectsByType)
} catch (err) {
console.error(err)
}
}
xapi.on('connected', () => {
this.sessionId = xapi.sessionId
this.connected = true
this.emit('connected')
})
xapi.on('disconnected', () => {
Cookies.remove('sessionId')
this.emit('disconnected')
})
xapi.on('sessionId', (sessionId: string) => {
if (rememberMe) {
Cookies.set('rememberMe', 'true', { expires: 7 })
}
Cookies.set('sessionId', sessionId, rememberMe ? { expires: 7 } : undefined)
})
await xapi.connect()
await xapi._objectsFetched
updateObjects(xapi.objects.all)
this._resolveObjectsFetched()
xapi.objects.on('add', updateObjects)
xapi.objects.on('update', updateObjects)
xapi.objects.on('remove', updateObjects)
}
disconnect(): Promise<void> | undefined {
Cookies.remove('rememberMe')
Cookies.remove('sessionId')
const { _xapi } = this
if (_xapi !== undefined) {
return _xapi.disconnect()
}
}
call(method: string, ...args: unknown[]): Promise<unknown> {
const { _xapi, connected } = this
if (!connected || _xapi === undefined) {
throw new Error('Not connected to XAPI')
}
return _xapi.call(method, ...args)
}
}

View File

@@ -0,0 +1,63 @@
{
"compilerOptions": {
/* Basic Options */
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
"jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "incremental": true, /* Enable incremental compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
"noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"strictNullChecks": true, /* Enable strict null checks. */
"strictFunctionTypes": true, /* Enable strict checking of function types. */
"strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
"strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"resolveJsonModule": true
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}
}

6
@xen-orchestra/lite/types/decs.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
declare module '@novnc/novnc/lib/rfb'
declare module 'human-format'
declare module 'iterable-backoff'
declare module 'json-rpc-protocol'
declare module 'promise-toolbox'
declare module 'xen-api'

View File

@@ -0,0 +1,42 @@
type RenderParams<State, Props, Effects, Computed, ParentState, ParentEffects> = {
readonly effects: Effects & ParentEffects
readonly state: State & ParentState & Computed
readonly resetState: () => void
} & Props
interface EffectContext<State, Props, Effects, Computed, ParentState, ParentEffects> {
readonly effects: Effects & ParentEffects
readonly state: State & ParentState & Computed
readonly props: Props
}
interface StateSpec<State, Props, Effects, Computed, ParentState, ParentEffects> {
initialState?: State | ((props: Props) => State) // what about Reaclette's state inheritance?
effects?: {
initialize?: () => void | Promise<void>
finalize?: () => void | Promise<void>
} & Effects &
ThisType<EffectContext<State, Props, Effects, Computed, ParentState, ParentEffects>>
computed?: {
[ComputedName in keyof Computed]: (
state: State & ParentState & Computed,
props: Props
) => Computed[ComputedName] | Promise<Computed[ComputedName]>
}
}
declare module 'reaclette' {
function provideState<State, Props, Effects, Computed, ParentState, ParentEffects>(
stateSpec: StateSpec<State, Props, Effects, Computed, ParentState, ParentEffects>
): (component: React.Component<Props>) => React.Component<Props>
function injectState<State, Props, Effects, Computed, ParentState, ParentEffects>(
// FIXME: also accept class components
component: React.FC<RenderParams<State, Props, Effects, Computed, ParentState, ParentEffects>>
): React.ElementType<Props>
function withState<State, Props, Effects, Computed, ParentState, ParentEffects>(
stateSpec: StateSpec<State, Props, Effects, Computed, ParentState, ParentEffects>,
component: React.FC<RenderParams<State, Props, Effects, Computed, ParentState, ParentEffects>>
): React.ElementType<Props>
}

21
@xen-orchestra/lite/types/theme.d.ts vendored Normal file
View File

@@ -0,0 +1,21 @@
import { Theme as ThemeMui, ThemeOptions as ThemeOptionsMui } from '@mui/material/styles'
declare module '@mui/material/styles' {
// FIXME: when https://github.com/microsoft/TypeScript/issues/40315 is fixed.
// issue: Type 'Theme'/'ThemeOptions' recursively references itself as a base type.
interface Theme extends ThemeMui {
background: {
primary: {
dark: string
light: string
}
}
}
interface ThemeOptions extends ThemeOptionsMui {
background?: {
primary?: {
dark?: string
light?: string
}
}
}
}

View File

@@ -0,0 +1,72 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path')
const webpack = require('webpack')
const resolveApp = relative => path.resolve(__dirname, relative)
const { NODE_ENV = 'production' } = process.env
const __PROD__ = NODE_ENV === 'production'
// https://webpack.js.org/configuration/
module.exports = {
mode: NODE_ENV,
target: 'web',
devServer: {
historyApiFallback: true,
},
entry: resolveApp('src/index.tsx'),
output: {
filename: __PROD__ ? '[name].[contenthash:8].js' : '[name].js',
path: resolveApp('dist'),
},
optimization: {
moduleIds: __PROD__ ? 'deterministic' : undefined,
runtimeChunk: true,
splitChunks: {
chunks: 'all',
},
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
},
},
{
test: /\.css$/i,
use: ['css-loader'],
},
],
},
resolve: {
alias: {
dns: false,
},
extensions: ['.tsx', '.ts', '.js'],
},
devtool: __PROD__ ? 'source-map' : 'eval-cheap-module-source-map',
plugins: [
new (require('clean-webpack-plugin').CleanWebpackPlugin)(),
new (require('copy-webpack-plugin'))({
patterns: [
{
from: resolveApp('public'),
to: resolveApp('dist'),
filter: file => file !== resolveApp('public/index.html'),
},
],
}),
new (require('html-webpack-plugin'))({
template: resolveApp('public/index.html'),
}),
new webpack.EnvironmentPlugin({ XAPI_HOST: '', NPM_VERSION: require('./package.json').version }),
new (require('node-polyfill-webpack-plugin'))(),
].filter(Boolean),
}

View File

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

View File

@@ -4,6 +4,42 @@ const { compileGlobPattern } = require('./utils')
// ===================================================================
const compileFilter = filter => {
if (filter === undefined) {
return
}
const type = typeof filter
if (type === 'function') {
return filter
}
if (type === 'string') {
const re = compileGlobPattern(filter)
return log => re.test(log.namespace)
}
if (Array.isArray(filter)) {
const filters = filter.map(compileFilter).filter(_ => _ !== undefined)
const { length } = filters
if (length === 0) {
return
}
if (length === 1) {
return filters[0]
}
return log => {
for (let i = 0; i < length; ++i) {
if (filters[i](log)) {
return true
}
}
return false
}
}
throw new TypeError('unsupported `filter`')
}
const createTransport = config => {
if (typeof config === 'function') {
return config
@@ -19,26 +55,15 @@ const createTransport = config => {
}
}
let { filter } = config
let transport = createTransport(config.transport)
const level = resolve(config.level)
const filter = compileFilter([config.filter, level === undefined ? undefined : log => log.level >= level])
let transport = createTransport(config.transport)
if (filter !== undefined) {
if (typeof filter === 'string') {
const re = compileGlobPattern(filter)
filter = log => re.test(log.namespace)
}
const orig = transport
transport = function (log) {
if ((level !== undefined && log.level >= level) || filter(log)) {
return orig.apply(this, arguments)
}
}
} else if (level !== undefined) {
const orig = transport
transport = function (log) {
if (log.level >= level) {
if (filter(log)) {
return orig.apply(this, arguments)
}
}

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/log",
"version": "0.2.0",
"version": "0.3.0",
"license": "ISC",
"description": "Logging system with decoupled producers/consumer",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/log",

View File

@@ -20,36 +20,8 @@ if (process.stdout !== undefined && process.stdout.isTTY && process.stderr !== u
}
const NAMESPACE_COLORS = [
196,
202,
208,
214,
220,
226,
190,
154,
118,
82,
46,
47,
48,
49,
50,
51,
45,
39,
33,
27,
21,
57,
93,
129,
165,
201,
200,
199,
198,
197,
196, 202, 208, 214, 220, 226, 190, 154, 118, 82, 46, 47, 48, 49, 50, 51, 45, 39, 33, 27, 21, 57, 93, 129, 165, 201,
200, 199, 198, 197,
]
formatNamespace = namespace => {
// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/

View File

@@ -1,5 +1,6 @@
const get = require('lodash/get')
const identity = require('lodash/identity')
const isEqual = require('lodash/isEqual')
const { createLogger } = require('@xen-orchestra/log')
const { parseDuration } = require('@vates/parse-duration')
const { watch } = require('app-conf')
@@ -48,7 +49,7 @@ module.exports = class Config {
const watcher = config => {
try {
const value = processor(get(config, path))
if (value !== prev) {
if (!isEqual(value, prev)) {
prev = value
cb(value)
}

View File

@@ -14,14 +14,14 @@
"url": "https://vates.fr"
},
"license": "AGPL-3.0-or-later",
"version": "0.1.0",
"version": "0.1.1",
"engines": {
"node": ">=12"
},
"dependencies": {
"@vates/parse-duration": "^0.1.1",
"@xen-orchestra/emit-async": "^0.0.0",
"@xen-orchestra/log": "^0.2.0",
"@xen-orchestra/emit-async": "^0.1.0",
"@xen-orchestra/log": "^0.3.0",
"app-conf": "^0.9.0",
"lodash": "^4.17.21"
},

View File

@@ -28,9 +28,10 @@ export default {
buffer.toString('hex', offset + 5, offset + 6),
stringToEth: (string, buffer, offset) => {
const eth = /^([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2})$/.exec(
string
)
const eth =
/^([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2}):([0-9A-Fa-f]{2})$/.exec(
string
)
assert(eth !== null)
buffer.writeUInt8(parseInt(eth[1], 16), offset)
buffer.writeUInt8(parseInt(eth[2], 16), offset + 1)
@@ -50,9 +51,10 @@ export default {
),
stringToip4: (string, buffer, offset) => {
const ip = /^([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$/.exec(
string
)
const ip =
/^([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$/.exec(
string
)
assert(ip !== null)
buffer.writeUInt8(parseInt(ip[1], 10), offset)
buffer.writeUInt8(parseInt(ip[2], 10), offset + 1)

View File

@@ -33,7 +33,7 @@
"content-type": "^1.0.4",
"cson-parser": "^4.0.7",
"getopts": "^2.2.3",
"http-request-plus": "^0.10.0",
"http-request-plus": "^0.12",
"json-rpc-protocol": "^0.13.1",
"promise-toolbox": "^0.19.2",
"pump": "^3.0.0",

View File

@@ -36,7 +36,14 @@ async function main(argv) {
const { hostname = 'localhost', port } = config?.http?.listen?.https ?? {}
const { _: args, file, help, host, raw, token } = getopts(argv, {
const {
_: args,
file,
help,
host,
raw,
token,
} = getopts(argv, {
alias: { file: 'f', help: 'h' },
boolean: ['help', 'raw'],
default: {
@@ -140,16 +147,6 @@ ${pkg.name} v${pkg.version}`
}
}
const $import = ({ $import: path }) => {
const data = fs.readFileSync(path, 'utf8')
const ext = extname(path).slice(1).toLowerCase()
const parse = FORMATS[ext]
if (parse === undefined) {
throw new Error(`unsupported file: ${path}`)
}
return visit(parse(data))
}
const seq = async seq => {
const j = callPath.length
for (let i = 0, n = seq.length; i < n; ++i) {
@@ -163,13 +160,17 @@ ${pkg.name} v${pkg.version}`
if (Array.isArray(node)) {
return seq(node)
}
const keys = Object.keys(node)
return keys.length === 1 && keys[0] === '$import' ? $import(node) : call(node)
return call(node)
}
let node
if (file !== '') {
node = { $import: file }
const data = fs.readFileSync(file, 'utf8')
const ext = extname(file).slice(1).toLowerCase()
const parse = FORMATS[ext]
if (parse === undefined) {
throw new Error(`unsupported file: ${file}`)
}
await visit(parse(data))
} else {
const method = args[0]
const params = {}
@@ -182,9 +183,8 @@ ${pkg.name} v${pkg.version}`
params[param.slice(0, j)] = parseValue(param.slice(j + 1))
}
node = { method, params }
await call({ method, params })
}
await visit(node)
}
main(process.argv.slice(2)).then(
() => {

View File

@@ -93,10 +93,7 @@ declare namespace event {
declare namespace backup {
type SimpleIdPattern = { id: string | { __or: string[] } }
declare namespace backup {
type SimpleIdPattern = { id: string | { __or: string[] } }
interface BackupJob {
interface BackupJob {
id: string
type: 'backup'
compression?: 'native' | 'zstd' | ''
@@ -146,13 +143,13 @@ declare namespace backup {
}
function listXoMetadataBackups(_: { remotes: { [id: string]: Remote } }): { [remoteId: string]: object[] }
function run(_: {
job: BackupJob | MetadataBackupJob
function run(_: {
job: BackupJob | MetadataBackupJob
remotes: { [id: string]: Remote }
schedule: Schedule
xapis?: { [id: string]: Xapi }
recordToXapi?: { [recordUuid: string]: string }
schedule: Schedule
xapis?: { [id: string]: Xapi }
recordToXapi?: { [recordUuid: string]: string }
streamLogs: boolean = false
}): string

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@xen-orchestra/proxy",
"version": "0.13.1",
"version": "0.14.7",
"license": "AGPL-3.0-or-later",
"description": "XO Proxy used to remotely execute backup jobs",
"keywords": [
@@ -18,9 +18,8 @@
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"preferGlobal": true,
"main": "dist/",
"bin": {
"xo-proxy": "dist/index.js"
"xo-proxy": "dist/index.mjs"
},
"engines": {
"node": ">=14.13"
@@ -29,20 +28,20 @@
"@iarna/toml": "^2.2.0",
"@koa/router": "^10.0.0",
"@vates/compose": "^2.0.0",
"@vates/decorate-with": "^0.0.1",
"@vates/decorate-with": "^0.1.0",
"@vates/disposable": "^0.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.11.0",
"@xen-orchestra/fs": "^0.17.0",
"@xen-orchestra/log": "^0.2.0",
"@xen-orchestra/backups": "^0.13.0",
"@xen-orchestra/fs": "^0.18.0",
"@xen-orchestra/log": "^0.3.0",
"@xen-orchestra/mixin": "^0.1.0",
"@xen-orchestra/mixins": "^0.1.0",
"@xen-orchestra/mixins": "^0.1.1",
"@xen-orchestra/self-signed": "^0.1.0",
"@xen-orchestra/xapi": "^0.6.2",
"@xen-orchestra/xapi": "^0.7.0",
"ajv": "^8.0.3",
"app-conf": "^0.9.0",
"async-iterator-to-stream": "^1.1.0",
"fs-extra": "^9.1.0",
"fs-extra": "^10.0.0",
"get-stream": "^6.0.0",
"getopts": "^2.2.3",
"golike-defer": "^0.5.1",
@@ -59,7 +58,7 @@
"source-map-support": "^0.5.16",
"stoppable": "^1.0.6",
"xdg-basedir": "^4.0.0",
"xen-api": "^0.32.0",
"xen-api": "^0.34.3",
"xo-common": "^0.7.0"
},
"devDependencies": {
@@ -73,7 +72,7 @@
"@vates/toggle-scripts": "^1.0.0",
"babel-plugin-transform-dev": "^2.0.1",
"cross-env": "^7.0.2",
"index-modules": "^0.4.0"
"index-modules": "^0.4.3"
},
"scripts": {
"_build": "index-modules --index-file index.mjs src/app/mixins && babel --delete-dir-on-start --keep-file-extension --source-maps --out-dir=dist/ src/",
@@ -84,7 +83,7 @@
"prepack": "toggle-scripts +postinstall +preuninstall",
"prepublishOnly": "yarn run build",
"_preuninstall": "./scripts/systemd-service-installer",
"start": "./dist/index.js"
"start": "./dist/index.mjs"
},
"author": {
"name": "Vates SAS",

View File

@@ -15,12 +15,23 @@ import { createLogger } from '@xen-orchestra/log'
const { debug, warn } = createLogger('xo:proxy:api')
const ndJsonStream = asyncIteratorToStream(async function* (responseId, iterable) {
yield format.response(responseId, { $responseType: 'ndjson' }) + '\n'
for await (const data of iterable) {
try {
yield JSON.stringify(data) + '\n'
} catch (error) {
warn('ndJsonStream', { error })
let headerSent = false
try {
for await (const data of iterable) {
if (!headerSent) {
yield format.response(responseId, { $responseType: 'ndjson' }) + '\n'
headerSent = true
}
try {
yield JSON.stringify(data) + '\n'
} catch (error) {
warn('ndJsonStream, item error', { error })
}
}
} catch (error) {
warn('ndJsonStream, fatal error', { error })
if (!headerSent) {
yield format.error(responseId, error)
}
}
})

View File

@@ -1,5 +1,3 @@
import Cancel from 'promise-toolbox/Cancel'
import CancelToken from 'promise-toolbox/CancelToken'
import Disposable from 'promise-toolbox/Disposable.js'
import fromCallback from 'promise-toolbox/fromCallback.js'
import { asyncMap } from '@xen-orchestra/async-map'
@@ -13,6 +11,7 @@ import { DurablePartition } from '@xen-orchestra/backups/DurablePartition.js'
import { execFile } from 'child_process'
import { formatVmBackups } from '@xen-orchestra/backups/formatVmBackups.js'
import { ImportVmBackup } from '@xen-orchestra/backups/ImportVmBackup.js'
import { JsonRpcError } from 'json-rpc-protocol'
import { Readable } from 'stream'
import { RemoteAdapter } from '@xen-orchestra/backups/RemoteAdapter.js'
import { RestoreMetadataBackup } from '@xen-orchestra/backups/RestoreMetadataBackup.js'
@@ -97,8 +96,7 @@ export default class Backups {
error.jobId = jobId
throw error
}
const source = CancelToken.source()
runningJobs[jobId] = source.cancel
runningJobs[jobId] = true
try {
return await run.apply(this, arguments)
} finally {
@@ -111,7 +109,7 @@ export default class Backups {
if (!__DEV__) {
const license = await app.appliance.getSelfLicense()
if (license === undefined) {
throw new Error('no valid proxy license')
throw new JsonRpcError('no valid proxy license')
}
}
return run.apply(this, arguments)

View File

@@ -1,38 +0,0 @@
import { asyncMapSettled } from '@xen-orchestra/async-map'
export default class Task {
#tasks = new Map()
constructor(app) {
const tasks = new Map()
this.#tasks = tasks
app.api.addMethods({
task: {
*list() {
for (const id of tasks.keys()) {
yield { id }
}
},
cancel: [
({ taskId }) => this.cancel(taskId),
{
params: {
taskId: { type: 'string' },
},
},
],
},
})
app.hooks.on('stop', () => asyncMapSettled(tasks.values(), task => task.cancel()))
}
async cancel(taskId) {
await this.tasks.get(taskId).cancel()
}
register(task) {
this.#tasks.set(task.id, task)
}
}

View File

@@ -27,20 +27,18 @@
"xo-upload-ova": "dist/index.js"
},
"engines": {
"node": ">=8.10"
"node": ">=10"
},
"dependencies": {
"chalk": "^4.1.0",
"exec-promise": "^0.7.0",
"form-data": "^4.0.0",
"fs-extra": "^9.0.0",
"fs-promise": "^2.0.3",
"fs-extra": "^10.0.0",
"get-stream": "^6.0.0",
"http-request-plus": "^0.10.0",
"http-request-plus": "^0.12",
"human-format": "^0.11.0",
"l33teral": "^3.0.3",
"lodash": "^4.17.4",
"nice-pipe": "0.0.0",
"pretty-ms": "^7.0.0",
"progress-stream": "^2.0.0",
"pw": "^0.0.4",

View File

@@ -6,7 +6,7 @@ import chalk from 'chalk'
import execPromise from 'exec-promise'
import FormData from 'form-data'
import { createReadStream } from 'fs'
import { stat } from 'fs-promise'
import { stat } from 'fs-extra'
import getStream from 'get-stream'
import hrp from 'http-request-plus'
import humanFormat from 'human-format'
@@ -14,7 +14,6 @@ import l33t from 'l33teral'
import isObject from 'lodash/isObject'
import getKeys from 'lodash/keys'
import startsWith from 'lodash/startsWith'
import nicePipe from 'nice-pipe'
import prettyMs from 'pretty-ms'
import progressStream from 'progress-stream'
import pw from 'pw'
@@ -22,10 +21,13 @@ import stripIndent from 'strip-indent'
import { URL } from 'url'
import Xo from 'xo-lib'
import { parseOVAFile } from 'xo-vmdk-to-vhd'
import { pipeline } from 'stream'
import pkg from '../package'
import { load as loadConfig, set as setConfig, unset as unsetConfig } from './config'
const noop = Function.prototype
function help() {
return stripIndent(
`
@@ -206,7 +208,7 @@ export async function upload(args) {
url = new URL(result[key], baseUrl)
const { size: length } = await stat(file)
const input = nicePipe([
const input = pipeline(
createReadStream(file),
progressStream(
{
@@ -215,7 +217,8 @@ export async function upload(args) {
},
printProgress
),
])
noop
)
formData.append('file', input, { filename: 'file', knownLength: length })
try {
return await hrp.post(url.toString(), { body: formData, headers: formData.getHeaders() }).readAll('utf-8')

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/xapi",
"version": "0.6.2",
"version": "0.7.0",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/xapi",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
@@ -25,7 +25,7 @@
"xo-common": "^0.7.0"
},
"peerDependencies": {
"xen-api": "^0.32.0"
"xen-api": "^0.34.3"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
@@ -38,9 +38,9 @@
"prepublishOnly": "yarn run build"
},
"dependencies": {
"@vates/decorate-with": "^0.0.1",
"@vates/decorate-with": "^0.1.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.2.0",
"@xen-orchestra/log": "^0.3.0",
"d3-time-format": "^3.0.0",
"golike-defer": "^0.5.1",
"lodash": "^4.17.15",

View File

@@ -2,14 +2,181 @@
## **next**
### Enhancements
- [Backup] Go back to previous page instead of going to the overview after editing a job: keeps current filters and page (PR [#5913](https://github.com/vatesfr/xen-orchestra/pull/5913))
- [Health] Do not take into consideration duplicated MAC addresses from CR VMs (PR [#5916](https://github.com/vatesfr/xen-orchestra/pull/5916))
- [Health] Ability to filter duplicated MAC addresses by running VMs (PR [#5917](https://github.com/vatesfr/xen-orchestra/pull/5917))
- [Tables] Move the search bar and pagination to the top of the table (PR [#5914](https://github.com/vatesfr/xen-orchestra/pull/5914))
### Bug fixes
- [SSH keys] Allow SSH key to be broken anywhere to avoid breaking page formatting (Thanks [@tstivers1990](https://github.com/tstivers1990)!) [#5891](https://github.com/vatesfr/xen-orchestra/issues/5891) (PR [#5892](https://github.com/vatesfr/xen-orchestra/pull/5892))
- [Netbox] Handle nested prefixes by always assigning an IP to the smallest prefix it matches (PR [#5908](https://github.com/vatesfr/xen-orchestra/pull/5908))
- [Netbox] Better handling and error messages when encountering issues due to UUID custom field not being configured correctly [#5905](https://github.com/vatesfr/xen-orchestra/issues/5905) [#5806](https://github.com/vatesfr/xen-orchestra/issues/5806) [#5834](https://github.com/vatesfr/xen-orchestra/issues/5834) (PR [#5909](https://github.com/vatesfr/xen-orchestra/pull/5909))
- [New VM] Don't send network config if untouched as all commented config can make Cloud-init fail [#5918](https://github.com/vatesfr/xen-orchestra/issues/5918) (PR [#5923](https://github.com/vatesfr/xen-orchestra/pull/5923))
### Released packages
- xen-api 0.32
- xen-api 0.34.3
- vhd-lib 1.2.0
- xo-server-netbox 0.3.1
- @xen-orchestra/proxy 0.14.7
- xo-server 5.82.3
- xo-web 5.88.0
## **5.58.1** (2021-05-06)
## **5.62.1** (2021-09-17)
### Bug fixes
- [VM/Advanced] Fix conversion from UEFI to BIOS boot firmware (PR [#5895](https://github.com/vatesfr/xen-orchestra/pull/5895))
- [VM/network] Support newline-delimited IP addresses reported by some guest tools
- Fix VM/host stats, VM creation with Cloud-init, and VM backups, with NATted hosts [#5896](https://github.com/vatesfr/xen-orchestra/issues/5896)
- [VM/import] Very small VMDK and OVA files were mangled upon import (PR [#5903](https://github.com/vatesfr/xen-orchestra/pull/5903))
### Released packages
- xen-api 0.34.2
- @xen-orchestra/proxy 0.14.6
- xo-server 5.82.2
## **5.62.0** (2021-08-31)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Highlights
- [Host] Add warning in case of unmaintained host version [#5840](https://github.com/vatesfr/xen-orchestra/issues/5840) (PR [#5847](https://github.com/vatesfr/xen-orchestra/pull/5847))
- [Backup] Use default migration network if set when importing/exporting VMs/VDIs (PR [#5883](https://github.com/vatesfr/xen-orchestra/pull/5883))
### Enhancements
- [New network] Ability for pool's admin to create a new network within the pool (PR [#5873](https://github.com/vatesfr/xen-orchestra/pull/5873))
- [Netbox] Synchronize primary IPv4 and IPv6 addresses [#5633](https://github.com/vatesfr/xen-orchestra/issues/5633) (PR [#5879](https://github.com/vatesfr/xen-orchestra/pull/5879))
### Bug fixes
- [VM/network] Fix an issue where multiple IPs would be displayed in the same tag when using old Xen tools. This also fixes Netbox's IP synchronization for the affected VMs. (PR [#5860](https://github.com/vatesfr/xen-orchestra/pull/5860))
- [LDAP] Handle groups with no members (PR [#5862](https://github.com/vatesfr/xen-orchestra/pull/5862))
- Fix empty button on small size screen (PR [#5874](https://github.com/vatesfr/xen-orchestra/pull/5874))
- [Host] Fix `Cannot read property 'other_config' of undefined` error when enabling maintenance mode (PR [#5875](https://github.com/vatesfr/xen-orchestra/pull/5875))
### Released packages
- xen-api 0.34.1
- @xen-orchestra/xapi 0.7.0
- @xen-orchestra/backups 0.13.0
- @xen-orchestra/fs 0.18.0
- @xen-orchestra/log 0.3.0
- @xen-orchestra/mixins 0.1.1
- xo-server-auth-ldap 0.10.4
- xo-server-netbox 0.3.0
- xo-server 5.82.1
- xo-web 5.87.0
## **5.61.0** (2021-07-30)
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
### Highlights
- [SR/disks] Display base copies' active VDIs (PR [#5826](https://github.com/vatesfr/xen-orchestra/pull/5826))
- [Netbox] Optionally allow self-signed certificates (PR [#5850](https://github.com/vatesfr/xen-orchestra/pull/5850))
- [Host] When supported, use pool's default migration network to evacuate host [#5802](https://github.com/vatesfr/xen-orchestra/issues/5802) (PR [#5851](https://github.com/vatesfr/xen-orchestra/pull/5851))
- [VM] shutdown/reboot: offer to force shutdown/reboot the VM if no Xen tools were detected [#5838](https://github.com/vatesfr/xen-orchestra/issues/5838) (PR [#5855](https://github.com/vatesfr/xen-orchestra/pull/5855))
### Enhancements
- [Netbox] Add information about a failed request to the error log to help better understand what happened [#5834](https://github.com/vatesfr/xen-orchestra/issues/5834) (PR [#5842](https://github.com/vatesfr/xen-orchestra/pull/5842))
- [VM/console] Ability to rescan ISO SRs (PR [#5841](https://github.com/vatesfr/xen-orchestra/pull/5841))
### Bug fixes
- [VM/disks] Fix `an error has occured` when self service user was on VM disk view (PR [#5841](https://github.com/vatesfr/xen-orchestra/pull/5841))
- [Backup] Protect replicated VMs from being started on specific hosts (PR [#5852](https://github.com/vatesfr/xen-orchestra/pull/5852))
### Released packages
- @xen-orchestra/backups 0.12.2
- @xen-orchestra/proxy 0.14.4
- xo-server-netbox 0.2.0
- xo-web 5.86.0
- xo-server 5.81.2
## **5.60.0** (2021-06-30)
### Highlights
- [VM/disks] Ability to rescan ISO SRs (PR [#5814](https://github.com/vatesfr/xen-orchestra/pull/5814))
- [VM/snapshots] Identify VM's current snapshot with an icon next to the snapshot's name (PR [#5824](https://github.com/vatesfr/xen-orchestra/pull/5824))
### Enhancements
- [OVA import] improve OVA import error reporting (PR [#5797](https://github.com/vatesfr/xen-orchestra/pull/5797))
- [Backup] Distinguish error messages between cancelation and interrupted HTTP connection
- [Jobs] Add `host.emergencyShutdownHost` to the list of methods that jobs can call (PR [#5818](https://github.com/vatesfr/xen-orchestra/pull/5818))
- [Host/Load-balancer] Log VM and host names when a VM is migrated + category (density, performance, ...) (PR [#5808](https://github.com/vatesfr/xen-orchestra/pull/5808))
- [VM/new disk] Auto-fill disk name input with generated unique name (PR [#5828](https://github.com/vatesfr/xen-orchestra/pull/5828))
### Bug fixes
- [IPs] Handle space-delimited IP address format provided by outdated guest tools [5801](https://github.com/vatesfr/xen-orchestra/issues/5801) (PR [5805](https://github.com/vatesfr/xen-orchestra/pull/5805))
- [API/pool.listPoolsMatchingCriteria] fix `unknown error from the peer` error (PR [5807](https://github.com/vatesfr/xen-orchestra/pull/5807))
- [Backup] Limit number of connections to hosts, which should reduce the occurences of `ECONNRESET`
- [Plugins/perf-alert] All mode: only selects running hosts and VMs (PR [5811](https://github.com/vatesfr/xen-orchestra/pull/5811))
- [New VM] Fix summary section always showing "0 B" for RAM (PR [#5817](https://github.com/vatesfr/xen-orchestra/pull/5817))
- [Backup/Restore] Fix _start VM after restore_ [5820](https://github.com/vatesfr/xen-orchestra/issues/5820)
- [Netbox] Fix a bug where some devices' IPs would get deleted from Netbox (PR [#5821](https://github.com/vatesfr/xen-orchestra/pull/5821))
- [Netbox] Fix an issue where some IPv6 would be deleted just to be immediately created again (PR [#5822](https://github.com/vatesfr/xen-orchestra/pull/5822))
### Released packages
- @vates/decorate-with 0.1.0
- xen-api 0.33.1
- @xen-orchestra/xapi 0.6.4
- @xen-orchestra/backups 0.12.0
- @xen-orchestra/proxy 0.14.3
- vhd-lib 1.1.0
- vhd-cli 0.4.0
- xo-server-netbox 0.1.2
- xo-server-perf-alert 0.3.2
- xo-server-load-balancer 0.7.0
- xo-server 5.80.0
- xo-web 5.84.0
## **5.59.0** (2021-05-31)
### Highlights
- [Smart backup] Report missing pools [#2844](https://github.com/vatesfr/xen-orchestra/issues/2844) (PR [#5768](https://github.com/vatesfr/xen-orchestra/pull/5768))
- [Metadata Backup] Add a warning on restoring a metadata backup (PR [#5769](https://github.com/vatesfr/xen-orchestra/pull/5769))
- [Netbox] [Plugin](https://xen-orchestra.com/docs/advanced.html#netbox) to synchronize pools, VMs and IPs with [Netbox](https://netbox.readthedocs.io/en/stable/) (PR [#5783](https://github.com/vatesfr/xen-orchestra/pull/5783))
### Enhancements
- [SAML] Compatible with users created with other authentication providers (PR [#5781](https://github.com/vatesfr/xen-orchestra/pull/5781))
### Bug fixes
- [SDN Controller] Private network creation failure when the tunnels were created on different devices [Forum #4620](https://xcp-ng.org/forum/topic/4620/no-pif-found-in-center) (PR [#5793](https://github.com/vatesfr/xen-orchestra/pull/5793))
### Released packages
- @xen-orchestra/emit-async 0.1.0
- @xen-orchestra/defined 0.0.1
- xo-collection 0.5.0
- @xen-orchestra/log 0.2.1
- xen-api 0.33.0
- @xen-orchestra/xapi 0.6.3
- xo-server-auth-saml 0.9.0
- xo-server-backup-reports 0.16.10
- xo-server-netbox 0.1.1
- xo-server-sdn-controller 1.0.5
- xo-web 5.82.0
- xo-server 5.79.5
## **5.58.1** (2021-05-06)
### Bug fixes
- [Backups] Better handling of errors in remotes, fix `task has already ended`
@@ -64,8 +231,6 @@
## **5.57.1** (2021-04-13)
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
### Enhancements
- [Host/Load-balancer] Add option to disable migration (PR [#5706](https://github.com/vatesfr/xen-orchestra/pull/5706))

View File

@@ -7,28 +7,16 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [Metadata Backup] Add a warning on restoring a metadata backup (PR [#5769](https://github.com/vatesfr/xen-orchestra/pull/5769))
- [SAML] Compatible with users created with other authentication providers (PR [#5781](https://github.com/vatesfr/xen-orchestra/pull/5781))
- [Netbox] [Plugin](https://xen-orchestra.com/docs/advanced.html#netbox) to synchronize pools, VMs and IPs with [Netbox](https://netbox.readthedocs.io/en/stable/) (PR [#5783](https://github.com/vatesfr/xen-orchestra/pull/5783))
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [Smart backup] Report missing pools [#2844](https://github.com/vatesfr/xen-orchestra/issues/2844) (PR [#5768](https://github.com/vatesfr/xen-orchestra/pull/5768))
### Packages to release
> Packages will be released in the order they are here, therefore, they should
> be listed by inverse order of dependency.
>
> Global order:
>
> - @vates/...
> - @xen-orchestra/...
> - xo-server-...
> - xo-server
> - xo-web
> Rule of thumb: add packages on top.
>
> The format is the following: - `$packageName` `$version`
>
@@ -39,14 +27,3 @@
> - major: if the change breaks compatibility
>
> In case of conflict, the highest (lowest in previous list) `$version` wins.
- @xen-orchestra/emit-async minor
- @xen-orchestra/defined patch
- xo-collection minor
- @xen-orchestra/log patch
- xen-api minor
- xo-server-auth-saml minor
- xo-server-backup-reports patch
- xo-server-netbox minor
- xo-web minor
- xo-server patch

View File

@@ -114,17 +114,18 @@ We need your feedback on this feature!
The plugin "web-hooks" needs to be installed and loaded for this feature to work.
You can trigger an HTTP POST request to a URL when a Xen Orchestra API method is called.
You can trigger an HTTP POST request to a URL when a Xen Orchestra API method is called or when a backup job runs.
- Go to Settings > Plugins > Web hooks
- Add new hooks
- For each hook, configure:
- Method: the XO API method that will trigger the HTTP request when called
- Method: the XO API method that will trigger the HTTP request when called. For backup jobs, choose `backupNg.runJob`.
- Type:
- pre: the request will be sent when the method is called
- post: the request will be sent after the method action is completed
- pre/post: both
- URL: the full URL which the requests will be sent to
- Wait for response: you can choose to wait for the web hook response before the method is actually called ("pre" hooks only). This can be useful if you need to automatically run some tasks before a certain method is called.
- Save the plugin configuration
From now on, a request will be sent to the corresponding URLs when a configured method is called by an XO client.
@@ -340,13 +341,14 @@ XO will try to find the right prefix for each IP address. If it can't find a pre
- Create a token with "Write enabled"
- Add a UUID custom field:
- Got to Admin > Custom fields > Add custom field
- Create a custom field called "uuid"
- Create a custom field called "uuid" (lower case!)
- Assign it to object types `virtualization > cluster` and `virtualization > virtual machine`
![](./assets/customfield.png)
- Go to Xen Orchestra > Settings > Plugins > Netbox and fill out the configuration:
- Endpoint: the URL of your Netbox instance (e.g.: `https://netbox.company.net`)
- Unauthorized certificate: only for HTTPS, enable this option if your Netbox instance uses a self-signed SSL certificate
- Token: the token you generated earlier
- Pools: the pools you wish to automatically synchronize with Netbox
- Interval: the time interval (in hours) between 2 auto-synchronizations. Leave empty if you don't want to synchronize automatically.

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -87,3 +87,7 @@ You need to be an admin:
![Mattermost configuration](./assets/DocImg8.png)
![Mattermost](./assets/DocImg9.png)
## Web hooks
You can also configure web hooks to be sent to a custom server before and/or after a backup job runs. This won't send a formatted report but raw JSON data that you can use in custom scripts on your side. Follow the [web-hooks plugin documentation](./advanced.html#web-hooks) to configure it.

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