Compare commits

...

343 Commits

Author SHA1 Message Date
Pierre Donias
d77894310f feat(xo-web): 5.33.0 2019-01-11 15:18:50 +01:00
Pierre Donias
cbee05e0c7 feat(xo-server): 5.33.0 2019-01-11 15:18:12 +01:00
Pierre Donias
7f36dddefb feat(fs): 0.6.0 2019-01-11 15:16:53 +01:00
Pierre Donias
56b2dbd4fd feat(xen-api): 0.24.0 2019-01-11 15:14:54 +01:00
Pierre Donias
df67908784 feat(vhd-cli): 0.2.0 2019-01-11 15:13:03 +01:00
Pierre Donias
5dcdb81843 feat(vhd-lib): 0.5.0 2019-01-11 15:11:35 +01:00
Julien Fontanet
7f85935e43 feat(xo-server/backup NG): check complete VHD chain (#3820)
This triggers a full export if the chain appears incomplete.
2019-01-11 15:04:24 +01:00
marcpezin
2ab820d511 New store documentation update (#3837)
* new store documentation

* grammar fix
2019-01-11 15:03:39 +01:00
Rajaa.BARHTAOUI
db19668453 fix(xo-web/new/sr): remove warning modal for already used path (#3851)
Fixes #3844
2019-01-11 14:20:29 +01:00
Rajaa.BARHTAOUI
0f0ad029a6 feat(xo-web/new/sr): add tooltip for reattach action button (#3852)
Fixes #3845
2019-01-11 14:00:44 +01:00
badrAZ
062a98839c fix(xo-web/backup-ng): typo in the backup form (#3855)
Fixes #3854
2019-01-11 13:13:52 +01:00
Julien Fontanet
c38f21b76b feat(xen-api#{get,put}Resource): add 24h timeout on HTTP requests (#3834)
The Xen API does not support longer requests and it's necessary to properly detect broken requests.
2019-01-11 10:19:38 +01:00
Enishowk
e34a0a6e33 feat({fs,xo-server,xo-web}/remotes): show free space and usage (#3767)
Fixes #3055
2019-01-11 10:07:22 +01:00
badrAZ
f3c3889531 fix(xo-server/new-vm): fix creating a VM on a different local SR (#3827)
Fixes #3084
2019-01-10 15:39:40 +01:00
Rajaa.BARHTAOUI
7de22013a4 feat(xo-web/home): grouped VM snapshot (#3787)
Fixes #3778
2019-01-09 14:27:05 +01:00
Julien Fontanet
711d88765b chore(xen-api/examples/export-vdi): various stats 2019-01-09 13:50:36 +01:00
badrAZ
e9a7421be6 fix(xo-server/xen-servers): handle loosing connection case (#3841)
Fixes #3839
2019-01-09 13:46:17 +01:00
badrAZ
83fe490dbb fix(xo-server/xen-servers): fix connecting status (#3838) 2019-01-09 11:51:20 +01:00
Rajaa.BARHTAOUI
20c92c668b fix(xo-web/self): sort Resource Sets in Self view (#3823)
Fixes #3818
2019-01-09 10:01:46 +01:00
Enishowk
5d0f1c9cce feat(xo-web): autofocus username login page (#3836)
Fixes #3835
2019-01-09 09:44:38 +01:00
badrAZ
20317448a1 feat(xo-web/backup-ng/health): add number of lone snapshots to tab title (#3824)
Fixes #3500
2019-01-08 16:53:03 +01:00
badrAZ
b8a3d00343 feat(xo-server,xo-web/snapshot): ability to snapshot the VM memory (#3812)
Fixes #3795
2019-01-08 16:10:00 +01:00
Julien Fontanet
b459f74a8c fix(xen-api/getRecord): pass type and ref to _wrapRecord 2019-01-08 14:52:53 +01:00
Jon Sands
96a966b9ea fix(docs/CR/manual seed): update to Backup NG (#3758)
Fix #3754
2019-01-08 10:07:06 +01:00
Rajaa.BARHTAOUI
1af42617c2 feat(xo-web/vm/advanced): ACL management from VM view (#3774)
Fixes #3040
2019-01-07 15:35:24 +01:00
Julien Fontanet
100dd38c33 fix(fs/smb-mount#_sync): issue if already mounted
`mount.cifs` does not pring `already mounted`.
2019-01-07 14:43:27 +01:00
Julien Fontanet
2bf4950f4f chore: update dependencies 2019-01-07 10:46:12 +01:00
Enishowk
e8a98945f5 feat(@xen-orchestra/fs): implement SMB on top of mount (#3708)
Fixes #2257
2019-01-07 10:05:18 +01:00
Julien Fontanet
6c2e493576 chore(xo-server): use TOML for vendor conf 2019-01-04 17:28:38 +01:00
Julien Fontanet
f4fb0a1c79 feat(xo-web/xoa/update): make pkgs list copiable 2019-01-03 15:33:28 +01:00
Julien Fontanet
3a9b68fd8d fix(xo-server-auth-ldap/test-cli): pforOwn → pForOwn
Introduced in b3004a38aa, never released.
2019-01-02 16:01:44 +01:00
Julien Fontanet
c9f0481efc fix(xo-web/vm/advanced): only show nested virt for HVM
Fixes #3743
2018-12-24 10:49:25 +01:00
Julien Fontanet
93724218b3 chore(xo-server/backup NG): remove work-around
It should no longer be necessary.
2018-12-24 01:15:25 +01:00
Julien Fontanet
74b97e6518 feat(fs): can use sudo for mount/umount (#3819)
Fixes #3419
2018-12-21 17:24:12 +01:00
Julien Fontanet
f096bdc5d8 chore: update dependencies 2018-12-21 14:44:30 +01:00
badrAZ
0c64596a17 fix(xo-server/Xapi#checkpointVm): returns the created snapshot (#3810) 2018-12-21 11:27:54 +01:00
badrAZ
267be8e904 chore(backup-ng/logs): create a dedicated folder (#3816) 2018-12-21 11:07:34 +01:00
badrAZ
841a8ed1a5 feat(xo-web/backup-ng/logs): move restore logs to the restore tab (#3802)
Fixes #3772
2018-12-20 17:17:33 +01:00
Julien Fontanet
c55daae734 Update CHANGELOG.md 2018-12-20 15:31:05 +01:00
Julien Fontanet
9762fb1912 feat(xo-server): 5.32.2 2018-12-20 14:27:21 +01:00
Julien Fontanet
4047d11b2f feat(xo-server/conf): support TOML 2018-12-20 14:27:21 +01:00
badrAZ
d4215eb452 fix(xo-web/backup-ng): fix compression (#3809) 2018-12-20 09:39:31 +01:00
Julien Fontanet
17014c2819 feat(xo-web): 5.32.1 2018-12-19 16:49:52 +01:00
Julien Fontanet
4d24803b72 feat(xo-server): 5.32.1 2018-12-19 16:48:41 +01:00
Julien Fontanet
6b30465ef2 fix(xo-web/backup-ng): handle compression default value (#3796)
Missing from c5a21922d and e0a3b8ace.
2018-12-19 16:47:54 +01:00
Julien Fontanet
eac9ce597b chore(xen-api/_watchEvents): dont return a promise
This removes potential unhandled rejection warnings.
2018-12-19 14:24:49 +01:00
Pierre Donias
5c8c18fbe6 feat: XOA registration notification (#3808)
Fixes #3803
2018-12-19 14:08:28 +01:00
Shayan Ostadhassan
6f35a1a850 fix(xo-server/sr/probeHba): cast size to number (#3806)
Fixes #3805

This fixes an exception thrown when list of discovered HBAs return string
instead of integer as disk size.
2018-12-19 12:02:58 +01:00
Julien Fontanet
917701e2f6 feat(xo-server/vm.snapshot): add saveMemory param (#3807)
Related to #3795
2018-12-19 10:39:53 +01:00
Pierre Donias
4d4e87aa93 feat(xo-web): 5.32.0 2018-12-18 14:02:20 +01:00
Pierre Donias
e3bbfc6b19 feat(xo-server): 5.32.0 2018-12-18 14:01:38 +01:00
Pierre Donias
0d26ac9858 feat(xo-acl-resolver): 0.4.1 2018-12-18 14:00:00 +01:00
Pierre Donias
4069264ad8 feat(xen-api): 0.23.0 2018-12-18 13:57:58 +01:00
Pierre Donias
120e01897d feat(fs): 0.5.0 2018-12-18 13:55:16 +01:00
badrAZ
06755cb6b6 feat(xo-web/backup-ng/logs): merge status and display log details (#3800)
Fixes #3797
2018-12-18 11:00:47 +01:00
Julien Fontanet
27409f4fd5 fix(xo-server/worker): sync the remote handlers
Fixes #3739
2018-12-17 18:00:02 +01:00
Julien Fontanet
82253509d0 fix(xo-server/backup NG): fix issues due to fs changes 2018-12-17 17:59:54 +01:00
Julien Fontanet
c450685ddd chore(fs/{sync,forget}): add comments 2018-12-17 17:59:54 +01:00
badrAZ
9a79088e8a chore(xo-web/backup-ng): use btnStyle (#3798) 2018-12-17 15:43:39 +01:00
Julien Fontanet
83760157ad chore(xen-api): silence Bluebird warning 2018-12-17 15:32:52 +01:00
Julien Fontanet
985aa2225e fix(fs/local#create{Read,Write}Stream): wait for file to be opened
This also fix `createOutputStream` when directories are missing.
2018-12-17 15:25:35 +01:00
badrAZ
0ad340d971 feat(xo-web/backup-ng/overview): ability to copy schedule/job id to clipboard (#3791)
Fixes #3753
2018-12-17 10:20:51 +01:00
Rajaa.BARHTAOUI
97726dce12 fix(xo-web/self): all objects are missing (#3096)
Fixes #2689
2018-12-14 16:24:10 +01:00
Julien Fontanet
342320b481 feat(xo-server/xen-servers): add metadata to PoolAlreadyConnected (#3792) 2018-12-14 15:09:43 +01:00
badrAZ
1bfcbf49b9 feat(xo-web/backup-ng/new): add a link to the documentation (#3790)
Fixes #3789
2018-12-13 15:49:37 +01:00
Julien Fontanet
9d1eb8182b fix(xo-server/remote.list): add path to Handler#list()
Fixes #3768

Introduced in 043b381733
2018-12-13 15:19:04 +01:00
Julien Fontanet
18a6c57f02 chore(CHANGELOG): add fs to the list of packages 2018-12-13 14:53:59 +01:00
badrAZ
d3b6d1a97f fix(xo-server/servers): always remove pool→connection association (#3782)
* fix(xo-server/servers): delete correctly the connection from the cashe

* useless findKey

* CHANGELOG

* fix error

* code improvement

* code improvement

* code improvement

* Update CHANGELOG.md

* fix errors

* Update xen-servers.js

* missing entry in CHANGELOG
2018-12-13 12:21:02 +01:00
Julien Fontanet
ece881c02c fix(fs#unlink): remove timeout
This is not an `O(1)` operation.
2018-12-13 11:07:28 +01:00
badrAZ
631e8ce52d feat(xo-web/backup-ng/overview): display last run status (#3779)
Fixes #3769
2018-12-12 16:27:34 +01:00
badrAZ
cb5d3b9750 feat(xo-server/backup-ng): disable VM HA on DR/CR (#3755)
Fixes #2359
2018-12-12 16:04:03 +01:00
Julien Fontanet
995e6664f9 fix(fs/smb#create{Read,Write}Stream): works with opened files (#3785) 2018-12-12 15:38:54 +01:00
Julien Fontanet
1f497aa4df chore(fs): update @marsaud/smb2 to v0.13 2018-12-12 15:12:58 +01:00
Julien Fontanet
184dbc5516 chore(fs/smb): use a single client/connection (#3775) 2018-12-11 18:47:46 +01:00
Julien Fontanet
a1f25a4e3e chore(docs/supported-versions): add XS and XCP-ng 7.6 2018-12-11 10:46:13 +01:00
Julien Fontanet
cc4ab94428 chore: reformat with Prettier 2018-12-11 10:45:00 +01:00
Rajaa.BARHTAOUI
48727740c4 feat(xo-web/new-xosan): use SortedTable (#3691)
See #2416
2018-12-10 16:15:04 +01:00
Julien Fontanet
cc26e378e5 chore(fs/{createOutputStream,outputFile}): move dir logic in abstract 2018-12-07 17:12:12 +01:00
Julien Fontanet
28579258b3 feat(docs/sources): add git checkout before pull
Related to #3764.
2018-12-07 14:54:26 +01:00
Julien Fontanet
70b9b67f67 chore(fs/test): add TEST_DATA_LEN constant 2018-12-07 11:14:02 +01:00
Julien Fontanet
224b053eb1 chore(fs/test): test read() 2018-12-07 11:12:00 +01:00
Julien Fontanet
39bce978bc chore(fs/test): testWithFileDescriptor helper 2018-12-07 10:57:14 +01:00
Julien Fontanet
9435bd5493 chore(fs#openFile): flags are not optional 2018-12-07 10:45:56 +01:00
badrAZ
2284b3ef0a feat(xo-server): obfuscate token param (#3761) 2018-12-07 10:35:54 +01:00
Julien Fontanet
99dc64e8bb fix: occured → occurred (2) 2018-12-06 18:53:35 +01:00
Julien Fontanet
e47525b60b chore(fs/test): test list prependDir option 2018-12-06 18:53:09 +01:00
Julien Fontanet
10d4782ee2 feat(fs/mk{dir,tree}): new methods to create dirs (#3763) 2018-12-06 18:50:39 +01:00
Julien Fontanet
11cff2c065 feat(fs): prefix get/set → addPrefix method (#3766)
This method returns a new object which will transparently prefix all paths.

It is better than previous API because:

- the same handler can be reused and will not be invalidated when changing the prefix
- Abstract and implementations can call high-level methods without worrying about doubling the prefix
2018-12-06 17:22:37 +01:00
Julien Fontanet
11f742b020 fix: occured → occurred 2018-12-06 17:18:08 +01:00
Julien Fontanet
2353552e11 chore(fs/abstract): use path.posix
All paths in this lib are Posix paths and should be treated as such, even on Windows.
2018-12-06 13:59:51 +01:00
Rajaa.BARHTAOUI
d6012d8639 feat(xo-web/VM): add tooltip for VM status icon (#3765)
Fixes #3749
2018-12-06 11:48:21 +01:00
Julien Fontanet
7089ee778a fix(xo-server/migrateLegacyBackupJob): dont migrate when ids contain : 2018-12-05 17:49:07 +01:00
Julien Fontanet
629931782e feat(fs/{rmdir,unlink}): dont throw on missing entries 2018-12-05 17:20:51 +01:00
Julien Fontanet
1b48c626f4 fix(fs/smb): create{Read,Output}Stream and getSize with opened files 2018-12-05 16:31:38 +01:00
Julien Fontanet
ba35f51459 chore(fs/test): test createOutputStream 2018-12-05 16:31:38 +01:00
Julien Fontanet
ee5f3fc68d chore(fs/test): test createReadStream and getSize on opened files 2018-12-05 16:31:39 +01:00
Julien Fontanet
7be671f0f7 chore(fs/test): ensure test dir exists 2018-12-05 16:26:38 +01:00
Julien Fontanet
48c3748c28 feat(fs/abstract#rmdir): implement timeout 2018-12-05 15:37:21 +01:00
Julien Fontanet
3814a261d6 fix(fs/abstract#kResolve): edge case with / path 2018-12-05 15:35:24 +01:00
Julien Fontanet
81b82ce06b fix(fs/smb#_rmdir): error code on non-empty dir 2018-12-05 15:35:24 +01:00
Julien Fontanet
20c3f76278 feat(fs): split rmdir into rmdir and rmtree 2018-12-05 15:35:17 +01:00
Julien Fontanet
2c93b69144 chore(fs): simplify sync/forget
- These methods are now optional
- sync no longer returns anything
2018-12-05 14:39:12 +01:00
Julien Fontanet
043b381733 chore(fs/abstract#list): remove default value 2018-12-05 14:30:55 +01:00
Julien Fontanet
ff014df231 chore(fs): remove unnecessary default params 2018-12-05 14:29:35 +01:00
Julien Fontanet
055d1e81da chore(fs): sort methods 2018-12-05 14:27:20 +01:00
Julien Fontanet
b60678e79f fix(fs/abstract#test): resolve path 2018-12-05 14:06:53 +01:00
Julien Fontanet
fd93dfbc18 feat(fs): prefix support (#3759)
It is a prefix that will be used as root for all other paths.
2018-12-05 13:51:38 +01:00
Julien Fontanet
d74a5d73f0 chore(fs/abstract#test): Buffer#compare → Buffer#equals 2018-12-05 13:49:33 +01:00
Julien Fontanet
16a6d395c8 chore(fs/abstract): only call low-level methods 2018-12-05 13:48:42 +01:00
badrAZ
d6654807fa fix(xo-server/logs): sensitive params not hidden (#3760) 2018-12-05 13:41:32 +01:00
Julien Fontanet
e25ff221ba fix(fs/abstract#_outputFile): missing callback 2018-12-05 11:05:01 +01:00
Julien Fontanet
7df965ccd7 chore(fs): remove unnecessary tests 2018-12-05 11:00:55 +01:00
Julien Fontanet
f6b73b8303 chore(fs/abstract#_outputFile): use readable-stream/finished 2018-12-05 11:00:12 +01:00
Julien Fontanet
d617214c62 chore: update dependencies 2018-12-05 10:39:38 +01:00
Julien Fontanet
e85744cec0 chore(fs/smb): preserve stack on wrapped errors 2018-12-05 10:27:13 +01:00
Julien Fontanet
da74555e02 feat(xo-remote-parser/smb): handle missing path 2018-12-04 17:48:38 +01:00
Enishowk
4a9f489f20 fix(changelog): XOSAN line in wrong place (#3756) 2018-12-04 17:46:01 +01:00
Enishowk
18b17bda7c feat(@xen-orchestra/fs): add unit tests (#3736) 2018-12-04 16:36:10 +01:00
badrAZ
7faff824ff feat(xo-server/xen-servers): auto-connect to ejected host (#3738)
See #2238
2018-12-04 13:28:52 +01:00
Pierre Donias
e08d03687e feat(xo-web/render-xo-item/SR): minor improvements (#3752)
- Space left before container
- Hide context to self users
- Don't use text-muted on links
2018-12-04 11:35:57 +01:00
Rajaa.BARHTAOUI
75f1d80a86 feat(xo-web): contextualize SR (#3751)
See  #3021
2018-12-04 10:09:31 +01:00
Julien Fontanet
3967bfa099 chore(xo-server/config/remoteOptions.timeout): add comment 2018-12-03 17:50:34 +01:00
Rajaa.BARHTAOUI
6f6f463592 feat(xo-web/health/Orphan VDIs): display SR container (#3747)
See #3021
2018-12-03 16:56:34 +01:00
Rajaa.BARHTAOUI
8a760823b8 feat(xo-web/render-xo-item): contextualize VDI (#3285)
See #3021
2018-12-03 15:27:58 +01:00
Enishowk
8cd66af3f8 feat(backup-ng): add XOSAN in excluded tags by default (#3563)
Fixes #2128
2018-12-03 15:04:38 +01:00
Julien Fontanet
8569dbf985 chore: String#substring() → String#slice()
- homogeneity
- [faster in v8](https://bugs.chromium.org/p/v8/issues/detail?id=6730)
2018-12-03 14:59:59 +01:00
Julien Fontanet
9a03a70a3d fix(ESLint): disable react/jsx-indent rule 2018-12-03 14:59:48 +01:00
Julien Fontanet
42badbb08e fix(lint-staged): ESLint should not format 2018-12-03 14:54:41 +01:00
Julien Fontanet
956bdf0e03 feat(fs/smb#_writeFile): support flags 2018-12-03 14:39:24 +01:00
Julien Fontanet
91ff02d5c3 fix(fs/smb): always normalize errors 2018-12-03 14:38:51 +01:00
Julien Fontanet
84d88cf2b9 fix(fs/local#_{output,read}File): correctly handle flags 2018-12-03 14:35:52 +01:00
Julien Fontanet
fca2693730 feat(fs/{output,read}File): only support flags option 2018-12-03 14:35:17 +01:00
Julien Fontanet
d7ac1b9659 feat(fs/smb): implement rmdir 2018-12-03 13:11:31 +01:00
Julien Fontanet
ddb1a8ff51 fix(fs/smb): ensure connection is always closed (#3733) 2018-12-03 11:41:27 +01:00
Pierre Donias
56a2f8858b feat(xo-web/render-xo-item): simpler item components (#3547)
See #2605
2018-12-03 11:11:55 +01:00
Pierre Donias
55d7a1def0 fix(xo-web/backup-ng): canDeltaBackup import (#3745) 2018-12-03 10:34:46 +01:00
Pierre Donias
7b354f364c fix(xo-acl-resolver): add PBD resolution rule (#3742)
Fixes #2204
2018-12-03 09:32:18 +01:00
Pierre Donias
12dd40d330 fix(xo-web): missingPaths → missingPatches (#3741) 2018-11-30 15:12:10 +01:00
badrAZ
205f09a633 feat(xo-web/settings/users): display user groups (#3740)
Fixes #3719
2018-11-30 14:34:44 +01:00
badrAZ
f7dcccd8af chore(xo-web/backup-ng/new): improve setting pools implementation (#3715)
See #2578
2018-11-30 11:00:05 +01:00
Julien Fontanet
75592023f2 fix(xo-server/file restore): correctly use _getFilePath
Fixes #3739
2018-11-30 08:24:52 +01:00
Pierre Donias
a794a61c9b feat(xo-server): 5.31.1 2018-11-29 17:10:51 +01:00
Pierre Donias
804da115c9 chore(xo-server): use app-conf@0.6.1 2018-11-29 17:10:20 +01:00
Julien Fontanet
89df4f771b fix(fs/local#_rmdir): correctly prefix path 2018-11-29 15:35:58 +01:00
Pierre Donias
db3c5cfcb8 chore(CHANGELOG): 5.29.0 2018-11-29 14:22:08 +01:00
Pierre Donias
19d191a472 feat(xo-web): 5.31.0 2018-11-29 14:15:55 +01:00
Pierre Donias
d906fec236 feat(xo-server): 5.31.0 2018-11-29 14:15:18 +01:00
Pierre Donias
552482275d feat(xo-server-usage-report): 0.7.1 2018-11-29 14:13:54 +01:00
Pierre Donias
f06d40cf95 feat(xo-server-perf-alert): 0.2.0 2018-11-29 14:13:03 +01:00
Pierre Donias
cf3f1a1705 feat(xen-api): 0.22.0 2018-11-29 14:10:42 +01:00
Pierre Donias
08583c06ef feat({xo-server,xo-web}/VM): pause/unpause (#3731)
Fixes #3727
2018-11-29 11:51:42 +01:00
Julien Fontanet
5271a5c984 chore(xo-web/addSubscriptions): dont use deprecated componentWillMount (#3734) 2018-11-29 11:17:46 +01:00
Julien Fontanet
e69610643b feat(xo-server/xosan): use XOSAN as VM tag (#3735)
It previously was `XOSAN-${pool.name_label}` which is unnecessary, not easy to use for filtering and problematic for #2128.
2018-11-28 22:27:45 +01:00
Julien Fontanet
ef61e4fe6d chore(xo-server/api/xosan): reformat code 2018-11-28 22:10:12 +01:00
Julien Fontanet
4f776e1370 chore(fs): normalize paths in Abstract (#3732) 2018-11-28 21:44:49 +01:00
Julien Fontanet
aa72708996 chore: reformat some files 2018-11-28 19:10:51 +01:00
Enishowk
8751180634 feat(fs): rmdir method (#3730) 2018-11-28 17:42:32 +01:00
Julien Fontanet
2e327be49d chore(xo-web): reformat some files 2018-11-28 17:40:56 +01:00
badrAZ
f06a937c9c fix(xo-server/mergeXenPools): pool id → server id (#3728)
See #2238
2018-11-28 15:56:30 +01:00
Julien Fontanet
e65b3200cd chore(fs): fix some Flow types 2018-11-28 13:39:38 +01:00
Julien Fontanet
30d3701ab1 chore(fs): reformat code 2018-11-28 13:34:12 +01:00
badrAZ
05fa76dad3 feat(xo-server/xen-servers): prevent server connection when pool already connected (#3724)
See #2238
2018-11-28 10:42:55 +01:00
Julien Fontanet
4020081492 feat(xo-server/api): use per message deflate (#3720)
Related to #3699
2018-11-27 17:51:26 +01:00
Pierre Donias
2fbd4a62b2 chore(prettier): use jsxSingleQuote 2018-11-27 16:03:45 +01:00
Julien Fontanet
b773f5e821 fix(xo-server/xen-servers): pool.id → pool.$id (#3723)
Correctly unregister a Xapi connection from its pool.
2018-11-27 10:24:52 +01:00
Julien Fontanet
76c5ced1dd Update xen-servers.js 2018-11-27 10:23:46 +01:00
badrAZ
197768875b fix error 2018-11-27 10:21:39 +01:00
badrAZ
f0483862a5 fix(xo-server/xen-servers): a xapi object doesn't have an id 2018-11-27 10:12:47 +01:00
Julien Fontanet
ac46d3a5a2 chore: update dependencies
Fixes #3722
2018-11-27 09:26:39 +01:00
Julien Fontanet
2da576a1f8 fix(xo-web): avoid using TextDecoder (#3718)
This is an experimental API and not really necessary here.
2018-11-23 16:54:50 +01:00
Julien Fontanet
2e1ac27cf5 chore: dont format with ESLint 2018-11-23 16:38:24 +01:00
Julien Fontanet
258404affc fix(xo-web/settings/remotes): dont wait for test on create/edit 2018-11-23 16:03:00 +01:00
badrAZ
5121d9d1d7 feat(xo-web/logs): ability to filter logs by the VM name (#3711)
Fixes #2947

To be able to filter logs by the VM name, a property in the top level is needed. `vmNames` is an array which contains all found VMs name. It can be `undefined` if none VM is found.
2018-11-23 14:10:26 +01:00
Pierre Donias
f2a38c5ddd fix(xo-web/xoa/licenses): tip: XOA license mgmt not supported (#3714)
Fixes #3713
2018-11-21 13:12:29 +01:00
badrAZ
97a77b1a33 fix(xo-web/backup-ng-logs): differentiate not fetched yet from empty (#3712)
**NoObjects:**

The `NoObjects` is a wrapper which shows a spinner if a collection is `undefined` and a default message if it's empty.

**The Issue:**

In the line `323`, `groupBy` always returns an object even if `logs` is `undefined`.
2018-11-21 10:31:14 +01:00
Rajaa.BARHTAOUI
88ca41231f feat(xo-web/home/VMs): display pool's name (#3709)
Fixes #2226
2018-11-20 15:59:37 +01:00
badrAZ
9a8f84ccb5 chore(xo-server-perf-alert): add documentation (#3710)
This PR completes the merged PR #3675
2018-11-20 14:18:08 +01:00
Julien Fontanet
dd50fc37fe feat(xen-api): callTimeout option (#3707) 2018-11-20 10:12:58 +01:00
badrAZ
cafcadb286 feat(xo-server-perf-alert): notify if value is below threshold (#3675)
Fixes #3612
2018-11-20 10:07:42 +01:00
Pierre Donias
db3d6bba79 fix(xo-web/acls): action should be reset to undefined (#3704) 2018-11-19 16:14:25 +01:00
Julien Fontanet
11a0fc2a22 chore: update dependencies 2018-11-19 16:09:55 +01:00
badrAZ
1e0a8a5034 fix(CHANGELOG): missing entry (#3703) 2018-11-19 14:28:48 +01:00
Pierre Donias
34ef3e5998 fix(xo-web/xoa): tab icons (#3702) 2018-11-19 13:59:06 +01:00
Enishowk
e73fcc450d feat(xo-server/vm.set): use set_domain_type when available (#3700) 2018-11-19 09:39:16 +01:00
Julien Fontanet
2946eaa156 feat(xo-server): 5.30.1 2018-11-16 18:11:39 +01:00
Julien Fontanet
6dcae9a7d7 fix(xo-server/deleteVm): pass array to Promise.all 2018-11-16 18:10:47 +01:00
Pierre Donias
abeb36f06c chore(CHANGELOG): 5.28.2 2018-11-16 17:53:21 +01:00
Pierre Donias
41139578ba feat(xo-web): 0.30.0 2018-11-16 17:50:25 +01:00
Pierre Donias
cda7621b5d feat(xo-server): 5.30.0 2018-11-16 17:47:58 +01:00
Pierre Donias
b75dd2d424 feat(xo-acl-resolver): 0.4.0 2018-11-16 17:42:41 +01:00
Pierre Donias
273f208722 feat(xo-common): 0.2.0 2018-11-16 17:41:08 +01:00
Pierre Donias
c01e8e892e feat(xen-api): 0.21.0 2018-11-16 17:39:16 +01:00
Julien Fontanet
9dfd81c28f feat(xo-web/subscriptions): keep cached data for 10m (#3701)
Related to #3699

This should improve user experience when changing pages.
2018-11-16 17:37:33 +01:00
Pierre Donias
5dd26ebe33 fix(CHANGELOG): missing entries and links 2018-11-16 16:58:48 +01:00
Julien Fontanet
4c0fe3c14f fix(xo-server/importVdiContent): only throws when compiled with yarn dev
Related to #3678
2018-11-16 14:19:34 +01:00
badrAZ
2353581da8 chore(xo-web/backup-ng): improve setting power state implementation (#3684) 2018-11-16 14:05:58 +01:00
Julien Fontanet
2934b23d2f fix(xo-server/vm.delete): fix template handling (#3695)
Fixes #3498

It seems a recent version of XenServer forbids from destroying VM templates directly, we need to set `is_a_template` to `false` before calling `VM.destroy`.
2018-11-16 11:04:56 +01:00
Julien Fontanet
82e4197237 fix(xo-web/deleteVm): dont ask to force if error (#3696) 2018-11-16 10:39:09 +01:00
Julien Fontanet
a23189f132 fix(xo-server): update VM object when {,guest_}metrics have changed (#3694)
Fixes #3533
2018-11-16 10:32:13 +01:00
Julien Fontanet
47fa1ec81e fix(xo-web/settings/remotes): missing form id (#3697) 2018-11-16 09:35:14 +01:00
Julien Fontanet
4b468663f3 feat(xo-server): ability to set XAPI options in config (#3692) 2018-11-15 16:00:52 +01:00
Nicolas Raynaud
6628dc777d fix(xo-server/xosan.createSR): fix network detection (#3689)
Fixes #3688
2018-11-15 11:10:50 +01:00
Julien Fontanet
3ef3ae0166 fix(xo-server/remote.get{,All}): obfuscate sensitive values (#3687)
Fixes #3682
2018-11-14 14:39:20 +01:00
Julien Fontanet
bc6dbe2771 chore(xen-api/README): add 7.{4..6} compatibility 2018-11-14 14:18:14 +01:00
badrAZ
5651160d1c fix(xo-web/settings/remotes): fix incorrect input type (#3683)
Fixes #3681
2018-11-14 11:21:38 +01:00
Julien Fontanet
6da2669c6f fix(fs/smb#list): throws ENOTDIR instead of SMB error (#3685)
Fixes support#1014

This is more in line of other handlers and fix an issue in Backup NG where listing VM backups would fail when there are files in `xo-vm-backups/`.
2018-11-14 10:11:52 +01:00
Julien Fontanet
8094b5097f feat(xo-server/reaclette-utils/generateId): used as a computed for generated ID (#3680) 2018-11-13 22:58:37 +01:00
Pierre Donias
bdb0547b86 fix(xo-server/vm.create): merge double limit allocation (#3667)
This prevents unwanted error if the limits are exceeded in a transitory fashion.
2018-11-13 17:18:56 +01:00
Pierre Donias
ea08fbbfba fix(xo-server-cloud): missing protocol (#3686) 2018-11-13 17:01:03 +01:00
Julien Fontanet
b4cbd8b2b5 chore(fs): ignore enabled option
It makes no sense for this library to handled disabled remote.
2018-11-13 15:59:16 +01:00
Julien Fontanet
f8fbb6b7d3 feat(xen-api): add call params to errors (#3679)
It also moves `.method` to `.call.method`, but it appears it was not currently used in our code.
2018-11-13 12:03:05 +01:00
Julien Fontanet
c8da9fec0a chore: minor Flow fixes 2018-11-13 11:24:23 +01:00
Julien Fontanet
79fb3ec8bd fix(xo-server/plugin.get): hide sensitive config values (#3671) 2018-11-13 11:09:58 +01:00
Julien Fontanet
2243966ce1 feat(xo-server/checkPermissions): similar to hasPermissions but throws 2018-11-13 10:46:38 +01:00
Julien Fontanet
ca7d520997 feat(xo-acl-resolver/assert): throw error if unauthorized 2018-11-13 10:46:38 +01:00
Julien Fontanet
df44487363 chore(xo-web/backup-ng/log): disable bug reporting
Temporary measure because it is currently broken due to the size of the log in the HTTP request.
2018-11-13 10:13:20 +01:00
Julien Fontanet
b39eb0f60d chore(xo-server/api): add expected permission to unauthorized error 2018-11-12 17:34:15 +01:00
badrAZ
a3dcdc4fd5 chore(xo-server-perf-alert): unused property (#3672) 2018-11-12 15:38:58 +01:00
Julien Fontanet
2daac73c17 chore(xo-common/api-errors/unauthorized): add metadata
```ts
let data: {
  permission?: string,
  object: {
    id?: string,
    type?: string,
  },
}
```
2018-11-12 15:30:28 +01:00
Julien Fontanet
23eb3c3094 chore(xo-server/resourceSet.get{,All}): explicit permission check 2018-11-12 15:25:51 +01:00
Julien Fontanet
776d0f9e4a chore(xo-server/ipPool.getAll): explicit permission check 2018-11-12 15:25:51 +01:00
Enishowk
54bdcc6dd2 feat(xo-web/VM): switch virtualization mode (#3669)
Fixes #2372
2018-11-12 13:57:23 +01:00
Julien Fontanet
38084c8199 chore(xo-common/api-errors/unauthorized): remove "unauthorized" in message
It confuses users and the message is still relevant without in case of unauthorized users.
2018-11-12 13:50:33 +01:00
badrAZ
4525ee7491 fix(xo-server-usage-report): gracefully handle fetching stats error (#3656)
Fixes #3600
2018-11-09 16:10:54 +01:00
badrAZ
66a476bd21 feat(xo-web/backup-ng): warning & omitting VMs/pools on XS < 6.5 (#3668)
Fixes #3540
2018-11-09 15:02:29 +01:00
Pierre Donias
be6cc12632 fix(xo-server/vm.create): revert 37a906a2 (#3666)
Fixes #3658, fixes support#1064
2018-11-08 17:31:07 +01:00
Enishowk
673475dcb2 fix(xo-web/vm): restore display of pvhvm virtualization mode (#3662)
Fixes #3576
2018-11-08 17:05:46 +01:00
badrAZ
7dc1a80a83 fix(xo-server/xapi-stats/getVmStats): avoid sync exceptions (#3661) 2018-11-08 16:31:40 +01:00
Julien Fontanet
d49294849f chore(xo-server/vm.import): remove host parameter (#3663) 2018-11-08 16:30:50 +01:00
Rajaa.BARHTAOUI
6b394302c1 feat(xo-web/sortedTable): mutualize actions (#3594) 2018-11-08 14:40:54 +01:00
Julien Fontanet
00e1601f85 chore(xo-server): use VM.domain_type when available (#3664)
And fallback to our previous detection when unavailable.

Starting from this commit, `virtualizationMode` will no longer contain `'pvhvm'`, this must be computed UI side by using both `virtualizationMode` and `xenTools` properties.
2018-11-08 14:37:47 +01:00
Rajaa.BARHTAOUI
b75e746586 fix(xo-web/StrongConfirm): input loses focus (#3649) 2018-11-08 11:38:17 +01:00
Enishowk
32a9fa9bb0 feat(xo-web/migration): auto-select host/SR when there's only one (#3654)
Fixes #3502
2018-11-08 11:30:38 +01:00
badrAZ
79d68dece4 fix(xo-server/xapi-stats): re-fetch host stats if VM missing (#3660) 2018-11-08 11:28:17 +01:00
Julien Fontanet
1701e1d4ba feat(xo-server/_importVdiContent): require length in dev mode 2018-11-08 10:45:11 +01:00
Julien Fontanet
497b3eb296 feat(xo-server/disk.import): extract length from header 2018-11-08 10:44:35 +01:00
Julien Fontanet
ecfafa0fea chore: npm → yarn 2018-11-07 22:24:46 +01:00
Julien Fontanet
def66d8218 chore(xapi-explore-sr): remove Jest config/scripts 2018-11-07 22:23:17 +01:00
Julien Fontanet
eeb08abec2 chore(xapi-explore-sr): use Babel 7 2018-11-07 22:18:58 +01:00
Julien Fontanet
90923c657d chore: re-format code 2018-11-07 18:37:23 +01:00
Julien Fontanet
4ff6eeb424 chore: update dependencies 2018-11-07 18:15:57 +01:00
Julien Fontanet
2d98fb40f1 feat(xo-server/_assertConsistentHostServerTime): display delta 2018-11-07 17:06:17 +01:00
Julien Fontanet
256a58ded2 feat(xo-server/_assertConsistentHostServerTime): threshold 2s → 30s 2018-11-07 17:00:29 +01:00
badrAZ
bf3b31a9ef fix(xo-web/logs): properly display restore failures (#3648) 2018-11-07 15:20:25 +01:00
badrAZ
7fc8d59605 feat(xo-server/backup-ng): warnings for missing VMs (#3647) 2018-11-07 13:12:26 +01:00
Julien Fontanet
1a39b2113a fix(docs): XO requires Node 8 2018-11-07 10:53:02 +01:00
badrAZ
cb9f3fbb2c chore(xo-server/backup-ng): restore tasks documentation (#3652) 2018-11-07 10:44:33 +01:00
badrAZ
487f413cdd feat(xo-web/backup): move "restore/file-restore" to Backup NG view (#3610)
Fixes #3499
2018-11-07 10:22:31 +01:00
Pierre Donias
f847969206 fix(xo-server/patching): correctly ignore upgrade patches (#3651)
Possibly related to support#1009
2018-11-07 10:10:36 +01:00
Julien Fontanet
5d9aad44c2 Merge branch 'xapi-explore-sr/master' 2018-11-06 18:26:44 +01:00
Julien Fontanet
ba2027e6d7 feat(xapi-explore-sr): move all files to packages/xapi-explore-sr 2018-11-06 18:19:39 +01:00
Julien Fontanet
087da9376f Merge branch 'xo-import-servers-csv/master' 2018-11-06 18:11:58 +01:00
Julien Fontanet
218e3b46e0 feat(xo-import-servers-csv): move all files to packages/xo-import-servers-csv 2018-11-06 17:55:20 +01:00
Rajaa.BARHTAOUI
f9921e354e feat(xo-web/StrongConfirm): press Enter to validate (#2890)
Fixes #2735
2018-11-06 15:29:01 +01:00
badrAZ
341148a7d3 fix(xo-web/logs): fix restarting VMs with concurrency issue (#3634)
Fixes #3603
2018-11-06 13:45:30 +01:00
Julien Fontanet
7216165f1e chore: update dependencies 2018-11-06 13:27:33 +01:00
Julien Fontanet
a9557af04b fix(CHANGELOG): fix versions 2018-11-05 16:48:09 +01:00
Julien Fontanet
abb80270ad feat(xo-web): 5.29.3 2018-11-05 16:47:23 +01:00
Julien Fontanet
72e93384a5 feat(xo-server): 5.29.4 2018-11-05 16:46:54 +01:00
Julien Fontanet
663b1b76ec fix(CHANGELOG): move entry to correct release 2018-11-05 16:46:18 +01:00
Julien Fontanet
24b8c671fa fix(xo-server/mergeVhd): use remote options 2018-11-05 16:41:37 +01:00
Julien Fontanet
986fec1cd3 feat(xo-server): pass config to workers 2018-11-05 16:41:37 +01:00
badrAZ
f6c2cbc5cf chore(xo-web/backup): devs can create legacy backups (#3645)
Fixes #3624
2018-11-05 14:40:42 +01:00
Pierre Donias
289ed89a78 fix(xo-server/vm.create): dont extract cpus & memoryMax from params (#3646)
Fixes #3644
2018-11-05 14:34:58 +01:00
Enishowk
73de421d47 feat(xo-web/vm/advanced): add nested virt toggle (#3625)
Fixes #3619
2018-11-05 14:17:19 +01:00
badrAZ
dc1eb82295 chore(xo-server/backup-ng): tasks' documentation (#3640) 2018-11-05 14:16:36 +01:00
Enishowk
6629c12166 fix(xo-web/form/Toggle): dont emit onChange if disabled (#3643)
fix(xo-web/form/Toggle): dont emit onChange if disabled
2018-11-05 14:09:13 +01:00
badrAZ
ec5bc1db95 fix(xo-web/backup-ng-logs): incorrect started jobs filter (#3641)
Fixes #3636
2018-11-05 12:09:52 +01:00
Julien Fontanet
ac2c40c842 fix(xo-server/vm.*): ensure force params are optional 2018-11-05 10:42:28 +01:00
Julien Fontanet
61bf669252 feat(fs/nfs): ensure mount error not hidden 2018-11-05 10:11:57 +01:00
Julien Fontanet
4105c53155 chore(CHANGELOG): 5.28.1 2018-11-05 09:59:36 +01:00
Julien Fontanet
aeab2b2a08 feat(xo-web): 5.29.2 2018-11-05 09:57:50 +01:00
Julien Fontanet
95e33ee612 feat(xo-server): 5.29.3 2018-11-05 09:57:01 +01:00
Julien Fontanet
093bda7039 feat(fs): 0.4.1 2018-11-05 09:53:43 +01:00
Julien Fontanet
4e35b19ac5 fix(xo-web/xoa/update): fix re-registration 2018-11-05 09:46:22 +01:00
Julien Fontanet
244d8a51e8 chore(xo-web/xoa/update): dont hide error 2018-11-05 09:42:59 +01:00
Julien Fontanet
9d6cc77cc8 chore(changelog): add timeout entry 2018-11-03 17:45:50 +01:00
Julien Fontanet
d5e0150880 chore: update promise-toolbox to v0.11
`timeout()` now provides better stack traces and support 0 delay to
disable the timeout.
2018-11-03 17:45:50 +01:00
Julien Fontanet
5cf29a98b3 feat(fs): configurable timeout 2018-11-03 17:45:50 +01:00
Julien Fontanet
165c2262c0 fix(nfs): default timeout 10s → 10m 2018-11-03 17:45:50 +01:00
badrAZ
74f5d2e0cd fix(xo-server/backup-ng): "vms" should be a dictionary (#3635) 2018-11-02 16:29:44 +01:00
badrAZ
2d93456f52 feat(xo-server/backup-ng): log scheduled VMs if concurrency above 0 (#3633)
See #3603
2018-11-02 14:56:46 +01:00
Julien Fontanet
fd401ca335 fix(fs/nfs): opts param is optional 2018-11-02 10:09:19 +01:00
Julien Fontanet
97ba93a9ad chore(fs): configurable mounts dir (#3413) 2018-11-02 09:37:35 +01:00
Pierre Donias
0788c25710 feat(xo-web): 5.29.1 2018-10-31 17:20:15 +01:00
Julien Fontanet
82bba951db feat(xo-web/xoa-updater/logs): from old to new
Fixes #2708
2018-10-31 17:18:53 +01:00
Julien Fontanet
6efd611b80 feat(xo-web/xoa/update): use pre for updater logs 2018-10-31 17:18:53 +01:00
Julien Fontanet
b7d43b42b9 fix(xo-web/xoa/update): toggleState not compatible with ActionButton 2018-10-31 17:18:52 +01:00
Julien Fontanet
801b71d9ae fix(xo-web/xoa/update): typo 2018-10-31 17:18:52 +01:00
Julien Fontanet
873db3bf26 0.2.1 2018-04-13 11:32:47 +02:00
Julien Fontanet
c795887a35 fix: display unmanaged snapshots as unmanaged 2018-04-13 11:32:33 +02:00
Julien Fontanet
23824bafe8 1.1.0 2018-04-09 16:07:31 +02:00
Julien Fontanet
5cca58f2b3 feat(README): add documentation 2018-04-09 16:03:25 +02:00
Julien Fontanet
d05c9b6133 chore: use console.error to display errors 2018-04-09 16:03:04 +02:00
Julien Fontanet
39a84a1ac0 feat: support allowUnauthorized, autoConnect and label 2018-04-09 16:02:46 +02:00
Julien Fontanet
b1c851c9d6 chore(package): prepublishOnly script 2018-04-09 16:02:17 +02:00
Julien Fontanet
6280a9365c chore(package): update dependencies 2018-04-09 16:02:04 +02:00
Julien Fontanet
2741dacd64 0.2.0 2018-04-09 14:01:47 +02:00
Julien Fontanet
4c2c2390bd chore(package): prepublish → prepublishOnly 2018-04-09 14:01:18 +02:00
Julien Fontanet
635b8ce5f0 chore(package): update dependencies 2018-04-09 14:00:17 +02:00
Julien Fontanet
efc13cc456 fix: display VDI with missing parent
Consider them parentless even though they are simply unknown.
2018-04-09 11:34:56 +02:00
Julien Fontanet
078f319fe1 0.1.1 2017-06-07 11:25:44 +02:00
Julien Fontanet
0f0e785871 fix: ensure vdi.physical_utilisation is a number 2017-06-07 11:25:39 +02:00
Julien Fontanet
4e4c85121c 0.1.0 2017-05-11 15:27:15 +02:00
Julien Fontanet
019d6f4cb6 feat: display VDI size 2017-05-11 15:27:06 +02:00
Julien Fontanet
725b0342d1 fix: Xen → XenServer 2017-05-11 15:22:49 +02:00
Julien Fontanet
c93ccb8111 feat: handle -h and --help flags 2017-05-11 15:22:12 +02:00
Julien Fontanet
670befdaf6 chore(package): update all dependencies 2017-05-11 15:19:08 +02:00
Julien Fontanet
55eefd865f 0.0.4 2017-03-30 16:51:27 +02:00
Julien Fontanet
43e5d610e3 fix(package): jest config testPathDirs → roots 2017-03-30 16:51:17 +02:00
Julien Fontanet
b1245bc5be fix(usage): Xen → XenServer 2017-03-30 16:50:44 +02:00
Julien Fontanet
c2feab245e fix: update yarn.lock 2017-03-30 16:48:18 +02:00
Julien Fontanet
cb3753213e fix(README): Xen → XenServer 2017-03-08 14:19:21 +01:00
greenkeeper[bot]
ec8c7a24af chore(package): update jest to version 19.0.1 (#2)
https://greenkeeper.io/
2017-02-22 12:10:24 +01:00
greenkeeper[bot]
2456be2da3 chore(package): update tslint-config-standard to version 3.0.0 (#6)
https://greenkeeper.io/
2017-01-19 10:10:08 +01:00
Julien Fontanet
8c5d4240f9 chore(package): update all dependencies 2017-01-17 10:34:25 +01:00
Julien Fontanet
b1e12d1542 chore: add yarn.lock 2017-01-17 10:28:34 +01:00
Julien Fontanet
a58d7d2ff4 chore(package): use husky instead of ghooks 2017-01-17 10:28:03 +01:00
Julien Fontanet
5308b8b9ed fix(README): should be installed globally 2017-01-17 10:26:32 +01:00
greenkeeper[bot]
c15dffce8f chore(package): update @types/node to version 7.0.0 (#5)
https://greenkeeper.io/
2017-01-11 09:22:18 +01:00
greenkeeper[bot]
874680462e chore(package): update dependencies (#4)
https://greenkeeper.io/
2016-11-28 15:13:31 +01:00
greenkeeper[bot]
bb42540775 chore(package): update tslint-config-standard to version 2.0.0 (#3)
https://greenkeeper.io/
2016-11-21 23:27:47 +01:00
Julien Fontanet
b18511c905 chore(package): update all dependencies 2016-11-08 15:44:21 +01:00
greenkeeper[bot]
5c660f4f64 chore(package): update dependencies (#1)
https://greenkeeper.io/
2016-11-02 09:33:33 +01:00
Julien Fontanet
f2bae73f77 0.0.3 2016-10-31 17:32:32 +01:00
Julien Fontanet
e54d34f269 feat(cli): prefix labels if colors not supported 2016-10-31 17:32:00 +01:00
Julien Fontanet
6470cbd2ee 0.0.2 2016-10-31 17:08:02 +01:00
Julien Fontanet
c06ebcb4a4 fix(askPassword): prompt on stderr 2016-10-31 17:07:34 +01:00
Julien Fontanet
3eaa72c98c 0.0.1 2016-10-31 16:37:40 +01:00
Julien Fontanet
694fff060d fix(package): fix bin 2016-10-31 16:37:36 +01:00
Julien Fontanet
2705062ac3 chore(README): replace placeholders 2016-10-31 16:31:59 +01:00
Julien Fontanet
3df055a296 chore(package): publish 2016-10-31 16:29:36 +01:00
Julien Fontanet
802bc15e0c initial commit 2016-10-31 16:27:15 +01:00
Julien Fontanet
ad2de40a9d chore(package): update @types/through2 to v2.0.29 2016-09-23 09:41:05 +02:00
Julien Fontanet
19298570f8 chore(package): remove unused dep 2016-09-19 14:55:50 +02:00
Julien Fontanet
1da4d1f1e9 chore: repo moved to vatesfr 2016-09-19 14:53:33 +02:00
Julien Fontanet
fe4e9c18fa feat(cli): print usage on missing argument 2016-09-19 14:48:52 +02:00
Julien Fontanet
2c9f84f17f feat(package): add description and keywords 2016-09-19 14:48:52 +02:00
Julien Fontanet
0b2e76600b feat(README): add usage 2016-09-19 14:48:52 +02:00
Julien Fontanet
873554fc01 It works! 2016-09-19 14:43:39 +02:00
Julien Fontanet
82e2d013ae chore(package): reorder entry in package.json 2016-09-19 10:23:33 +02:00
Julien Fontanet
1eb5e80f1f fix(types): fix type definitions 2016-09-19 10:23:18 +02:00
Julien Fontanet
9c0ab5b3cb Initial commit 2016-09-16 18:09:18 +02:00
448 changed files with 9719 additions and 6600 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
# xo_fs_nfs=nfs://ip:/folder
# xo_fs_smb=smb://login:pass@domain\\ip\folder

View File

@@ -1,9 +1,10 @@
module.exports = {
extends: ['standard', 'standard-jsx'],
extends: ['standard', 'standard-jsx', 'prettier'],
globals: {
__DEV__: true,
$Dict: true,
$Diff: true,
$ElementType: true,
$Exact: true,
$Keys: true,
$PropertyType: true,
@@ -16,12 +17,12 @@ module.exports = {
},
},
rules: {
'comma-dangle': ['error', 'always-multiline'],
indent: 'off',
'no-var': 'error',
'node/no-extraneous-import': 'error',
'node/no-extraneous-require': 'error',
'prefer-const': 'error',
// See https://github.com/prettier/eslint-config-prettier/issues/65
'react/jsx-indent': 'off',
},
}

1
.gitignore vendored
View File

@@ -30,3 +30,4 @@ pnpm-debug.log
pnpm-debug.log.*
yarn-error.log
yarn-error.log.*
.env

View File

@@ -1,4 +1,5 @@
module.exports = {
jsxSingleQuote: true,
semi: false,
singleQuote: true,
trailingComma: 'es5',

View File

@@ -14,7 +14,7 @@ const configs = {
'@babel/plugin-proposal-pipeline-operator': {
proposal: 'minimal',
},
'@babel/preset-env' (pkg) {
'@babel/preset-env'(pkg) {
return {
debug: !__TEST__,
@@ -42,11 +42,11 @@ const getConfig = (key, ...args) => {
return config === undefined
? {}
: typeof config === 'function'
? config(...args)
: config
? config(...args)
: config
}
module.exports = function (pkg, plugins, presets) {
module.exports = function(pkg, plugins, presets) {
plugins === undefined && (plugins = {})
presets === undefined && (presets = {})

View File

@@ -5,7 +5,7 @@ const { NULL_REF, Xapi } = require('xen-api')
const pkg = require('./package.json')
Xapi.prototype.getVmDisks = async function (vm) {
Xapi.prototype.getVmDisks = async function(vm) {
const disks = { __proto__: null }
await Promise.all([
...vm.VBDs.map(async vbdRef => {
@@ -19,7 +19,7 @@ Xapi.prototype.getVmDisks = async function (vm) {
return disks
}
defer(async function main ($defer, args) {
defer(async function main($defer, args) {
if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
const cliName = Object.keys(pkg.bin)[0]
return console.error(

View File

@@ -15,6 +15,6 @@
},
"dependencies": {
"golike-defer": "^0.4.1",
"xen-api": "^0.20.0"
"xen-api": "^0.24.0"
}
}

View File

@@ -6,7 +6,7 @@ import parse from './parse'
const MAX_DELAY = 2 ** 31 - 1
class Job {
constructor (schedule, fn) {
constructor(schedule, fn) {
const wrapper = () => {
let result
try {
@@ -33,32 +33,32 @@ class Job {
this._timeout = undefined
}
start () {
start() {
this.stop()
this._scheduleNext()
}
stop () {
stop() {
clearTimeout(this._timeout)
}
}
class Schedule {
constructor (pattern, zone = 'utc') {
constructor(pattern, zone = 'utc') {
this._schedule = parse(pattern)
this._createDate =
zone.toLowerCase() === 'utc'
? moment.utc
: zone === 'local'
? moment
: () => moment.tz(zone)
? moment
: () => moment.tz(zone)
}
createJob (fn) {
createJob(fn) {
return new Job(this, fn)
}
next (n) {
next(n) {
const dates = new Array(n)
const schedule = this._schedule
let date = this._createDate()
@@ -68,12 +68,12 @@ class Schedule {
return dates
}
_nextDelay () {
_nextDelay() {
const now = this._createDate()
return next(this._schedule, now) - now
}
startJob (fn) {
startJob(fn) {
const job = this.createJob(fn)
job.start()
return job.stop.bind(job)

View File

@@ -13,7 +13,7 @@
// process.env.http_proxy
// ])
// ```
export default function defined () {
export default function defined() {
let args = arguments
let n = args.length
if (n === 1) {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/fs",
"version": "0.4.0",
"version": "0.6.0",
"license": "AGPL-3.0",
"description": "The File System for Xen Orchestra backups.",
"keywords": [],
@@ -20,24 +20,30 @@
"node": ">=6"
},
"dependencies": {
"@marsaud/smb2": "^0.9.0",
"@marsaud/smb2": "^0.13.0",
"@sindresorhus/df": "^2.1.0",
"@xen-orchestra/async-map": "^0.0.0",
"execa": "^1.0.0",
"fs-extra": "^7.0.0",
"get-stream": "^4.0.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.10.1",
"through2": "^2.0.3",
"promise-toolbox": "^0.11.0",
"readable-stream": "^3.0.6",
"through2": "^3.0.0",
"tmp": "^0.0.33",
"xo-remote-parser": "^0.5.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/plugin-proposal-decorators": "^7.1.6",
"@babel/plugin-proposal-function-bind": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"async-iterator-to-stream": "^1.1.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"dotenv": "^6.1.0",
"index-modules": "^0.3.0",
"rimraf": "^2.6.2"
},

View File

@@ -0,0 +1,77 @@
import execa from 'execa'
import fs from 'fs-extra'
import { join } from 'path'
import { tmpdir } from 'os'
import LocalHandler from './local'
const sudoExeca = (command, args, opts) =>
execa('sudo', [command, ...args], opts)
export default class MountHandler extends LocalHandler {
constructor(
remote,
{
mountsDir = join(tmpdir(), 'xo-fs-mounts'),
useSudo = false,
...opts
} = {},
params
) {
super(remote, opts)
this._execa = useSudo ? sudoExeca : execa
this._params = params
this._realPath = join(
mountsDir,
remote.id ||
Math.random()
.toString(36)
.slice(2)
)
}
async _forget() {
await this._execa('umount', ['--force', this._getRealPath()], {
env: {
LANG: 'C',
},
}).catch(error => {
if (
error == null ||
typeof error.stderr !== 'string' ||
!error.stderr.includes('not mounted')
) {
throw error
}
})
}
_getRealPath() {
return this._realPath
}
async _sync() {
await fs.ensureDir(this._getRealPath())
const { type, device, options, env } = this._params
return this._execa(
'mount',
['-t', type, device, this._getRealPath(), '-o', options],
{
env: {
LANG: 'C',
...env,
},
}
).catch(error => {
let stderr
if (
error == null ||
typeof (stderr = error.stderr) !== 'string' ||
!(stderr.includes('already mounted') || stderr.includes('busy'))
) {
throw error
}
})
}
}

View File

@@ -0,0 +1,9 @@
import path from 'path'
const { resolve } = path.posix
// normalize the path:
// - does not contains `.` or `..` (cannot escape root dir)
// - always starts with `/`
const normalizePath = path => resolve('/', path)
export { normalizePath as default }

View File

@@ -1,27 +1,74 @@
// @flow
// $FlowFixMe
import getStream from 'get-stream'
import { randomBytes } from 'crypto'
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
import { type Readable, type Writable } from 'stream'
import { parse } from 'xo-remote-parser'
import asyncMap from '@xen-orchestra/async-map'
import path from 'path'
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
import { parse } from 'xo-remote-parser'
import { randomBytes } from 'crypto'
import { type Readable, type Writable } from 'stream'
import normalizePath from './_normalizePath'
import { createChecksumStream, validChecksumOfReadStream } from './checksum'
const { dirname } = path.posix
type Data = Buffer | Readable | string
type FileDescriptor = {| fd: mixed, path: string |}
type LaxReadable = Readable & Object
type LaxWritable = Writable & Object
type RemoteInfo = { used?: number, size?: number }
type File = FileDescriptor | string
const checksumFile = file => file + '.checksum'
export const DEFAULT_TIMEOUT = 10000
const DEFAULT_TIMEOUT = 6e5 // 10 min
const ignoreEnoent = error => {
if (error == null || error.code !== 'ENOENT') {
throw error
}
}
class PrefixWrapper {
constructor(remote, prefix) {
this._prefix = prefix
this._remote = remote
}
get type() {
return this._remote.type
}
// necessary to remove the prefix from the path with `prependDir` option
async list(dir, opts) {
const entries = await this._remote.list(this._resolve(dir), opts)
if (opts != null && opts.prependDir) {
const n = this._prefix.length
entries.forEach((entry, i, entries) => {
entries[i] = entry.slice(n)
})
}
return entries
}
rename(oldPath, newPath) {
return this._remote.rename(this._resolve(oldPath), this._resolve(newPath))
}
_resolve(path) {
return this._prefix + normalizePath(path)
}
}
export default class RemoteHandlerAbstract {
_remote: Object
constructor (remote: any) {
_timeout: number
constructor(remote: any, options: Object = {}) {
if (remote.url === 'test://') {
this._remote = remote
} else {
@@ -30,149 +77,73 @@ export default class RemoteHandlerAbstract {
throw new Error('Incorrect remote type')
}
}
;({ timeout: this._timeout = DEFAULT_TIMEOUT } = options)
}
get type (): string {
// Public members
get type(): string {
throw new Error('Not implemented')
}
/**
* Asks the handler to sync the state of the effective remote with its' metadata
*/
async sync (): Promise<mixed> {
return this._sync()
addPrefix(prefix: string) {
prefix = normalizePath(prefix)
return prefix === '/' ? this : new PrefixWrapper(this, prefix)
}
async _sync (): Promise<mixed> {
throw new Error('Not implemented')
async closeFile(fd: FileDescriptor): Promise<void> {
await timeout.call(this._closeFile(fd.fd), this._timeout)
}
/**
* Free the resources possibly dedicated to put the remote at work, when it is no more needed
*/
async forget (): Promise<void> {
await this._forget()
}
async _forget (): Promise<void> {
throw new Error('Not implemented')
}
async test (): Promise<Object> {
const testFileName = `${Date.now()}.test`
const data = await fromCallback(cb => randomBytes(1024 * 1024, cb))
let step = 'write'
try {
await this.outputFile(testFileName, data)
step = 'read'
const read = await this.readFile(testFileName)
if (data.compare(read) !== 0) {
throw new Error('output and input did not match')
}
return {
success: true,
}
} catch (error) {
return {
success: false,
step,
file: testFileName,
error: error.message || String(error),
}
} finally {
ignoreErrors.call(this.unlink(testFileName))
}
}
async outputFile (file: string, data: Data, options?: Object): Promise<void> {
return this._outputFile(file, data, {
flags: 'wx',
...options,
})
}
async _outputFile (file: string, data: Data, options?: Object): Promise<void> {
const stream = await this.createOutputStream(file, options)
const promise = fromEvent(stream, 'finish')
stream.end(data)
await promise
}
async read (
async createOutputStream(
file: File,
buffer: Buffer,
position?: number
): Promise<{| bytesRead: number, buffer: Buffer |}> {
return this._read(file, buffer, position)
{ checksum = false, ...options }: Object = {}
): Promise<LaxWritable> {
if (typeof file === 'string') {
file = normalizePath(file)
}
const path = typeof file === 'string' ? file : file.path
const streamP = timeout.call(
this._createOutputStream(file, {
flags: 'wx',
...options,
}),
this._timeout
)
if (!checksum) {
return streamP
}
const checksumStream = createChecksumStream()
const forwardError = error => {
checksumStream.emit('error', error)
}
const stream = await streamP
stream.on('error', forwardError)
checksumStream.pipe(stream)
// $FlowFixMe
checksumStream.checksumWritten = checksumStream.checksum
.then(value =>
this._outputFile(checksumFile(path), value, { flags: 'wx' })
)
.catch(forwardError)
return checksumStream
}
_read (
createReadStream(
file: File,
buffer: Buffer,
position?: number
): Promise<{| bytesRead: number, buffer: Buffer |}> {
throw new Error('Not implemented')
}
async readFile (file: string, options?: Object): Promise<Buffer> {
return this._readFile(file, options)
}
_readFile (file: string, options?: Object): Promise<Buffer> {
return this.createReadStream(file, options).then(getStream.buffer)
}
async rename (
oldPath: string,
newPath: string,
{ checksum = false }: Object = {}
) {
let p = timeout.call(this._rename(oldPath, newPath), DEFAULT_TIMEOUT)
if (checksum) {
p = Promise.all([
p,
this._rename(checksumFile(oldPath), checksumFile(newPath)),
])
}
return p
}
async _rename (oldPath: string, newPath: string) {
throw new Error('Not implemented')
}
async list (
dir: string = '.',
{
filter,
prependDir = false,
}: { filter?: (name: string) => boolean, prependDir?: boolean } = {}
): Promise<string[]> {
let entries = await timeout.call(this._list(dir), DEFAULT_TIMEOUT)
if (filter !== undefined) {
entries = entries.filter(filter)
}
if (prependDir) {
entries.forEach((entry, i) => {
entries[i] = dir + '/' + entry
})
}
return entries
}
async _list (dir: string): Promise<string[]> {
throw new Error('Not implemented')
}
createReadStream (
file: string,
{ checksum = false, ignoreMissingChecksum = false, ...options }: Object = {}
): Promise<LaxReadable> {
if (typeof file === 'string') {
file = normalizePath(file)
}
const path = typeof file === 'string' ? file : file.path
const streamP = timeout
.call(this._createReadStream(file, options), DEFAULT_TIMEOUT)
.call(this._createReadStream(file, options), this._timeout)
.then(stream => {
// detect early errors
let promise = fromEvent(stream, 'readable')
@@ -186,7 +157,7 @@ export default class RemoteHandlerAbstract {
promise = Promise.all([
promise,
ignoreErrors.call(
this.getSize(file).then(size => {
this._getSize(file).then(size => {
stream.length = size
})
),
@@ -203,7 +174,7 @@ export default class RemoteHandlerAbstract {
// avoid a unhandled rejection warning
ignoreErrors.call(streamP)
return this.readFile(checksumFile(path)).then(
return this._readFile(checksumFile(path), { flags: 'r' }).then(
checksum =>
streamP.then(stream => {
const { length } = stream
@@ -224,98 +195,380 @@ export default class RemoteHandlerAbstract {
)
}
async _createReadStream (
file: string,
options?: Object
): Promise<LaxReadable> {
throw new Error('Not implemented')
createWriteStream(
file: File,
options: { end?: number, flags?: string, start?: number } = {}
): Promise<LaxWritable> {
return timeout.call(
this._createWriteStream(
typeof file === 'string' ? normalizePath(file) : file,
{
flags: 'wx',
...options,
}
)
)
}
async openFile (path: string, flags?: string): Promise<FileDescriptor> {
// Free the resources possibly dedicated to put the remote at work, when it
// is no more needed
//
// FIXME: Some handlers are implemented based on system-wide mecanisms (such
// as mount), forgetting them might breaking other processes using the same
// remote.
async forget(): Promise<void> {
await this._forget()
}
async getInfo(): Promise<RemoteInfo> {
return timeout.call(this._getInfo(), this._timeout)
}
async getSize(file: File): Promise<number> {
return timeout.call(
this._getSize(typeof file === 'string' ? normalizePath(file) : file),
this._timeout
)
}
async list(
dir: string,
{
filter,
prependDir = false,
}: { filter?: (name: string) => boolean, prependDir?: boolean } = {}
): Promise<string[]> {
const virtualDir = normalizePath(dir)
dir = normalizePath(dir)
let entries = await timeout.call(this._list(dir), this._timeout)
if (filter !== undefined) {
entries = entries.filter(filter)
}
if (prependDir) {
entries.forEach((entry, i) => {
entries[i] = virtualDir + '/' + entry
})
}
return entries
}
async mkdir(dir: string): Promise<void> {
dir = normalizePath(dir)
try {
await this._mkdir(dir)
} catch (error) {
if (error == null || error.code !== 'EEXIST') {
throw error
}
// this operation will throw if it's not already a directory
await this._list(dir)
}
}
async mktree(dir: string): Promise<void> {
await this._mktree(normalizePath(dir))
}
async openFile(path: string, flags: string): Promise<FileDescriptor> {
path = normalizePath(path)
return {
fd: await timeout.call(this._openFile(path, flags), DEFAULT_TIMEOUT),
fd: await timeout.call(this._openFile(path, flags), this._timeout),
path,
}
}
async _openFile (path: string, flags?: string): Promise<mixed> {
throw new Error('Not implemented')
async outputFile(
file: string,
data: Data,
{ flags = 'wx' }: { flags?: string } = {}
): Promise<void> {
await this._outputFile(normalizePath(file), data, { flags })
}
async closeFile (fd: FileDescriptor): Promise<void> {
await timeout.call(this._closeFile(fd.fd), DEFAULT_TIMEOUT)
async read(
file: File,
buffer: Buffer,
position?: number
): Promise<{| bytesRead: number, buffer: Buffer |}> {
return this._read(
typeof file === 'string' ? normalizePath(file) : file,
buffer,
position
)
}
async _closeFile (fd: mixed): Promise<void> {
throw new Error('Not implemented')
async readFile(
file: string,
{ flags = 'r' }: { flags?: string } = {}
): Promise<Buffer> {
return this._readFile(normalizePath(file), { flags })
}
async refreshChecksum (path: string): Promise<void> {
const stream = (await this.createReadStream(path)).pipe(
async refreshChecksum(path: string): Promise<void> {
path = normalizePath(path)
const stream = (await this._createReadStream(path, { flags: 'r' })).pipe(
createChecksumStream()
)
stream.resume() // start reading the whole file
await this.outputFile(checksumFile(path), await stream.checksum)
await this._outputFile(checksumFile(path), await stream.checksum, {
flags: 'wx',
})
}
async createOutputStream (
file: File,
{ checksum = false, ...options }: Object = {}
): Promise<LaxWritable> {
const path = typeof file === 'string' ? file : file.path
const streamP = timeout.call(
this._createOutputStream(file, {
flags: 'wx',
...options,
}),
DEFAULT_TIMEOUT
async rename(
oldPath: string,
newPath: string,
{ checksum = false }: Object = {}
) {
oldPath = normalizePath(oldPath)
newPath = normalizePath(newPath)
let p = timeout.call(this._rename(oldPath, newPath), this._timeout)
if (checksum) {
p = Promise.all([
p,
this._rename(checksumFile(oldPath), checksumFile(newPath)),
])
}
return p
}
async rmdir(dir: string): Promise<void> {
await timeout.call(
this._rmdir(normalizePath(dir)).catch(ignoreEnoent),
this._timeout
)
if (!checksum) {
return streamP
}
const checksumStream = createChecksumStream()
const forwardError = error => {
checksumStream.emit('error', error)
}
const stream = await streamP
stream.on('error', forwardError)
checksumStream.pipe(stream)
// $FlowFixMe
checksumStream.checksumWritten = checksumStream.checksum
.then(value => this.outputFile(checksumFile(path), value))
.catch(forwardError)
return checksumStream
}
async _createOutputStream (
file: mixed,
options?: Object
): Promise<LaxWritable> {
throw new Error('Not implemented')
async rmtree(dir: string): Promise<void> {
await this._rmtree(normalizePath(dir))
}
async unlink (file: string, { checksum = true }: Object = {}): Promise<void> {
// Asks the handler to sync the state of the effective remote with its'
// metadata
//
// This method MUST ALWAYS be called before using the handler.
async sync(): Promise<void> {
await this._sync()
}
async test(): Promise<Object> {
const testFileName = normalizePath(`${Date.now()}.test`)
const data = await fromCallback(cb => randomBytes(1024 * 1024, cb))
let step = 'write'
try {
await this._outputFile(testFileName, data, { flags: 'wx' })
step = 'read'
const read = await this._readFile(testFileName, { flags: 'r' })
if (!data.equals(read)) {
throw new Error('output and input did not match')
}
return {
success: true,
}
} catch (error) {
return {
success: false,
step,
file: testFileName,
error: error.message || String(error),
}
} finally {
ignoreErrors.call(this._unlink(testFileName))
}
}
async unlink(file: string, { checksum = true }: Object = {}): Promise<void> {
file = normalizePath(file)
if (checksum) {
ignoreErrors.call(this._unlink(checksumFile(file)))
}
await timeout.call(this._unlink(file), DEFAULT_TIMEOUT)
await this._unlink(file).catch(ignoreEnoent)
}
async _unlink (file: mixed): Promise<void> {
async writeFile(
file: string,
data: Data,
{ flags = 'wx' }: { flags?: string } = {}
): Promise<void> {
await this._writeFile(normalizePath(file), data, { flags })
}
// Methods that can be implemented by inheriting classes
async _closeFile(fd: mixed): Promise<void> {
throw new Error('Not implemented')
}
async getSize (file: mixed): Promise<number> {
return timeout.call(this._getSize(file), DEFAULT_TIMEOUT)
async _createOutputStream(file: File, options: Object): Promise<LaxWritable> {
try {
return await this._createWriteStream(file, options)
} catch (error) {
if (typeof file !== 'string' || error.code !== 'ENOENT') {
throw error
}
}
await this._mktree(dirname(file))
return this._createOutputStream(file, options)
}
async _getSize (file: mixed): Promise<number> {
async _createReadStream(file: File, options?: Object): Promise<LaxReadable> {
throw new Error('Not implemented')
}
async _createWriteStream(file: File, options: Object): Promise<LaxWritable> {
throw new Error('Not implemented')
}
// called to finalize the remote
async _forget(): Promise<void> {}
async _getInfo(): Promise<Object> {
return {}
}
async _getSize(file: File): Promise<number> {
throw new Error('Not implemented')
}
async _list(dir: string): Promise<string[]> {
throw new Error('Not implemented')
}
async _mkdir(dir: string): Promise<void> {
throw new Error('Not implemented')
}
async _mktree(dir: string): Promise<void> {
try {
return await this.mkdir(dir)
} catch (error) {
if (error.code !== 'ENOENT') {
throw error
}
}
await this._mktree(dirname(dir))
return this._mktree(dir)
}
async _openFile(path: string, flags: string): Promise<mixed> {
throw new Error('Not implemented')
}
async _outputFile(
file: string,
data: Data,
options: { flags?: string }
): Promise<void> {
try {
return await this._writeFile(file, data, options)
} catch (error) {
if (error.code !== 'ENOENT') {
throw error
}
}
await this._mktree(dirname(file))
return this._outputFile(file, data, options)
}
_read(
file: File,
buffer: Buffer,
position?: number
): Promise<{| bytesRead: number, buffer: Buffer |}> {
throw new Error('Not implemented')
}
_readFile(file: string, options?: Object): Promise<Buffer> {
return this._createReadStream(file, options).then(getStream.buffer)
}
async _rename(oldPath: string, newPath: string) {
throw new Error('Not implemented')
}
async _rmdir(dir: string) {
throw new Error('Not implemented')
}
async _rmtree(dir: string) {
try {
return await this._rmdir(dir)
} catch (error) {
if (error.code !== 'ENOTEMPTY') {
throw error
}
}
const files = await this._list(dir)
await asyncMap(files, file =>
this._unlink(`${dir}/${file}`).catch(error => {
if (error.code === 'EISDIR') {
return this._rmtree(`${dir}/${file}`)
}
throw error
})
)
return this._rmtree(dir)
}
// called to initialize the remote
async _sync(): Promise<void> {}
async _unlink(file: string): Promise<void> {
throw new Error('Not implemented')
}
async _writeFile(
file: string,
data: Data,
options: { flags?: string }
): Promise<void> {
throw new Error('Not implemented')
}
}
function createPrefixWrapperMethods() {
const pPw = PrefixWrapper.prototype
const pRha = RemoteHandlerAbstract.prototype
const {
defineProperty,
getOwnPropertyDescriptor,
prototype: { hasOwnProperty },
} = Object
Object.getOwnPropertyNames(pRha).forEach(name => {
let descriptor, value
if (
hasOwnProperty.call(pPw, name) ||
name[0] === '_' ||
typeof (value = (descriptor = getOwnPropertyDescriptor(pRha, name))
.value) !== 'function'
) {
return
}
descriptor.value = function() {
let path
if (arguments.length !== 0 && typeof (path = arguments[0]) === 'string') {
arguments[0] = this._resolve(path)
}
return value.apply(this._remote, arguments)
}
defineProperty(pPw, name, descriptor)
})
}
createPrefixWrapperMethods()

View File

@@ -2,11 +2,13 @@
import { TimeoutError } from 'promise-toolbox'
import AbstractHandler, { DEFAULT_TIMEOUT } from './abstract'
import AbstractHandler from './abstract'
const TIMEOUT = 10e3
class TestHandler extends AbstractHandler {
constructor (impl) {
super({ url: 'test://' })
constructor(impl) {
super({ url: 'test://' }, { timeout: TIMEOUT })
Object.keys(impl).forEach(method => {
this[`_${method}`] = impl[method]
@@ -14,98 +16,110 @@ class TestHandler extends AbstractHandler {
}
}
describe('rename()', () => {
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
const testHandler = new TestHandler({
rename: () => new Promise(() => {}),
})
const promise = testHandler.rename('oldPath', 'newPath')
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('list()', () => {
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
const testHandler = new TestHandler({
list: () => new Promise(() => {}),
})
const promise = testHandler.list()
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('createReadStream()', () => {
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
const testHandler = new TestHandler({
createReadStream: () => new Promise(() => {}),
})
const promise = testHandler.createReadStream('file')
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('openFile()', () => {
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
const testHandler = new TestHandler({
openFile: () => new Promise(() => {}),
})
const promise = testHandler.openFile('path')
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('closeFile()', () => {
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
closeFile: () => new Promise(() => {}),
})
const promise = testHandler.closeFile({ fd: undefined, path: '' })
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('createOutputStream()', () => {
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
createOutputStream: () => new Promise(() => {}),
})
const promise = testHandler.createOutputStream('File')
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('unlink()', () => {
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
describe('createReadStream()', () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
unlink: () => new Promise(() => {}),
createReadStream: () => new Promise(() => {}),
})
const promise = testHandler.unlink('')
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
const promise = testHandler.createReadStream('file')
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('getInfo()', () => {
it('throws in case of timeout', async () => {
const testHandler = new TestHandler({
getInfo: () => new Promise(() => {}),
})
const promise = testHandler.getInfo()
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('getSize()', () => {
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
getSize: () => new Promise(() => {}),
})
const promise = testHandler.getSize('')
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('list()', () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
list: () => new Promise(() => {}),
})
const promise = testHandler.list('.')
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('openFile()', () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
openFile: () => new Promise(() => {}),
})
const promise = testHandler.openFile('path')
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('rename()', () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
rename: () => new Promise(() => {}),
})
const promise = testHandler.rename('oldPath', 'newPath')
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('rmdir()', () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
rmdir: () => new Promise(() => {}),
})
const promise = testHandler.rmdir('dir')
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})

View File

@@ -1,6 +1,5 @@
// @flow
// $FlowFixMe
import through2 from 'through2'
import { createHash } from 'crypto'
import { defer, fromEvent } from 'promise-toolbox'

View File

@@ -1,26 +0,0 @@
/* eslint-env jest */
import rimraf from 'rimraf'
import tmp from 'tmp'
import { pFromCallback } from 'promise-toolbox'
import { getHandler } from '.'
const initialDir = process.cwd()
beforeEach(async () => {
const dir = await pFromCallback(cb => tmp.dir(cb))
process.chdir(dir)
})
afterEach(async () => {
const tmpDir = process.cwd()
process.chdir(initialDir)
await pFromCallback(cb => rimraf(tmpDir, cb))
})
test("fs test doesn't crash", async () => {
const handler = getHandler({ url: 'file://' + process.cwd() })
const result = await handler.test()
expect(result.success).toBeTruthy()
})

View File

@@ -0,0 +1,312 @@
/* eslint-env jest */
import 'dotenv/config'
import asyncIteratorToStream from 'async-iterator-to-stream'
import getStream from 'get-stream'
import { fromCallback } from 'promise-toolbox'
import { pipeline } from 'readable-stream'
import { random } from 'lodash'
import { tmpdir } from 'os'
import { getHandler } from '.'
// https://gist.github.com/julien-f/3228c3f34fdac01ade09
const unsecureRandomBytes = n => {
const bytes = Buffer.alloc(n)
const odd = n & 1
for (let i = 0, m = n - odd; i < m; i += 2) {
bytes.writeUInt16BE((Math.random() * 65536) | 0, i)
}
if (odd) {
bytes.writeUInt8((Math.random() * 256) | 0, n - 1)
}
return bytes
}
const TEST_DATA_LEN = 1024
const TEST_DATA = unsecureRandomBytes(TEST_DATA_LEN)
const createTestDataStream = asyncIteratorToStream(function*() {
yield TEST_DATA
})
const rejectionOf = p =>
p.then(
value => {
throw value
},
reason => reason
)
const handlers = [`file://${tmpdir()}`]
if (process.env.xo_fs_nfs) handlers.push(process.env.xo_fs_nfs)
if (process.env.xo_fs_smb) handlers.push(process.env.xo_fs_smb)
handlers.forEach(url => {
describe(url, () => {
let handler
const testWithFileDescriptor = (path, flags, fn) => {
it('with path', () => fn({ file: path, flags }))
it('with file descriptor', async () => {
const file = await handler.openFile(path, flags)
try {
await fn({ file })
} finally {
await handler.closeFile(file)
}
})
}
beforeAll(async () => {
handler = getHandler({ url }).addPrefix(`xo-fs-tests-${Date.now()}`)
await handler.sync()
})
afterAll(async () => {
await handler.forget()
handler = undefined
})
beforeEach(async () => {
// ensure test dir exists
await handler.mkdir('.')
})
afterEach(async () => {
await handler.rmtree('.')
})
describe('#type', () => {
it('returns the type of the remote', () => {
expect(typeof handler.type).toBe('string')
})
})
describe('#createOutputStream()', () => {
it('creates parent dir if missing', async () => {
const stream = await handler.createOutputStream('dir/file')
await fromCallback(cb => pipeline(createTestDataStream(), stream, cb))
await expect(await handler.readFile('dir/file')).toEqual(TEST_DATA)
})
})
describe('#createReadStream()', () => {
beforeEach(() => handler.outputFile('file', TEST_DATA))
testWithFileDescriptor('file', 'r', async ({ file, flags }) => {
await expect(
await getStream.buffer(
await handler.createReadStream(file, { flags })
)
).toEqual(TEST_DATA)
})
})
describe('#createWriteStream()', () => {
testWithFileDescriptor('file', 'wx', async ({ file, flags }) => {
const stream = await handler.createWriteStream(file, { flags })
await fromCallback(cb => pipeline(createTestDataStream(), stream, cb))
await expect(await handler.readFile('file')).toEqual(TEST_DATA)
})
it('fails if parent dir is missing', async () => {
const error = await rejectionOf(handler.createWriteStream('dir/file'))
expect(error.code).toBe('ENOENT')
})
})
describe('#getInfo()', () => {
let info
beforeAll(async () => {
info = await handler.getInfo()
})
it('should return an object with info', async () => {
expect(typeof info).toBe('object')
})
it('should return correct type of attribute', async () => {
if (info.size !== undefined) {
expect(typeof info.size).toBe('number')
}
if (info.used !== undefined) {
expect(typeof info.used).toBe('number')
}
})
})
describe('#getSize()', () => {
beforeEach(() => handler.outputFile('file', TEST_DATA))
testWithFileDescriptor('file', 'r', async () => {
expect(await handler.getSize('file')).toEqual(TEST_DATA_LEN)
})
})
describe('#list()', () => {
it(`should list the content of folder`, async () => {
await handler.outputFile('file', TEST_DATA)
await expect(await handler.list('.')).toEqual(['file'])
})
it('can prepend the directory to entries', async () => {
await handler.outputFile('dir/file', '')
expect(await handler.list('dir', { prependDir: true })).toEqual([
'/dir/file',
])
})
it('can prepend the directory to entries', async () => {
await handler.outputFile('dir/file', '')
expect(await handler.list('dir', { prependDir: true })).toEqual([
'/dir/file',
])
})
})
describe('#mkdir()', () => {
it('creates a directory', async () => {
await handler.mkdir('dir')
await expect(await handler.list('.')).toEqual(['dir'])
})
it('does not throw on existing directory', async () => {
await handler.mkdir('dir')
await handler.mkdir('dir')
})
it('throws ENOTDIR on existing file', async () => {
await handler.outputFile('file', '')
const error = await rejectionOf(handler.mkdir('file'))
expect(error.code).toBe('ENOTDIR')
})
})
describe('#mktree()', () => {
it('creates a tree of directories', async () => {
await handler.mktree('dir/dir')
await expect(await handler.list('.')).toEqual(['dir'])
await expect(await handler.list('dir')).toEqual(['dir'])
})
it('does not throw on existing directory', async () => {
await handler.mktree('dir/dir')
await handler.mktree('dir/dir')
})
it('throws ENOTDIR on existing file', async () => {
await handler.outputFile('dir/file', '')
const error = await rejectionOf(handler.mktree('dir/file'))
expect(error.code).toBe('ENOTDIR')
})
it('throws ENOTDIR on existing file in path', async () => {
await handler.outputFile('file', '')
const error = await rejectionOf(handler.mktree('file/dir'))
expect(error.code).toBe('ENOTDIR')
})
})
describe('#outputFile()', () => {
it('writes data to a file', async () => {
await handler.outputFile('file', TEST_DATA)
expect(await handler.readFile('file')).toEqual(TEST_DATA)
})
it('throws on existing files', async () => {
await handler.outputFile('file', '')
const error = await rejectionOf(handler.outputFile('file', ''))
expect(error.code).toBe('EEXIST')
})
})
describe('#read()', () => {
beforeEach(() => handler.outputFile('file', TEST_DATA))
const start = random(TEST_DATA_LEN)
const size = random(TEST_DATA_LEN)
testWithFileDescriptor('file', 'r', async ({ file }) => {
const buffer = Buffer.alloc(size)
const result = await handler.read(file, buffer, start)
expect(result.buffer).toBe(buffer)
expect(result).toEqual({
buffer,
bytesRead: Math.min(size, TEST_DATA_LEN - start),
})
})
})
describe('#readFile', () => {
it('returns a buffer containing the contents of the file', async () => {
await handler.outputFile('file', TEST_DATA)
expect(await handler.readFile('file')).toEqual(TEST_DATA)
})
it('throws on missing file', async () => {
const error = await rejectionOf(handler.readFile('file'))
expect(error.code).toBe('ENOENT')
})
})
describe('#rename()', () => {
it(`should rename the file`, async () => {
await handler.outputFile('file', TEST_DATA)
await handler.rename('file', `file2`)
expect(await handler.list('.')).toEqual(['file2'])
expect(await handler.readFile(`file2`)).toEqual(TEST_DATA)
})
})
describe('#rmdir()', () => {
it('should remove an empty directory', async () => {
await handler.mkdir('dir')
await handler.rmdir('dir')
expect(await handler.list('.')).toEqual([])
})
it(`should throw on non-empty directory`, async () => {
await handler.outputFile('dir/file', '')
const error = await rejectionOf(handler.rmdir('.'))
await expect(error.code).toEqual('ENOTEMPTY')
})
it('does not throw on missing directory', async () => {
await handler.rmdir('dir')
})
})
describe('#rmtree', () => {
it(`should remove a directory resursively`, async () => {
await handler.outputFile('dir/file', '')
await handler.rmtree('dir')
expect(await handler.list('.')).toEqual([])
})
})
describe('#test()', () => {
it('tests the remote appears to be working', async () => {
expect(await handler.test()).toEqual({
success: true,
})
})
})
describe('#unlink()', () => {
it(`should remove the file`, async () => {
await handler.outputFile('file', TEST_DATA)
await handler.unlink('file')
await expect(await handler.list('.')).toEqual([])
})
it('does not throw on missing file', async () => {
await handler.unlink('file')
})
})
})
})

View File

@@ -1,20 +1,28 @@
// @flow
import execa from 'execa'
import type RemoteHandler from './abstract'
import RemoteHandlerLocal from './local'
import RemoteHandlerNfs from './nfs'
import RemoteHandlerSmb from './smb'
import RemoteHandlerSmbMount from './smb-mount'
export type { default as RemoteHandler } from './abstract'
export type Remote = { url: string }
const HANDLERS = {
file: RemoteHandlerLocal,
smb: RemoteHandlerSmb,
nfs: RemoteHandlerNfs,
}
export const getHandler = (remote: Remote): RemoteHandler => {
try {
execa.sync('mount.cifs', ['-V'])
HANDLERS.smb = RemoteHandlerSmbMount
} catch (_) {
HANDLERS.smb = RemoteHandlerSmb
}
export const getHandler = (remote: Remote, ...rest: any): RemoteHandler => {
// FIXME: should be done in xo-remote-parser.
const type = remote.url.split('://')[0]
@@ -22,5 +30,5 @@ export const getHandler = (remote: Remote): RemoteHandler => {
if (!Handler) {
throw new Error('Unhandled remote type')
}
return new Handler(remote)
return new Handler(remote, ...rest)
}

View File

@@ -1,51 +1,76 @@
import df from '@sindresorhus/df'
import fs from 'fs-extra'
import { dirname, resolve } from 'path'
import { noop, startsWith } from 'lodash'
import { fromEvent } from 'promise-toolbox'
import RemoteHandlerAbstract from './abstract'
export default class LocalHandler extends RemoteHandlerAbstract {
get type () {
get type() {
return 'file'
}
_getRealPath () {
_getRealPath() {
return this._remote.path
}
_getFilePath (file) {
const realPath = this._getRealPath()
const parts = [realPath]
if (file) {
parts.push(file)
_getFilePath(file) {
return this._getRealPath() + file
}
async _closeFile(fd) {
return fs.close(fd)
}
async _createReadStream(file, options) {
if (typeof file === 'string') {
const stream = fs.createReadStream(this._getFilePath(file), options)
await fromEvent(stream, 'open')
return stream
}
const path = resolve.apply(null, parts)
if (!startsWith(path, realPath)) {
throw new Error('Remote path is unavailable')
return fs.createReadStream('', {
autoClose: false,
...options,
fd: file.fd,
})
}
async _createWriteStream(file, options) {
if (typeof file === 'string') {
const stream = fs.createWriteStream(this._getFilePath(file), options)
await fromEvent(stream, 'open')
return stream
}
return path
return fs.createWriteStream('', {
autoClose: false,
...options,
fd: file.fd,
})
}
async _sync () {
if (this._remote.enabled) {
const path = this._getRealPath()
await fs.ensureDir(path)
await fs.access(path, fs.R_OK | fs.W_OK)
}
return this._remote
_getInfo() {
return df.file(this._getFilePath('/'))
}
async _forget () {
return noop()
async _getSize(file) {
const stats = await fs.stat(
this._getFilePath(typeof file === 'string' ? file : file.path)
)
return stats.size
}
async _outputFile (file, data, options) {
const path = this._getFilePath(file)
await fs.ensureDir(dirname(path))
await fs.writeFile(path, data, options)
async _list(dir) {
return fs.readdir(this._getFilePath(dir))
}
async _read (file, buffer, position) {
_mkdir(dir) {
return fs.mkdir(this._getFilePath(dir))
}
async _openFile(path, flags) {
return fs.open(this._getFilePath(path), flags)
}
async _read(file, buffer, position) {
const needsClose = typeof file === 'string'
file = needsClose ? await fs.open(this._getFilePath(file), 'r') : file.fd
try {
@@ -63,62 +88,29 @@ export default class LocalHandler extends RemoteHandlerAbstract {
}
}
async _readFile (file, options) {
async _readFile(file, options) {
return fs.readFile(this._getFilePath(file), options)
}
async _rename (oldPath, newPath) {
async _rename(oldPath, newPath) {
return fs.rename(this._getFilePath(oldPath), this._getFilePath(newPath))
}
async _list (dir = '.') {
return fs.readdir(this._getFilePath(dir))
async _rmdir(dir) {
return fs.rmdir(this._getFilePath(dir))
}
async _createReadStream (file, options) {
return typeof file === 'string'
? fs.createReadStream(this._getFilePath(file), options)
: fs.createReadStream('', {
autoClose: false,
...options,
fd: file.fd,
})
async _sync() {
const path = this._getRealPath('/')
await fs.ensureDir(path)
await fs.access(path, fs.R_OK | fs.W_OK)
}
async _createOutputStream (file, options) {
if (typeof file === 'string') {
const path = this._getFilePath(file)
await fs.ensureDir(dirname(path))
return fs.createWriteStream(path, options)
}
return fs.createWriteStream('', {
autoClose: false,
...options,
fd: file.fd,
})
async _unlink(file) {
return fs.unlink(this._getFilePath(file))
}
async _unlink (file) {
return fs.unlink(this._getFilePath(file)).catch(error => {
// do not throw if the file did not exist
if (error == null || error.code !== 'ENOENT') {
throw error
}
})
}
async _getSize (file) {
const stats = await fs.stat(
this._getFilePath(typeof file === 'string' ? file : file.path)
)
return stats.size
}
async _openFile (path, flags) {
return fs.open(this._getFilePath(path), flags)
}
async _closeFile (fd) {
return fs.close(fd)
_writeFile(file, data, { flags }) {
return fs.writeFile(this._getFilePath(file), data, { flag: flags })
}
}

View File

@@ -1,71 +1,21 @@
import execa from 'execa'
import fs from 'fs-extra'
import { parse } from 'xo-remote-parser'
import LocalHandler from './local'
import MountHandler from './_mount'
const DEFAULT_NFS_OPTIONS = 'vers=3'
export default class NfsHandler extends LocalHandler {
get type () {
export default class NfsHandler extends MountHandler {
constructor(remote, opts) {
const { host, port, path, options } = parse(remote.url)
super(remote, opts, {
type: 'nfs',
device: `${host}${port !== undefined ? ':' + port : ''}:${path}`,
options:
DEFAULT_NFS_OPTIONS + (options !== undefined ? `,${options}` : ''),
})
}
get type() {
return 'nfs'
}
_getRealPath () {
return `/run/xo-server/mounts/${this._remote.id}`
}
async _mount () {
await fs.ensureDir(this._getRealPath())
const { host, path, port, options } = this._remote
return execa(
'mount',
[
'-t',
'nfs',
'-o',
DEFAULT_NFS_OPTIONS + (options !== undefined ? `,${options}` : ''),
`${host}${port !== undefined ? ':' + port : ''}:${path}`,
this._getRealPath(),
],
{
env: {
LANG: 'C',
},
}
).catch(error => {
if (!error.stderr.includes('already mounted')) {
throw error
}
})
}
async _sync () {
if (this._remote.enabled) {
await this._mount()
} else {
await this._umount()
}
return this._remote
}
async _forget () {
try {
await this._umount(this._remote)
} catch (_) {
// We have to go on...
}
}
async _umount () {
await execa('umount', ['--force', this._getRealPath()], {
env: {
LANG: 'C',
},
}).catch(error => {
if (!error.stderr.includes('not mounted')) {
throw error
}
})
}
}

View File

@@ -0,0 +1,31 @@
import { parse } from 'xo-remote-parser'
import MountHandler from './_mount'
import normalizePath from './_normalizePath'
export default class SmbMountHandler extends MountHandler {
constructor(remote, opts) {
const {
domain = 'WORKGROUP',
host,
options,
password,
path,
username,
} = parse(remote.url)
super(remote, opts, {
type: 'cifs',
device: '//' + host + normalizePath(path),
options:
`domain=${domain}` + (options !== undefined ? `,${options}` : ''),
env: {
USER: username,
PASSWD: password,
},
})
}
get type() {
return 'smb'
}
}

View File

@@ -1,244 +1,167 @@
import Smb2 from '@marsaud/smb2'
import { pFinally } from 'promise-toolbox'
import RemoteHandlerAbstract from './abstract'
const noop = () => {}
// Normalize the error code for file not found.
const normalizeError = error => {
const wrapError = (error, code) => ({
__proto__: error,
cause: error,
code,
})
const normalizeError = (error, shouldBeDirectory) => {
const { code } = error
return code === 'STATUS_OBJECT_NAME_NOT_FOUND' ||
code === 'STATUS_OBJECT_PATH_NOT_FOUND'
? Object.create(error, {
code: {
configurable: true,
readable: true,
value: 'ENOENT',
writable: true,
},
})
throw code === 'STATUS_DIRECTORY_NOT_EMPTY'
? wrapError(error, 'ENOTEMPTY')
: code === 'STATUS_FILE_IS_A_DIRECTORY'
? wrapError(error, 'EISDIR')
: code === 'STATUS_NOT_A_DIRECTORY'
? wrapError(error, 'ENOTDIR')
: code === 'STATUS_OBJECT_NAME_NOT_FOUND' ||
code === 'STATUS_OBJECT_PATH_NOT_FOUND'
? wrapError(error, 'ENOENT')
: code === 'STATUS_OBJECT_NAME_COLLISION'
? wrapError(error, 'EEXIST')
: code === 'STATUS_NOT_SUPPORTED' || code === 'STATUS_INVALID_PARAMETER'
? wrapError(error, shouldBeDirectory ? 'ENOTDIR' : 'EISDIR')
: error
}
const normalizeDirError = error => normalizeError(error, true)
export default class SmbHandler extends RemoteHandlerAbstract {
constructor (remote) {
super(remote)
this._forget = noop
constructor(remote, opts) {
super(remote, opts)
// defined in _sync()
this._client = undefined
const prefix = this._remote.path
this._prefix = prefix !== '' ? prefix + '\\' : prefix
}
get type () {
get type() {
return 'smb'
}
_getClient () {
_getFilePath(file) {
return (
this._prefix +
(typeof file === 'string' ? file : file.path)
.slice(1)
.replace(/\//g, '\\')
)
}
_dirname(file) {
const parts = file.split('\\')
parts.pop()
return parts.join('\\')
}
_closeFile(file) {
return this._client.close(file).catch(normalizeError)
}
_createReadStream(file, options) {
if (typeof file === 'string') {
file = this._getFilePath(file)
} else {
options = { autoClose: false, ...options, fd: file.fd }
file = ''
}
return this._client.createReadStream(file, options).catch(normalizeError)
}
_createWriteStream(file, options) {
if (typeof file === 'string') {
file = this._getFilePath(file)
} else {
options = { autoClose: false, ...options, fd: file.fd }
file = ''
}
return this._client.createWriteStream(file, options).catch(normalizeError)
}
_forget() {
const client = this._client
this._client = undefined
return client.disconnect()
}
_getSize(file) {
return this._client.getSize(this._getFilePath(file)).catch(normalizeError)
}
_list(dir) {
return this._client.readdir(this._getFilePath(dir)).catch(normalizeDirError)
}
_mkdir(dir) {
return this._client.mkdir(this._getFilePath(dir)).catch(normalizeDirError)
}
// TODO: add flags
_openFile(path, flags) {
return this._client
.open(this._getFilePath(path), flags)
.catch(normalizeError)
}
async _read(file, buffer, position) {
const client = this._client
const needsClose = typeof file === 'string'
file = needsClose ? await client.open(this._getFilePath(file)) : file.fd
try {
return await client.read(file, buffer, 0, buffer.length, position)
} catch (error) {
normalizeError(error)
} finally {
if (needsClose) {
await client.close(file)
}
}
}
_readFile(file, options) {
return this._client
.readFile(this._getFilePath(file), options)
.catch(normalizeError)
}
_rename(oldPath, newPath) {
return this._client
.rename(this._getFilePath(oldPath), this._getFilePath(newPath), {
replace: true,
})
.catch(normalizeError)
}
_rmdir(dir) {
return this._client.rmdir(this._getFilePath(dir)).catch(normalizeDirError)
}
_sync() {
const remote = this._remote
return new Smb2({
this._client = new Smb2({
share: `\\\\${remote.host}`,
domain: remote.domain,
username: remote.username,
password: remote.password,
autoCloseTimeout: 0,
})
// Check access (smb2 does not expose connect in public so far...)
return this.list('.')
}
_getFilePath (file) {
if (file === '.') {
file = undefined
}
let path = this._remote.path !== '' ? this._remote.path : ''
// Ensure remote path is a directory.
if (path !== '' && path[path.length - 1] !== '\\') {
path += '\\'
}
if (file) {
path += file.replace(/\//g, '\\')
}
return path
_unlink(file) {
return this._client.unlink(this._getFilePath(file)).catch(normalizeError)
}
_dirname (file) {
const parts = file.split('\\')
parts.pop()
return parts.join('\\')
}
async _sync () {
if (this._remote.enabled) {
// Check access (smb2 does not expose connect in public so far...)
await this.list()
}
return this._remote
}
async _outputFile (file, data, options = {}) {
const client = this._getClient()
const path = this._getFilePath(file)
const dir = this._dirname(path)
if (dir) {
await client.ensureDir(dir)
}
return client.writeFile(path, data, options)::pFinally(() => {
client.disconnect()
})
}
async _read (file, buffer, position) {
const needsClose = typeof file === 'string'
let client
if (needsClose) {
client = this._getClient()
file = await client.open(this._getFilePath(file))
} else {
;({ client, file } = file.fd)
}
try {
return await client.read(file, buffer, 0, buffer.length, position)
} finally {
if (needsClose) {
await client.close(file)
client.disconnect()
}
}
}
async _readFile (file, options = {}) {
const client = this._getClient()
let content
try {
content = await client
.readFile(this._getFilePath(file), options)
::pFinally(() => {
client.disconnect()
})
} catch (error) {
throw normalizeError(error)
}
return content
}
async _rename (oldPath, newPath) {
const client = this._getClient()
try {
await client
.rename(this._getFilePath(oldPath), this._getFilePath(newPath), {
replace: true,
})
::pFinally(() => {
client.disconnect()
})
} catch (error) {
throw normalizeError(error)
}
}
async _list (dir = '.') {
const client = this._getClient()
let list
try {
list = await client.readdir(this._getFilePath(dir))::pFinally(() => {
client.disconnect()
})
} catch (error) {
throw normalizeError(error)
}
return list
}
async _createReadStream (file, options = {}) {
if (typeof file !== 'string') {
file = file.path
}
const client = this._getClient()
let stream
try {
// FIXME ensure that options are properly handled by @marsaud/smb2
stream = await client.createReadStream(this._getFilePath(file), options)
stream.on('end', () => client.disconnect())
} catch (error) {
throw normalizeError(error)
}
return stream
}
async _createOutputStream (file, options = {}) {
if (typeof file !== 'string') {
file = file.path
}
const client = this._getClient()
const path = this._getFilePath(file)
const dir = this._dirname(path)
let stream
try {
if (dir) {
await client.ensureDir(dir)
}
stream = await client.createWriteStream(path, options) // FIXME ensure that options are properly handled by @marsaud/smb2
} catch (err) {
client.disconnect()
throw err
}
stream.on('finish', () => client.disconnect())
return stream
}
async _unlink (file) {
const client = this._getClient()
try {
await client.unlink(this._getFilePath(file))::pFinally(() => {
client.disconnect()
})
} catch (error) {
throw normalizeError(error)
}
}
async _getSize (file) {
const client = await this._getClient()
let size
try {
size = await client
.getSize(this._getFilePath(typeof file === 'string' ? file : file.path))
::pFinally(() => {
client.disconnect()
})
} catch (error) {
throw normalizeError(error)
}
return size
}
// TODO: add flags
async _openFile (path) {
const client = this._getClient()
return {
client,
file: await client.open(this._getFilePath(path)),
}
}
async _closeFile ({ client, file }) {
await client.close(file)
client.disconnect()
_writeFile(file, data, options) {
return this._client
.writeFile(this._getFilePath(file), data, options)
.catch(normalizeError)
}
}

View File

@@ -30,7 +30,7 @@
},
"dependencies": {
"lodash": "^4.17.4",
"promise-toolbox": "^0.10.1"
"promise-toolbox": "^0.11.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -12,7 +12,7 @@ const createTransport = config => {
if (Array.isArray(config)) {
const transports = config.map(createTransport)
const { length } = transports
return function () {
return function() {
for (let i = 0; i < length; ++i) {
transports[i].apply(this, arguments)
}
@@ -29,14 +29,14 @@ const createTransport = config => {
}
const orig = transport
transport = function (log) {
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) {
transport = function(log) {
if (log.level >= level) {
return orig.apply(this, arguments)
}
@@ -85,7 +85,7 @@ export const catchGlobalErrors = logger => {
const EventEmitter = require('events')
const { prototype } = EventEmitter
const { emit } = prototype
function patchedEmit (event, error) {
function patchedEmit(event, error) {
if (event === 'error' && this.listenerCount(event) === 0) {
logger.error('unhandled error event', { error })
return false

View File

@@ -14,7 +14,7 @@ if (!(symbol in global)) {
// -------------------------------------------------------------------
function Log (data, level, namespace, message, time) {
function Log(data, level, namespace, message, time) {
this.data = data
this.level = level
this.namespace = namespace
@@ -22,7 +22,7 @@ function Log (data, level, namespace, message, time) {
this.time = time
}
function Logger (namespace) {
function Logger(namespace) {
this._namespace = namespace
// bind all logging methods
@@ -37,11 +37,11 @@ const { prototype } = Logger
for (const name in LEVELS) {
const level = LEVELS[name]
prototype[name.toLowerCase()] = function (message, data) {
prototype[name.toLowerCase()] = function(message, data) {
if (typeof message !== 'string') {
if (message instanceof Error) {
data = { error: message }
;({ message = 'an error has occured' } = message)
;({ message = 'an error has occurred' } = message)
} else {
return this.warn('incorrect value passed to logger', {
level,
@@ -53,13 +53,13 @@ for (const name in LEVELS) {
}
}
prototype.wrap = function (message, fn) {
prototype.wrap = function(message, fn) {
const logger = this
const warnAndRethrow = error => {
logger.warn(message, { error })
throw error
}
return function () {
return function() {
try {
const result = fn.apply(this, arguments)
const then = result != null && result.then

View File

@@ -13,10 +13,10 @@ const consoleTransport = ({ data, level, namespace, message, time }) => {
level < INFO
? debugConsole
: level < WARN
? infoConsole
: level < ERROR
? warnConsole
: errorConsole
? infoConsole
: level < ERROR
? warnConsole
: errorConsole
fn('%s - %s - [%s] %s', time.toISOString(), namespace, NAMES[level], message)
data != null && fn(data)

View File

@@ -53,14 +53,12 @@ export default ({
fromCallback(cb =>
transporter.sendMail(
{
subject: evalTemplate(
subject,
key =>
key === 'level'
? NAMES[log.level]
: key === 'time'
? log.time.toISOString()
: log[key]
subject: evalTemplate(subject, key =>
key === 'level'
? NAMES[log.level]
: key === 'time'
? log.time.toISOString()
: log[key]
),
text: prettyFormat(log.data),
},

View File

@@ -61,7 +61,7 @@ const mixin = Mixins => Class => {
const n = Mixins.length
function DecoratedClass (...args) {
function DecoratedClass(...args) {
const instance = new Class(...args)
for (let i = 0; i < n; ++i) {

View File

@@ -4,13 +4,140 @@
### Enhancements
- [Backup NG] Restore logs moved to restore tab [#3772](https://github.com/vatesfr/xen-orchestra/issues/3772) (PR [#3802](https://github.com/vatesfr/xen-orchestra/pull/3802))
- [Remotes] New SMB implementation that provides better stability and performance [#2257](https://github.com/vatesfr/xen-orchestra/issues/2257) (PR [#3708](https://github.com/vatesfr/xen-orchestra/pull/3708))
- [VM/advanced] ACL management from VM view [#3040](https://github.com/vatesfr/xen-orchestra/issues/3727) (PR [#3040](https://github.com/vatesfr/xen-orchestra/pull/3774))
- [VM / snapshots] Ability to save the VM memory [#3795](https://github.com/vatesfr/xen-orchestra/issues/3795) (PR [#3812](https://github.com/vatesfr/xen-orchestra/pull/3812))
- [Backup NG / Health] Show number of lone snapshots in tab label [#3500](https://github.com/vatesfr/xen-orchestra/issues/3500) (PR [#3824](https://github.com/vatesfr/xen-orchestra/pull/3824))
- [Login] Add autofocus on username input on login page [#3835](https://github.com/vatesfr/xen-orchestra/issues/3835) (PR [#3836](https://github.com/vatesfr/xen-orchestra/pull/3836))
- [Home/VM] Bulk snapshot: specify snapshots' names [#3778](https://github.com/vatesfr/xen-orchestra/issues/3778) (PR [#3787](https://github.com/vatesfr/xen-orchestra/pull/3787))
- [Remotes] Show free space and disk usage on remote [#3055](https://github.com/vatesfr/xen-orchestra/issues/3055) (PR [#3767](https://github.com/vatesfr/xen-orchestra/pull/3767))
- [New SR] Add tooltip for reattach action button [#3845](https://github.com/vatesfr/xen-orchestra/issues/3845) (PR [#3852](https://github.com/vatesfr/xen-orchestra/pull/3852))
### Bug fixes
- [Self] Display sorted Resource Sets [#3818](https://github.com/vatesfr/xen-orchestra/issues/3818) (PR [#3823](https://github.com/vatesfr/xen-orchestra/pull/3823))
- [Servers] Correctly report connecting status (PR [#3838](https://github.com/vatesfr/xen-orchestra/pull/3838))
- [Servers] Fix cannot reconnect to a server after connection has been lost [#3839](https://github.com/vatesfr/xen-orchestra/issues/3839) (PR [#3841](https://github.com/vatesfr/xen-orchestra/pull/3841))
- [New VM] Fix `NO_HOSTS_AVAILABLE()` error when creating a VM on a local SR from template on another local SR [#3084](https://github.com/vatesfr/xen-orchestra/issues/3084) (PR [#3827](https://github.com/vatesfr/xen-orchestra/pull/3827))
- [Backup NG] Fix typo in the form [#3854](https://github.com/vatesfr/xen-orchestra/issues/3854) (PR [#3855](https://github.com/vatesfr/xen-orchestra/pull/3855))
- [New SR] No warning when creating a NFS SR on a path that is already used as NFS SR [#3844](https://github.com/vatesfr/xen-orchestra/issues/3844) (PR [#3851](https://github.com/vatesfr/xen-orchestra/pull/3851))
### Released packages
- xo-server v5.30.0
- vhd-lib v0.5.0
- vhd-cli v0.2.0
- xen-api v0.24.0
- @xen-orchestra/fs v0.6.0
- xo-server v5.33.0
- xo-web v5.33.0
## **5.30.0** (2018-12-20)
### Enhancements
- [Users] Display user groups [#3719](https://github.com/vatesfr/xen-orchestra/issues/3719) (PR [#3740](https://github.com/vatesfr/xen-orchestra/pull/3740))
- [VDI] Display VDI's SR [3021](https://github.com/vatesfr/xen-orchestra/issues/3021) (PR [#3285](https://github.com/vatesfr/xen-orchestra/pull/3285))
- [Health, VM/disks] Display SR's container [#3021](https://github.com/vatesfr/xen-orchestra/issues/3021) (PRs [#3747](https://github.com/vatesfr/xen-orchestra/pull/3747), [#3751](https://github.com/vatesfr/xen-orchestra/pull/3751))
- [Servers] Auto-connect to ejected host [#2238](https://github.com/vatesfr/xen-orchestra/issues/2238) (PR [#3738](https://github.com/vatesfr/xen-orchestra/pull/3738))
- [Backup NG] Add "XOSAN" in excluded tags by default [#2128](https://github.com/vatesfr/xen-orchestra/issues/3563) (PR [#3559](https://github.com/vatesfr/xen-orchestra/pull/3563))
- [VM] add tooltip for VM status icon [#3749](https://github.com/vatesfr/xen-orchestra/issues/3749) (PR [#3765](https://github.com/vatesfr/xen-orchestra/pull/3765))
- [New XOSAN] Improve view and possibility to sort SRs by name/size/free space [#2416](https://github.com/vatesfr/xen-orchestra/issues/2416) (PR [#3691](https://github.com/vatesfr/xen-orchestra/pull/3691))
- [Backup NG] Disable HA on replicated VM (CR, DR) [#2359](https://github.com/vatesfr/xen-orchestra/issues/2359) (PR [#3755](https://github.com/vatesfr/xen-orchestra/pull/3755))
- [Backup NG] Display the last run status for each schedule with the possibility to show the associated log [#3769](https://github.com/vatesfr/xen-orchestra/issues/3769) (PR [#3779](https://github.com/vatesfr/xen-orchestra/pull/3779))
- [Backup NG] Add a link to the documentation [#3789](https://github.com/vatesfr/xen-orchestra/issues/3789) (PR [#3790](https://github.com/vatesfr/xen-orchestra/pull/3790))
- [Backup NG] Ability to copy schedule/job id to the clipboard [#3753](https://github.com/vatesfr/xen-orchestra/issues/3753) (PR [#3791](https://github.com/vatesfr/xen-orchestra/pull/3791))
- [Backup NG / logs] Merge the job log status with the display details button [#3797](https://github.com/vatesfr/xen-orchestra/issues/3797) (PR [#3800](https://github.com/vatesfr/xen-orchestra/pull/3800))
- [XOA] Notification banner when XOA is not registered [#3803](https://github.com/vatesfr/xen-orchestra/issues/3803) (PR [#3808](https://github.com/vatesfr/xen-orchestra/pull/3808))
### Bug fixes
- [Home/SRs] Fixed SR status for non admin users [#2204](https://github.com/vatesfr/xen-orchestra/issues/2204) (PR [#3742](https://github.com/vatesfr/xen-orchestra/pull/3742))
- [Servers] Fix occasional "server's pool already connected" errors when pool is not connected (PR [#3782](https://github.com/vatesfr/xen-orchestra/pull/3782))
- [Self] Fix missing objects when the self service view is the first one to be loaded when opening XO [#2689](https://github.com/vatesfr/xen-orchestra/issues/2689) (PR [#3096](https://github.com/vatesfr/xen-orchestra/pull/3096))
### Released packages
- @xen-orchestra/fs v0.5.0
- xen-api v0.23.0
- xo-acl-resolver v0.4.1
- xo-server v5.32.0
- xo-web v5.32.0
## **5.29.0** (2018-11-29)
### Enhancements
- [Perf alert] Ability to trigger an alarm if a host/VM/SR usage value is below the threshold [#3612](https://github.com/vatesfr/xen-orchestra/issues/3612) (PR [#3675](https://github.com/vatesfr/xen-orchestra/pull/3675))
- [Home/VMs] Display pool's name [#2226](https://github.com/vatesfr/xen-orchestra/issues/2226) (PR [#3709](https://github.com/vatesfr/xen-orchestra/pull/3709))
- [Servers] Prevent new connection if pool is already connected [#2238](https://github.com/vatesfr/xen-orchestra/issues/2238) (PR [#3724](https://github.com/vatesfr/xen-orchestra/pull/3724))
- [VM] Pause (like Suspend but doesn't copy RAM on disk) [#3727](https://github.com/vatesfr/xen-orchestra/issues/3727) (PR [#3731](https://github.com/vatesfr/xen-orchestra/pull/3731))
### Bug fixes
- [Servers] Fix deleting server on joining a pool [#2238](https://github.com/vatesfr/xen-orchestra/issues/2238) (PR [#3728](https://github.com/vatesfr/xen-orchestra/pull/3728))
### Released packages
- xen-api v0.22.0
- xo-server-perf-alert v0.2.0
- xo-server-usage-report v0.7.1
- xo-server v5.31.0
- xo-web v5.31.0
## **5.28.2** (2018-11-16)
### Enhancements
- [VM] Ability to set nested virtualization in settings [#3619](https://github.com/vatesfr/xen-orchestra/issues/3619) (PR [#3625](https://github.com/vatesfr/xen-orchestra/pull/3625))
- [Legacy Backup] Restore and File restore functionalities moved to the Backup NG view [#3499](https://github.com/vatesfr/xen-orchestra/issues/3499) (PR [#3610](https://github.com/vatesfr/xen-orchestra/pull/3610))
- [Backup NG logs] Display warning in case of missing VMs instead of a ghosts VMs tasks (PR [#3647](https://github.com/vatesfr/xen-orchestra/pull/3647))
- [VM] On migration, automatically selects the host and SR when only one is available [#3502](https://github.com/vatesfr/xen-orchestra/issues/3502) (PR [#3654](https://github.com/vatesfr/xen-orchestra/pull/3654))
- [VM] Display VGA and video RAM for PVHVM guests [#3576](https://github.com/vatesfr/xen-orchestra/issues/3576) (PR [#3664](https://github.com/vatesfr/xen-orchestra/pull/3664))
- [Backup NG form] Display a warning to let the user know that the Delta Backup and the Continuous Replication are not supported on XenServer < 6.5 [#3540](https://github.com/vatesfr/xen-orchestra/issues/3540) (PR [#3668](https://github.com/vatesfr/xen-orchestra/pull/3668))
- [Backup NG form] Omit VMs(Simple Backup)/pools(Smart Backup/Resident on) with XenServer < 6.5 from the selection when the Delta Backup mode or the Continuous Replication mode are selected [#3540](https://github.com/vatesfr/xen-orchestra/issues/3540) (PR [#3668](https://github.com/vatesfr/xen-orchestra/pull/3668))
- [VM] Allow to switch the Virtualization mode [#2372](https://github.com/vatesfr/xen-orchestra/issues/2372) (PR [#3669](https://github.com/vatesfr/xen-orchestra/pull/3669))
### Bug fixes
- [Backup ng logs] Fix restarting VMs with concurrency issue [#3603](https://github.com/vatesfr/xen-orchestra/issues/3603) (PR [#3634](https://github.com/vatesfr/xen-orchestra/pull/3634))
- Validate modal containing a confirm text input by pressing the Enter key [#2735](https://github.com/vatesfr/xen-orchestra/issues/2735) (PR [#2890](https://github.com/vatesfr/xen-orchestra/pull/2890))
- [Patches] Bulk install correctly ignores upgrade patches on licensed hosts (PR [#3651](https://github.com/vatesfr/xen-orchestra/pull/3651))
- [Backup NG logs] Handle failed restores (PR [#3648](https://github.com/vatesfr/xen-orchestra/pull/3648))
- [Self/New VM] Incorrect limit computation [#3658](https://github.com/vatesfr/xen-orchestra/issues/3658) (PR [#3666](https://github.com/vatesfr/xen-orchestra/pull/3666))
- [Plugins] Don't expose credentials in config to users (PR [#3671](https://github.com/vatesfr/xen-orchestra/pull/3671))
- [Self/New VM] `not enough … available in the set …` error in some cases (PR [#3667](https://github.com/vatesfr/xen-orchestra/pull/3667))
- [XOSAN] Creation stuck at "Configuring VMs" [#3688](https://github.com/vatesfr/xen-orchestra/issues/3688) (PR [#3689](https://github.com/vatesfr/xen-orchestra/pull/3689))
- [Backup NG] Errors listing backups on SMB remotes with extraneous files (PR [#3685](https://github.com/vatesfr/xen-orchestra/pull/3685))
- [Remotes] Don't expose credentials to users [#3682](https://github.com/vatesfr/xen-orchestra/issues/3682) (PR [#3687](https://github.com/vatesfr/xen-orchestra/pull/3687))
- [VM] Correctly display guest metrics updates (tools, network, etc.) [#3533](https://github.com/vatesfr/xen-orchestra/issues/3533) (PR [#3694](https://github.com/vatesfr/xen-orchestra/pull/3694))
- [VM Templates] Fix deletion [#3498](https://github.com/vatesfr/xen-orchestra/issues/3498) (PR [#3695](https://github.com/vatesfr/xen-orchestra/pull/3695))
### Released packages
- xen-api v0.21.0
- xo-common v0.2.0
- xo-acl-resolver v0.4.0
- xo-server v5.30.1
- xo-web v5.30.0
## **5.28.1** (2018-11-05)
### Enhancements
### Bug fixes
- [Backup NG] Increase timeout in stale remotes detection to limit false positives (PR [#3632](https://github.com/vatesfr/xen-orchestra/pull/3632))
- Fix re-registration issue ([4e35b19ac](https://github.com/vatesfr/xen-orchestra/commit/4e35b19ac56c60f61c0e771cde70a50402797b8a))
- [Backup NG logs] Fix started jobs filter [#3636](https://github.com/vatesfr/xen-orchestra/issues/3636) (PR [#3641](https://github.com/vatesfr/xen-orchestra/pull/3641))
- [New VM] CPU and memory user inputs were ignored since previous release [#3644](https://github.com/vatesfr/xen-orchestra/issues/3644) (PR [#3646](https://github.com/vatesfr/xen-orchestra/pull/3646))
### Released packages
- @xen-orchestra/fs v0.4.1
- xo-server v5.29.4
- xo-web v5.29.3
## **5.28.0** (2018-10-31)
### Enhancements

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -141,3 +141,12 @@ To make the mount point persistent in XOA, edit the `/etc/fstab` file, and add:
```
This way, without modifying your previous scheduled snapshot, they will be written to this new local mountpoint!
## High availability (HA) disabled on replicated VMs
Replicated VMs HA are taken into account by XS/XCP-ng. To avoid the resultant troubles, HA will be disabled from the replicated VMs and a tag indicating this change will be added.
![](./assets/disabled-dr-ha-tag.png)
![](./assets/disabled-cr-ha-tag.png)
> The tag won't be automatically removed by XO on the replicated VMs, even if HA is re-enabled.

View File

@@ -36,38 +36,52 @@ To protect the replication, we removed the possibility to boot your copied VM di
## Manual initial seed
> This is **only** if you need to make the initial copy without making the whole transfer through your network. Otherwise, **you don't need this**.
**If you can't transfer the first backup through your network because it's too large**, you can make a seed locally. In order to do this, follow this procedure (until we make it accessible directly in XO).
**If you can't transfer the first backup through your network**, you can make a seed locally. In order to do this, follow this procedure (until we make it accessible directly in XO):
> This is **only** if you need to make the initial copy without making the whole transfer through your network. Otherwise, **you don't need this**. These instructions are for Backup-NG jobs, and will not work to seed a legacy backup job. Please migrate any legacy jobs to Backup-NG!
### Preparation
1. create a cont. rep job to a non-distant SR (even the SR where the VM currently is). Do NOT enable the job during creation.
1. manually start the first replication (only the first)
1. when finished, export the replicated VM (via XOA or any other means, doesn't matter how you get your XVA file)
1. import the replicated VM on your distant destination
1. you can now remove your local replicated copy
### Job creation
### Modifications
Create the Continuous Replication backup job, and leave it disabled for now. On the main Backup-NG page, note its identifiers, the main `backupJobId` and the ID of one on the schedules for the job, `backupScheduleId`.
In your source host:
### Seed creation
1. Get the UUID of the remote destination SR where your VM was imported
1. On the source host: `xe vm-param-list uuid=<SourceVM_UUID> | grep other-config`.
* You should see somewhere in other-config: `xo:base_delta:<SR_UUID>: <VM_snapshot_UUID>;`
* Remove this entry with `xe vm-param-remove uuid=<OriginalVM_UUID> param-name=other-config param-key=xo:base_delta:<SR_UUID>`
* Recreate the correct param: `xe vm-param-set uuid=<OriginalVM_UUID> other-config:xo:base_delta:<destination_SR_UUID>=<VM_snapshot_UUID>`
Manually create a snapshot on the VM to backup, and note its UUID as `snapshotUuid` from the snapshot panel for the VM.
In XO:
> DO NOT ever delete or alter this snapshot, feel free to rename it to make that clear.
1. Edit the replication job and select the new destination SR
### Seed copy
On the destination host; to avoid data corruption, you need to avoid any VM start:
Export this snapshot to a file, then import it on the target SR.
Note the UUID of this newly created VM as `targetVmUuid`.
> DO not start this VM or it will break the Continuous Replication job! You can rename this VM to more easily remember this.
### Set up metadata
The XOA backup system requires metadata to correctly associate the source snapshot and the target VM to the backup job. We're going to use the `xo-cr-seed` utility to help us set them up.
First install the tool (all the following is done from the XOA VM CLI):
```
xe vm-param-set blocked-operations:start uuid=<DestinationVM_UUID>
npm i -g xo-cr-seed
```
### Enable
Here is an example of how the utility expects the UUIDs and info passed to it:
Manually run the job the first time to check if everything is OK. Then, enable the job. **Now, only the deltas are sent, your initial seed saved you a LOT of time if you have a slow network.**
```
xo-cr-seed
Usage: xo-cr-seed <source XAPI URL> <source snapshot UUID> <target XAPI URL> <target VM UUID> <backup job id> <backup schedule id>
xo-cr-seed v0.2.0
```
Putting it altogether and putting our values and UUID's into the command, it will look like this (it is a long command):
```
xo-cr-seed https://root:password@xen1.company.tld 4a21c1cd-e8bd-4466-910a-f7524ecc07b1 https://root:password@xen2.company.tld 5aaf86ca-ae06-4a4e-b6e1-d04f0609e64d 90d11a94-a88f-4a84-b7c1-ed207d3de2f9 369a26f0-da77-41ab-a998-fa6b02c69b9a
```
### Finished
Your backup job should now be working correctly! Manually run the job the first time to check if everything is OK. Then, enable the job. **Now, only the deltas are sent, your initial seed saved you a LOT of time if you have a slow network.**

View File

@@ -2,26 +2,50 @@
This is the easiest purchase option: you can buy XOA with your registered email account on `xen-orchestra.com`.
## Choose your edition
You can choose the edition you want in two places:
* [the pricing page](https://xen-orchestra.com/#!/pricing)
* [your account/member page](https://xen-orchestra.com/#!/member)
* [your account/purchases page](https://xen-orchestra.com/#!/purchases)
> You need to be logged to make a purchase. If you don't have an account, please [register here](https://xen-orchestra.com/#!/signup).
> You need to be logged in to make a purchase. If you don't have an account, please [register here](https://xen-orchestra.com/#!/signup).
From your account page, click on the purchase menu, then select the edition you need:
![](./assets/directpurchase.png)
Then you need to fill in your information and select **"buy it for my own use"**:
## Purchase options
The second step is to select your purchase option:
- Subscription: only available with a credit card payment. Choose this option for a monthly payment or a yearly payment **renewed automatically** each year.
- Paid period: **check or wire transfer only**. This purchase allows you to subscribe for a one, two or three year period
> A 2 year subscription period grants you 1 month discounted
> A 3 year subscription period grants you 2 months discounted
Then you need to fill in your information and select **"Buy for my own use"** (direct purchase)
![](./assets/member_purchase_2.png)
The default payment method is by **credit card**. But you can also choose the "wire transfer" tab (with the "bank" icon):
## Billing information
You need to complete all the required information on this page in order to move forward.
![](./assets/member_purchase3.png)
> Note: If you are part of the Eurozone, you will need to provide a valid EU VAT number in order to proceed to payment. Transactions between companies inside the Eurozone are VAT free.
Transactions outside the Eurozone are VAT free.
## Wire transfer process
![](./assets/billing_info.png)
If you select wire transfer, you need to upload a proof of transfer before we can unlock your XOA. If you don't, you'll have to wait for funds to actually be transferred into our account.
## Select your payment mode
Credit Card, Wire transfer or Bank check are the three payment methods available on our store. Some methods can be unavailable regarding the purchase option you have selected during step one.
> Wire transfer is not available for monthly and yearly subscription - Credit Card is not available for paid period.
![](./assets/payment_mode.png)
> All required information for wire transfer and Check payment will be available in the last step of the payment AND on your proforma invoice.
> ⚠ Please, use an explicit reference for your wire transfer in order for us to easily identify your payment.

View File

@@ -14,13 +14,13 @@ As you may have seen,in other parts of the documentation, XO is composed of two
### NodeJS
XO needs Node.js. **Please always use the LTS version of Node**.
XO needs Node.js. **Please use Node 8**.
We'll consider at this point that you've got a working node on your box. E.g:
```
$ node -v
v8.9.1
v8.12.0
```
If not, see [this page](https://nodejs.org/en/download/package-manager/) for instructions on how to install Node.
@@ -100,6 +100,9 @@ That's it! Use your browser to visit the xo-server IP address, and it works! :)
If you would like to update your current version, enter your `xen-orchestra` directory and run the following:
```
# This will clear any changes you made in the repository!!
$ git checkout .
$ git pull --ff-only
$ yarn
$ yarn build

View File

@@ -6,10 +6,11 @@ Xen Orchestra is designed to work exclusively on [XCP-ng](https://xcp-ng.org/) a
Backup restore for large VM disks (>1TiB usage) is [broken on all XenServer versions](https://bugs.xenserver.org/browse/XSO-868) until Citrix release a fix.
* XenServer 7.6
* XenServer 7.5
* [VDI I/O error](https://bugs.xenserver.org/browse/XSO-873), waiting for Citrix to release our fix
* XenServer 7.4
* XenServer 7.3
* XenServer 7.3
* XenServer 7.2
* XenServer 7.1
* XenServer 7.0
@@ -26,7 +27,8 @@ Backup restore for large VM disks (>1TiB usage) is [broken on all XenServer vers
All the pending fixes are already integrated in the latest XCP-ng version. We strongly suggest people to keep using the latest XCP-ng version as far as possible.
* XCP-ng 7.6
* XCP-ng 7.5
* XCP-ng 7.4.1
![](https://xen-orchestra.com/blog/content/images/2018/02/logo1glossy.png)
![](https://xen-orchestra.com/blog/content/images/2018/02/logo1glossy.png)

View File

@@ -4,7 +4,7 @@ If you can't purchase using your own account, usually because you need to go thr
Typically, you will provide two contacts:
* The "billing contact" (in general, the purchaser email). This account will have access to invoices. This is the account which makes the purchase and then binds the XO plan to the second contact account, the technical contact.
* The "billing contact" (in general, the purchaser email). This account will have access to invoices. This is the account doing the purchase. Once purchased, the license needs to be bound to the second contact account, the technical contact.
* The "technical contact", the email of the system administrator using the solution and making support requests.
## As "billing contact"
@@ -17,10 +17,10 @@ Typically, you will provide two contacts:
Now, you just have to pick the edition of Xen Orchestra you want to purchase for your IT team.
2. You will then see the payment screen. If your are not purchasing the edition for yourself, you have to pick the **buy for another account** option.
2. On the first payment screen, after you choose the plan and the subscription method. You can select the option "Buy for another account"
![](./assets/purchase-for-another.png)
![](./assets/member_purchase_2.png)
3. Once the payment is completed, you will have to bind the plan with the end-user account (technical contact). If the end-user doesn't have an account yet, the system will create one and send an e-mail to your end user.
@@ -29,4 +29,4 @@ Now, you just have to pick the edition of Xen Orchestra you want to purchase for
That's it, you have now completed the purchase.
**Once you have bound the plan to your end user account, you cannot change it. Double check the spelling of the e-mail before binding the account.**
**Once you have bound the plan to your end user account, you cannot change it. Double check the spelling of the e-mail before binding the account.**

View File

@@ -103,6 +103,6 @@ encoding by prefixing with `json:`:
##### VM import
```
> xo-cli vm.import host=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
> xo-cli vm.import sr=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
```
> Note: `xo-cli` only supports the import of XVA files. It will not import OVA files. To import OVA images, you must use the XOA web UI.

View File

@@ -7,21 +7,22 @@
"babel-jest": "^23.0.1",
"benchmark": "^2.1.4",
"eslint": "^5.1.0",
"eslint-config-prettier": "^3.3.0",
"eslint-config-standard": "12.0.0",
"eslint-config-standard-jsx": "^6.0.2",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^7.0.1",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-promise": "^4.0.0",
"eslint-plugin-react": "^7.6.1",
"eslint-plugin-standard": "^4.0.0",
"exec-promise": "^0.7.0",
"flow-bin": "^0.82.0",
"flow-bin": "^0.89.0",
"globby": "^8.0.0",
"husky": "^1.0.0-rc.15",
"husky": "^1.2.1",
"jest": "^23.0.1",
"lodash": "^4.17.4",
"prettier": "^1.10.2",
"promise-toolbox": "^0.10.1",
"promise-toolbox": "^0.11.0",
"sorted-object": "^2.0.1"
},
"engines": {

View File

@@ -33,17 +33,17 @@ const isRawString = string => {
// -------------------------------------------------------------------
class Node {
createPredicate () {
createPredicate() {
return value => this.match(value)
}
}
export class Null extends Node {
match () {
match() {
return true
}
toString () {
toString() {
return ''
}
}
@@ -51,7 +51,7 @@ export class Null extends Node {
const formatTerms = terms => terms.map(term => term.toString(true)).join(' ')
export class And extends Node {
constructor (children) {
constructor(children) {
super()
if (children.length === 1) {
@@ -60,29 +60,29 @@ export class And extends Node {
this.children = children
}
match (value) {
match(value) {
return this.children.every(child => child.match(value))
}
toString (isNested) {
toString(isNested) {
const terms = formatTerms(this.children)
return isNested ? `(${terms})` : terms
}
}
export class Comparison extends Node {
constructor (operator, value) {
constructor(operator, value) {
super()
this._comparator = Comparison.comparators[operator]
this._operator = operator
this._value = value
}
match (value) {
match(value) {
return typeof value === 'number' && this._comparator(value, this._value)
}
toString () {
toString() {
return this._operator + String(this._value)
}
}
@@ -94,7 +94,7 @@ Comparison.comparators = {
}
export class Or extends Node {
constructor (children) {
constructor(children) {
super()
if (children.length === 1) {
@@ -103,33 +103,33 @@ export class Or extends Node {
this.children = children
}
match (value) {
match(value) {
return this.children.some(child => child.match(value))
}
toString () {
toString() {
return `|(${formatTerms(this.children)})`
}
}
export class Not extends Node {
constructor (child) {
constructor(child) {
super()
this.child = child
}
match (value) {
match(value) {
return !this.child.match(value)
}
toString () {
toString() {
return '!' + this.child.toString(true)
}
}
export class NumberNode extends Node {
constructor (value) {
constructor(value) {
super()
this.value = value
@@ -140,21 +140,21 @@ export class NumberNode extends Node {
})
}
match (value) {
match(value) {
return (
value === this.value ||
(value !== null && typeof value === 'object' && some(value, this.match))
)
}
toString () {
toString() {
return String(this.value)
}
}
export { NumberNode as Number }
export class NumberOrStringNode extends Node {
constructor (value) {
constructor(value) {
super()
this.value = value
@@ -165,7 +165,7 @@ export class NumberOrStringNode extends Node {
})
}
match (lcValue, numValue, value) {
match(lcValue, numValue, value) {
return (
value === numValue ||
(typeof value === 'string'
@@ -175,25 +175,25 @@ export class NumberOrStringNode extends Node {
)
}
toString () {
toString() {
return this.value
}
}
export { NumberOrStringNode as NumberOrString }
export class Property extends Node {
constructor (name, child) {
constructor(name, child) {
super()
this.name = name
this.child = child
}
match (value) {
match(value) {
return value != null && this.child.match(value[this.name])
}
toString () {
toString() {
return `${formatString(this.name)}:${this.child.toString(true)}`
}
}
@@ -207,7 +207,7 @@ const formatString = value =>
: `"${value}"`
export class GlobPattern extends Node {
constructor (value) {
constructor(value) {
// fallback to string node if no wildcard
if (value.indexOf('*') === -1) {
return new StringNode(value)
@@ -232,7 +232,7 @@ export class GlobPattern extends Node {
})
}
match (re, value) {
match(re, value) {
if (typeof value === 'string') {
return re.test(value)
}
@@ -244,13 +244,13 @@ export class GlobPattern extends Node {
return false
}
toString () {
toString() {
return this.value
}
}
export class RegExpNode extends Node {
constructor (pattern, flags) {
constructor(pattern, flags) {
super()
this.re = new RegExp(pattern, flags)
@@ -261,7 +261,7 @@ export class RegExpNode extends Node {
})
}
match (value) {
match(value) {
if (typeof value === 'string') {
return this.re.test(value)
}
@@ -273,14 +273,14 @@ export class RegExpNode extends Node {
return false
}
toString () {
toString() {
return this.re.toString()
}
}
export { RegExpNode as RegExp }
export class StringNode extends Node {
constructor (value) {
constructor(value) {
super()
this.value = value
@@ -291,7 +291,7 @@ export class StringNode extends Node {
})
}
match (lcValue, value) {
match(lcValue, value) {
if (typeof value === 'string') {
return value.toLowerCase().indexOf(lcValue) !== -1
}
@@ -303,24 +303,24 @@ export class StringNode extends Node {
return false
}
toString () {
toString() {
return formatString(this.value)
}
}
export { StringNode as String }
export class TruthyProperty extends Node {
constructor (name) {
constructor(name) {
super()
this.name = name
}
match (value) {
match(value) {
return value != null && !!value[this.name]
}
toString () {
toString() {
return formatString(this.name) + '?'
}
}
@@ -330,12 +330,12 @@ export class TruthyProperty extends Node {
// https://gist.github.com/yelouafi/556e5159e869952335e01f6b473c4ec1
class Failure {
constructor (pos, expected) {
constructor(pos, expected) {
this.expected = expected
this.pos = pos
}
get value () {
get value() {
throw new Error(
`parse error: expected ${this.expected} at position ${this.pos}`
)
@@ -343,7 +343,7 @@ class Failure {
}
class Success {
constructor (pos, value) {
constructor(pos, value) {
this.pos = pos
this.value = value
}
@@ -352,7 +352,7 @@ class Success {
// -------------------------------------------------------------------
class P {
static alt (...parsers) {
static alt(...parsers) {
const { length } = parsers
return new P((input, pos, end) => {
for (let i = 0; i < length; ++i) {
@@ -365,7 +365,7 @@ class P {
})
}
static grammar (rules) {
static grammar(rules) {
const grammar = {}
Object.keys(rules).forEach(k => {
const rule = rules[k]
@@ -374,14 +374,14 @@ class P {
return grammar
}
static lazy (parserCreator, arg) {
static lazy(parserCreator, arg) {
const parser = new P((input, pos, end) =>
(parser._parse = parserCreator(arg)._parse)(input, pos, end)
)
return parser
}
static regex (regex) {
static regex(regex) {
regex = new RegExp(regex.source, 'y')
return new P((input, pos) => {
regex.lastIndex = pos
@@ -392,7 +392,7 @@ class P {
})
}
static seq (...parsers) {
static seq(...parsers) {
const { length } = parsers
return new P((input, pos, end) => {
const values = new Array(length)
@@ -408,21 +408,20 @@ class P {
})
}
static text (text) {
static text(text) {
const { length } = text
return new P(
(input, pos) =>
input.startsWith(text, pos)
? new Success(pos + length, text)
: new Failure(pos, `'${text}'`)
return new P((input, pos) =>
input.startsWith(text, pos)
? new Success(pos + length, text)
: new Failure(pos, `'${text}'`)
)
}
constructor (parse) {
constructor(parse) {
this._parse = parse
}
map (fn) {
map(fn) {
return new P((input, pos, end) => {
const result = this._parse(input, pos, end)
if (result instanceof Success) {
@@ -432,11 +431,11 @@ class P {
})
}
parse (input, pos = 0, end = input.length) {
parse(input, pos = 0, end = input.length) {
return this._parse(input, pos, end).value
}
repeat (min = 0, max = Infinity) {
repeat(min = 0, max = Infinity) {
return new P((input, pos, end) => {
const value = []
let result
@@ -462,7 +461,7 @@ class P {
})
}
skip (otherParser) {
skip(otherParser) {
return new P((input, pos, end) => {
const result = this._parse(input, pos, end)
if (result instanceof Failure) {
@@ -478,17 +477,16 @@ class P {
}
}
P.eof = new P(
(input, pos, end) =>
pos < end ? new Failure(pos, 'end of input') : new Success(pos)
P.eof = new P((input, pos, end) =>
pos < end ? new Failure(pos, 'end of input') : new Success(pos)
)
// -------------------------------------------------------------------
const parser = P.grammar({
default: r =>
P.seq(r.ws, r.term.repeat(), P.eof).map(
([, terms]) => (terms.length === 0 ? new Null() : new And(terms))
P.seq(r.ws, r.term.repeat(), P.eof).map(([, terms]) =>
terms.length === 0 ? new Null() : new And(terms)
),
globPattern: new P((input, pos, end) => {
let value = ''

View File

@@ -1,6 +1,6 @@
{
"name": "vhd-cli",
"version": "0.1.0",
"version": "0.2.0",
"license": "ISC",
"description": "",
"keywords": [],
@@ -26,11 +26,12 @@
"node": ">=6"
},
"dependencies": {
"@xen-orchestra/fs": "^0.4.0",
"@xen-orchestra/fs": "^0.6.0",
"cli-progress": "^2.0.0",
"exec-promise": "^0.7.0",
"getopts": "^2.2.3",
"struct-fu": "^1.2.0",
"vhd-lib": "^0.4.0"
"vhd-lib": "^0.5.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
@@ -40,7 +41,7 @@
"cross-env": "^5.1.3",
"execa": "^1.0.0",
"index-modules": "^0.3.0",
"promise-toolbox": "^0.10.1",
"promise-toolbox": "^0.11.0",
"rimraf": "^2.6.1",
"tmp": "^0.0.33"
},

View File

@@ -1,12 +1,24 @@
import Vhd from 'vhd-lib'
import Vhd, { checkVhdChain } from 'vhd-lib'
import getopts from 'getopts'
import { getHandler } from '@xen-orchestra/fs'
import { resolve } from 'path'
export default async args => {
const checkVhd = (handler, path) => new Vhd(handler, path).readHeaderAndFooter()
export default async rawArgs => {
const { chain, _: args } = getopts(rawArgs, {
boolean: ['chain'],
default: {
chain: false,
},
})
const check = chain ? checkVhdChain : checkVhd
const handler = getHandler({ url: 'file:///' })
for (const vhd of args) {
try {
await new Vhd(handler, resolve(vhd)).readHeaderAndFooter()
await check(handler, resolve(vhd))
console.log('ok:', vhd)
} catch (error) {
console.error('nok:', vhd, error)

View File

@@ -3,7 +3,7 @@ import { mergeVhd } from 'vhd-lib'
import { getHandler } from '@xen-orchestra/fs'
import { resolve } from 'path'
export default async function main (args) {
export default async function main(args) {
if (args.length < 2 || args.some(_ => _ === '-h' || _ === '--help')) {
return `Usage: ${this.command} <child VHD> <parent VHD>`
}
@@ -11,7 +11,7 @@ export default async function main (args) {
const handler = getHandler({ url: 'file:///' })
let bar
await mergeVhd(handler, resolve(args[1]), handler, resolve(args[0]), {
onProgress ({ done, total }) {
onProgress({ done, total }) {
if (bar === undefined) {
bar = new Bar({
format:

View File

@@ -3,7 +3,7 @@ import { createSyntheticStream } from 'vhd-lib'
import { createWriteStream } from 'fs'
import { getHandler } from '@xen-orchestra/fs'
export default async function main (args) {
export default async function main(args) {
if (args.length < 2 || args.some(_ => _ === '-h' || _ === '--help')) {
return `Usage: ${this.command} <input VHD> <output VHD>`
}

View File

@@ -4,7 +4,7 @@ import execPromise from 'exec-promise'
import commands from './commands'
function runCommand (commands, [command, ...args]) {
function runCommand(commands, [command, ...args]) {
if (command === undefined || command === '-h' || command === '--help') {
command = 'help'
}

View File

@@ -28,18 +28,18 @@ afterEach(async () => {
await pFromCallback(cb => rimraf(tmpDir, cb))
})
async function createRandomFile (name, sizeMb) {
async function createRandomFile(name, sizeMb) {
await execa('bash', [
'-c',
`< /dev/urandom tr -dc "\\t\\n [:alnum:]" | head -c ${sizeMb}M >${name}`,
])
}
async function checkFile (vhdName) {
async function checkFile(vhdName) {
await execa('vhd-util', ['check', '-p', '-b', '-t', '-n', vhdName])
}
async function recoverRawContent (vhdName, rawName, originalSize) {
async function recoverRawContent(vhdName, rawName, originalSize) {
await checkFile(vhdName)
await execa('qemu-img', ['convert', '-fvpc', '-Oraw', vhdName, rawName])
if (originalSize !== undefined) {
@@ -47,7 +47,7 @@ async function recoverRawContent (vhdName, rawName, originalSize) {
}
}
async function convertFromRawToVhd (rawName, vhdName) {
async function convertFromRawToVhd(rawName, vhdName) {
await execa('qemu-img', ['convert', '-f', 'raw', '-Ovpc', rawName, vhdName])
}

View File

@@ -1,6 +1,6 @@
{
"name": "vhd-lib",
"version": "0.4.0",
"version": "0.5.0",
"license": "AGPL-3.0",
"description": "Primitives for VHD file handling",
"keywords": [],
@@ -25,7 +25,7 @@
"from2": "^2.3.0",
"fs-extra": "^7.0.0",
"limit-concurrency-decorator": "^0.4.0",
"promise-toolbox": "^0.10.1",
"promise-toolbox": "^0.11.0",
"struct-fu": "^1.2.0",
"uuid": "^3.0.1"
},
@@ -34,7 +34,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"@xen-orchestra/fs": "^0.4.0",
"@xen-orchestra/fs": "^0.6.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"execa": "^1.0.0",

View File

@@ -1,6 +1,6 @@
import { SECTOR_SIZE } from './_constants'
export default function computeGeometryForSize (size) {
export default function computeGeometryForSize(size) {
const totalSectors = Math.min(Math.ceil(size / 512), 65535 * 16 * 255)
let sectorsPerTrackCylinder
let heads

View File

@@ -14,7 +14,7 @@ import {
PLATFORM_WI2K,
} from './_constants'
export function createFooter (
export function createFooter(
size,
timestamp,
geometry,
@@ -39,7 +39,7 @@ export function createFooter (
return footer
}
export function createHeader (
export function createHeader(
maxTableEntries,
tableOffset = HEADER_SIZE + FOOTER_SIZE,
blockSize = VHD_BLOCK_SIZE_BYTES

View File

@@ -0,0 +1,6 @@
import { dirname, resolve } from 'path'
const resolveRelativeFromFile = (file, path) =>
resolve('/', dirname(file), path).slice(1)
export { resolveRelativeFromFile as default }

View File

@@ -95,7 +95,7 @@ export const unpackField = (field, buf) => {
// Returns the checksum of a raw struct.
// The raw struct (footer or header) is altered with the new sum.
export function checksumStruct (buf, struct) {
export function checksumStruct(buf, struct) {
const checksumField = struct.fields.checksum
let sum = 0

View File

@@ -3,7 +3,7 @@ import { dirname, relative } from 'path'
import Vhd from './vhd'
import { DISK_TYPE_DIFFERENCING } from './_constants'
export default async function chain (
export default async function chain(
parentHandler,
parentPath,
childHandler,

View File

@@ -0,0 +1,16 @@
import Vhd from './vhd'
import resolveRelativeFromFile from './_resolveRelativeFromFile'
import { DISK_TYPE_DYNAMIC } from './_constants'
export default async function checkChain(handler, path) {
while (true) {
const vhd = new Vhd(handler, path)
await vhd.readHeaderAndFooter()
if (vhd.footer.diskType === DISK_TYPE_DYNAMIC) {
break
}
path = resolveRelativeFromFile(path, vhd.header.parentUnicodeName)
}
}

View File

@@ -2,7 +2,7 @@ import asyncIteratorToStream from 'async-iterator-to-stream'
import Vhd from './vhd'
export default asyncIteratorToStream(async function * (handler, path) {
export default asyncIteratorToStream(async function*(handler, path) {
const fd = await handler.openFile(path, 'r')
try {
const vhd = new Vhd(handler, fd)

View File

@@ -3,7 +3,7 @@ import asyncIteratorToStream from 'async-iterator-to-stream'
import computeGeometryForSize from './_computeGeometryForSize'
import { createFooter } from './_createFooterHeader'
export default asyncIteratorToStream(async function * (size, blockParser) {
export default asyncIteratorToStream(async function*(size, blockParser) {
const geometry = computeGeometryForSize(size)
const actualSize = geometry.actualSize
const footer = createFooter(
@@ -13,7 +13,7 @@ export default asyncIteratorToStream(async function * (size, blockParser) {
)
let position = 0
function * filePadding (paddingLength) {
function* filePadding(paddingLength) {
if (paddingLength > 0) {
const chunkSize = 1024 * 1024 // 1Mo
for (
@@ -33,10 +33,10 @@ export default asyncIteratorToStream(async function * (size, blockParser) {
if (paddingLength < 0) {
throw new Error('Received out of order blocks')
}
yield * filePadding(paddingLength)
yield* filePadding(paddingLength)
yield next.data
position = next.offsetBytes + next.data.length
}
yield * filePadding(actualSize - position)
yield* filePadding(actualSize - position)
yield footer
})

View File

@@ -19,7 +19,7 @@ const VHD_BLOCK_SIZE_SECTORS = VHD_BLOCK_SIZE_BYTES / SECTOR_SIZE
/**
* @returns currentVhdPositionSector the first free sector after the data
*/
function createBAT (
function createBAT(
firstBlockPosition,
blockAddressList,
ratio,
@@ -39,7 +39,7 @@ function createBAT (
return currentVhdPositionSector
}
export default async function createReadableStream (
export default async function createReadableStream(
diskSize,
incomingBlockSize,
blockAddressList,
@@ -89,7 +89,7 @@ export default async function createReadableStream (
)
const fileSize = endOfData * SECTOR_SIZE + FOOTER_SIZE
let position = 0
function * yieldAndTrack (buffer, expectedPosition) {
function* yieldAndTrack(buffer, expectedPosition) {
if (expectedPosition !== undefined) {
assert.strictEqual(position, expectedPosition)
}
@@ -98,7 +98,7 @@ export default async function createReadableStream (
position += buffer.length
}
}
async function * generateFileContent (blockIterator, bitmapSize, ratio) {
async function* generateFileContent(blockIterator, bitmapSize, ratio) {
let currentBlock = -1
let currentVhdBlockIndex = -1
let currentBlockWithBitmap = Buffer.alloc(0)
@@ -108,7 +108,7 @@ export default async function createReadableStream (
const batIndex = Math.floor(next.offsetBytes / VHD_BLOCK_SIZE_BYTES)
if (batIndex !== currentVhdBlockIndex) {
if (currentVhdBlockIndex >= 0) {
yield * yieldAndTrack(
yield* yieldAndTrack(
currentBlockWithBitmap,
bat.readUInt32BE(currentVhdBlockIndex * 4) * SECTOR_SIZE
)
@@ -126,15 +126,15 @@ export default async function createReadableStream (
bitmapSize + (next.offsetBytes % VHD_BLOCK_SIZE_BYTES)
)
}
yield * yieldAndTrack(currentBlockWithBitmap)
yield* yieldAndTrack(currentBlockWithBitmap)
}
async function * iterator () {
yield * yieldAndTrack(footer, 0)
yield * yieldAndTrack(header, FOOTER_SIZE)
yield * yieldAndTrack(bat, FOOTER_SIZE + HEADER_SIZE)
yield * generateFileContent(blockIterator, bitmapSize, ratio)
yield * yieldAndTrack(footer)
async function* iterator() {
yield* yieldAndTrack(footer, 0)
yield* yieldAndTrack(header, FOOTER_SIZE)
yield* yieldAndTrack(bat, FOOTER_SIZE + HEADER_SIZE)
yield* generateFileContent(blockIterator, bitmapSize, ratio)
yield* yieldAndTrack(footer)
}
const stream = asyncIteratorToStream(iterator())

View File

@@ -1,5 +1,6 @@
import asyncIteratorToStream from 'async-iterator-to-stream'
import { dirname, resolve } from 'path'
import resolveRelativeFromFile from './_resolveRelativeFromFile'
import Vhd from './vhd'
import {
@@ -12,10 +13,7 @@ import {
import { fuFooter, fuHeader, checksumStruct } from './_structs'
import { test as mapTestBit } from './_bitmap'
const resolveRelativeFromFile = (file, path) =>
resolve('/', dirname(file), path).slice(1)
export default async function createSyntheticStream (handler, path) {
export default async function createSyntheticStream(handler, path) {
const fds = []
const cleanup = () => {
for (let i = 0, n = fds.length; i < n; ++i) {
@@ -85,7 +83,7 @@ export default async function createSyntheticStream (handler, path) {
}
const fileSize = blockOffset * SECTOR_SIZE + FOOTER_SIZE
const iterator = function * () {
const iterator = function*() {
try {
footer = fuFooter.pack(footer)
checksumStruct(footer, fuFooter)
@@ -108,14 +106,14 @@ export default async function createSyntheticStream (handler, path) {
yield bitmap
const blocksByVhd = new Map()
const emitBlockSectors = function * (iVhd, i, n) {
const emitBlockSectors = function*(iVhd, i, n) {
const vhd = vhds[iVhd]
const isRootVhd = vhd === rootVhd
if (!vhd.containsBlock(iBlock)) {
if (isRootVhd) {
yield Buffer.alloc((n - i) * SECTOR_SIZE)
} else {
yield * emitBlockSectors(iVhd + 1, i, n)
yield* emitBlockSectors(iVhd + 1, i, n)
}
return
}
@@ -138,11 +136,11 @@ export default async function createSyntheticStream (handler, path) {
if (hasData) {
yield data.slice(start * SECTOR_SIZE, i * SECTOR_SIZE)
} else {
yield * emitBlockSectors(iVhd + 1, start, i)
yield* emitBlockSectors(iVhd + 1, start, i)
}
}
}
yield * emitBlockSectors(owner, 0, sectorsPerBlockData)
yield* emitBlockSectors(owner, 0, sectorsPerBlockData)
}
yield footer
} finally {

View File

@@ -3,6 +3,7 @@ import 'core-js/features/symbol/async-iterator'
export { default } from './vhd'
export { default as chainVhd } from './chain'
export { default as checkVhdChain } from './checkChain'
export { default as createContentStream } from './createContentStream'
export { default as createReadableRawStream } from './createReadableRawStream'
export {

View File

@@ -10,7 +10,7 @@ import { DISK_TYPE_DIFFERENCING, DISK_TYPE_DYNAMIC } from './_constants'
// Merge vhd child into vhd parent.
//
// TODO: rename the VHD file during the merge
export default concurrency(2)(async function merge (
export default concurrency(2)(async function merge(
parentHandler,
parentPath,
childHandler,

View File

@@ -79,11 +79,11 @@ BUF_BLOCK_UNUSED.writeUInt32BE(BLOCK_UNUSED, 0)
// - sectorSize = 512
export default class Vhd {
get batSize () {
get batSize() {
return computeBatSize(this.header.maxTableEntries)
}
constructor (handler, path) {
constructor(handler, path) {
this._handler = handler
this._path = path
}
@@ -92,7 +92,7 @@ export default class Vhd {
// Read functions.
// =================================================================
async _read (start, n) {
async _read(start, n) {
const { bytesRead, buffer } = await this._handler.read(
this._path,
Buffer.alloc(n),
@@ -102,12 +102,12 @@ export default class Vhd {
return buffer
}
containsBlock (id) {
containsBlock(id) {
return this._getBatEntry(id) !== BLOCK_UNUSED
}
// Returns the first address after metadata. (In bytes)
getEndOfHeaders () {
getEndOfHeaders() {
const { header } = this
let end = FOOTER_SIZE + HEADER_SIZE
@@ -132,7 +132,7 @@ export default class Vhd {
}
// Returns the first sector after data.
getEndOfData () {
getEndOfData() {
let end = Math.ceil(this.getEndOfHeaders() / SECTOR_SIZE)
const fullBlockSize = this.sectorsOfBitmap + this.sectorsPerBlock
@@ -153,7 +153,7 @@ export default class Vhd {
// TODO: extract the checks into reusable functions:
// - better human reporting
// - auto repair if possible
async readHeaderAndFooter (checkSecondFooter = true) {
async readHeaderAndFooter(checkSecondFooter = true) {
const buf = await this._read(0, FOOTER_SIZE + HEADER_SIZE)
const bufFooter = buf.slice(0, FOOTER_SIZE)
const bufHeader = buf.slice(FOOTER_SIZE)
@@ -206,7 +206,7 @@ export default class Vhd {
}
// Returns a buffer that contains the block allocation table of a vhd file.
async readBlockAllocationTable () {
async readBlockAllocationTable() {
const { header } = this
this.blockTable = await this._read(
header.tableOffset,
@@ -215,11 +215,11 @@ export default class Vhd {
}
// return the first sector (bitmap) of a block
_getBatEntry (block) {
_getBatEntry(block) {
return this.blockTable.readUInt32BE(block * 4)
}
_readBlock (blockId, onlyBitmap = false) {
_readBlock(blockId, onlyBitmap = false) {
const blockAddr = this._getBatEntry(blockId)
if (blockAddr === BLOCK_UNUSED) {
throw new Error(`no such block ${blockId}`)
@@ -228,23 +228,22 @@ export default class Vhd {
return this._read(
sectorsToBytes(blockAddr),
onlyBitmap ? this.bitmapSize : this.fullBlockSize
).then(
buf =>
onlyBitmap
? { id: blockId, bitmap: buf }
: {
id: blockId,
bitmap: buf.slice(0, this.bitmapSize),
data: buf.slice(this.bitmapSize),
buffer: buf,
}
).then(buf =>
onlyBitmap
? { id: blockId, bitmap: buf }
: {
id: blockId,
bitmap: buf.slice(0, this.bitmapSize),
data: buf.slice(this.bitmapSize),
buffer: buf,
}
)
}
// get the identifiers and first sectors of the first and last block
// in the file
//
_getFirstAndLastBlocks () {
_getFirstAndLastBlocks() {
const n = this.header.maxTableEntries
const bat = this.blockTable
let i = 0
@@ -289,7 +288,7 @@ export default class Vhd {
// =================================================================
// Write a buffer/stream at a given position in a vhd file.
async _write (data, offset) {
async _write(data, offset) {
debug(
`_write offset=${offset} size=${
Buffer.isBuffer(data) ? data.length : '???'
@@ -308,7 +307,7 @@ export default class Vhd {
: fromEvent(data.pipe(stream), 'finish')
}
async _freeFirstBlockSpace (spaceNeededBytes) {
async _freeFirstBlockSpace(spaceNeededBytes) {
try {
const { first, firstSector, lastSector } = this._getFirstAndLastBlocks()
const tableOffset = this.header.tableOffset
@@ -348,7 +347,7 @@ export default class Vhd {
}
}
async ensureBatSize (entries) {
async ensureBatSize(entries) {
const { header } = this
const prevMaxTableEntries = header.maxTableEntries
if (prevMaxTableEntries >= entries) {
@@ -373,7 +372,7 @@ export default class Vhd {
}
// set the first sector (bitmap) of a block
_setBatEntry (block, blockSector) {
_setBatEntry(block, blockSector) {
const i = block * 4
const { blockTable } = this
@@ -384,7 +383,7 @@ export default class Vhd {
// Make a new empty block at vhd end.
// Update block allocation table in context and in file.
async createBlock (blockId) {
async createBlock(blockId) {
const blockAddr = Math.ceil(this.getEndOfData() / SECTOR_SIZE)
debug(`create block ${blockId} at ${blockAddr}`)
@@ -403,7 +402,7 @@ export default class Vhd {
}
// Write a bitmap at a block address.
async writeBlockBitmap (blockAddr, bitmap) {
async writeBlockBitmap(blockAddr, bitmap) {
const { bitmapSize } = this
if (bitmap.length !== bitmapSize) {
@@ -420,7 +419,7 @@ export default class Vhd {
await this._write(bitmap, sectorsToBytes(blockAddr))
}
async writeEntireBlock (block) {
async writeEntireBlock(block) {
let blockAddr = this._getBatEntry(block.id)
if (blockAddr === BLOCK_UNUSED) {
@@ -429,7 +428,7 @@ export default class Vhd {
await this._write(block.buffer, sectorsToBytes(blockAddr))
}
async writeBlockSectors (block, beginSectorId, endSectorId, parentBitmap) {
async writeBlockSectors(block, beginSectorId, endSectorId, parentBitmap) {
let blockAddr = this._getBatEntry(block.id)
if (blockAddr === BLOCK_UNUSED) {
@@ -461,7 +460,7 @@ export default class Vhd {
)
}
async coalesceBlock (child, blockId) {
async coalesceBlock(child, blockId) {
const block = await child._readBlock(blockId)
const { bitmap, data } = block
@@ -503,7 +502,7 @@ export default class Vhd {
}
// Write a context footer. (At the end and beginning of a vhd file.)
async writeFooter (onlyEndFooter = false) {
async writeFooter(onlyEndFooter = false) {
const { footer } = this
const rawFooter = fuFooter.pack(footer)
@@ -523,7 +522,7 @@ export default class Vhd {
await this._write(rawFooter, offset)
}
writeHeader () {
writeHeader() {
const { header } = this
const rawHeader = fuHeader.pack(header)
header.checksum = checksumStruct(rawHeader, fuHeader)
@@ -536,7 +535,7 @@ export default class Vhd {
return this._write(rawHeader, offset)
}
async writeData (offsetSectors, buffer) {
async writeData(offsetSectors, buffer) {
const bufferSizeSectors = Math.ceil(buffer.length / SECTOR_SIZE)
const startBlock = Math.floor(offsetSectors / this.sectorsPerBlock)
const endBufferSectors = offsetSectors + bufferSizeSectors
@@ -589,7 +588,7 @@ export default class Vhd {
await this.writeFooter()
}
async ensureSpaceForParentLocators (neededSectors) {
async ensureSpaceForParentLocators(neededSectors) {
const firstLocatorOffset = FOOTER_SIZE + HEADER_SIZE
const currentSpace =
Math.floor(this.header.tableOffset / SECTOR_SIZE) -
@@ -603,7 +602,7 @@ export default class Vhd {
return firstLocatorOffset
}
async setUniqueParentLocator (fileNameString) {
async setUniqueParentLocator(fileNameString) {
const { header } = this
header.parentLocatorEntry[0].platformCode = PLATFORM_W2KU
const encodedFilename = Buffer.from(fileNameString, 'utf16le')

View File

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

View File

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

View File

@@ -0,0 +1,52 @@
# xapi-explore-sr [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
> Display the list of VDIs (unmanaged and snapshots included) of a SR
## Install
Installation of the [npm package](https://npmjs.org/package/xapi-explore-sr):
```
> npm install --global xapi-explore-sr
```
## Usage
```
> xapi-explore-sr
Usage: xapi-explore-sr [--full] <SR UUID> <XenServer URL> <XenServer user> [<XenServer password>]
```
## Development
```
# Install dependencies
> npm install
# Run the tests
> npm test
# Continuously compile
> npm run dev
# Continuously run the tests
> npm run dev-test
# Build for production (automatically called by npm install)
> npm run build
```
## Contributions
Contributions are *very* welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
ISC © [Vates SAS](https://vates.fr)

View File

@@ -0,0 +1,60 @@
{
"name": "xapi-explore-sr",
"version": "0.2.1",
"license": "ISC",
"description": "Display the list of VDIs (unmanaged and snapshots included) of a SR",
"keywords": [
"api",
"sr",
"vdi",
"vdis",
"xen",
"xen-api",
"xenapi"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xapi-explore-sr",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@isonoe.net"
},
"preferGlobal": true,
"main": "dist/",
"bin": {
"xapi-explore-sr": "dist/index.js"
},
"files": [
"dist/"
],
"engines": {
"node": ">=8"
},
"dependencies": {
"archy": "^1.0.0",
"chalk": "^2.3.2",
"exec-promise": "^0.7.0",
"human-format": "^0.10.0",
"lodash": "^4.17.4",
"pw": "^0.0.4",
"xen-api": "^0.24.0"
},
"devDependencies": {
"@babel/cli": "^7.1.5",
"@babel/core": "^7.1.5",
"@babel/preset-env": "^7.1.5",
"babel-plugin-lodash": "^3.2.11",
"cross-env": "^5.1.4",
"rimraf": "^2.6.1"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
}

View File

@@ -0,0 +1,161 @@
#!/usr/bin/env node
import archy from 'archy'
import chalk from 'chalk'
import execPromise from 'exec-promise'
import humanFormat from 'human-format'
import pw from 'pw'
import { createClient } from 'xen-api'
import { forEach, map, orderBy } from 'lodash'
// ===================================================================
const askPassword = prompt =>
new Promise(resolve => {
prompt && process.stderr.write(`${prompt}: `)
pw(resolve)
})
const formatSize = bytes =>
humanFormat(bytes, {
prefix: 'Gi',
scale: 'binary',
})
const required = name => {
const e = `missing required argument <${name}>`
throw e
}
// -------------------------------------------------------------------
const STYLES = [
[
vdi => !vdi.managed,
chalk.enabled ? chalk.red : label => `[unmanaged] ${label}`,
],
[
vdi => vdi.is_a_snapshot,
chalk.enabled ? chalk.yellow : label => `[snapshot] ${label}`,
],
]
const getStyle = vdi => {
for (let i = 0, n = STYLES.length; i < n; ++i) {
const entry = STYLES[i]
if (entry[0](vdi)) {
return entry[1]
}
}
}
const mapFilter = (collection, iteratee, results = []) => {
forEach(collection, function() {
const result = iteratee.apply(this, arguments)
if (result !== undefined) {
results.push(result)
}
})
return results
}
// -------------------------------------------------------------------
execPromise(async args => {
if (args.length === 0 || args[0] === '-h' || args[0] === '--help') {
return `Usage: xapi-explore-sr [--full] <SR UUID> <XenServer URL> <XenServer user> [<XenServer password>]`
}
const full = args[0] === '--full'
if (full) {
args.shift()
}
const [
srUuid = required('SR UUID'),
url = required('XenServer URL'),
user = required('XenServer user'),
password = await askPassword('XenServer password'),
] = args
const xapi = createClient({
allowUnauthorized: true,
auth: { user, password },
readOnly: true,
url,
watchEvents: false,
})
await xapi.connect()
const srRef = await xapi.call('SR.get_by_uuid', srUuid)
const sr = await xapi.call('SR.get_record', srRef)
const vdisByRef = {}
await Promise.all(
map(sr.VDIs, async ref => {
const vdi = await xapi.call('VDI.get_record', ref)
vdisByRef[ref] = vdi
})
)
const hasParents = {}
const vhdChildrenByUuid = {}
forEach(vdisByRef, vdi => {
const vhdParent = vdi.sm_config['vhd-parent']
if (vhdParent) {
;(
vhdChildrenByUuid[vhdParent] || (vhdChildrenByUuid[vhdParent] = [])
).push(vdi)
} else if (!(vdi.snapshot_of in vdisByRef)) {
return
}
hasParents[vdi.uuid] = true
})
const makeVdiNode = vdi => {
const { uuid } = vdi
let label = `${vdi.name_label} - ${uuid} - ${formatSize(
+vdi.physical_utilisation
)}`
const nodes = []
const vhdChildren = vhdChildrenByUuid[uuid]
if (vhdChildren) {
mapFilter(
orderBy(vhdChildren, 'is_a_snapshot', 'desc'),
makeVdiNode,
nodes
)
}
mapFilter(
vdi.snapshots,
ref => {
const vdi = vdisByRef[ref]
if (full || !vdi.sm_config['vhd-parent']) {
return makeVdiNode(vdi)
}
},
nodes
)
const style = getStyle(vdi)
if (style) {
label = style(label)
}
return { label, nodes }
}
const nodes = mapFilter(orderBy(vdisByRef, ['name_label', 'uuid']), vdi => {
if (!hasParents[vdi.uuid]) {
return makeVdiNode(vdi)
}
})
return archy({
label: `${sr.name_label} (${sr.VDIs.length} VDIs)`,
nodes,
})
})

View File

@@ -4,6 +4,9 @@
Tested with:
- XenServer 7.6
- XenServer 7.5
- XenServer 7.4
- XenServer 7.3
- XenServer 7.2
- XenServer 7.1
@@ -44,6 +47,7 @@ Options:
- `allowUnauthorized`: whether to accept self-signed certificates
- `auth`: credentials used to sign in (can also be specified in the URL)
- `readOnly = false`: if true, no methods with side-effects can be called
- `callTimeout`: number of milliseconds after which a call is considered failed (can also be a map of timeouts by methods)
```js
// Force connection.

View File

@@ -1,30 +1,59 @@
#!/usr/bin/env node
process.env.DEBUG = '*'
process.env.DEBUG = 'xen-api'
const createProgress = require('progress-stream')
const createTop = require('process-top')
const defer = require('golike-defer').default
const pump = require('pump')
const { CancelToken, fromCallback } = require('promise-toolbox')
const getopts = require('getopts')
const humanFormat = require('human-format')
const { CancelToken } = require('promise-toolbox')
const { createClient } = require('../')
const { createOutputStream, resolveRef } = require('./utils')
const {
createOutputStream,
pipeline,
resolveRecord,
throttle,
} = require('./utils')
defer(async ($defer, args) => {
let raw = false
if (args[0] === '--raw') {
raw = true
args.shift()
}
const formatSize = bytes => humanFormat(bytes, { scale: 'binary', unit: 'B' })
function Progress$toString() {
return [
formatSize(this.transferred),
' / ',
formatSize(this.length),
' | ',
this.runtime,
's / ',
this.eta,
's | ',
formatSize(this.speed),
'/s',
].join('')
}
defer(async ($defer, rawArgs) => {
const { raw, throttle: bps, _: args } = getopts(rawArgs, {
boolean: 'raw',
alias: {
raw: 'r',
throttle: 't',
},
})
if (args.length < 2) {
return console.log('Usage: export-vdi [--raw] <XS URL> <VDI identifier> [<VHD file>]')
return console.log(
'Usage: export-vdi [--raw] <XS URL> <VDI identifier> [<VHD file>]'
)
}
const xapi = createClient({
allowUnauthorized: true,
url: args[0],
watchEvents: false
watchEvents: false,
})
await xapi.connect()
@@ -33,21 +62,32 @@ defer(async ($defer, args) => {
const { cancel, token } = CancelToken.source()
process.on('SIGINT', cancel)
const vdi = await resolveRecord(xapi, 'VDI', args[1])
// https://xapi-project.github.io/xen-api/snapshots.html#downloading-a-disk-or-snapshot
const exportStream = await xapi.getResource(token, '/export_raw_vdi/', {
query: {
format: raw ? 'raw' : 'vhd',
vdi: await resolveRef(xapi, 'VDI', args[1])
}
vdi: vdi.$ref,
},
})
console.warn('Export task:', exportStream.headers['task-id'])
await fromCallback(cb => pump(
const top = createTop()
const progressStream = createProgress()
$defer(
clearInterval,
setInterval(() => {
console.warn('\r %s | %s', top.toString(), Progress$toString.call(progressStream.progress()))
}, 1e3)
)
await pipeline(
exportStream,
createOutputStream(args[2]),
cb
))
})(process.argv.slice(2)).catch(
console.error.bind(console, 'error')
)
progressStream,
throttle(bps),
createOutputStream(args[2])
)
})(process.argv.slice(2)).catch(console.error.bind(console, 'error'))

View File

@@ -1,6 +1,12 @@
{
"dependencies": {
"getopts": "^2.2.3",
"golike-defer": "^0.4.1",
"pump": "^3.0.0"
"human-format": "^0.10.1",
"process-top": "^1.0.0",
"progress-stream": "^2.0.0",
"promise-toolbox": "^0.11.0",
"readable-stream": "^3.1.1",
"throttle": "^1.0.3"
}
}

View File

@@ -1,5 +1,7 @@
const { createReadStream, createWriteStream, statSync } = require('fs')
const { PassThrough } = require('stream')
const { fromCallback } = require('promise-toolbox')
const { PassThrough, pipeline } = require('readable-stream')
const Throttle = require('throttle')
const { isOpaqueRef } = require('../')
@@ -26,7 +28,15 @@ exports.createOutputStream = path => {
return stream
}
exports.resolveRef = (xapi, type, refOrUuidOrNameLabel) =>
exports.pipeline = (...streams) => {
return fromCallback(cb => {
streams = streams.filter(_ => _ != null)
streams.push(cb)
pipeline.apply(undefined, streams)
})
}
const resolveRef = (xapi, type, refOrUuidOrNameLabel) =>
isOpaqueRef(refOrUuidOrNameLabel)
? refOrUuidOrNameLabel
: xapi.call(`${type}.get_by_uuid`, refOrUuidOrNameLabel).catch(() =>
@@ -41,3 +51,10 @@ exports.resolveRef = (xapi, type, refOrUuidOrNameLabel) =>
)
})
)
exports.resolveRecord = async (xapi, type, refOrUuidOrNameLabel) =>
xapi.getRecord(type, await resolveRef(xapi, type, refOrUuidOrNameLabel))
exports.resolveRef = resolveRef
exports.throttle = opts => (opts != null ? new Throttle(opts) : undefined)

View File

@@ -2,29 +2,178 @@
# yarn lockfile v1
end-of-stream@^1.1.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
debug@2:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
once "^1.4.0"
ms "2.0.0"
event-loop-delay@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/event-loop-delay/-/event-loop-delay-1.0.0.tgz#5af6282549494fd0d868c499cbdd33e027978b8c"
integrity sha512-8YtyeIWHXrvTqlAhv+fmtaGGARmgStbvocERYzrZ3pwhnQULe5PuvMUTjIWw/emxssoaftfHZsJtkeY8xjiXCg==
dependencies:
napi-macros "^1.8.2"
node-gyp-build "^3.7.0"
getopts@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.3.tgz#11d229775e2ec2067ed8be6fcc39d9b4bf39cf7d"
integrity sha512-viEcb8TpgeG05+Nqo5EzZ8QR0hxdyrYDp6ZSTZqe2M/h53Bk036NmqG38Vhf5RGirC/Of9Xql+v66B2gp256SQ==
golike-defer@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/golike-defer/-/golike-defer-0.4.1.tgz#7a1cd435d61e461305805d980b133a0f3db4e1cc"
once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
dependencies:
wrappy "1"
human-format@^0.10.1:
version "0.10.1"
resolved "https://registry.yarnpkg.com/human-format/-/human-format-0.10.1.tgz#107793f355912e256148d5b5dcf66a0230187ee9"
integrity sha512-UzCHToSw3HI9MxH9tYzMr1JbHJbgzr6o0hZCun7sruv59S1leps21bmgpBkkwEvQon5n/2OWKH1iU7BEko02cg==
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
dependencies:
end-of-stream "^1.1.0"
once "^1.3.1"
inherits@^2.0.3, inherits@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
wrappy@1:
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
make-error@^1.3.2:
version "1.3.5"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8"
integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
napi-macros@^1.8.2:
version "1.8.2"
resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-1.8.2.tgz#299265c1d8aa401351ad0675107d751228c03eda"
integrity sha512-Tr0DNY4RzTaBG2W2m3l7ZtFuJChTH6VZhXVhkGGjF/4cZTt+i8GcM9ozD+30Lmr4mDoZ5Xx34t2o4GJqYWDGcg==
node-gyp-build@^3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.7.0.tgz#daa77a4f547b9aed3e2aac779eaf151afd60ec8d"
integrity sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==
prettier-bytes@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/prettier-bytes/-/prettier-bytes-1.0.4.tgz#994b02aa46f699c50b6257b5faaa7fe2557e62d6"
integrity sha1-mUsCqkb2mcULYle1+qp/4lV+YtY=
process-nextick-args@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==
process-top@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/process-top/-/process-top-1.0.0.tgz#52892bedb581c5abf0df2d0aa5c429e34275cc7e"
integrity sha512-er8iSmBMslOt5cgIHg9m6zilTPsuUqpEb1yfQ4bDmO80zr/e/5hNn+Tay3CJM/FOBnJo8Bt3fFiDDH6GvIgeAg==
dependencies:
event-loop-delay "^1.0.0"
prettier-bytes "^1.0.4"
progress-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-2.0.0.tgz#fac63a0b3d11deacbb0969abcc93b214bce19ed5"
integrity sha1-+sY6Cz0R3qy7CWmrzJOyFLzhntU=
dependencies:
speedometer "~1.0.0"
through2 "~2.0.3"
promise-toolbox@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/promise-toolbox/-/promise-toolbox-0.11.0.tgz#9ed928355355395072dace3f879879504e07d1bc"
integrity sha512-bjHk0kq+Ke3J3zbkbbJH6kXCyQZbFHwOTrE/Et7vS0uS0tluoV+PLqU/kEyxl8aARM7v04y2wFoDo/wWAEPvjA==
dependencies:
make-error "^1.3.2"
"readable-stream@>= 0.3.0", readable-stream@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.1.1.tgz#ed6bbc6c5ba58b090039ff18ce670515795aeb06"
integrity sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readable-stream@~2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
speedometer@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-1.0.0.tgz#cd671cb06752c22bca3370e2f334440be4fc62e2"
integrity sha1-zWccsGdSwivKM3Di8zREC+T8YuI=
"stream-parser@>= 0.0.2":
version "0.3.1"
resolved "https://registry.yarnpkg.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773"
integrity sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M=
dependencies:
debug "2"
string_decoder@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==
dependencies:
safe-buffer "~5.1.0"
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
dependencies:
safe-buffer "~5.1.0"
throttle@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/throttle/-/throttle-1.0.3.tgz#8a32e4a15f1763d997948317c5ebe3ad8a41e4b7"
integrity sha1-ijLkoV8XY9mXlIMXxevjrYpB5Lc=
dependencies:
readable-stream ">= 0.3.0"
stream-parser ">= 0.0.2"
through2@~2.0.3:
version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
dependencies:
readable-stream "~2.3.6"
xtend "~4.0.1"
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
xtend@~4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=

View File

@@ -1,6 +1,6 @@
{
"name": "xen-api",
"version": "0.20.0",
"version": "0.24.0",
"license": "ISC",
"description": "Connector to the Xen API",
"keywords": [
@@ -39,13 +39,13 @@
"http-request-plus": "^0.6.0",
"iterable-backoff": "^0.0.0",
"jest-diff": "^23.5.0",
"json-rpc-protocol": "^0.12.0",
"json-rpc-protocol": "^0.13.1",
"kindof": "^2.0.0",
"lodash": "^4.17.4",
"make-error": "^1.3.0",
"minimist": "^1.2.0",
"ms": "^2.1.1",
"promise-toolbox": "^0.10.1",
"promise-toolbox": "^0.11.0",
"pw": "0.0.4",
"xmlrpc": "^1.3.2",
"xo-collection": "^0.4.1"

View File

@@ -0,0 +1,17 @@
import mapValues from 'lodash/mapValues'
export default function replaceSensitiveValues(value, replacement) {
function helper(value, name) {
if (name === 'password' && typeof value === 'string') {
return replacement
}
if (typeof value !== 'object' || value === null) {
return value
}
return Array.isArray(value) ? value.map(helper) : mapValues(value, helper)
}
return helper(value)
}

View File

@@ -15,7 +15,7 @@ import { createClient } from './'
// ===================================================================
function askPassword (prompt = 'Password: ') {
function askPassword(prompt = 'Password: ') {
if (prompt) {
process.stdout.write(prompt)
}

View File

@@ -22,6 +22,7 @@ import {
cancelable,
defer,
fromEvents,
ignoreErrors,
pCatch,
pDelay,
pFinally,
@@ -30,6 +31,7 @@ import {
} from 'promise-toolbox'
import autoTransport from './transports/auto'
import replaceSensitiveValues from './_replaceSensitiveValues'
const debug = createDebug('xen-api')
@@ -86,14 +88,14 @@ const isSessionInvalid = ({ code }) => code === 'SESSION_INVALID'
// -------------------------------------------------------------------
class XapiError extends BaseError {
constructor (code, params) {
constructor(code, params) {
super(`${code}(${params.join(', ')})`)
this.code = code
this.params = params
// slots than can be assigned later
this.method = undefined
this.call = undefined
this.url = undefined
this.task = undefined
}
@@ -208,6 +210,34 @@ const getTaskResult = task => {
}
}
function defined() {
for (let i = 0, n = arguments.length; i < n; ++i) {
const arg = arguments[i]
if (arg !== undefined) {
return arg
}
}
}
// TODO: find a better name
// TODO: merge into promise-toolbox?
const dontWait = promise => {
// https://github.com/JsCommunity/promise-toolbox#promiseignoreerrors
ignoreErrors.call(promise)
// http://bluebirdjs.com/docs/warning-explanations.html#warning-a-promise-was-created-in-a-handler-but-was-not-returned-from-it
return null
}
const makeCallSetting = (setting, defaultValue) =>
setting === undefined
? () => defaultValue
: typeof setting === 'function'
? setting
: typeof setting !== 'object'
? () => setting
: method => defined(setting[method], setting['*'], defaultValue)
// -------------------------------------------------------------------
const RESERVED_FIELDS = {
@@ -223,18 +253,25 @@ const CONNECTED = 'connected'
const CONNECTING = 'connecting'
const DISCONNECTED = 'disconnected'
// timeout of XenAPI HTTP connections
const HTTP_TIMEOUT = 24 * 3600 * 1e3
// -------------------------------------------------------------------
export class Xapi extends EventEmitter {
constructor (opts) {
constructor(opts) {
super()
this._allowUnauthorized = opts.allowUnauthorized
this._auth = opts.auth
this._callTimeout = makeCallSetting(opts.callTimeout, 0)
this._debounce = opts.debounce == null ? 200 : opts.debounce
this._pool = null
this._readOnly = Boolean(opts.readOnly)
this._RecordsByType = createObject(null)
this._sessionId = null
;(this._objects = new Collection()).getKey = getKey
;(this._objectsByRef = createObject(null))[NULL_REF] = undefined
const url = (this._url = parseUrl(opts.url))
if (this._auth === undefined) {
@@ -249,39 +286,39 @@ export class Xapi extends EventEmitter {
}
}
// Memoize this function _addObject().
this._getPool = () => this._pool
if (opts.watchEvents !== false) {
this._debounce = opts.debounce == null ? 200 : opts.debounce
this._eventWatchers = createObject(null)
this._fromToken = ''
// Memoize this function _addObject().
this._getPool = () => this._pool
this._nTasks = 0
const objects = (this._objects = new Collection())
objects.getKey = getKey
this._objectsByRef = createObject(null)
this._objectsByRef[NULL_REF] = undefined
this._taskWatchers = Object.create(null)
this.on('connected', this._watchEvents)
this.on('disconnected', () => {
this._fromToken = ''
objects.clear()
})
this.watchEvents()
}
}
get _url () {
watchEvents() {
this._eventWatchers = createObject(null)
this._fromToken = ''
this._nTasks = 0
this._taskWatchers = Object.create(null)
if (this.status === CONNECTED) {
this._watchEvents()
}
this.on('connected', this._watchEvents)
this.on('disconnected', () => {
this._fromToken = ''
this._objects.clear()
})
}
get _url() {
return this.__url
}
set _url (url) {
set _url(url) {
this.__url = url
this._call = autoTransport({
allowUnauthorized: this._allowUnauthorized,
@@ -289,15 +326,15 @@ export class Xapi extends EventEmitter {
})
}
get readOnly () {
get readOnly() {
return this._readOnly
}
set readOnly (ro) {
set readOnly(ro) {
this._readOnly = Boolean(ro)
}
get sessionId () {
get sessionId() {
const id = this._sessionId
if (!id || id === CONNECTING) {
@@ -307,20 +344,20 @@ export class Xapi extends EventEmitter {
return id
}
get status () {
get status() {
const id = this._sessionId
return id ? (id === CONNECTING ? CONNECTING : CONNECTED) : DISCONNECTED
}
get _humanId () {
get _humanId() {
return `${this._auth.user}@${this._url.hostname}`
}
// ensure we have received all events up to this call
//
// optionally returns the up to date object for the given ref
barrier (ref) {
barrier(ref) {
const eventWatchers = this._eventWatchers
if (eventWatchers === undefined) {
return Promise.reject(
@@ -361,7 +398,7 @@ export class Xapi extends EventEmitter {
)
}
connect () {
connect() {
const { status } = this
if (status === CONNECTED) {
@@ -383,8 +420,9 @@ export class Xapi extends EventEmitter {
auth.user,
auth.password,
]).then(
sessionId => {
async sessionId => {
this._sessionId = sessionId
this._pool = (await this.getAllRecords('pool'))[0]
debug('%s: connected', this._humanId)
@@ -398,7 +436,7 @@ export class Xapi extends EventEmitter {
)
}
disconnect () {
disconnect() {
return Promise.resolve().then(() => {
const { status } = this
@@ -417,25 +455,25 @@ export class Xapi extends EventEmitter {
}
// High level calls.
call (method, ...args) {
call(method, ...args) {
return this._readOnly && !isReadOnlyCall(method, args)
? Promise.reject(new Error(`cannot call ${method}() in read only mode`))
: this._sessionCall(method, prepareParam(args))
}
@cancelable
callAsync ($cancelToken, method, ...args) {
callAsync($cancelToken, method, ...args) {
return this._readOnly && !isReadOnlyCall(method, args)
? Promise.reject(new Error(`cannot call ${method}() in read only mode`))
: this._sessionCall(`Async.${method}`, args).then(taskRef => {
$cancelToken.promise.then(() => {
$cancelToken.promise.then(() =>
// TODO: do not trigger if the task is already over
this._sessionCall('task.cancel', [taskRef]).catch(noop)
})
dontWait(this._sessionCall('task.cancel', [taskRef]))
)
return pFinally.call(this.watchTask(taskRef), () => {
this._sessionCall('task.destroy', [taskRef]).catch(noop)
})
return pFinally.call(this.watchTask(taskRef), () =>
dontWait(this._sessionCall('task.destroy', [taskRef]))
)
})
}
@@ -443,7 +481,7 @@ export class Xapi extends EventEmitter {
//
// allowed even in read-only mode because it does not have impact on the
// XenServer and it's necessary for getResource()
createTask (nameLabel, nameDescription = '') {
createTask(nameLabel, nameDescription = '') {
const promise = this._sessionCall('task.create', [
nameLabel,
nameDescription,
@@ -461,7 +499,7 @@ export class Xapi extends EventEmitter {
// Nice getter which returns the object for a given $id (internal to
// this lib), UUID (unique identifier that some objects have) or
// opaque reference (internal to XAPI).
getObject (idOrUuidOrRef, defaultValue) {
getObject(idOrUuidOrRef, defaultValue) {
if (typeof idOrUuidOrRef === 'object') {
idOrUuidOrRef = idOrUuidOrRef.$id
}
@@ -478,7 +516,7 @@ export class Xapi extends EventEmitter {
// Returns the object for a given opaque reference (internal to
// XAPI).
getObjectByRef (ref, defaultValue) {
getObjectByRef(ref, defaultValue) {
const object = this._objectsByRef[ref]
if (object !== undefined) return object
@@ -490,7 +528,7 @@ export class Xapi extends EventEmitter {
// Returns the object for a given UUID (unique identifier that some
// objects have).
getObjectByUuid (uuid, defaultValue) {
getObjectByUuid(uuid, defaultValue) {
// Objects ids are already UUIDs if they have one.
const object = this._objects.all[uuid]
@@ -501,13 +539,22 @@ export class Xapi extends EventEmitter {
throw new Error('no object with UUID: ' + uuid)
}
async getRecord (type, ref) {
async getRecord(type, ref) {
return this._wrapRecord(
type,
ref,
await this._sessionCall(`${type}.get_record`, [ref])
)
}
async getRecordByUuid (type, uuid) {
async getAllRecords(type) {
return map(
await this._sessionCall(`${type}.get_all_records`),
(record, ref) => this._wrapRecord(type, ref, record)
)
}
async getRecordByUuid(type, uuid) {
return this.getRecord(
type,
await this._sessionCall(`${type}.get_by_uuid`, [uuid])
@@ -515,7 +562,7 @@ export class Xapi extends EventEmitter {
}
@cancelable
getResource ($cancelToken, pathname, { host, query, task }) {
getResource($cancelToken, pathname, { host, query, task }) {
return this._autoTask(task, `Xapi#getResource ${pathname}`).then(
taskRef => {
query = { ...query, session_id: this.sessionId }
@@ -529,17 +576,20 @@ export class Xapi extends EventEmitter {
}
}
let promise = httpRequest(
$cancelToken,
this._url,
host && {
hostname: this.getObject(host).address,
},
{
pathname,
query,
rejectUnauthorized: !this._allowUnauthorized,
}
let promise = pTimeout.call(
httpRequest(
$cancelToken,
this._url,
host && {
hostname: this.getObject(host).address,
},
{
pathname,
query,
rejectUnauthorized: !this._allowUnauthorized,
}
),
HTTP_TIMEOUT
)
if (taskResult !== undefined) {
@@ -555,7 +605,7 @@ export class Xapi extends EventEmitter {
}
@cancelable
putResource ($cancelToken, body, pathname, { host, query, task } = {}) {
putResource($cancelToken, body, pathname, { host, query, task } = {}) {
if (this._readOnly) {
return Promise.reject(
new Error(new Error('cannot put resource in read only mode'))
@@ -587,21 +637,24 @@ export class Xapi extends EventEmitter {
}
const doRequest = (...opts) =>
httpRequest.put(
$cancelToken,
this._url,
host && {
hostname: this.getObject(host).address,
},
{
body,
headers,
query,
pathname,
maxRedirects: 0,
rejectUnauthorized: !this._allowUnauthorized,
},
...opts
pTimeout.call(
httpRequest.put(
$cancelToken,
this._url,
host && {
hostname: this.getObject(host).address,
},
{
body,
headers,
query,
pathname,
maxRedirects: 0,
rejectUnauthorized: !this._allowUnauthorized,
},
...opts
),
HTTP_TIMEOUT
)
// if a stream, sends a dummy request to probe for a
@@ -661,11 +714,11 @@ export class Xapi extends EventEmitter {
)
}
setField ({ $type, $ref }, field, value) {
setField({ $type, $ref }, field, value) {
return this.call(`${$type}.set_${field}`, $ref, value).then(noop)
}
setFieldEntries (record, field, entries) {
setFieldEntries(record, field, entries) {
return Promise.all(
getKeys(entries).map(entry => {
const value = entries[entry]
@@ -678,7 +731,7 @@ export class Xapi extends EventEmitter {
).then(noop)
}
async setFieldEntry ({ $type, $ref }, field, entry, value) {
async setFieldEntry({ $type, $ref }, field, entry, value) {
while (true) {
try {
await this.call(`${$type}.add_to_${field}`, $ref, entry, value)
@@ -692,11 +745,11 @@ export class Xapi extends EventEmitter {
}
}
unsetFieldEntry ({ $type, $ref }, field, entry) {
unsetFieldEntry({ $type, $ref }, field, entry) {
return this.call(`${$type}.remove_from_${field}`, $ref, entry)
}
watchTask (ref) {
watchTask(ref) {
const watchers = this._taskWatchers
if (watchers === undefined) {
throw new Error('Xapi#watchTask() requires events watching')
@@ -721,16 +774,16 @@ export class Xapi extends EventEmitter {
return watcher.promise
}
get pool () {
get pool() {
return this._pool
}
get objects () {
get objects() {
return this._objects
}
// return a promise which resolves to a task ref or undefined
_autoTask (task = this._taskWatchers !== undefined, name) {
_autoTask(task = this._taskWatchers !== undefined, name) {
if (task === false) {
return Promise.resolve()
}
@@ -744,7 +797,7 @@ export class Xapi extends EventEmitter {
}
// Medium level call: handle session errors.
_sessionCall (method, args) {
_sessionCall(method, args) {
try {
if (startsWith(method, 'session.')) {
throw new Error('session.*() methods are disabled from this interface')
@@ -755,24 +808,27 @@ export class Xapi extends EventEmitter {
newArgs.push.apply(newArgs, args)
}
return pCatch.call(
this._transportCall(method, newArgs),
isSessionInvalid,
() => {
// XAPI is sometimes reinitialized and sessions are lost.
// Try to login again.
debug('%s: the session has been reinitialized', this._humanId)
return pTimeout.call(
pCatch.call(
this._transportCall(method, newArgs),
isSessionInvalid,
() => {
// XAPI is sometimes reinitialized and sessions are lost.
// Try to login again.
debug('%s: the session has been reinitialized', this._humanId)
this._sessionId = null
return this.connect().then(() => this._sessionCall(method, args))
}
this._sessionId = null
return this.connect().then(() => this._sessionCall(method, args))
}
),
this._callTimeout(method, args)
)
} catch (error) {
return Promise.reject(error)
}
}
_addObject (type, ref, object) {
_addObject(type, ref, object) {
object = this._wrapRecord(type, ref, object)
// Finally freezes the object.
@@ -819,7 +875,7 @@ export class Xapi extends EventEmitter {
}
}
_removeObject (type, ref) {
_removeObject(type, ref) {
const byRefs = this._objectsByRef
const object = byRefs[ref]
if (object !== undefined) {
@@ -842,7 +898,7 @@ export class Xapi extends EventEmitter {
}
}
_processEvents (events) {
_processEvents(events) {
forEach(events, event => {
const { class: type, ref } = event
if (event.operation === 'del') {
@@ -853,7 +909,7 @@ export class Xapi extends EventEmitter {
})
}
_watchEvents () {
_watchEvents() {
const loop = () =>
this.status === CONNECTED &&
pTimeout
@@ -909,16 +965,18 @@ export class Xapi extends EventEmitter {
throw error
}
return pCatch.call(
loop(),
isMethodUnknown,
ignoreErrors.call(
pCatch.call(
loop(),
isMethodUnknown,
// If the server failed, it is probably due to an excessively
// large response.
// Falling back to legacy events watch should be enough.
error => error && error.res && error.res.statusCode === 500,
// If the server failed, it is probably due to an excessively
// large response.
// Falling back to legacy events watch should be enough.
error => error && error.res && error.res.statusCode === 500,
() => this._watchEventsLegacy()
() => this._watchEventsLegacy()
)
)
}
@@ -926,7 +984,7 @@ export class Xapi extends EventEmitter {
// methods.
//
// It also has to manually get all objects first.
_watchEventsLegacy () {
_watchEventsLegacy() {
const getAllObjects = () => {
return this._sessionCall('system.listMethods').then(methods => {
// Uses introspection to determine the methods to use to get
@@ -978,7 +1036,7 @@ export class Xapi extends EventEmitter {
return getAllObjects().then(watchEvents)
}
_wrapRecord (type, ref, data) {
_wrapRecord(type, ref, data) {
const RecordsByType = this._RecordsByType
let Record = RecordsByType[type]
if (Record === undefined) {
@@ -989,7 +1047,7 @@ export class Xapi extends EventEmitter {
const objectsByRef = this._objectsByRef
const getObjectByRef = ref => objectsByRef[ref]
Record = function (ref, data) {
Record = function(ref, data) {
defineProperties(this, {
$id: { value: data.uuid || ref },
$ref: { value: ref },
@@ -1003,7 +1061,7 @@ export class Xapi extends EventEmitter {
const getters = { $pool: this._getPool }
const props = { $type: type }
fields.forEach(field => {
props[`set_${field}`] = function (value) {
props[`set_${field}`] = function(value) {
return xapi.setField(this, field, value)
}
@@ -1012,19 +1070,19 @@ export class Xapi extends EventEmitter {
const value = data[field]
if (isArray(value)) {
if (value.length === 0 || isOpaqueRef(value[0])) {
getters[$field] = function () {
getters[$field] = function() {
const value = this[field]
return value.length === 0 ? value : value.map(getObjectByRef)
}
}
props[`add_to_${field}`] = function (...values) {
props[`add_to_${field}`] = function(...values) {
return xapi
.call(`${type}.add_${field}`, this.$ref, values)
.then(noop)
}
} else if (value !== null && typeof value === 'object') {
getters[$field] = function () {
getters[$field] = function() {
const value = this[field]
const result = {}
getKeys(value).forEach(key => {
@@ -1032,11 +1090,11 @@ export class Xapi extends EventEmitter {
})
return result
}
props[`update_${field}`] = function (entries) {
props[`update_${field}`] = function(entries) {
return xapi.setFieldEntries(this, field, entries)
}
} else if (isOpaqueRef(value)) {
getters[$field] = function () {
getters[$field] = function() {
return objectsByRef[this[field]]
}
}
@@ -1065,18 +1123,21 @@ export class Xapi extends EventEmitter {
Xapi.prototype._transportCall = reduce(
[
function (method, args) {
function(method, args) {
return this._call(method, args).catch(error => {
if (!(error instanceof Error)) {
error = wrapError(error)
}
error.method = method
error.call = {
method,
params: replaceSensitiveValues(args, '* obfuscated *'),
}
throw error
})
},
call =>
function () {
function() {
let iterator // lazily created
const loop = () =>
pCatch.call(
@@ -1117,7 +1178,7 @@ Xapi.prototype._transportCall = reduce(
return loop()
},
call =>
function loop () {
function loop() {
return pCatch.call(
call.apply(this, arguments),
isHostSlave,
@@ -1140,7 +1201,7 @@ Xapi.prototype._transportCall = reduce(
)
},
call =>
function (method) {
function(method) {
const startTime = Date.now()
return call.apply(this, arguments).then(
result => {

View File

@@ -10,7 +10,7 @@ export default opts => {
let i = 0
let call
function create () {
function create() {
const current = factories[i++](opts)
if (i < length) {
const currentI = i

View File

@@ -1,5 +1,9 @@
'use strict'
const { unauthorized } = require('xo-common/api-errors')
// ===================================================================
// These global variables are not a problem because the algorithm is
// synchronous.
let permissionsByObject
@@ -52,6 +56,8 @@ const checkAuthorizationByTypes = {
network: or(checkSelf, checkMember('$pool')),
PBD: or(checkMember('host'), checkMember('SR')),
PIF: checkMember('$host'),
SR: or(checkSelf, checkMember('$container')),
@@ -62,7 +68,7 @@ const checkAuthorizationByTypes = {
// Access to a VDI is granted if the user has access to the
// containing SR or to a linked VM.
VDI (vdi, permission) {
VDI(vdi, permission) {
// Check authorization for the containing SR.
if (checkAuthorization(vdi.$SR, permission)) {
return true
@@ -92,7 +98,7 @@ const checkAuthorizationByTypes = {
}
// Hoisting is important for this function.
function checkAuthorization (objectId, permission) {
function checkAuthorization(objectId, permission) {
const object = getObject(objectId)
if (!object) {
return false
@@ -105,23 +111,26 @@ function checkAuthorization (objectId, permission) {
// -------------------------------------------------------------------
module.exports = (
function assertPermissions(
permissionsByObject_,
getObject_,
permissions,
permission
) => {
) {
// Assign global variables.
permissionsByObject = permissionsByObject_
getObject = getObject_
try {
if (permission) {
return checkAuthorization(permissions, permission)
if (permission !== undefined) {
const objectId = permissions
if (!checkAuthorization(objectId, permission)) {
throw unauthorized(permission, objectId)
}
} else {
for (const [objectId, permission] of permissions) {
if (!checkAuthorization(objectId, permission)) {
return false
throw unauthorized(permission, objectId)
}
}
}
@@ -132,3 +141,16 @@ module.exports = (
permissionsByObject = getObject = null
}
}
exports.assert = assertPermissions
exports.check = function checkPermissions() {
try {
assertPermissions.apply(undefined, arguments)
return true
} catch (error) {
if (unauthorized.is(error)) {
return false
}
throw error
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "xo-acl-resolver",
"version": "0.3.0",
"version": "0.4.1",
"license": "ISC",
"description": "Xen-Orchestra internal: do ACLs resolution",
"keywords": [],
@@ -21,5 +21,8 @@
],
"engines": {
"node": ">=6"
},
"dependencies": {
"xo-common": "^0.2.0"
}
}

View File

@@ -120,7 +120,7 @@ encoding by prefixing with `json:`:
##### VM import
```
> xo-cli vm.import host=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
> xo-cli vm.import sr=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
```
## Development

View File

@@ -42,7 +42,7 @@
"nice-pipe": "0.0.0",
"pretty-ms": "^4.0.0",
"progress-stream": "^2.0.0",
"promise-toolbox": "^0.10.1",
"promise-toolbox": "^0.11.0",
"pump": "^3.0.0",
"pw": "^0.0.4",
"strip-indent": "^2.0.0",

View File

@@ -19,36 +19,36 @@ const configFile = configPath + '/config.json'
// ===================================================================
const load = (exports.load = function () {
const load = (exports.load = function() {
return readFile(configFile)
.then(JSON.parse)
.catch(function () {
.catch(function() {
return {}
})
})
exports.get = function (path) {
return load().then(function (config) {
exports.get = function(path) {
return load().then(function(config) {
return l33t(config).tap(path)
})
}
const save = (exports.save = function (config) {
return mkdirp(configPath).then(function () {
const save = (exports.save = function(config) {
return mkdirp(configPath).then(function() {
return writeFile(configFile, JSON.stringify(config))
})
})
exports.set = function (data) {
return load().then(function (config) {
exports.set = function(data) {
return load().then(function(config) {
return save(assign(config, data))
})
}
exports.unset = function (paths) {
return load().then(function (config) {
exports.unset = function(paths) {
return load().then(function(config) {
const l33tConfig = l33t(config)
;[].concat(paths).forEach(function (path) {
;[].concat(paths).forEach(function(path) {
l33tConfig.purge(path, true)
})
return save(config)

View File

@@ -36,7 +36,7 @@ const config = require('./config')
// ===================================================================
async function connect () {
async function connect() {
const { server, token } = await config.load()
if (server === undefined) {
throw new Error('no server to connect to!')
@@ -53,7 +53,7 @@ async function connect () {
}
const FLAG_RE = /^--([^=]+)(?:=([^]*))?$/
function extractFlags (args) {
function extractFlags(args) {
const flags = {}
let i = 0
@@ -71,9 +71,9 @@ function extractFlags (args) {
}
const PARAM_RE = /^([^=]+)=([^]*)$/
function parseParameters (args) {
function parseParameters(args) {
const params = {}
forEach(args, function (arg) {
forEach(args, function(arg) {
let matches
if (!(matches = arg.match(PARAM_RE))) {
throw new Error('invalid arg: ' + arg)
@@ -107,7 +107,7 @@ const humanFormatOpts = {
scale: 'binary',
}
function printProgress (progress) {
function printProgress(progress) {
if (progress.length) {
console.warn(
'%s% of %s @ %s/s - ETA %s',
@@ -125,8 +125,8 @@ function printProgress (progress) {
}
}
function wrap (val) {
return function wrappedValue () {
function wrap(val) {
return function wrappedValue() {
return val
}
}
@@ -134,7 +134,7 @@ function wrap (val) {
// ===================================================================
const help = wrap(
(function (pkg) {
(function(pkg) {
return require('strip-indent')(
`
Usage:
@@ -168,7 +168,7 @@ const help = wrap(
$name v$version
`
).replace(/<([^>]+)>|\$(\w+)/g, function (_, arg, key) {
).replace(/<([^>]+)>|\$(\w+)/g, function(_, arg, key) {
if (arg) {
return '<' + chalk.yellow(arg) + '>'
}
@@ -184,12 +184,12 @@ const help = wrap(
// -------------------------------------------------------------------
function main (args) {
function main(args) {
if (!args || !args.length || args[0] === '-h') {
return help()
}
const fnName = args[0].replace(/^--|-\w/g, function (match) {
const fnName = args[0].replace(/^--|-\w/g, function(match) {
if (match === '--') {
return ''
}
@@ -208,7 +208,7 @@ exports = module.exports = main
exports.help = help
async function register (args) {
async function register(args) {
let expiresIn
if (args[0] === '--expiresIn') {
expiresIn = args[1]
@@ -218,7 +218,7 @@ async function register (args) {
const [
url,
email,
password = await new Promise(function (resolve) {
password = await new Promise(function(resolve) {
process.stdout.write('Password: ')
pw(resolve)
}),
@@ -236,18 +236,18 @@ async function register (args) {
}
exports.register = register
function unregister () {
function unregister() {
return config.unset(['server', 'token'])
}
exports.unregister = unregister
async function listCommands (args) {
async function listCommands(args) {
const xo = await connect()
let methods = await xo.call('system.getMethodsInfo')
let json = false
const patterns = []
forEach(args, function (arg) {
forEach(args, function(arg) {
if (arg === '--json') {
json = true
} else {
@@ -264,7 +264,7 @@ async function listCommands (args) {
}
methods = pairs(methods)
methods.sort(function (a, b) {
methods.sort(function(a, b) {
a = a[0]
b = b[0]
if (a < b) {
@@ -274,11 +274,11 @@ async function listCommands (args) {
})
const str = []
forEach(methods, function (method) {
forEach(methods, function(method) {
const name = method[0]
const info = method[1]
str.push(chalk.bold.blue(name))
forEach(info.params || [], function (info, name) {
forEach(info.params || [], function(info, name) {
str.push(' ')
if (info.optional) {
str.push('[')
@@ -305,10 +305,10 @@ async function listCommands (args) {
}
exports.listCommands = listCommands
async function listObjects (args) {
async function listObjects(args) {
const properties = getKeys(extractFlags(args))
const filterProperties = properties.length
? function (object) {
? function(object) {
return pick(object, properties)
}
: identity
@@ -321,7 +321,7 @@ async function listObjects (args) {
const stdout = process.stdout
stdout.write('[\n')
const keys = Object.keys(objects)
for (let i = 0, n = keys.length; i < n;) {
for (let i = 0, n = keys.length; i < n; ) {
stdout.write(JSON.stringify(filterProperties(objects[keys[i]]), null, 2))
stdout.write(++i < n ? ',\n' : '\n')
}
@@ -329,7 +329,7 @@ async function listObjects (args) {
}
exports.listObjects = listObjects
function ensurePathParam (method, value) {
function ensurePathParam(method, value) {
if (typeof value !== 'string') {
const error =
method +
@@ -338,7 +338,7 @@ function ensurePathParam (method, value) {
}
}
async function call (args) {
async function call(args) {
if (!args.length) {
throw new Error('missing command name')
}

View File

@@ -1,4 +1,4 @@
export default function clearObject (object) {
export default function clearObject(object) {
for (const key in object) {
delete object[key]
}

View File

@@ -20,43 +20,43 @@ export const ACTION_REMOVE = 'remove'
// ===================================================================
export class BufferAlreadyFlushed extends BaseError {
constructor () {
constructor() {
super('buffer flush already requested')
}
}
export class DuplicateIndex extends BaseError {
constructor (name) {
constructor(name) {
super('there is already an index with the name ' + name)
}
}
export class DuplicateItem extends BaseError {
constructor (key) {
constructor(key) {
super('there is already a item with the key ' + key)
}
}
export class IllegalTouch extends BaseError {
constructor (value) {
constructor(value) {
super('only an object value can be touched (found a ' + kindOf(value) + ')')
}
}
export class InvalidKey extends BaseError {
constructor (key) {
constructor(key) {
super('invalid key of type ' + kindOf(key))
}
}
export class NoSuchIndex extends BaseError {
constructor (name) {
constructor(name) {
super('there is no index with the name ' + name)
}
}
export class NoSuchItem extends BaseError {
constructor (key) {
constructor(key) {
super('there is no item with the key ' + key)
}
}
@@ -64,7 +64,7 @@ export class NoSuchItem extends BaseError {
// -------------------------------------------------------------------
export default class Collection extends EventEmitter {
constructor () {
constructor() {
super()
this._buffer = createObject(null)
@@ -79,7 +79,7 @@ export default class Collection extends EventEmitter {
// unspecified.
//
// Default implementation returns the `id` property.
getKey (value) {
getKey(value) {
return value && value.id
}
@@ -87,15 +87,15 @@ export default class Collection extends EventEmitter {
// Properties
// -----------------------------------------------------------------
get all () {
get all() {
return this._items
}
get indexes () {
get indexes() {
return this._indexedItems
}
get size () {
get size() {
return this._size
}
@@ -103,7 +103,7 @@ export default class Collection extends EventEmitter {
// Manipulation
// -----------------------------------------------------------------
add (keyOrObjectWithId, valueIfKey = undefined) {
add(keyOrObjectWithId, valueIfKey = undefined) {
const [key, value] = this._resolveItem(keyOrObjectWithId, valueIfKey)
this._assertHasNot(key)
@@ -112,18 +112,18 @@ export default class Collection extends EventEmitter {
this._touch(ACTION_ADD, key)
}
clear () {
clear() {
forEach(this._items, (_, key) => this._remove(key))
}
remove (keyOrObjectWithId) {
remove(keyOrObjectWithId) {
const [key] = this._resolveItem(keyOrObjectWithId)
this._assertHas(key)
this._remove(key)
}
set (keyOrObjectWithId, valueIfKey = undefined) {
set(keyOrObjectWithId, valueIfKey = undefined) {
const [key, value] = this._resolveItem(keyOrObjectWithId, valueIfKey)
const action = this.has(key) ? ACTION_UPDATE : ACTION_ADD
@@ -134,7 +134,7 @@ export default class Collection extends EventEmitter {
this._touch(action, key)
}
touch (keyOrObjectWithId) {
touch(keyOrObjectWithId) {
const [key] = this._resolveItem(keyOrObjectWithId)
this._assertHas(key)
const value = this.get(key)
@@ -147,7 +147,7 @@ export default class Collection extends EventEmitter {
return this.get(key)
}
unset (keyOrObjectWithId) {
unset(keyOrObjectWithId) {
const [key] = this._resolveItem(keyOrObjectWithId)
if (this.has(key)) {
@@ -155,7 +155,7 @@ export default class Collection extends EventEmitter {
}
}
update (keyOrObjectWithId, valueIfKey = undefined) {
update(keyOrObjectWithId, valueIfKey = undefined) {
const [key, value] = this._resolveItem(keyOrObjectWithId, valueIfKey)
this._assertHas(key)
@@ -167,7 +167,7 @@ export default class Collection extends EventEmitter {
// Query
// -----------------------------------------------------------------
get (key, defaultValue) {
get(key, defaultValue) {
if (this.has(key)) {
return this._items[key]
}
@@ -180,7 +180,7 @@ export default class Collection extends EventEmitter {
this._assertHas(key)
}
has (key) {
has(key) {
return hasOwnProperty.call(this._items, key)
}
@@ -188,7 +188,7 @@ export default class Collection extends EventEmitter {
// Indexes
// -----------------------------------------------------------------
createIndex (name, index) {
createIndex(name, index) {
const { _indexes: indexes } = this
if (hasOwnProperty.call(indexes, name)) {
throw new DuplicateIndex(name)
@@ -200,7 +200,7 @@ export default class Collection extends EventEmitter {
index._attachCollection(this)
}
deleteIndex (name) {
deleteIndex(name) {
const { _indexes: indexes } = this
if (!hasOwnProperty.call(indexes, name)) {
throw new NoSuchIndex(name)
@@ -217,7 +217,7 @@ export default class Collection extends EventEmitter {
// Iteration
// -----------------------------------------------------------------
* [Symbol.iterator] () {
*[Symbol.iterator]() {
const { _items: items } = this
for (const key in items) {
@@ -225,7 +225,7 @@ export default class Collection extends EventEmitter {
}
}
* keys () {
*keys() {
const { _items: items } = this
for (const key in items) {
@@ -233,7 +233,7 @@ export default class Collection extends EventEmitter {
}
}
* values () {
*values() {
const { _items: items } = this
for (const key in items) {
@@ -245,7 +245,7 @@ export default class Collection extends EventEmitter {
// Events buffering
// -----------------------------------------------------------------
bufferEvents () {
bufferEvents() {
++this._buffering
let called = false
@@ -294,35 +294,35 @@ export default class Collection extends EventEmitter {
// =================================================================
_assertHas (key) {
_assertHas(key) {
if (!this.has(key)) {
throw new NoSuchItem(key)
}
}
_assertHasNot (key) {
_assertHasNot(key) {
if (this.has(key)) {
throw new DuplicateItem(key)
}
}
_assertValidKey (key) {
_assertValidKey(key) {
if (!this._isValidKey(key)) {
throw new InvalidKey(key)
}
}
_isValidKey (key) {
_isValidKey(key) {
return typeof key === 'number' || typeof key === 'string'
}
_remove (key) {
_remove(key) {
delete this._items[key]
this._size--
this._touch(ACTION_REMOVE, key)
}
_resolveItem (keyOrObjectWithId, valueIfKey = undefined) {
_resolveItem(keyOrObjectWithId, valueIfKey = undefined) {
if (valueIfKey !== undefined) {
this._assertValidKey(keyOrObjectWithId)
@@ -339,7 +339,7 @@ export default class Collection extends EventEmitter {
return [key, keyOrObjectWithId]
}
_touch (action, key) {
_touch(action, key) {
if (this._buffering === 0) {
const flush = this.bufferEvents()

View File

@@ -7,11 +7,11 @@ import Collection, { DuplicateItem, NoSuchItem } from './collection'
// ===================================================================
function waitTicks (n = 2) {
function waitTicks(n = 2) {
const { nextTick } = process
return new Promise(function (resolve) {
;(function waitNextTick () {
return new Promise(function(resolve) {
;(function waitNextTick() {
// The first tick is handled by Promise#then()
if (--n) {
nextTick(waitNextTick)
@@ -22,24 +22,24 @@ function waitTicks (n = 2) {
})
}
describe('Collection', function () {
describe('Collection', function() {
let col
beforeEach(function () {
beforeEach(function() {
col = new Collection()
col.add('bar', 0)
return waitTicks()
})
it('is iterable', function () {
it('is iterable', function() {
const iterator = col[Symbol.iterator]()
expect(iterator.next()).toEqual({ done: false, value: ['bar', 0] })
expect(iterator.next()).toEqual({ done: true, value: undefined })
})
describe('#keys()', function () {
it('returns an iterator over the keys', function () {
describe('#keys()', function() {
it('returns an iterator over the keys', function() {
const iterator = col.keys()
expect(iterator.next()).toEqual({ done: false, value: 'bar' })
@@ -47,8 +47,8 @@ describe('Collection', function () {
})
})
describe('#values()', function () {
it('returns an iterator over the values', function () {
describe('#values()', function() {
it('returns an iterator over the values', function() {
const iterator = col.values()
expect(iterator.next()).toEqual({ done: false, value: 0 })
@@ -56,8 +56,8 @@ describe('Collection', function () {
})
})
describe('#add()', function () {
it('adds item to the collection', function () {
describe('#add()', function() {
it('adds item to the collection', function() {
const spy = jest.fn()
col.on('add', spy)
@@ -69,17 +69,17 @@ describe('Collection', function () {
expect(spy).not.toHaveBeenCalled()
// Async event.
return eventToPromise(col, 'add').then(function (added) {
return eventToPromise(col, 'add').then(function(added) {
expect(Object.keys(added)).toEqual(['foo'])
expect(added.foo).toBe(true)
})
})
it('throws an exception if the item already exists', function () {
it('throws an exception if the item already exists', function() {
expect(() => col.add('bar', true)).toThrowError(DuplicateItem)
})
it('accepts an object with an id property', function () {
it('accepts an object with an id property', function() {
const foo = { id: 'foo' }
col.add(foo)
@@ -88,8 +88,8 @@ describe('Collection', function () {
})
})
describe('#update()', function () {
it('updates an item of the collection', function () {
describe('#update()', function() {
it('updates an item of the collection', function() {
const spy = jest.fn()
col.on('update', spy)
@@ -102,17 +102,17 @@ describe('Collection', function () {
expect(spy).not.toHaveBeenCalled()
// Async event.
return eventToPromise(col, 'update').then(function (updated) {
return eventToPromise(col, 'update').then(function(updated) {
expect(Object.keys(updated)).toEqual(['bar'])
expect(updated.bar).toBe(2)
})
})
it('throws an exception if the item does not exist', function () {
it('throws an exception if the item does not exist', function() {
expect(() => col.update('baz', true)).toThrowError(NoSuchItem)
})
it('accepts an object with an id property', function () {
it('accepts an object with an id property', function() {
const bar = { id: 'bar' }
col.update(bar)
@@ -121,8 +121,8 @@ describe('Collection', function () {
})
})
describe('#remove()', function () {
it('removes an item of the collection', function () {
describe('#remove()', function() {
it('removes an item of the collection', function() {
const spy = jest.fn()
col.on('remove', spy)
@@ -134,17 +134,17 @@ describe('Collection', function () {
expect(spy).not.toHaveBeenCalled()
// Async event.
return eventToPromise(col, 'remove').then(function (removed) {
return eventToPromise(col, 'remove').then(function(removed) {
expect(Object.keys(removed)).toEqual(['bar'])
expect(removed.bar).toBeUndefined()
})
})
it('throws an exception if the item does not exist', function () {
it('throws an exception if the item does not exist', function() {
expect(() => col.remove('baz', true)).toThrowError(NoSuchItem)
})
it('accepts an object with an id property', function () {
it('accepts an object with an id property', function() {
const bar = { id: 'bar' }
col.remove(bar)
@@ -153,8 +153,8 @@ describe('Collection', function () {
})
})
describe('#set()', function () {
it('adds item if collection has not key', function () {
describe('#set()', function() {
it('adds item if collection has not key', function() {
const spy = jest.fn()
col.on('add', spy)
@@ -166,13 +166,13 @@ describe('Collection', function () {
expect(spy).not.toHaveBeenCalled()
// Async events.
return eventToPromise(col, 'add').then(function (added) {
return eventToPromise(col, 'add').then(function(added) {
expect(Object.keys(added)).toEqual(['foo'])
expect(added.foo).toBe(true)
})
})
it('updates item if collection has key', function () {
it('updates item if collection has key', function() {
const spy = jest.fn()
col.on('udpate', spy)
@@ -184,13 +184,13 @@ describe('Collection', function () {
expect(spy).not.toHaveBeenCalled()
// Async events.
return eventToPromise(col, 'update').then(function (updated) {
return eventToPromise(col, 'update').then(function(updated) {
expect(Object.keys(updated)).toEqual(['bar'])
expect(updated.bar).toBe(1)
})
})
it('accepts an object with an id property', function () {
it('accepts an object with an id property', function() {
const foo = { id: 'foo' }
col.set(foo)
@@ -199,36 +199,36 @@ describe('Collection', function () {
})
})
describe('#unset()', function () {
it('removes an existing item', function () {
describe('#unset()', function() {
it('removes an existing item', function() {
col.unset('bar')
expect(col.has('bar')).toBe(false)
return eventToPromise(col, 'remove').then(function (removed) {
return eventToPromise(col, 'remove').then(function(removed) {
expect(Object.keys(removed)).toEqual(['bar'])
expect(removed.bar).toBeUndefined()
})
})
it('does not throw if the item does not exists', function () {
it('does not throw if the item does not exists', function() {
col.unset('foo')
})
it('accepts an object with an id property', function () {
it('accepts an object with an id property', function() {
col.unset({ id: 'bar' })
expect(col.has('bar')).toBe(false)
return eventToPromise(col, 'remove').then(function (removed) {
return eventToPromise(col, 'remove').then(function(removed) {
expect(Object.keys(removed)).toEqual(['bar'])
expect(removed.bar).toBeUndefined()
})
})
})
describe('touch()', function () {
it('can be used to signal an indirect update', function () {
describe('touch()', function() {
it('can be used to signal an indirect update', function() {
const foo = { id: 'foo' }
col.add(foo)
@@ -243,8 +243,8 @@ describe('Collection', function () {
})
})
describe('clear()', function () {
it('removes all items from the collection', function () {
describe('clear()', function() {
it('removes all items from the collection', function() {
col.clear()
expect(col.size).toBe(0)
@@ -256,7 +256,7 @@ describe('Collection', function () {
})
})
describe('deduplicates events', function () {
describe('deduplicates events', function() {
forEach(
{
'add & update → add': [
@@ -298,7 +298,7 @@ describe('Collection', function () {
],
},
([operations, results], label) => {
it(label, function () {
it(label, function() {
forEach(operations, ([method, ...args]) => {
col[method](...args)
})

View File

@@ -8,7 +8,7 @@ import { ACTION_ADD, ACTION_UPDATE, ACTION_REMOVE } from './collection'
// ===================================================================
export default class Index {
constructor (computeHash) {
constructor(computeHash) {
if (computeHash) {
this.computeHash = iteratee(computeHash)
}
@@ -24,12 +24,12 @@ export default class Index {
// This method is used to compute the hash under which an item must
// be saved.
computeHash (value, key) {
computeHash(value, key) {
throw new NotImplemented('this method must be overridden')
}
// Remove empty items lists.
sweep () {
sweep() {
const { _itemsByHash: itemsByHash } = this
for (const hash in itemsByHash) {
if (isEmpty(itemsByHash[hash])) {
@@ -40,13 +40,13 @@ export default class Index {
// -----------------------------------------------------------------
get items () {
get items() {
return this._itemsByHash
}
// -----------------------------------------------------------------
_attachCollection (collection) {
_attachCollection(collection) {
// Add existing entries.
//
// FIXME: I think there may be a race condition if the `add` event
@@ -58,7 +58,7 @@ export default class Index {
collection.on(ACTION_REMOVE, this._onRemove)
}
_detachCollection (collection) {
_detachCollection(collection) {
collection.removeListener(ACTION_ADD, this._onAdd)
collection.removeListener(ACTION_UPDATE, this._onUpdate)
collection.removeListener(ACTION_REMOVE, this._onRemove)
@@ -69,7 +69,7 @@ export default class Index {
// -----------------------------------------------------------------
_onAdd (items) {
_onAdd(items) {
const {
computeHash,
_itemsByHash: itemsByHash,
@@ -93,7 +93,7 @@ export default class Index {
}
}
_onUpdate (items) {
_onUpdate(items) {
const {
computeHash,
_itemsByHash: itemsByHash,
@@ -122,7 +122,7 @@ export default class Index {
}
}
_onRemove (items) {
_onRemove(items) {
const { _itemsByHash: itemsByHash, _keysToHash: keysToHash } = this
for (const key in items) {

View File

@@ -12,7 +12,7 @@ const waitTicks = (n = 2) => {
const { nextTick } = process
return new Promise(resolve => {
;(function waitNextTick () {
;(function waitNextTick() {
// The first tick is handled by Promise#then()
if (--n) {
nextTick(waitNextTick)
@@ -25,7 +25,7 @@ const waitTicks = (n = 2) => {
// ===================================================================
describe('Index', function () {
describe('Index', function() {
let col, byGroup
const item1 = {
id: '2ccb8a72-dc65-48e4-88fe-45ef541f2cba',
@@ -43,7 +43,7 @@ describe('Index', function () {
id: 'd90b7335-e540-4a44-ad22-c4baae9cd0a9',
}
beforeEach(function () {
beforeEach(function() {
col = new Collection()
forEach([item1, item2, item3, item4], item => {
col.add(item)
@@ -56,7 +56,7 @@ describe('Index', function () {
return waitTicks()
})
it('works with existing items', function () {
it('works with existing items', function() {
expect(col.indexes).toEqual({
byGroup: {
foo: {
@@ -70,7 +70,7 @@ describe('Index', function () {
})
})
it('works with added items', function () {
it('works with added items', function() {
const item5 = {
id: '823b56c4-4b96-4f3a-9533-5d08177167ac',
group: 'baz',
@@ -96,7 +96,7 @@ describe('Index', function () {
})
})
it('works with updated items', function () {
it('works with updated items', function() {
const item1bis = {
id: item1.id,
group: 'bar',
@@ -119,7 +119,7 @@ describe('Index', function () {
})
})
it('works with removed items', function () {
it('works with removed items', function() {
col.remove(item2)
return waitTicks().then(() => {
@@ -135,7 +135,7 @@ describe('Index', function () {
})
})
it('correctly updates the value even the same object has the same hash', function () {
it('correctly updates the value even the same object has the same hash', function() {
const item1bis = {
id: item1.id,
group: item1.group,
@@ -159,8 +159,8 @@ describe('Index', function () {
})
})
describe('#sweep()', function () {
it('removes empty items lists', function () {
describe('#sweep()', function() {
it('removes empty items lists', function() {
col.remove(item2)
return waitTicks().then(() => {

View File

@@ -1,4 +1,4 @@
export default function isEmpty (object) {
export default function isEmpty(object) {
/* eslint no-unused-vars: 0 */
for (const key in object) {
return false

View File

@@ -1,3 +1,3 @@
export default function isObject (value) {
export default function isObject(value) {
return value !== null && typeof value === 'object'
}

View File

@@ -1,7 +1,7 @@
import { BaseError } from 'make-error'
export default class NotImplemented extends BaseError {
constructor (message) {
constructor(message) {
super(message || 'this method is not implemented')
}
}

View File

@@ -7,7 +7,7 @@ import { ACTION_ADD, ACTION_UPDATE, ACTION_REMOVE } from './collection'
// ===================================================================
export default class UniqueIndex {
constructor (computeHash) {
constructor(computeHash) {
if (computeHash) {
this.computeHash = iteratee(computeHash)
}
@@ -23,19 +23,19 @@ export default class UniqueIndex {
// This method is used to compute the hash under which an item must
// be saved.
computeHash (value, key) {
computeHash(value, key) {
throw new NotImplemented('this method must be overridden')
}
// -----------------------------------------------------------------
get items () {
get items() {
return this._itemByHash
}
// -----------------------------------------------------------------
_attachCollection (collection) {
_attachCollection(collection) {
// Add existing entries.
//
// FIXME: I think there may be a race condition if the `add` event
@@ -47,7 +47,7 @@ export default class UniqueIndex {
collection.on(ACTION_REMOVE, this._onRemove)
}
_detachCollection (collection) {
_detachCollection(collection) {
collection.removeListener(ACTION_ADD, this._onAdd)
collection.removeListener(ACTION_UPDATE, this._onUpdate)
collection.removeListener(ACTION_REMOVE, this._onRemove)
@@ -58,7 +58,7 @@ export default class UniqueIndex {
// -----------------------------------------------------------------
_onAdd (items) {
_onAdd(items) {
const {
computeHash,
_itemByHash: itemByHash,
@@ -77,7 +77,7 @@ export default class UniqueIndex {
}
}
_onUpdate (items) {
_onUpdate(items) {
const {
computeHash,
_itemByHash: itemByHash,
@@ -103,7 +103,7 @@ export default class UniqueIndex {
}
}
_onRemove (items) {
_onRemove(items) {
const { _itemByHash: itemByHash, _keysToHash: keysToHash } = this
for (const key in items) {

View File

@@ -12,7 +12,7 @@ const waitTicks = (n = 2) => {
const { nextTick } = process
return new Promise(resolve => {
;(function waitNextTick () {
;(function waitNextTick() {
// The first tick is handled by Promise#then()
if (--n) {
nextTick(waitNextTick)
@@ -25,7 +25,7 @@ const waitTicks = (n = 2) => {
// ===================================================================
describe('UniqueIndex', function () {
describe('UniqueIndex', function() {
let col, byKey
const item1 = {
id: '2ccb8a72-dc65-48e4-88fe-45ef541f2cba',
@@ -39,7 +39,7 @@ describe('UniqueIndex', function () {
id: '668c1274-4442-44a6-b99a-512188e0bb09',
}
beforeEach(function () {
beforeEach(function() {
col = new Collection()
forEach([item1, item2, item3], item => {
col.add(item)
@@ -52,7 +52,7 @@ describe('UniqueIndex', function () {
return waitTicks()
})
it('works with existing items', function () {
it('works with existing items', function() {
expect(col.indexes).toEqual({
byKey: {
[item1.key]: item1,
@@ -61,7 +61,7 @@ describe('UniqueIndex', function () {
})
})
it('works with added items', function () {
it('works with added items', function() {
const item4 = {
id: '823b56c4-4b96-4f3a-9533-5d08177167ac',
key: '1437af14-429a-40db-8a51-8a2f5ed03201',
@@ -80,7 +80,7 @@ describe('UniqueIndex', function () {
})
})
it('works with updated items', function () {
it('works with updated items', function() {
const item1bis = {
id: item1.id,
key: 'e03d4a3a-0331-4aca-97a2-016bbd43a29b',
@@ -98,7 +98,7 @@ describe('UniqueIndex', function () {
})
})
it('works with removed items', function () {
it('works with removed items', function() {
col.remove(item2)
return waitTicks().then(() => {
@@ -110,7 +110,7 @@ describe('UniqueIndex', function () {
})
})
it('correctly updates the value even the same object has the same hash', function () {
it('correctly updates the value even the same object has the same hash', function() {
const item1bis = {
id: item1.id,
key: item1.key,

View File

@@ -43,7 +43,7 @@ activeUsers.on('remove', users => {
})
// Make some changes in the future.
setTimeout(function () {
setTimeout(function() {
console.log('-----')
users.set({

View File

@@ -9,7 +9,7 @@ import Collection, {
// ===================================================================
export default class View extends Collection {
constructor (collection, predicate) {
constructor(collection, predicate) {
super()
this._collection = collection
@@ -31,29 +31,29 @@ export default class View extends Collection {
// This method is necessary to free the memory of the view if its
// life span is shorter than the collection.
destroy () {
destroy() {
this._collection.removeListener(ACTION_ADD, this._onAdd)
this._collection.removeListener(ACTION_UPDATE, this._onUpdate)
this._collection.removeListener(ACTION_REMOVE, this._onRemove)
}
add () {
add() {
throw new Error('a view is read only')
}
clear () {
clear() {
throw new Error('a view is read only')
}
set () {
set() {
throw new Error('a view is read only')
}
update () {
update() {
throw new Error('a view is read only')
}
_onAdd (items) {
_onAdd(items) {
const { _predicate: predicate } = this
forEach(items, (value, key) => {
@@ -66,7 +66,7 @@ export default class View extends Collection {
})
}
_onUpdate (items) {
_onUpdate(items) {
const { _predicate: predicate } = this
forEach(items, (value, key) => {
@@ -78,7 +78,7 @@ export default class View extends Collection {
})
}
_onRemove (items) {
_onRemove(items) {
forEach(items, (value, key) => {
if (super.has(key)) {
super.remove(key)

View File

@@ -1,6 +1,6 @@
{
"name": "xo-common",
"version": "0.1.2",
"version": "0.2.0",
"license": "AGPL-3.0",
"description": "Code shared between [XO](https://xen-orchestra.com) server and clients",
"keywords": [],

View File

@@ -2,13 +2,13 @@ import { BaseError } from 'make-error'
import { isArray, iteratee } from 'lodash'
class XoError extends BaseError {
constructor ({ code, message, data }) {
constructor({ code, message, data }) {
super(message)
this.code = code
this.data = data
}
toJsonRpcError () {
toJsonRpcError() {
return {
message: this.message,
code: this.code,
@@ -37,8 +37,15 @@ export const noSuchObject = create(1, (id, type) => ({
message: `no such ${type || 'object'} ${id}`,
}))
export const unauthorized = create(2, () => ({
message: 'not authenticated or not enough permissions',
export const unauthorized = create(2, (permission, objectId, objectType) => ({
data: {
permission,
object: {
id: objectId,
type: objectType,
},
},
message: 'not enough permissions',
}))
export const invalidCredentials = create(3, () => ({

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