Compare commits
5 Commits
xo-web-v5.
...
xo-web-v5.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff9782b088 | ||
|
|
e8f2b74b5e | ||
|
|
4535c81b44 | ||
|
|
f39312e789 | ||
|
|
6aad769995 |
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.4.1",
|
||||
"xen-api": "^0.27.1"
|
||||
"xen-api": "^0.27.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/fs",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "The File System for Xen Orchestra backups.",
|
||||
"keywords": [],
|
||||
|
||||
79
CHANGELOG.md
79
CHANGELOG.md
@@ -4,88 +4,13 @@
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [VM/Attach disk] Display confirmation modal when VDI is already attached [#3381](https://github.com/vatesfr/xen-orchestra/issues/3381) (PR [#4366](https://github.com/vatesfr/xen-orchestra/pull/4366))
|
||||
- [Zstd]
|
||||
- [VM/copy, VM/export] Only show zstd option when it's supported [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PRs [#4326](https://github.com/vatesfr/xen-orchestra/pull/4326) [#4368](https://github.com/vatesfr/xen-orchestra/pull/4368))
|
||||
- [VM/Bulk copy] Show warning if zstd compression is not supported on a VM [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PR [#4346](https://github.com/vatesfr/xen-orchestra/pull/4346))
|
||||
- [VM import & Continuous Replication] Enable `guessVhdSizeOnImport` by default, this fix some `VDI_IO_ERROR` with XenServer 7.1 and XCP-ng 8.0 (PR [#4436](https://github.com/vatesfr/xen-orchestra/pull/4436))
|
||||
- [SDN Controller] Add possibility to create multiple GRE networks and VxLAN networks within a same pool (PR [#4435](https://github.com/vatesfr/xen-orchestra/pull/4435))
|
||||
- [SDN Controller] Add possibility to create cross-pool private networks (PR [#4405](https://github.com/vatesfr/xen-orchestra/pull/4405))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [SR/General] Display VDI VM name in SR usage graph (PR [#4370](https://github.com/vatesfr/xen-orchestra/pull/4370))
|
||||
- [VM/Attach disk] Fix checking VDI mode (PR [#4373](https://github.com/vatesfr/xen-orchestra/pull/4373))
|
||||
- [VM revert] Snapshot before: add admin ACLs on created snapshot [#4331](https://github.com/vatesfr/xen-orchestra/issues/4331) (PR [#4391](https://github.com/vatesfr/xen-orchestra/pull/4391))
|
||||
- [Network] Fixed "invalid parameters" error when creating bonded network [#4425](https://github.com/vatesfr/xen-orchestra/issues/4425) (PR [#4429](https://github.com/vatesfr/xen-orchestra/pull/4429))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server-sdn-controller v0.2.0
|
||||
- xo-server-usage-report v0.7.3
|
||||
- xo-server v5.48.0
|
||||
- xo-web v5.48.0
|
||||
|
||||
## **5.37.1** (2019-08-06)
|
||||
|
||||

|
||||
|
||||
### Enhancements
|
||||
|
||||
- [SDN Controller] Let the user choose on which PIF to create a private network (PR [#4379](https://github.com/vatesfr/xen-orchestra/pull/4379))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [SDN Controller] Better detect host shutting down to adapt network topology (PR [#4314](https://github.com/vatesfr/xen-orchestra/pull/4314))
|
||||
- [SDN Controller] Add new hosts to pool's private networks (PR [#4382](https://github.com/vatesfr/xen-orchestra/pull/4382))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server-sdn-controller v0.1.2
|
||||
|
||||
## **5.37.0** (2019-07-25)
|
||||
|
||||
### Highlights
|
||||
|
||||
- [Pool] Ability to add multiple hosts on the pool [#2402](https://github.com/vatesfr/xen-orchestra/issues/2402) (PR [#3716](https://github.com/vatesfr/xen-orchestra/pull/3716))
|
||||
- [SR/General] Improve SR usage graph [#3608](https://github.com/vatesfr/xen-orchestra/issues/3608) (PR [#3830](https://github.com/vatesfr/xen-orchestra/pull/3830))
|
||||
- [VM] Permission to revert to any snapshot for VM operators [#3928](https://github.com/vatesfr/xen-orchestra/issues/3928) (PR [#4247](https://github.com/vatesfr/xen-orchestra/pull/4247))
|
||||
- [Backup NG] Ability to bypass unhealthy VDI chains check [#4324](https://github.com/vatesfr/xen-orchestra/issues/4324) (PR [#4340](https://github.com/vatesfr/xen-orchestra/pull/4340))
|
||||
- [VM/console] Multiline copy/pasting [#4261](https://github.com/vatesfr/xen-orchestra/issues/4261) (PR [#4341](https://github.com/vatesfr/xen-orchestra/pull/4341))
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Stats] Ability to display last day stats [#4160](https://github.com/vatesfr/xen-orchestra/issues/4160) (PR [#4168](https://github.com/vatesfr/xen-orchestra/pull/4168))
|
||||
- [Settings/servers] Display servers connection issues [#4300](https://github.com/vatesfr/xen-orchestra/issues/4300) (PR [#4310](https://github.com/vatesfr/xen-orchestra/pull/4310))
|
||||
- [VM] Show current operations and progress [#3811](https://github.com/vatesfr/xen-orchestra/issues/3811) (PR [#3982](https://github.com/vatesfr/xen-orchestra/pull/3982))
|
||||
- [Backup NG/New] Generate default schedule if no schedule is specified [#4036](https://github.com/vatesfr/xen-orchestra/issues/4036) (PR [#4183](https://github.com/vatesfr/xen-orchestra/pull/4183))
|
||||
- [Host/Advanced] Ability to edit iSCSI IQN [#4048](https://github.com/vatesfr/xen-orchestra/issues/4048) (PR [#4208](https://github.com/vatesfr/xen-orchestra/pull/4208))
|
||||
- [VM,host] Improved state icons/pills (colors and tooltips) (PR [#4363](https://github.com/vatesfr/xen-orchestra/pull/4363))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Settings/Servers] Fix read-only setting toggling
|
||||
- [SDN Controller] Do not choose physical PIF without IP configuration for tunnels. (PR [#4319](https://github.com/vatesfr/xen-orchestra/pull/4319))
|
||||
- [Xen servers] Fix `no connection found for object` error if pool master is reinstalled [#4299](https://github.com/vatesfr/xen-orchestra/issues/4299) (PR [#4302](https://github.com/vatesfr/xen-orchestra/pull/4302))
|
||||
- [Backup-ng/restore] Display correct size for full VM backup [#4316](https://github.com/vatesfr/xen-orchestra/issues/4316) (PR [#4332](https://github.com/vatesfr/xen-orchestra/pull/4332))
|
||||
- [VM/tab-advanced] Fix CPU limits edition (PR [#4337](https://github.com/vatesfr/xen-orchestra/pull/4337))
|
||||
- [Remotes] Fix `EIO` errors due to massive parallel fs operations [#4323](https://github.com/vatesfr/xen-orchestra/issues/4323) (PR [#4330](https://github.com/vatesfr/xen-orchestra/pull/4330))
|
||||
- [VM/Advanced] Fix virtualization mode switch (PV/HVM) (PR [#4349](https://github.com/vatesfr/xen-orchestra/pull/4349))
|
||||
- [Task] fix hidden notification by search field [#3874](https://github.com/vatesfr/xen-orchestra/issues/3874) (PR [#4305](https://github.com/vatesfr/xen-orchestra/pull/4305)
|
||||
- [VM] Fail to change affinity (PR [#4361](https://github.com/vatesfr/xen-orchestra/pull/4361)
|
||||
- [VM] Number of CPUs not correctly changed on running VMs (PR [#4360](https://github.com/vatesfr/xen-orchestra/pull/4360)
|
||||
|
||||
### Released packages
|
||||
|
||||
- @xen-orchestra/fs v0.10.1
|
||||
- xo-server-sdn-controller v0.1.1
|
||||
- xen-api v0.27.1
|
||||
- xo-server v5.46.0
|
||||
- xo-web v5.46.0
|
||||
|
||||
## **5.36.0** (2019-06-27)
|
||||
|
||||

|
||||

|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -124,6 +49,8 @@
|
||||
|
||||
## **5.35.0** (2019-05-29)
|
||||
|
||||

|
||||
|
||||
### Enhancements
|
||||
|
||||
- [VM/general] Display 'Started... ago' instead of 'Halted... ago' for paused state [#3750](https://github.com/vatesfr/xen-orchestra/issues/3750) (PR [#4170](https://github.com/vatesfr/xen-orchestra/pull/4170))
|
||||
|
||||
@@ -7,12 +7,22 @@
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [Stats] Ability to display last day stats [#4160](https://github.com/vatesfr/xen-orchestra/issues/4160) (PR [#4168](https://github.com/vatesfr/xen-orchestra/pull/4168))
|
||||
- [Settings/servers] Display servers connection issues [#4300](https://github.com/vatesfr/xen-orchestra/issues/4300) (PR [#4310](https://github.com/vatesfr/xen-orchestra/pull/4310))
|
||||
- [VM] Permission to revert to any snapshot for VM operators [#3928](https://github.com/vatesfr/xen-orchestra/issues/3928) (PR [#4247](https://github.com/vatesfr/xen-orchestra/pull/4247))
|
||||
- [VM] Show current operations and progress [#3811](https://github.com/vatesfr/xen-orchestra/issues/3811) (PR [#3982](https://github.com/vatesfr/xen-orchestra/pull/3982))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [PBD] Obfuscate cifs password from device config [#4384](https://github.com/vatesfr/xen-orchestra/issues/4384) (PR [#4401](https://github.com/vatesfr/xen-orchestra/pull/4401))
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [Settings/Servers] Fix read-only setting toggling
|
||||
- [SDN Controller] Do not choose physical PIF without IP configuration for tunnels. (PR [#4319](https://github.com/vatesfr/xen-orchestra/pull/4319))
|
||||
- [Xen servers] Fix `no connection found for object` error if pool master is reinstalled [#4299](https://github.com/vatesfr/xen-orchestra/issues/4299) (PR [#4302](https://github.com/vatesfr/xen-orchestra/pull/4302))
|
||||
- [Backup-ng/restore] Display correct size for full VM backup [#4316](https://github.com/vatesfr/xen-orchestra/issues/4316) (PR [#4332](https://github.com/vatesfr/xen-orchestra/pull/4332))
|
||||
- [VM/tab-advanced] Fix CPU limits edition (PR [#4337](https://github.com/vatesfr/xen-orchestra/pull/4337))
|
||||
- [Remotes] Fix `EIO` errors due to massive parallel fs operations [#4323](https://github.com/vatesfr/xen-orchestra/issues/4323) (PR [#4330](https://github.com/vatesfr/xen-orchestra/pull/4330))
|
||||
|
||||
### Released packages
|
||||
|
||||
> Packages will be released in the order they are here, therefore, they should
|
||||
@@ -20,5 +30,8 @@
|
||||
>
|
||||
> Rule of thumb: add packages on top.
|
||||
|
||||
- xo-server v5.49.0
|
||||
- xo-web v5.49.0
|
||||
- @xen-orchestra/fs v0.10.0
|
||||
- xo-server-sdn-controller v0.1.1
|
||||
- xen-api v0.26.0
|
||||
- xo-server v5.45.0
|
||||
- xo-web v5.45.0
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
- [ ] PR reference the relevant issue (e.g. `Fixes #007`)
|
||||
- [ ] if UI changes, a screenshot has been added to the PR
|
||||
- [ ] if `xo-server` API changes, the corresponding test has been added to/updated on [`xo-server-test`](https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-test)
|
||||
- [ ] `CHANGELOG.unreleased.md`:
|
||||
- enhancement/bug fix entry added
|
||||
- list of packages to release updated (`${name} v${new version}`)
|
||||
|
||||
@@ -110,17 +110,16 @@ $ systemctl restart xo-server
|
||||
|
||||
### Behind a transparent proxy
|
||||
|
||||
If you're behind a transparent proxy, you'll probably have issues with the updater (SSL/TLS issues).
|
||||
If your are behind a transparent proxy, you'll probably have issues with the updater (SSL/TLS issues).
|
||||
|
||||
Run the following commands to allow the updater to work:
|
||||
First, run the following commands:
|
||||
|
||||
```
|
||||
$ sudo -s
|
||||
$ echo NODE_TLS_REJECT_UNAUTHORIZED=0 >> /etc/xo-appliance/env
|
||||
$ npm config -g set strict-ssl=false
|
||||
$ systemctl restart xoa-updater
|
||||
```
|
||||
Now try running an update again.
|
||||
|
||||
Then, restart the updater with `systemctl restart xoa-updater`.
|
||||
|
||||
### Updating SSL self-signed certificate
|
||||
|
||||
|
||||
16
docs/xoa.md
16
docs/xoa.md
@@ -22,34 +22,26 @@ For use on huge infrastructure (more than 500+ VMs), feel free to increase the R
|
||||
|
||||
### The quickest way
|
||||
|
||||
The **fastest and most secure way** to install Xen Orchestra is to use our web deploy page. Go on https://xen-orchestra.com/#!/xoa and follow instructions.
|
||||
|
||||
> **Note:** no data will be sent to our servers, it's running only between your browser and your host!
|
||||
|
||||
### Via a bash script
|
||||
|
||||
Alternatively, you can deploy it by connecting to your XenServer host and executing the following:
|
||||
The fastest way to install Xen Orchestra is to use our appliance deploy script. You can deploy it by connecting to your XenServer host and executing the following:
|
||||
|
||||
```
|
||||
bash -c "$(curl -s http://xoa.io/deploy)"
|
||||
```
|
||||
**Note:** This won't write or modify anything on your XenServer host: it will just import the XOA VM into your default storage repository.
|
||||
|
||||
> **Note:** This won't write or modify anything on your XenServer host: it will just import the XOA VM into your default storage repository.
|
||||
|
||||
Follow the instructions:
|
||||
Now follow the instructions:
|
||||
|
||||
* Your IP configuration will be requested: it's set to **DHCP by default**, otherwise you can enter a fixed IP address (eg `192.168.0.10`)
|
||||
* If DHCP is selected, the script will continue automatically. Otherwise a netmask, gateway, and DNS should be provided.
|
||||
* XOA will be deployed on your default storage repository. You can move it elsewhere anytime after.
|
||||
|
||||
### Via download the XVA
|
||||
### The alternative
|
||||
|
||||
Download XOA from xen-orchestra.com. Once you've got the XVA file, you can import it with `xe vm-import filename=xoa_unified.xva` or via XenCenter.
|
||||
|
||||
After the VM is imported, you just need to start it with `xe vm-start vm="XOA"` or with XenCenter.
|
||||
|
||||
## First Login
|
||||
|
||||
Once you have started the VM, you can access the web UI by putting the IP you configured during deployment into your web browser. If you did not configure an IP or are unsure, try one of the following methods to find it:
|
||||
|
||||
* Run `xe vm-list params=name-label,networks | grep -A 1 XOA` on your host
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
"testEnvironment": "node",
|
||||
"testPathIgnorePatterns": [
|
||||
"/dist/",
|
||||
"/xo-server-test/",
|
||||
"/xo-web/"
|
||||
],
|
||||
"testRegex": "\\.spec\\.js$",
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/fs": "^0.10.1",
|
||||
"@xen-orchestra/fs": "^0.10.0",
|
||||
"cli-progress": "^2.0.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"getopts": "^2.2.3",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"@xen-orchestra/fs": "^0.10.1",
|
||||
"@xen-orchestra/fs": "^0.10.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"execa": "^2.0.2",
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.17.4",
|
||||
"pw": "^0.0.4",
|
||||
"xen-api": "^0.27.1"
|
||||
"xen-api": "^0.27.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.1.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xen-api",
|
||||
"version": "0.27.1",
|
||||
"version": "0.27.0",
|
||||
"license": "ISC",
|
||||
"description": "Connector to the Xen API",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# xo-server-sdn-controller [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
XO Server plugin that allows the creation of pool-wide and cross-pool private networks.
|
||||
XO Server plugin that allows the creation of pool-wide private networks.
|
||||
|
||||
## Install
|
||||
|
||||
@@ -10,11 +10,8 @@ For installing XO and the plugins from the sources, please take a look at [the d
|
||||
|
||||
### Network creation
|
||||
|
||||
In the network creation view:
|
||||
- Select a `pool` and `Private network`
|
||||
- Select on which interface to create the network's tunnels
|
||||
- Select other pools to add them to the network if wanted
|
||||
- Create the network
|
||||
In the network creation view, select a `pool` and `Private network`.
|
||||
Create the network.
|
||||
|
||||
Choice is offer between `GRE` and `VxLAN`, if `VxLAN` is chosen, then the port 4789 must be open for UDP traffic.
|
||||
The following line needs to be added, if not already present, in `/etc/sysconfig/iptables` of all the hosts where `VxLAN` is wanted:
|
||||
@@ -28,7 +25,7 @@ the web interface, see [the plugin documentation](https://xen-orchestra.com/docs
|
||||
The plugin's configuration contains:
|
||||
- `cert-dir`: A path where to find the certificates to create SSL connections with the hosts.
|
||||
If none is provided, the plugin will create its own self-signed certificates.
|
||||
- `override-certs`: Whether or not to uninstall an already existing SDN controller CA certificate in order to replace it by the plugin's one.
|
||||
- `override-certs:` Whether or not to uninstall an already existing SDN controller CA certificate in order to replace it by the plugin's one.
|
||||
|
||||
## Contributions
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.1",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
@@ -30,8 +30,7 @@
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"lodash": "^4.17.11",
|
||||
"node-openssl-cert": "^0.0.84",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"uuid": "^3.3.2"
|
||||
"promise-toolbox": "^0.13.0"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,30 +12,35 @@ const OVSDB_PORT = 6640
|
||||
|
||||
export class OvsdbClient {
|
||||
constructor(host, clientKey, clientCert, caCert) {
|
||||
this._host = host
|
||||
this._numberOfPortAndInterface = 0
|
||||
this._requestId = 0
|
||||
|
||||
this._adding = []
|
||||
|
||||
this.host = host
|
||||
this._requestID = 0
|
||||
|
||||
this.updateCertificates(clientKey, clientCert, caCert)
|
||||
|
||||
log.debug('New OVSDB client', {
|
||||
host: this.host.name_label,
|
||||
})
|
||||
log.debug(`[${this._host.name_label}] New OVSDB client`)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
get address() {
|
||||
return this._host.address
|
||||
}
|
||||
|
||||
get host() {
|
||||
return this._host.$ref
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this._host.$id
|
||||
}
|
||||
|
||||
updateCertificates(clientKey, clientCert, caCert) {
|
||||
this._clientKey = clientKey
|
||||
this._clientCert = clientCert
|
||||
this._caCert = caCert
|
||||
|
||||
log.debug('Certificates have been updated', {
|
||||
host: this.host.name_label,
|
||||
})
|
||||
log.debug(`[${this._host.name_label}] Certificates have been updated`)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -44,31 +49,19 @@ export class OvsdbClient {
|
||||
networkUuid,
|
||||
networkName,
|
||||
remoteAddress,
|
||||
encapsulation,
|
||||
key,
|
||||
remoteNetwork
|
||||
encapsulation
|
||||
) {
|
||||
if (
|
||||
this._adding.find(
|
||||
elem => elem.id === networkUuid && elem.addr === remoteAddress
|
||||
) !== undefined
|
||||
) {
|
||||
return
|
||||
}
|
||||
const adding = { id: networkUuid, addr: remoteAddress }
|
||||
this._adding.push(adding)
|
||||
|
||||
const socket = await this._connect()
|
||||
const index = this._numberOfPortAndInterface
|
||||
++this._numberOfPortAndInterface
|
||||
|
||||
const [bridgeUuid, bridgeName] = await this._getBridgeUuidForNetwork(
|
||||
networkUuid,
|
||||
networkName,
|
||||
socket
|
||||
)
|
||||
if (bridgeUuid === undefined) {
|
||||
if (bridgeUuid == null) {
|
||||
socket.destroy()
|
||||
this._adding = this._adding.filter(
|
||||
elem => elem.id !== networkUuid || elem.addr !== remoteAddress
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -80,23 +73,14 @@ export class OvsdbClient {
|
||||
)
|
||||
if (alreadyExist) {
|
||||
socket.destroy()
|
||||
this._adding = this._adding.filter(
|
||||
elem => elem.id !== networkUuid || elem.addr !== remoteAddress
|
||||
)
|
||||
return bridgeName
|
||||
return
|
||||
}
|
||||
|
||||
const index = ++this._numberOfPortAndInterface
|
||||
const interfaceName = bridgeName + '_iface' + index
|
||||
const portName = bridgeName + '_port' + index
|
||||
const interfaceName = 'tunnel_iface' + index
|
||||
const portName = 'tunnel_port' + index
|
||||
|
||||
// Add interface and port to the bridge
|
||||
const options = ['map', [['remote_ip', remoteAddress], ['key', key]]]
|
||||
const otherConfig =
|
||||
remoteNetwork !== undefined
|
||||
? ['map', [['cross_pool', remoteNetwork]]]
|
||||
: ['map', [['private_pool_wide', 'true']]]
|
||||
|
||||
const options = ['map', [['remote_ip', remoteAddress]]]
|
||||
const addInterfaceOperation = {
|
||||
op: 'insert',
|
||||
table: 'Interface',
|
||||
@@ -104,7 +88,7 @@ export class OvsdbClient {
|
||||
type: encapsulation,
|
||||
options: options,
|
||||
name: interfaceName,
|
||||
other_config: otherConfig,
|
||||
other_config: ['map', [['private_pool_wide', 'true']]],
|
||||
},
|
||||
'uuid-name': 'new_iface',
|
||||
}
|
||||
@@ -114,7 +98,7 @@ export class OvsdbClient {
|
||||
row: {
|
||||
name: portName,
|
||||
interfaces: ['set', [['named-uuid', 'new_iface']]],
|
||||
other_config: otherConfig,
|
||||
other_config: ['map', [['private_pool_wide', 'true']]],
|
||||
},
|
||||
'uuid-name': 'new_port',
|
||||
}
|
||||
@@ -131,11 +115,7 @@ export class OvsdbClient {
|
||||
mutateBridgeOperation,
|
||||
]
|
||||
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
|
||||
|
||||
this._adding = this._adding.filter(
|
||||
elem => elem.id !== networkUuid || elem.addr !== remoteAddress
|
||||
)
|
||||
if (jsonObjects === undefined) {
|
||||
if (jsonObjects == null) {
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
@@ -146,58 +126,42 @@ export class OvsdbClient {
|
||||
let opResult
|
||||
do {
|
||||
opResult = jsonObjects[0].result[i]
|
||||
if (opResult?.error !== undefined) {
|
||||
if (opResult != null && opResult.error != null) {
|
||||
error = opResult.error
|
||||
details = opResult.details
|
||||
}
|
||||
++i
|
||||
} while (opResult !== undefined && error === undefined)
|
||||
} while (opResult && !error)
|
||||
|
||||
if (error !== undefined) {
|
||||
log.error('Error while adding port and interface to bridge', {
|
||||
error,
|
||||
details,
|
||||
port: portName,
|
||||
interface: interfaceName,
|
||||
bridge: bridgeName,
|
||||
network: networkName,
|
||||
host: this.host.name_label,
|
||||
})
|
||||
if (error != null) {
|
||||
log.error(
|
||||
`[${this._host.name_label}] Error while adding port: '${portName}' and interface: '${interfaceName}' to bridge: '${bridgeName}' on network: '${networkName}' because: ${error}: ${details}`
|
||||
)
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
|
||||
log.debug('Port and interface added to bridge', {
|
||||
port: portName,
|
||||
interface: interfaceName,
|
||||
bridge: bridgeName,
|
||||
network: networkName,
|
||||
host: this.host.name_label,
|
||||
})
|
||||
log.debug(
|
||||
`[${this._host.name_label}] Port: '${portName}' and interface: '${interfaceName}' added to bridge: '${bridgeName}' on network: '${networkName}'`
|
||||
)
|
||||
socket.destroy()
|
||||
return bridgeName
|
||||
}
|
||||
|
||||
async resetForNetwork(
|
||||
networkUuid,
|
||||
networkName,
|
||||
crossPoolOnly,
|
||||
remoteNetwork
|
||||
) {
|
||||
async resetForNetwork(networkUuid, networkName) {
|
||||
const socket = await this._connect()
|
||||
const [bridgeUuid, bridgeName] = await this._getBridgeUuidForNetwork(
|
||||
networkUuid,
|
||||
networkName,
|
||||
socket
|
||||
)
|
||||
if (bridgeUuid === undefined) {
|
||||
if (bridgeUuid == null) {
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
|
||||
// Delete old ports created by a SDN controller
|
||||
const ports = await this._getBridgePorts(bridgeUuid, bridgeName, socket)
|
||||
if (ports === undefined) {
|
||||
if (ports == null) {
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
@@ -212,17 +176,15 @@ export class OvsdbClient {
|
||||
where,
|
||||
socket
|
||||
)
|
||||
if (selectResult === undefined) {
|
||||
if (selectResult == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
forOwn(selectResult.other_config[1], config => {
|
||||
const shouldDelete =
|
||||
(config[0] === 'private_pool_wide' && !crossPoolOnly) ||
|
||||
(config[0] === 'cross_pool' &&
|
||||
(remoteNetwork === undefined || remoteNetwork === config[1]))
|
||||
|
||||
if (shouldDelete) {
|
||||
if (config[0] === 'private_pool_wide' && config[1] === 'true') {
|
||||
log.debug(
|
||||
`[${this._host.name_label}] Adding port: '${selectResult.name}' to delete list from bridge: '${bridgeName}'`
|
||||
)
|
||||
portsToDelete.push(['uuid', portUuid])
|
||||
}
|
||||
})
|
||||
@@ -243,25 +205,21 @@ export class OvsdbClient {
|
||||
|
||||
const params = ['Open_vSwitch', mutateBridgeOperation]
|
||||
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
|
||||
if (jsonObjects === undefined) {
|
||||
if (jsonObjects == null) {
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
if (jsonObjects[0].error != null) {
|
||||
log.error('Error while deleting ports from bridge', {
|
||||
error: jsonObjects[0].error,
|
||||
bridge: bridgeName,
|
||||
host: this.host.name_label,
|
||||
})
|
||||
log.error(
|
||||
`[${this._host.name_label}] Couldn't delete ports from bridge: '${bridgeName}' because: ${jsonObjects.error}`
|
||||
)
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
|
||||
log.debug('Ports deleted from bridge', {
|
||||
nPorts: jsonObjects[0].result[0].count,
|
||||
bridge: bridgeName,
|
||||
host: this.host.name_label,
|
||||
})
|
||||
log.debug(
|
||||
`[${this._host.name_label}] Deleted ${jsonObjects[0].result[0].count} ports from bridge: '${bridgeName}'`
|
||||
)
|
||||
socket.destroy()
|
||||
}
|
||||
|
||||
@@ -277,9 +235,9 @@ export class OvsdbClient {
|
||||
for (let i = pos; i < data.length; ++i) {
|
||||
const c = data.charAt(i)
|
||||
if (c === '{') {
|
||||
++depth
|
||||
depth++
|
||||
} else if (c === '}') {
|
||||
--depth
|
||||
depth--
|
||||
if (depth === 0) {
|
||||
const object = JSON.parse(buffer + data.substr(0, i + 1))
|
||||
objects.push(object)
|
||||
@@ -311,16 +269,15 @@ export class OvsdbClient {
|
||||
where,
|
||||
socket
|
||||
)
|
||||
if (selectResult === undefined) {
|
||||
log.error('No bridge found for network', {
|
||||
network: networkName,
|
||||
host: this.host.name_label,
|
||||
})
|
||||
return []
|
||||
if (selectResult == null) {
|
||||
return [null, null]
|
||||
}
|
||||
|
||||
const bridgeUuid = selectResult._uuid[1]
|
||||
const bridgeName = selectResult.name
|
||||
log.debug(
|
||||
`[${this._host.name_label}] Found bridge: '${bridgeName}' for network: '${networkName}'`
|
||||
)
|
||||
|
||||
return [bridgeUuid, bridgeName]
|
||||
}
|
||||
@@ -332,14 +289,14 @@ export class OvsdbClient {
|
||||
socket
|
||||
) {
|
||||
const ports = await this._getBridgePorts(bridgeUuid, bridgeName, socket)
|
||||
if (ports === undefined) {
|
||||
return false
|
||||
if (ports == null) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const port of ports) {
|
||||
const portUuid = port[1]
|
||||
const interfaces = await this._getPortInterfaces(portUuid, socket)
|
||||
if (interfaces === undefined) {
|
||||
if (interfaces == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -350,7 +307,7 @@ export class OvsdbClient {
|
||||
remoteAddress,
|
||||
socket
|
||||
)
|
||||
if (hasRemote) {
|
||||
if (hasRemote === true) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -362,8 +319,8 @@ export class OvsdbClient {
|
||||
async _getBridgePorts(bridgeUuid, bridgeName, socket) {
|
||||
const where = [['_uuid', '==', ['uuid', bridgeUuid]]]
|
||||
const selectResult = await this._select('Bridge', ['ports'], where, socket)
|
||||
if (selectResult === undefined) {
|
||||
return
|
||||
if (selectResult == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return selectResult.ports[0] === 'set'
|
||||
@@ -379,8 +336,8 @@ export class OvsdbClient {
|
||||
where,
|
||||
socket
|
||||
)
|
||||
if (selectResult === undefined) {
|
||||
return
|
||||
if (selectResult == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return selectResult.interfaces[0] === 'set'
|
||||
@@ -396,7 +353,7 @@ export class OvsdbClient {
|
||||
where,
|
||||
socket
|
||||
)
|
||||
if (selectResult === undefined) {
|
||||
if (selectResult == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -421,36 +378,28 @@ export class OvsdbClient {
|
||||
|
||||
const params = ['Open_vSwitch', selectOperation]
|
||||
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
|
||||
if (jsonObjects === undefined) {
|
||||
if (jsonObjects == null) {
|
||||
return
|
||||
}
|
||||
const jsonResult = jsonObjects[0].result[0]
|
||||
if (jsonResult.error !== undefined) {
|
||||
log.error('Error while selecting columns', {
|
||||
error: jsonResult.error,
|
||||
details: jsonResult.details,
|
||||
columns,
|
||||
table,
|
||||
where,
|
||||
host: this.host.name_label,
|
||||
})
|
||||
return
|
||||
if (jsonResult.error != null) {
|
||||
log.error(
|
||||
`[${this._host.name_label}] Couldn't retrieve: '${columns}' in: '${table}' because: ${jsonResult.error}: ${jsonResult.details}`
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (jsonResult.rows.length === 0) {
|
||||
log.error('No result for select', {
|
||||
columns,
|
||||
table,
|
||||
where,
|
||||
host: this.host.name_label,
|
||||
})
|
||||
return
|
||||
log.error(
|
||||
`[${this._host.name_label}] No '${columns}' found in: '${table}' where: '${where}'`
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
// For now all select operations should return only 1 row
|
||||
assert(
|
||||
jsonResult.rows.length === 1,
|
||||
`[${this.host.name_label}] There should be exactly 1 row when searching: '${columns}' in: '${table}' where: '${where}'`
|
||||
`[${this._host.name_label}] There should exactly 1 row when searching: '${columns}' in: '${table}' where: '${where}'`
|
||||
)
|
||||
|
||||
return jsonResult.rows[0]
|
||||
@@ -458,7 +407,9 @@ export class OvsdbClient {
|
||||
|
||||
async _sendOvsdbTransaction(params, socket) {
|
||||
const stream = socket
|
||||
const requestId = ++this._requestId
|
||||
|
||||
const requestId = this._requestID
|
||||
++this._requestID
|
||||
const req = {
|
||||
id: requestId,
|
||||
method: 'transact',
|
||||
@@ -468,11 +419,10 @@ export class OvsdbClient {
|
||||
try {
|
||||
stream.write(JSON.stringify(req))
|
||||
} catch (error) {
|
||||
log.error('Error while writing into stream', {
|
||||
error,
|
||||
host: this.host.name_label,
|
||||
})
|
||||
return
|
||||
log.error(
|
||||
`[${this._host.name_label}] Error while writing into stream: ${error}`
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
let result
|
||||
@@ -482,11 +432,10 @@ export class OvsdbClient {
|
||||
try {
|
||||
result = await fromEvent(stream, 'data', {})
|
||||
} catch (error) {
|
||||
log.error('Error while waiting for stream data', {
|
||||
error,
|
||||
host: this.host.name_label,
|
||||
})
|
||||
return
|
||||
log.error(
|
||||
`[${this._host.name_label}] Error while waiting for stream data: ${error}`
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
jsonObjects = this._parseJson(result)
|
||||
@@ -503,7 +452,7 @@ export class OvsdbClient {
|
||||
ca: this._caCert,
|
||||
key: this._clientKey,
|
||||
cert: this._clientCert,
|
||||
host: this.host.address,
|
||||
host: this._host.address,
|
||||
port: OVSDB_PORT,
|
||||
rejectUnauthorized: false,
|
||||
requestCert: false,
|
||||
@@ -513,20 +462,18 @@ export class OvsdbClient {
|
||||
try {
|
||||
await fromEvent(socket, 'secureConnect', {})
|
||||
} catch (error) {
|
||||
log.error('TLS connection failed', {
|
||||
error,
|
||||
code: error.code,
|
||||
host: this.host.name_label,
|
||||
})
|
||||
log.error(
|
||||
`[${this._host.name_label}] TLS connection failed because: ${error}: ${error.code}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
log.debug(`[${this._host.name_label}] TLS connection successful`)
|
||||
|
||||
socket.on('error', error => {
|
||||
log.error('Socket error', {
|
||||
error,
|
||||
code: error.code,
|
||||
host: this.host.name_label,
|
||||
})
|
||||
log.error(
|
||||
`[${this._host.name_label}] OVSDB client socket error: ${error} with code: ${error.code}`
|
||||
)
|
||||
})
|
||||
|
||||
return socket
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
const pkg = require('./package.json')
|
||||
|
||||
// `xo-server-test` is a special package which has no dev dependencies but our
|
||||
// babel config generator only looks in `devDependencies`.
|
||||
require('assert').strictEqual(pkg.devDependencies, undefined)
|
||||
pkg.devDependencies = pkg.dependencies
|
||||
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(pkg)
|
||||
@@ -1,24 +0,0 @@
|
||||
/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__/
|
||||
@@ -1,145 +0,0 @@
|
||||
# xo-server-test
|
||||
|
||||
> Test client for Xo-Server
|
||||
|
||||
Tests are ran sequentially to avoid concurrency issues.
|
||||
|
||||
## Adding a test
|
||||
|
||||
### Organization
|
||||
|
||||
```
|
||||
src
|
||||
├─ user
|
||||
| ├─ __snapshots__
|
||||
| | └─ index.spec.js.snap
|
||||
| └─ index.spec.js
|
||||
├─ job
|
||||
¦ └─ index.spec.js
|
||||
¦
|
||||
¦
|
||||
├─ _xoConnection.js
|
||||
└─ util.js
|
||||
```
|
||||
|
||||
The tests can describe xo methods or scenarios:
|
||||
```javascript
|
||||
import xo from "../_xoConnection";
|
||||
|
||||
describe("user", () => {
|
||||
|
||||
// testing a method
|
||||
describe(".set()", () => {
|
||||
it("sets an email", async () => {
|
||||
// some tests using xo methods and helpers from _xoConnection.js
|
||||
const id = await xo.createTempUser(SIMPLE_USER);
|
||||
expect(await xo.call("user.set", params)).toBe(true);
|
||||
expect(await xo.getUser(id)).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// testing a scenario
|
||||
test("create two users, modify a user email to be the same with the other and fail trying to connect them", () => {
|
||||
/* some tests */
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
### Best practices
|
||||
|
||||
- The test environment must remain the same before and after each test:
|
||||
* each resource created must be deleted
|
||||
* existing resources should not be altered
|
||||
|
||||
- Make a sentence for the title of the test. It must be clear and consistent.
|
||||
|
||||
- If the feature you want to test is not implemented : write it and skip it, using `it.skip()`.
|
||||
|
||||
- Take values that cover the maximum of testing possibilities.
|
||||
|
||||
- If you make tests which keep track of large object, it is better to use snapshots.
|
||||
|
||||
- `_xoConnection.js` contains helpers to create temporary resources and to interface with XO.
|
||||
You can use it if you need to create resources which will be automatically deleted after the test:
|
||||
```javascript
|
||||
import xo from "../_xoConnection";
|
||||
|
||||
describe(".create()", () => {
|
||||
it("creates a user without permission", async () => {
|
||||
// The user will be deleted automatically at the end of the test
|
||||
const userId = await xo.createTempUser({
|
||||
email: "wayne1@vates.fr",
|
||||
password: "batman1",
|
||||
});
|
||||
expect(await xo.getUser(userId)).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
The available helpers:
|
||||
* `createTempUser(params)`
|
||||
* `getUser(id)`
|
||||
* `createTempJob(params)`
|
||||
* `createTempBackupNgJob(params)`
|
||||
* `createTempVm(params)`
|
||||
* `getSchedule(predicate)`
|
||||
|
||||
## Usage
|
||||
|
||||
- Before running the tests, you have to create a config file for xo-server-test.
|
||||
```
|
||||
> cp sample.config.toml ~/.config/xo-server-test/config.toml
|
||||
```
|
||||
And complete it.
|
||||
|
||||
- To run the tests:
|
||||
```
|
||||
> npm ci
|
||||
> yarn test
|
||||
```
|
||||
|
||||
You get all the test suites passed (`PASS`) or failed (`FAIL`).
|
||||
```
|
||||
> yarn test
|
||||
yarn run v1.9.4
|
||||
$ jest
|
||||
PASS src/user/user.spec.js
|
||||
PASS src/job/job.spec.js
|
||||
PASS src/backupNg/backupNg.spec.js
|
||||
|
||||
Test Suites: 3 passed, 3 total
|
||||
Tests: 2 skipped, 36 passed, 38 total
|
||||
Snapshots: 35 passed, 35 total
|
||||
Time: 7.257s, estimated 8s
|
||||
Ran all test suites.
|
||||
Done in 7.92s.
|
||||
```
|
||||
|
||||
- You can run only tests related to changed files, and review the failed output by using: `> yarn test --watch`
|
||||
|
||||
- ⚠ Warning: snapshots ⚠
|
||||
After each run of the tests, check that snapshots are not inadvertently modified.
|
||||
|
||||
- ⚠ Jest known issue ⚠
|
||||
If a test timeout is triggered the next async tests can fail, it's due to an inadvertently modified snapshots.
|
||||
As a workaround, you can clean your git working tree and re-run jest using a large timeout: `> yarn test --testTimeout=100000`
|
||||
|
||||
## 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](http://vates.fr)
|
||||
@@ -1,56 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server-test",
|
||||
"version": "0.0.0",
|
||||
"license": "ISC",
|
||||
"description": "Test client for Xo-Server",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-test",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "packages/xo-server-test",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "^7.1.5",
|
||||
"@babel/core": "^7.1.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.4.0",
|
||||
"@babel/preset-env": "^7.1.6",
|
||||
"@iarna/toml": "^2.2.1",
|
||||
"app-conf": "^0.7.0",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"golike-defer": "^0.4.1",
|
||||
"jest": "^24.8.0",
|
||||
"lodash": "^4.17.11",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"xo-collection": "^0.4.1",
|
||||
"xo-common": "^0.2.0",
|
||||
"xo-lib": "^0.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev-test": "jest --bail --watch",
|
||||
"test": "jest"
|
||||
},
|
||||
"jest": {
|
||||
"modulePathIgnorePatterns": [
|
||||
"<rootDir>/src/old-tests"
|
||||
],
|
||||
"testEnvironment": "node",
|
||||
"testRegex": "\\.spec\\.js$",
|
||||
"maxConcurrency": 1
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
[xoConnection]
|
||||
url = ''
|
||||
email = ''
|
||||
password = ''
|
||||
|
||||
[servers]
|
||||
[servers.default]
|
||||
username = ''
|
||||
password = ''
|
||||
host = ''
|
||||
|
||||
[vms]
|
||||
default = ''
|
||||
# vmToBackup = ''
|
||||
|
||||
[templates]
|
||||
default = ''
|
||||
templateWithoutDisks = ''
|
||||
|
||||
[srs]
|
||||
default = ''
|
||||
|
||||
[remotes]
|
||||
default = { name = '', url = '' }
|
||||
remote1 = { name = '', url = '' }
|
||||
# remote2 = { name = '', url = '' }
|
||||
@@ -1,13 +0,0 @@
|
||||
import appConf from 'app-conf'
|
||||
import path from 'path'
|
||||
|
||||
/* eslint-env jest */
|
||||
|
||||
let config
|
||||
export { config as default }
|
||||
|
||||
beforeAll(async () => {
|
||||
config = await appConf.load('xo-server-test', {
|
||||
appDir: path.join(__dirname, '..'),
|
||||
})
|
||||
})
|
||||
@@ -1,6 +0,0 @@
|
||||
const randomId = () =>
|
||||
Math.random()
|
||||
.toString(36)
|
||||
.slice(2)
|
||||
|
||||
export { randomId as default }
|
||||
@@ -1,248 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
import defer from 'golike-defer'
|
||||
import Xo from 'xo-lib'
|
||||
import XoCollection from 'xo-collection'
|
||||
import { find, forOwn } from 'lodash'
|
||||
import { fromEvent } from 'promise-toolbox'
|
||||
|
||||
import config from './_config'
|
||||
|
||||
const getDefaultCredentials = () => {
|
||||
const { email, password } = config.xoConnection
|
||||
return { email, password }
|
||||
}
|
||||
|
||||
class XoConnection extends Xo {
|
||||
constructor(opts) {
|
||||
super(opts)
|
||||
|
||||
const objects = (this._objects = new XoCollection())
|
||||
const watchers = (this._watchers = {})
|
||||
this._tempResourceDisposers = []
|
||||
this._durableResourceDisposers = []
|
||||
|
||||
this.on('notification', ({ method, params }) => {
|
||||
if (method !== 'all') {
|
||||
return
|
||||
}
|
||||
|
||||
const fn = params.type === 'exit' ? objects.unset : objects.set
|
||||
forOwn(params.items, (item, id) => {
|
||||
fn.call(objects, id, item)
|
||||
|
||||
const watcher = watchers[id]
|
||||
if (watcher !== undefined) {
|
||||
watcher(item)
|
||||
delete watchers[id]
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
get objects() {
|
||||
return this._objects
|
||||
}
|
||||
|
||||
async _fetchObjects() {
|
||||
const { _objects: objects, _watchers: watchers } = this
|
||||
forOwn(await this.call('xo.getAllObjects'), (object, id) => {
|
||||
objects.set(id, object)
|
||||
|
||||
const watcher = watchers[id]
|
||||
if (watcher !== undefined) {
|
||||
watcher(object)
|
||||
delete watchers[id]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: integrate in xo-lib.
|
||||
waitObject(id) {
|
||||
return new Promise(resolve => {
|
||||
this._watchers[id] = resolve
|
||||
}) // FIXME: work with multiple listeners.
|
||||
}
|
||||
|
||||
async getOrWaitObject(id) {
|
||||
const object = this._objects.all[id]
|
||||
if (object !== undefined) {
|
||||
return object
|
||||
}
|
||||
return this.waitObject(id)
|
||||
}
|
||||
|
||||
@defer
|
||||
async connect($defer, credentials = getDefaultCredentials()) {
|
||||
await this.open()
|
||||
$defer.onFailure(() => this.close())
|
||||
|
||||
await this.signIn(credentials)
|
||||
await this._fetchObjects()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
async waitObjectState(id, predicate) {
|
||||
let obj = this._objects.all[id]
|
||||
while (true) {
|
||||
try {
|
||||
await predicate(obj)
|
||||
return
|
||||
} catch (_) {}
|
||||
// If failed, wait for next object state/update and retry.
|
||||
obj = await this.waitObject(id)
|
||||
}
|
||||
}
|
||||
|
||||
async createTempUser(params) {
|
||||
const id = await this.call('user.create', params)
|
||||
this._tempResourceDisposers.push('user.delete', { id })
|
||||
return id
|
||||
}
|
||||
|
||||
async getUser(id) {
|
||||
return find(await super.call('user.getAll'), { id })
|
||||
}
|
||||
|
||||
async createTempJob(params) {
|
||||
const id = await this.call('job.create', { job: params })
|
||||
this._tempResourceDisposers.push('job.delete', { id })
|
||||
return id
|
||||
}
|
||||
|
||||
async createTempBackupNgJob(params) {
|
||||
const job = await this.call('backupNg.createJob', params)
|
||||
this._tempResourceDisposers.push('backupNg.deleteJob', { id: job.id })
|
||||
return job
|
||||
}
|
||||
|
||||
async createTempVm(params) {
|
||||
const id = await this.call('vm.create', params)
|
||||
this._tempResourceDisposers.push('vm.delete', { id })
|
||||
await this.waitObjectState(id, vm => {
|
||||
if (vm.type !== 'VM') throw new Error('retry')
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
async createTempRemote(params) {
|
||||
const remote = await this.call('remote.create', params)
|
||||
this._tempResourceDisposers.push('remote.delete', { id: remote.id })
|
||||
return remote
|
||||
}
|
||||
|
||||
async createTempServer(params) {
|
||||
const servers = await this.call('server.getAll')
|
||||
const server = servers.find(server => server.host === params.host)
|
||||
if (server !== undefined) {
|
||||
if (server.status === 'disconnected') {
|
||||
await this.call('server.enable', { id: server.id })
|
||||
this._durableResourceDisposers.push('server.disable', { id: server.id })
|
||||
await fromEvent(this._objects, 'finish')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const id = await this.call('server.add', {
|
||||
...params,
|
||||
allowUnauthorized: true,
|
||||
autoConnect: false,
|
||||
})
|
||||
this._durableResourceDisposers.push('server.remove', { id })
|
||||
await this.call('server.enable', { id })
|
||||
await fromEvent(this._objects, 'finish')
|
||||
}
|
||||
|
||||
async getSchedule(predicate) {
|
||||
return find(await this.call('schedule.getAll'), predicate)
|
||||
}
|
||||
|
||||
async runBackupJob(jobId, scheduleId, { remotes, nExecutions = 1 }) {
|
||||
for (let i = 0; i < nExecutions; i++) {
|
||||
await xo.call('backupNg.runJob', { id: jobId, schedule: scheduleId })
|
||||
}
|
||||
const backups = {}
|
||||
if (remotes !== undefined) {
|
||||
const backupsByRemote = await xo.call('backupNg.listVmBackups', {
|
||||
remotes,
|
||||
})
|
||||
forOwn(backupsByRemote, (backupsByVm, remoteId) => {
|
||||
backups[remoteId] = []
|
||||
forOwn(backupsByVm, vmBackups => {
|
||||
vmBackups.forEach(
|
||||
({ jobId: backupJobId, scheduleId: backupScheduleId, id }) => {
|
||||
if (jobId === backupJobId && scheduleId === backupScheduleId) {
|
||||
this._tempResourceDisposers.push('backupNg.deleteVmBackup', {
|
||||
id,
|
||||
})
|
||||
backups[remoteId].push(id)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
forOwn(this.objects.all, (obj, id) => {
|
||||
if (
|
||||
obj.other !== undefined &&
|
||||
obj.other['xo:backup:job'] === jobId &&
|
||||
obj.other['xo:backup:schedule'] === scheduleId
|
||||
) {
|
||||
this._tempResourceDisposers.push('vm.delete', {
|
||||
id,
|
||||
})
|
||||
}
|
||||
})
|
||||
return backups
|
||||
}
|
||||
|
||||
async _cleanDisposers(disposers) {
|
||||
for (let n = disposers.length - 1; n > 0; ) {
|
||||
const params = disposers[n--]
|
||||
const method = disposers[n--]
|
||||
await this.call(method, params).catch(error => {
|
||||
console.warn('deleteTempResources', method, params, error)
|
||||
})
|
||||
}
|
||||
disposers.length = 0
|
||||
}
|
||||
|
||||
async deleteTempResources() {
|
||||
await this._cleanDisposers(this._tempResourceDisposers)
|
||||
}
|
||||
|
||||
async deleteDurableResources() {
|
||||
await this._cleanDisposers(this._durableResourceDisposers)
|
||||
}
|
||||
}
|
||||
|
||||
const getConnection = credentials => {
|
||||
const xo = new XoConnection({ url: config.xoConnection.url })
|
||||
return xo.connect(credentials)
|
||||
}
|
||||
|
||||
let xo
|
||||
beforeAll(async () => {
|
||||
// TOFIX: stop tests if the connection is not established properly and show the error
|
||||
xo = await getConnection()
|
||||
})
|
||||
afterAll(async () => {
|
||||
await xo.deleteDurableResources()
|
||||
await xo.close()
|
||||
xo = null
|
||||
})
|
||||
afterEach(() => xo.deleteTempResources())
|
||||
|
||||
export { xo as default }
|
||||
|
||||
export const testConnection = ({ credentials }) =>
|
||||
getConnection(credentials).then(connection => connection.close())
|
||||
|
||||
export const testWithOtherConnection = defer(
|
||||
async ($defer, credentials, functionToExecute) => {
|
||||
const xoUser = await getConnection(credentials)
|
||||
$defer(() => xoUser.close())
|
||||
await functionToExecute(xoUser)
|
||||
}
|
||||
)
|
||||
@@ -1,539 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`backupNg .createJob() : creates a new backup job with schedules 1`] = `
|
||||
Object {
|
||||
"id": Any<String>,
|
||||
"mode": "full",
|
||||
"name": "default-backupNg",
|
||||
"settings": Any<Object>,
|
||||
"type": "backup",
|
||||
"userId": Any<String>,
|
||||
"vms": Any<Object>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .createJob() : creates a new backup job with schedules 2`] = `
|
||||
Object {
|
||||
"cron": "0 * * * * *",
|
||||
"enabled": false,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"name": "scheduleTest",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .createJob() : creates a new backup job without schedules 1`] = `
|
||||
Object {
|
||||
"id": Any<String>,
|
||||
"mode": "full",
|
||||
"name": "default-backupNg",
|
||||
"settings": Object {
|
||||
"": Object {
|
||||
"reportWhen": "never",
|
||||
},
|
||||
},
|
||||
"type": "backup",
|
||||
"userId": Any<String>,
|
||||
"vms": Any<Object>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run a backup job with a VM without disks 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "full",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"jobName": "default-backupNg",
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "skipped",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run a backup job with a VM without disks 2`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"type": "VM",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"message": "no disks found",
|
||||
"name": "Error",
|
||||
"stack": Any<String>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "skipped",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run a backup job with no matching VMs 1`] = `[JsonRpcError: unknown error from the peer]`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run a backup job with non-existent vm 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
"vms": Array [
|
||||
"non-existent-id",
|
||||
],
|
||||
},
|
||||
"message": "missingVms",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run a backup job without schedule 1`] = `[JsonRpcError: invalid parameters]`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run backup job without retentions 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "full",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"jobName": "default-backupNg",
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "failure",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run backup job without retentions 2`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"type": "VM",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"message": "copy, export and snapshot retentions cannot both be 0",
|
||||
"name": "Error",
|
||||
"stack": Any<String>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "failure",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "delta",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 2`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"type": "VM",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 3`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": "snapshot",
|
||||
"result": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 4`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"isFull": true,
|
||||
"type": "remote",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 5`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 6`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 7`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"isFull": true,
|
||||
"type": "remote",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 8`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 9`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 10`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "delta",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 11`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"type": "VM",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 12`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": "snapshot",
|
||||
"result": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 13`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"isFull": false,
|
||||
"type": "remote",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 14`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 15`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 16`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"isFull": false,
|
||||
"type": "remote",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 17`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 18`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 19`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "delta",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 20`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"type": "VM",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 21`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": "snapshot",
|
||||
"result": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 22`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"isFull": true,
|
||||
"type": "remote",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 23`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 24`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 25`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"isFull": true,
|
||||
"type": "remote",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 26`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 27`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a rolling snapshot with 2 as retention & revert to an old state 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "full",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"jobName": "default-backupNg",
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a rolling snapshot with 2 as retention & revert to an old state 2`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": "snapshot",
|
||||
"result": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a rolling snapshot with 2 as retention & revert to an old state 3`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"type": "VM",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
@@ -1,582 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { forOwn } from 'lodash'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
import config from '../_config'
|
||||
import randomId from '../_randomId'
|
||||
import xo from '../_xoConnection'
|
||||
|
||||
const DEFAULT_SCHEDULE = {
|
||||
name: 'scheduleTest',
|
||||
cron: '0 * * * * *',
|
||||
}
|
||||
|
||||
const validateRootTask = (log, props) =>
|
||||
expect(log).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
jobId: expect.any(String),
|
||||
scheduleId: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
...props,
|
||||
})
|
||||
|
||||
const validateVmTask = (task, vmId, props) => {
|
||||
expect(task).toMatchSnapshot({
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
},
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
message: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
...props,
|
||||
})
|
||||
expect(task.data.id).toBe(vmId)
|
||||
}
|
||||
|
||||
const validateSnapshotTask = (task, props) =>
|
||||
expect(task).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
result: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
...props,
|
||||
})
|
||||
|
||||
const validateExportTask = (task, srOrRemoteIds, props) => {
|
||||
expect(task).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
message: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
...props,
|
||||
})
|
||||
expect(srOrRemoteIds).toContain(task.data.id)
|
||||
}
|
||||
|
||||
const validateOperationTask = (task, props) => {
|
||||
expect(task).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
message: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
...props,
|
||||
})
|
||||
}
|
||||
|
||||
describe('backupNg', () => {
|
||||
let defaultBackupNg
|
||||
|
||||
beforeAll(() => {
|
||||
defaultBackupNg = {
|
||||
name: 'default-backupNg',
|
||||
mode: 'full',
|
||||
vms: {
|
||||
id: config.vms.default,
|
||||
},
|
||||
settings: {
|
||||
'': {
|
||||
reportWhen: 'never',
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('.createJob() :', () => {
|
||||
it('creates a new backup job without schedules', async () => {
|
||||
const backupNg = await xo.createTempBackupNgJob(defaultBackupNg)
|
||||
expect(backupNg).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
userId: expect.any(String),
|
||||
vms: expect.any(Object),
|
||||
})
|
||||
expect(backupNg.vms).toEqual(defaultBackupNg.vms)
|
||||
expect(backupNg.userId).toBe(xo._user.id)
|
||||
})
|
||||
|
||||
it('creates a new backup job with schedules', async () => {
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
},
|
||||
settings: {
|
||||
...defaultBackupNg.settings,
|
||||
[scheduleTempId]: { snapshotRetention: 1 },
|
||||
},
|
||||
})
|
||||
|
||||
const backupNgJob = await xo.call('backupNg.getJob', { id: jobId })
|
||||
|
||||
expect(backupNgJob).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
userId: expect.any(String),
|
||||
settings: expect.any(Object),
|
||||
vms: expect.any(Object),
|
||||
})
|
||||
expect(backupNgJob.vms).toEqual(defaultBackupNg.vms)
|
||||
expect(backupNgJob.userId).toBe(xo._user.id)
|
||||
|
||||
expect(Object.keys(backupNgJob.settings).length).toBe(2)
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
expect(backupNgJob.settings[schedule.id]).toEqual({
|
||||
snapshotRetention: 1,
|
||||
})
|
||||
|
||||
expect(schedule).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
jobId: expect.any(String),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.delete() :', () => {
|
||||
it('deletes a backup job', async () => {
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.call('backupNg.createJob', {
|
||||
...defaultBackupNg,
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
},
|
||||
settings: {
|
||||
...defaultBackupNg.settings,
|
||||
[scheduleTempId]: { snapshotRetention: 1 },
|
||||
},
|
||||
})
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
|
||||
await xo.call('backupNg.deleteJob', { id: jobId })
|
||||
|
||||
let isRejectedJobErrorValid = false
|
||||
await xo.call('backupNg.getJob', { id: jobId }).catch(error => {
|
||||
isRejectedJobErrorValid = noSuchObject.is(error)
|
||||
})
|
||||
expect(isRejectedJobErrorValid).toBe(true)
|
||||
|
||||
let isRejectedScheduleErrorValid = false
|
||||
await xo.call('schedule.get', { id: schedule.id }).catch(error => {
|
||||
isRejectedScheduleErrorValid = noSuchObject.is(error)
|
||||
})
|
||||
expect(isRejectedScheduleErrorValid).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.runJob() :', () => {
|
||||
it('fails trying to run a backup job without schedule', async () => {
|
||||
const { id } = await xo.createTempBackupNgJob(defaultBackupNg)
|
||||
await expect(xo.call('backupNg.runJob', { id })).rejects.toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('fails trying to run a backup job with no matching VMs', async () => {
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
},
|
||||
settings: {
|
||||
[scheduleTempId]: { snapshotRetention: 1 },
|
||||
},
|
||||
vms: {
|
||||
id: config.vms.default,
|
||||
name: 'test-vm-backupNg',
|
||||
},
|
||||
})
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
|
||||
await expect(
|
||||
xo.call('backupNg.runJob', { id: jobId, schedule: schedule.id })
|
||||
).rejects.toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('fails trying to run a backup job with non-existent vm', async () => {
|
||||
jest.setTimeout(7e3)
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
},
|
||||
settings: {
|
||||
[scheduleTempId]: { snapshotRetention: 1 },
|
||||
},
|
||||
vms: {
|
||||
id: 'non-existent-id',
|
||||
},
|
||||
})
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
|
||||
await xo.call('backupNg.runJob', { id: jobId, schedule: schedule.id })
|
||||
const [log] = await xo.call('backupNg.getLogs', {
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
expect(log.warnings).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('fails trying to run a backup job with a VM without disks', async () => {
|
||||
jest.setTimeout(8e3)
|
||||
await xo.createTempServer(config.servers.default)
|
||||
const vmIdWithoutDisks = await xo.createTempVm({
|
||||
name_label: 'XO Test Without Disks',
|
||||
name_description: 'Creating a vm without disks',
|
||||
template: config.templates.templateWithoutDisks,
|
||||
})
|
||||
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
},
|
||||
settings: {
|
||||
...defaultBackupNg.settings,
|
||||
[scheduleTempId]: { snapshotRetention: 1 },
|
||||
},
|
||||
vms: {
|
||||
id: vmIdWithoutDisks,
|
||||
},
|
||||
})
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
await xo.call('backupNg.runJob', { id: jobId, schedule: schedule.id })
|
||||
|
||||
const [
|
||||
{
|
||||
tasks: [vmTask],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
expect(log).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
jobId: expect.any(String),
|
||||
scheduleId: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
})
|
||||
|
||||
expect(vmTask).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
},
|
||||
id: expect.any(String),
|
||||
message: expect.any(String),
|
||||
result: {
|
||||
stack: expect.any(String),
|
||||
},
|
||||
start: expect.any(Number),
|
||||
})
|
||||
|
||||
expect(vmTask.data.id).toBe(vmIdWithoutDisks)
|
||||
})
|
||||
|
||||
it('fails trying to run backup job without retentions', async () => {
|
||||
jest.setTimeout(7e3)
|
||||
const scheduleTempId = randomId()
|
||||
await xo.createTempServer(config.servers.default)
|
||||
const { id: remoteId } = await xo.createTempRemote(config.remotes.default)
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
remotes: {
|
||||
id: remoteId,
|
||||
},
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
},
|
||||
settings: {
|
||||
...defaultBackupNg.settings,
|
||||
[scheduleTempId]: {},
|
||||
},
|
||||
srs: {
|
||||
id: config.srs.default,
|
||||
},
|
||||
})
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
await xo.call('backupNg.runJob', { id: jobId, schedule: schedule.id })
|
||||
|
||||
const [
|
||||
{
|
||||
tasks: [task],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
|
||||
expect(log).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
jobId: expect.any(String),
|
||||
scheduleId: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
})
|
||||
|
||||
expect(task).toMatchSnapshot({
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
},
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
message: expect.any(String),
|
||||
result: {
|
||||
stack: expect.any(String),
|
||||
},
|
||||
start: expect.any(Number),
|
||||
})
|
||||
expect(task.data.id).toBe(config.vms.default)
|
||||
})
|
||||
})
|
||||
|
||||
test('execute three times a rolling snapshot with 2 as retention & revert to an old state', async () => {
|
||||
jest.setTimeout(6e4)
|
||||
await xo.createTempServer(config.servers.default)
|
||||
const vmId = await xo.createTempVm({
|
||||
name_label: 'XO Test Temp',
|
||||
name_description: 'Creating a temporary vm',
|
||||
template: config.templates.default,
|
||||
VDIs: [
|
||||
{
|
||||
size: 1,
|
||||
SR: config.srs.default,
|
||||
type: 'user',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
vms: {
|
||||
id: vmId,
|
||||
},
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
},
|
||||
settings: {
|
||||
...defaultBackupNg.settings,
|
||||
[scheduleTempId]: { snapshotRetention: 2 },
|
||||
},
|
||||
})
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const oldSnapshots = xo.objects.all[vmId].snapshots
|
||||
await xo.call('backupNg.runJob', { id: jobId, schedule: schedule.id })
|
||||
await xo.waitObjectState(vmId, ({ snapshots }) => {
|
||||
// Test on updating snapshots.
|
||||
expect(snapshots).not.toEqual(oldSnapshots)
|
||||
})
|
||||
}
|
||||
|
||||
const { snapshots, videoram: oldVideoram } = xo.objects.all[vmId]
|
||||
|
||||
// Test on the retention, how many snapshots should be saved.
|
||||
expect(snapshots.length).toBe(2)
|
||||
|
||||
const newVideoram = 16
|
||||
await xo.call('vm.set', { id: vmId, videoram: newVideoram })
|
||||
await xo.waitObjectState(vmId, ({ videoram }) => {
|
||||
expect(videoram).toBe(newVideoram.toString())
|
||||
})
|
||||
|
||||
await xo.call('vm.revert', {
|
||||
snapshot: snapshots[0],
|
||||
})
|
||||
|
||||
await xo.waitObjectState(vmId, ({ videoram }) => {
|
||||
expect(videoram).toBe(oldVideoram)
|
||||
})
|
||||
|
||||
const [
|
||||
{
|
||||
tasks: [{ tasks: subTasks, ...vmTask }],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
|
||||
expect(log).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
jobId: expect.any(String),
|
||||
scheduleId: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
})
|
||||
|
||||
const subTaskSnapshot = subTasks.find(
|
||||
({ message }) => message === 'snapshot'
|
||||
)
|
||||
expect(subTaskSnapshot).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
result: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
})
|
||||
|
||||
expect(vmTask).toMatchSnapshot({
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
},
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
message: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
})
|
||||
expect(vmTask.data.id).toBe(vmId)
|
||||
})
|
||||
|
||||
test('execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval', async () => {
|
||||
jest.setTimeout(12e5)
|
||||
const {
|
||||
vms: { default: defaultVm, vmToBackup = defaultVm },
|
||||
remotes: { default: defaultRemote, remote1, remote2 = defaultRemote },
|
||||
servers: { default: defaultServer },
|
||||
} = config
|
||||
|
||||
expect(vmToBackup).not.toBe(undefined)
|
||||
expect(remote1).not.toBe(undefined)
|
||||
expect(remote2).not.toBe(undefined)
|
||||
|
||||
await xo.createTempServer(defaultServer)
|
||||
const { id: remoteId1 } = await xo.createTempRemote(remote1)
|
||||
const { id: remoteId2 } = await xo.createTempRemote(remote2)
|
||||
const remotes = [remoteId1, remoteId2]
|
||||
|
||||
const exportRetention = 2
|
||||
const fullInterval = 2
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
mode: 'delta',
|
||||
remotes: {
|
||||
id: {
|
||||
__or: remotes,
|
||||
},
|
||||
},
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
},
|
||||
settings: {
|
||||
'': {
|
||||
reportWhen: 'never',
|
||||
fullInterval,
|
||||
},
|
||||
[remoteId1]: { deleteFirst: true },
|
||||
[scheduleTempId]: { exportRetention },
|
||||
},
|
||||
vms: {
|
||||
id: vmToBackup,
|
||||
},
|
||||
})
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
|
||||
const nExecutions = 3
|
||||
const backupsByRemote = await xo.runBackupJob(jobId, schedule.id, {
|
||||
remotes,
|
||||
nExecutions,
|
||||
})
|
||||
forOwn(backupsByRemote, backups =>
|
||||
expect(backups.length).toBe(exportRetention)
|
||||
)
|
||||
|
||||
const backupLogs = await xo.call('backupNg.getLogs', {
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
expect(backupLogs.length).toBe(nExecutions)
|
||||
|
||||
backupLogs.forEach(({ tasks = [], ...log }, key) => {
|
||||
validateRootTask(log, {
|
||||
data: {
|
||||
mode: 'delta',
|
||||
reportWhen: 'never',
|
||||
},
|
||||
message: 'backup',
|
||||
status: 'success',
|
||||
})
|
||||
|
||||
const numberOfTasks = {
|
||||
export: 0,
|
||||
merge: 0,
|
||||
snapshot: 0,
|
||||
transfer: 0,
|
||||
vm: 0,
|
||||
}
|
||||
tasks.forEach(({ tasks = [], ...vmTask }) => {
|
||||
if (vmTask.data !== undefined && vmTask.data.type === 'VM') {
|
||||
validateVmTask(vmTask, vmToBackup, { status: 'success' })
|
||||
numberOfTasks.vm++
|
||||
tasks.forEach(({ tasks = [], ...subTask }) => {
|
||||
if (subTask.message === 'snapshot') {
|
||||
validateSnapshotTask(subTask, { status: 'success' })
|
||||
numberOfTasks.snapshot++
|
||||
}
|
||||
if (subTask.message === 'export') {
|
||||
validateExportTask(subTask, remotes, {
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
isFull: key % fullInterval === 0,
|
||||
type: 'remote',
|
||||
},
|
||||
status: 'success',
|
||||
})
|
||||
numberOfTasks.export++
|
||||
let mergeTaskKey, transferTaskKey
|
||||
tasks.forEach((operationTask, key) => {
|
||||
if (
|
||||
operationTask.message === 'transfer' ||
|
||||
operationTask.message === 'merge'
|
||||
) {
|
||||
validateOperationTask(operationTask, {
|
||||
result: { size: expect.any(Number) },
|
||||
status: 'success',
|
||||
})
|
||||
if (operationTask.message === 'transfer') {
|
||||
mergeTaskKey = key
|
||||
numberOfTasks.merge++
|
||||
} else {
|
||||
transferTaskKey = key
|
||||
numberOfTasks.transfer++
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(
|
||||
subTask.data.id === remoteId1
|
||||
? mergeTaskKey > transferTaskKey
|
||||
: mergeTaskKey < transferTaskKey
|
||||
).toBe(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
expect(numberOfTasks).toEqual({
|
||||
export: 2,
|
||||
merge: 2,
|
||||
snapshot: 1,
|
||||
transfer: 2,
|
||||
vm: 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,76 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`job .create() : creates a new job 1`] = `
|
||||
Object {
|
||||
"id": Any<String>,
|
||||
"key": "snapshot",
|
||||
"method": "vm.snapshot",
|
||||
"name": "jobTest",
|
||||
"paramsVector": Any<Object>,
|
||||
"timeout": 2000,
|
||||
"type": "call",
|
||||
"userId": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`job .create() : fails trying to create a job without job params 1`] = `[JsonRpcError: invalid parameters]`;
|
||||
|
||||
exports[`job .delete() : deletes an existing job 1`] = `[JsonRpcError: no such job [object Object]]`;
|
||||
|
||||
exports[`job .delete() : deletes an existing job 2`] = `[JsonRpcError: no such schedule [object Object]]`;
|
||||
|
||||
exports[`job .get() : fails trying to get a job with a non existent id 1`] = `[JsonRpcError: no such job [object Object]]`;
|
||||
|
||||
exports[`job .get() : gets an existing job 1`] = `
|
||||
Object {
|
||||
"id": Any<String>,
|
||||
"key": "snapshot",
|
||||
"method": "vm.snapshot",
|
||||
"name": "jobTest",
|
||||
"paramsVector": Any<Object>,
|
||||
"timeout": 2000,
|
||||
"type": "call",
|
||||
"userId": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`job .getAll() : gets all available jobs 1`] = `
|
||||
Object {
|
||||
"id": Any<String>,
|
||||
"key": "snapshot",
|
||||
"method": "vm.snapshot",
|
||||
"name": "jobTest",
|
||||
"paramsVector": Any<Object>,
|
||||
"timeout": 2000,
|
||||
"type": "call",
|
||||
"userId": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`job .getAll() : gets all available jobs 2`] = `
|
||||
Object {
|
||||
"id": Any<String>,
|
||||
"key": "snapshot",
|
||||
"method": "vm.snapshot",
|
||||
"name": "jobTest2",
|
||||
"paramsVector": Any<Object>,
|
||||
"timeout": 2000,
|
||||
"type": "call",
|
||||
"userId": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`job .set() : fails trying to set a job without job.id 1`] = `[JsonRpcError: invalid parameters]`;
|
||||
|
||||
exports[`job .set() : sets a job 1`] = `
|
||||
Object {
|
||||
"id": Any<String>,
|
||||
"key": "snapshot",
|
||||
"method": "vm.clone",
|
||||
"name": "jobTest",
|
||||
"paramsVector": Any<Object>,
|
||||
"timeout": 2000,
|
||||
"type": "call",
|
||||
"userId": Any<String>,
|
||||
}
|
||||
`;
|
||||
@@ -1,226 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { difference, keyBy } from 'lodash'
|
||||
|
||||
import config from '../_config'
|
||||
import xo, { testWithOtherConnection } from '../_xoConnection'
|
||||
|
||||
const ADMIN_USER = {
|
||||
email: 'admin2@admin.net',
|
||||
password: 'admin',
|
||||
permission: 'admin',
|
||||
}
|
||||
|
||||
describe('job', () => {
|
||||
let defaultJob
|
||||
|
||||
beforeAll(() => {
|
||||
defaultJob = {
|
||||
name: 'jobTest',
|
||||
timeout: 2000,
|
||||
type: 'call',
|
||||
key: 'snapshot',
|
||||
method: 'vm.snapshot',
|
||||
paramsVector: {
|
||||
type: 'crossProduct',
|
||||
items: [
|
||||
{
|
||||
type: 'set',
|
||||
values: [
|
||||
{
|
||||
id: config.vms.default,
|
||||
name: 'test-snapshot',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('.create() :', () => {
|
||||
it('creates a new job', async () => {
|
||||
jest.setTimeout(6e3)
|
||||
const userId = await xo.createTempUser(ADMIN_USER)
|
||||
const { email, password } = ADMIN_USER
|
||||
await testWithOtherConnection({ email, password }, async xo => {
|
||||
const id = await xo.call('job.create', { job: defaultJob })
|
||||
expect(typeof id).toBe('string')
|
||||
|
||||
const job = await xo.call('job.get', { id })
|
||||
expect(job).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
paramsVector: expect.any(Object),
|
||||
userId: expect.any(String),
|
||||
})
|
||||
expect(job.paramsVector).toEqual(defaultJob.paramsVector)
|
||||
expect(job.userId).toBe(userId)
|
||||
await xo.call('job.delete', { id })
|
||||
})
|
||||
})
|
||||
|
||||
it('creates a job with a userId', async () => {
|
||||
const userId = await xo.createTempUser(ADMIN_USER)
|
||||
const id = await xo.createTempJob({ ...defaultJob, userId })
|
||||
const { userId: expectedUserId } = await xo.call('job.get', { id })
|
||||
expect(userId).toBe(expectedUserId)
|
||||
})
|
||||
|
||||
it('fails trying to create a job without job params', async () => {
|
||||
await expect(xo.createTempJob({})).rejects.toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('.getAll() :', () => {
|
||||
it('gets all available jobs', async () => {
|
||||
const jobId1 = await xo.createTempJob(defaultJob)
|
||||
const job2 = {
|
||||
...defaultJob,
|
||||
name: 'jobTest2',
|
||||
paramsVector: {
|
||||
type: 'crossProduct',
|
||||
items: [
|
||||
{
|
||||
type: 'set',
|
||||
values: [
|
||||
{
|
||||
id: config.vms.default,
|
||||
name: 'test2-snapshot',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
const jobId2 = await xo.createTempJob(job2)
|
||||
let jobs = await xo.call('job.getAll')
|
||||
expect(Array.isArray(jobs)).toBe(true)
|
||||
jobs = keyBy(jobs, 'id')
|
||||
|
||||
const newJob1 = jobs[jobId1]
|
||||
expect(newJob1).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
paramsVector: expect.any(Object),
|
||||
userId: expect.any(String),
|
||||
})
|
||||
expect(newJob1.paramsVector).toEqual(defaultJob.paramsVector)
|
||||
|
||||
const newJob2 = jobs[jobId2]
|
||||
expect(newJob2).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
paramsVector: expect.any(Object),
|
||||
userId: expect.any(String),
|
||||
})
|
||||
expect(newJob2.paramsVector).toEqual(job2.paramsVector)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.get() :', () => {
|
||||
it('gets an existing job', async () => {
|
||||
const id = await xo.createTempJob(defaultJob)
|
||||
const job = await xo.call('job.get', { id })
|
||||
expect(job).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
paramsVector: expect.any(Object),
|
||||
userId: expect.any(String),
|
||||
})
|
||||
expect(job.paramsVector).toEqual(defaultJob.paramsVector)
|
||||
})
|
||||
|
||||
it('fails trying to get a job with a non existent id', async () => {
|
||||
await expect(
|
||||
xo.call('job.get', { id: 'non-existent-id' })
|
||||
).rejects.toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('.set() :', () => {
|
||||
it('sets a job', async () => {
|
||||
const id = await xo.createTempJob(defaultJob)
|
||||
const job = {
|
||||
id,
|
||||
type: 'call',
|
||||
key: 'snapshot',
|
||||
method: 'vm.clone',
|
||||
paramsVector: {
|
||||
type: 'crossProduct',
|
||||
items: [
|
||||
{
|
||||
type: 'set',
|
||||
values: [
|
||||
{
|
||||
id: config.vms.default,
|
||||
name: 'clone',
|
||||
full_copy: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
await xo.call('job.set', {
|
||||
job,
|
||||
})
|
||||
|
||||
const newJob = await xo.call('job.get', { id })
|
||||
expect(newJob).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
paramsVector: expect.any(Object),
|
||||
userId: expect.any(String),
|
||||
})
|
||||
expect(newJob.paramsVector).toEqual(job.paramsVector)
|
||||
})
|
||||
|
||||
it('fails trying to set a job without job.id', async () => {
|
||||
await expect(xo.call('job.set', defaultJob)).rejects.toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('.delete() :', () => {
|
||||
it('deletes an existing job', async () => {
|
||||
const id = await xo.call('job.create', { job: defaultJob })
|
||||
const { id: scheduleId } = await xo.call('schedule.create', {
|
||||
jobId: id,
|
||||
cron: '* * * * * *',
|
||||
enabled: false,
|
||||
})
|
||||
await xo.call('job.delete', { id })
|
||||
await expect(xo.call('job.get', { id })).rejects.toMatchSnapshot()
|
||||
await expect(
|
||||
xo.call('schedule.get', { id: scheduleId })
|
||||
).rejects.toMatchSnapshot()
|
||||
})
|
||||
|
||||
it.skip('fails trying to delete a job with a non existent id', async () => {
|
||||
await expect(
|
||||
xo.call('job.delete', { id: 'non-existent-id' })
|
||||
).rejects.toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('.runSequence() :', () => {
|
||||
let id
|
||||
|
||||
afterEach(async () => {
|
||||
await xo
|
||||
.call('vm.delete', { id, deleteDisks: true })
|
||||
.catch(error => console.error(error))
|
||||
})
|
||||
|
||||
it('runs a job', async () => {
|
||||
jest.setTimeout(7e4)
|
||||
await xo.createTempServer(config.servers.default)
|
||||
const jobId = await xo.createTempJob(defaultJob)
|
||||
const snapshots = xo.objects.all[config.vms.default].snapshots
|
||||
await xo.call('job.runSequence', { idSequence: [jobId] })
|
||||
await xo.waitObjectState(
|
||||
config.vms.default,
|
||||
({ snapshots: actualSnapshots }) => {
|
||||
expect(actualSnapshots.length).toBe(snapshots.length + 1)
|
||||
id = difference(actualSnapshots, snapshots)[0]
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,156 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
|
||||
import expect from 'must'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
import { getConfig, getMainConnection, getSrId, waitObjectState } from './util'
|
||||
import { map, assign } from 'lodash'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('disk', () => {
|
||||
let diskId
|
||||
let diskIds = []
|
||||
let serverId
|
||||
let srId
|
||||
let xo
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setTimeout(10e3)
|
||||
xo = await getMainConnection()
|
||||
|
||||
const config = await getConfig()
|
||||
serverId = await xo.call(
|
||||
'server.add',
|
||||
assign({ autoConnect: false }, config.xenServer1)
|
||||
)
|
||||
await xo.call('server.connect', { id: serverId })
|
||||
await eventToPromise(xo.objects, 'finish')
|
||||
srId = await getSrId(xo)
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
map(diskIds, diskId => xo.call('vdi.delete', { id: diskId }))
|
||||
)
|
||||
diskIds = []
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
afterAll(async () => {
|
||||
await xo.call('server.remove', { id: serverId })
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async function createDisk(params) {
|
||||
const id = await xo.call('disk.create', params)
|
||||
diskIds.push(id)
|
||||
return id
|
||||
}
|
||||
|
||||
async function createDiskTest() {
|
||||
const id = await createDisk({
|
||||
name: 'diskTest',
|
||||
size: '1GB',
|
||||
sr: srId,
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('.create()', () => {
|
||||
it('create a new disk on a SR', async () => {
|
||||
diskId = await createDisk({
|
||||
name: 'diskTest',
|
||||
size: '1GB',
|
||||
sr: srId,
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
waitObjectState(xo, diskId, disk => {
|
||||
expect(disk.type).to.be.equal('VDI')
|
||||
expect(disk.name_label).to.be.equal('diskTest')
|
||||
// TODO: should not test an exact value but around 10%
|
||||
expect(disk.size).to.be.equal(1000341504)
|
||||
expect(disk.$SR).to.be.equal(srId)
|
||||
}),
|
||||
waitObjectState(xo, srId, sr => {
|
||||
expect(sr.VDIs).include(diskId)
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('.delete()', () => {
|
||||
beforeEach(async () => {
|
||||
diskId = await createDiskTest()
|
||||
})
|
||||
|
||||
it('deletes a disk', async () => {
|
||||
await Promise.all([
|
||||
xo.call('vdi.delete', { id: diskId }),
|
||||
waitObjectState(xo, diskId, disk => {
|
||||
expect(disk).to.be.undefined()
|
||||
}),
|
||||
waitObjectState(xo, srId, sr => {
|
||||
expect(sr.VDIs).not.include(diskId)
|
||||
}),
|
||||
])
|
||||
diskIds = []
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
describe('.set()', () => {
|
||||
beforeEach(async () => {
|
||||
diskId = await createDiskTest()
|
||||
})
|
||||
|
||||
it('set the name of the disk', async () => {
|
||||
await xo.call('vdi.set', {
|
||||
id: diskId,
|
||||
name_label: 'disk2',
|
||||
})
|
||||
|
||||
await waitObjectState(xo, diskId, disk => {
|
||||
expect(disk.name_label).to.be.equal('disk2')
|
||||
})
|
||||
})
|
||||
|
||||
it('set the description of the disk', async () => {
|
||||
await xo.call('vdi.set', {
|
||||
id: diskId,
|
||||
name_description: 'description',
|
||||
})
|
||||
|
||||
await waitObjectState(xo, diskId, disk => {
|
||||
expect(disk.name_description).to.be.equal('description')
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('set the size of the disk', async () => {
|
||||
await xo.getOrWaitObject(diskId)
|
||||
await xo.call('vdi.set', {
|
||||
id: diskId,
|
||||
size: '5MB',
|
||||
})
|
||||
|
||||
await waitObjectState(xo, diskId, disk => {
|
||||
expect(disk.size).to.be.equal(6291456)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,59 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
|
||||
// import expect from 'must'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// import {getConnection} from './util'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('docker', () => {
|
||||
// let xo
|
||||
// beforeAll(async () => {
|
||||
// xo = await getConnection()
|
||||
// })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('.register()', async () => {
|
||||
it('registers the VM for Docker management')
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('.deregister()', async () => {
|
||||
it('deregister the VM for Docker management')
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('.start()', async () => {
|
||||
it('starts the Docker')
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('.stop()', async () => {
|
||||
it('stops the Docker')
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('.restart()', async () => {
|
||||
it('restarts the Docker')
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('.pause()', async () => {
|
||||
it('pauses the Docker')
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('.unpause()', async () => {
|
||||
it('unpauses the Docker')
|
||||
})
|
||||
})
|
||||
@@ -1,377 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { find, map } from 'lodash'
|
||||
|
||||
import { createUser, deleteUsers, getUser, xo } from './util.js'
|
||||
|
||||
// ===================================================================
|
||||
describe('group', () => {
|
||||
const userIds = []
|
||||
const groupIds = []
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(map(groupIds, id => xo.call('group.delete', { id })))
|
||||
// Deleting users must be done AFTER deleting the group
|
||||
// because there is a race condition in xo-server
|
||||
// which cause some users to not be properly deleted.
|
||||
|
||||
// The test “delete the group with its users” highlight this issue.
|
||||
await deleteUsers(xo, userIds)
|
||||
userIds.length = groupIds.length = 0
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async function createGroup(params) {
|
||||
const groupId = await xo.call('group.create', params)
|
||||
groupIds.push(groupId)
|
||||
return groupId
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
function compareGroup(actual, expected) {
|
||||
expect(actual.name).toEqual(expected.name)
|
||||
expect(actual.id).toEqual(expected.id)
|
||||
expect(actual.users).toEqual(expected.users)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
function getAllGroups() {
|
||||
return xo.call('group.getAll')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
async function getGroup(id) {
|
||||
const groups = await getAllGroups()
|
||||
return find(groups, { id: id })
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
describe('.create()', () => {
|
||||
it('creates a group and return its id', async () => {
|
||||
const groupId = await createGroup({
|
||||
name: 'Avengers',
|
||||
})
|
||||
const group = await getGroup(groupId)
|
||||
compareGroup(group, {
|
||||
id: groupId,
|
||||
name: 'Avengers',
|
||||
users: [],
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('does not create two groups with the same name', async () => {
|
||||
await createGroup({
|
||||
name: 'Avengers',
|
||||
})
|
||||
|
||||
await createGroup({
|
||||
name: 'Avengers',
|
||||
}).then(
|
||||
() => {
|
||||
throw new Error('createGroup() should have thrown')
|
||||
},
|
||||
function(error) {
|
||||
expect(error.message).to.match(/duplicate group/i)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
describe('.delete()', () => {
|
||||
let groupId
|
||||
let userId1
|
||||
let userId2
|
||||
let userId3
|
||||
beforeEach(async () => {
|
||||
groupId = await xo.call('group.create', {
|
||||
name: 'Avengers',
|
||||
})
|
||||
})
|
||||
it('delete a group', async () => {
|
||||
await xo.call('group.delete', {
|
||||
id: groupId,
|
||||
})
|
||||
const group = await getGroup(groupId)
|
||||
expect(group).toBeUndefined()
|
||||
})
|
||||
|
||||
it.skip("erase the group from user's groups list", async () => {
|
||||
// create user and add it to the group
|
||||
const userId = await createUser(xo, userIds, {
|
||||
email: 'tony.stark@stark_industry.com',
|
||||
password: 'IronMan',
|
||||
})
|
||||
await xo.call('group.addUser', {
|
||||
id: groupId,
|
||||
userId: userId,
|
||||
})
|
||||
|
||||
// delete the group
|
||||
await xo.call('group.delete', { id: groupId })
|
||||
const user = await getUser(userId)
|
||||
expect(user.groups).toEqual([])
|
||||
})
|
||||
|
||||
it.skip("erase the user from group's users list", async () => {
|
||||
// create user and add it to the group
|
||||
const userId = await createUser(xo, userIds, {
|
||||
email: 'tony.stark@stark_industry.com',
|
||||
password: 'IronMan',
|
||||
})
|
||||
await xo.call('group.addUser', {
|
||||
id: groupId,
|
||||
userId: userId,
|
||||
})
|
||||
|
||||
// delete the group
|
||||
await xo.call('user.delete', { id: userId })
|
||||
const group = await getGroup(groupId)
|
||||
expect(group.users).toEqual([])
|
||||
})
|
||||
|
||||
// FIXME: some users are not properly deleted because of a race condition with group deletion.
|
||||
it.skip('delete the group with its users', async () => {
|
||||
// create users
|
||||
;[userId1, userId2, userId3] = await Promise.all([
|
||||
xo.call('user.create', {
|
||||
email: 'tony.stark@stark_industry.com',
|
||||
password: 'IronMan',
|
||||
}),
|
||||
xo.call('user.create', {
|
||||
email: 'natasha.romanov@shield.com',
|
||||
password: 'BlackWidow',
|
||||
}),
|
||||
xo.call('user.create', {
|
||||
email: 'pietro.maximoff@shield.com',
|
||||
password: 'QickSilver',
|
||||
}),
|
||||
])
|
||||
|
||||
await xo.call('group.setUsers', {
|
||||
id: groupId,
|
||||
userIds: [userId1, userId2, userId3],
|
||||
})
|
||||
|
||||
// delete the group with his users
|
||||
await Promise.all([
|
||||
xo.call('group.delete', {
|
||||
id: groupId,
|
||||
}),
|
||||
deleteUsers(xo, [userId1, userId2, userId3]),
|
||||
])
|
||||
|
||||
const [group, user1, user2, user3] = await Promise.all([
|
||||
getGroup(groupId),
|
||||
getUser(xo, userId1),
|
||||
getUser(xo, userId2),
|
||||
getUser(xo, userId3),
|
||||
])
|
||||
|
||||
expect(group).toBeUndefined()
|
||||
expect(user1).toBeUndefined()
|
||||
expect(user2).toBeUndefined()
|
||||
expect(user3).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('.getAll()', () => {
|
||||
it('returns an array', async () => {
|
||||
const groups = await xo.call('group.getAll')
|
||||
expect(groups).toBeInstanceOf(Array)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('.setUsers ()', () => {
|
||||
let groupId
|
||||
let userId1
|
||||
let userId2
|
||||
let userId3
|
||||
beforeEach(async () => {
|
||||
;[groupId, userId1, userId2, userId3] = await Promise.all([
|
||||
createGroup({
|
||||
name: 'Avengers',
|
||||
}),
|
||||
createUser(xo, userIds, {
|
||||
email: 'tony.stark@stark_industry.com',
|
||||
password: 'IronMan',
|
||||
}),
|
||||
createUser(xo, userIds, {
|
||||
email: 'natasha.romanov@shield.com',
|
||||
password: 'BlackWidow',
|
||||
}),
|
||||
createUser(xo, userIds, {
|
||||
email: 'pietro.maximoff@shield.com',
|
||||
password: 'QickSilver',
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it('can set users of a group', async () => {
|
||||
// add two users on the group
|
||||
await xo.call('group.setUsers', {
|
||||
id: groupId,
|
||||
userIds: [userId1, userId2],
|
||||
})
|
||||
{
|
||||
const [group, user1, user2, user3] = await Promise.all([
|
||||
getGroup(groupId),
|
||||
getUser(xo, userId1),
|
||||
getUser(xo, userId2),
|
||||
getUser(xo, userId3),
|
||||
])
|
||||
compareGroup(group, {
|
||||
id: groupId,
|
||||
name: 'Avengers',
|
||||
users: [userId1, userId2],
|
||||
})
|
||||
|
||||
expect(user1.groups).toEqual([groupId])
|
||||
expect(user2.groups).toEqual([groupId])
|
||||
expect(user3.groups).toEqual([])
|
||||
}
|
||||
|
||||
// change users of the group
|
||||
await xo.call('group.setUsers', {
|
||||
id: groupId,
|
||||
userIds: [userId1, userId3],
|
||||
})
|
||||
{
|
||||
const [group, user1, user2, user3] = await Promise.all([
|
||||
getGroup(groupId),
|
||||
getUser(xo, userId1),
|
||||
getUser(xo, userId2),
|
||||
getUser(xo, userId3),
|
||||
])
|
||||
|
||||
compareGroup(group, {
|
||||
id: groupId,
|
||||
name: 'Avengers',
|
||||
users: [userId1, userId3],
|
||||
})
|
||||
|
||||
expect(user1.groups).toEqual([groupId])
|
||||
expect(user2.groups).toEqual([])
|
||||
expect(user3.groups).toEqual([groupId])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('.addUser()', () => {
|
||||
let groupId
|
||||
let userId
|
||||
beforeEach(async () => {
|
||||
;[groupId, userId] = await Promise.all([
|
||||
createGroup({
|
||||
name: 'Avengers',
|
||||
}),
|
||||
createUser(xo, userIds, {
|
||||
email: 'tony.stark@stark_industry.com',
|
||||
password: 'IronMan',
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it('adds a user id to a group', async () => {
|
||||
await xo.call('group.addUser', {
|
||||
id: groupId,
|
||||
userId: userId,
|
||||
})
|
||||
|
||||
const [group, user] = await Promise.all([
|
||||
getGroup(groupId),
|
||||
getUser(xo, userId),
|
||||
])
|
||||
|
||||
compareGroup(group, {
|
||||
id: groupId,
|
||||
name: 'Avengers',
|
||||
users: [userId],
|
||||
})
|
||||
|
||||
expect(user.groups).toEqual([groupId])
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('removeUser()', () => {
|
||||
let groupId
|
||||
let userId
|
||||
beforeEach(async () => {
|
||||
;[groupId, userId] = await Promise.all([
|
||||
createGroup({
|
||||
name: 'Avengers',
|
||||
}),
|
||||
createUser(xo, userIds, {
|
||||
email: 'tony.stark@stark_industry.com',
|
||||
password: 'IronMan',
|
||||
}),
|
||||
])
|
||||
|
||||
await xo.call('group.addUser', {
|
||||
id: groupId,
|
||||
userId: userId,
|
||||
})
|
||||
})
|
||||
|
||||
it('removes a user to a group', async () => {
|
||||
await xo.call('group.removeUser', {
|
||||
id: groupId,
|
||||
userId: userId,
|
||||
})
|
||||
|
||||
const [group, user] = await Promise.all([
|
||||
getGroup(groupId),
|
||||
getUser(xo, userId),
|
||||
])
|
||||
|
||||
compareGroup(group, {
|
||||
id: groupId,
|
||||
name: 'Avengers',
|
||||
users: [],
|
||||
})
|
||||
|
||||
expect(user.groups).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('set()', () => {
|
||||
let groupId
|
||||
beforeEach(async () => {
|
||||
groupId = await createGroup({
|
||||
name: 'Avengers',
|
||||
})
|
||||
})
|
||||
|
||||
it('changes name of a group', async () => {
|
||||
await xo.call('group.set', {
|
||||
id: groupId,
|
||||
name: 'Guardians of the Galaxy',
|
||||
})
|
||||
|
||||
const group = await getGroup(groupId)
|
||||
compareGroup(group, {
|
||||
id: groupId,
|
||||
name: 'Guardians of the Galaxy',
|
||||
users: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,239 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
|
||||
|
||||
import expect from 'must'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import {
|
||||
getAllHosts,
|
||||
getConfig,
|
||||
getMainConnection,
|
||||
getVmToMigrateId,
|
||||
waitObjectState,
|
||||
} from './util'
|
||||
import { find, forEach } from 'lodash'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('host', () => {
|
||||
let xo
|
||||
let serverId
|
||||
let hostId
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setTimeout(10e3)
|
||||
let config
|
||||
;[xo, config] = await Promise.all([getMainConnection(), getConfig()])
|
||||
serverId = await xo.call('server.add', config.xenServer2).catch(() => {})
|
||||
await eventToPromise(xo.objects, 'finish')
|
||||
|
||||
hostId = getHost(config.host1)
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
afterAll(async () => {
|
||||
await xo.call('server.remove', {
|
||||
id: serverId,
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
function getHost(nameLabel) {
|
||||
const hosts = getAllHosts(xo)
|
||||
const host = find(hosts, { name_label: nameLabel })
|
||||
return host.id
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('.set()', () => {
|
||||
let nameLabel
|
||||
let nameDescription
|
||||
|
||||
beforeEach(async () => {
|
||||
// get values to set them at the end of the test
|
||||
const host = xo.objects.all[hostId]
|
||||
nameLabel = host.name_label
|
||||
nameDescription = host.name_description
|
||||
})
|
||||
afterEach(async () => {
|
||||
await xo.call('host.set', {
|
||||
id: hostId,
|
||||
name_label: nameLabel,
|
||||
name_description: nameDescription,
|
||||
})
|
||||
})
|
||||
|
||||
it('changes properties of the host', async () => {
|
||||
await xo.call('host.set', {
|
||||
id: hostId,
|
||||
name_label: 'labTest',
|
||||
name_description: 'description',
|
||||
})
|
||||
await waitObjectState(xo, hostId, host => {
|
||||
expect(host.name_label).to.be.equal('labTest')
|
||||
expect(host.name_description).to.be.equal('description')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
describe('.restart()', () => {
|
||||
jest.setTimeout(330e3)
|
||||
it('restart the host', async () => {
|
||||
await xo.call('host.restart', { id: hostId })
|
||||
|
||||
await waitObjectState(xo, hostId, host => {
|
||||
expect(host.current_operations)
|
||||
})
|
||||
await waitObjectState(xo, hostId, host => {
|
||||
expect(host.power_state).to.be.equal('Halted')
|
||||
})
|
||||
await waitObjectState(xo, hostId, host => {
|
||||
expect(host.power_state).to.be.equal('Running')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
describe('.restartAgent()', () => {
|
||||
it('restart a Xen agent on the host')
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
describe('.start()', () => {
|
||||
jest.setTimeout(300e3)
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
await xo.call('host.stop', { id: hostId })
|
||||
} catch (_) {}
|
||||
|
||||
// test if the host is shutdown
|
||||
await waitObjectState(xo, hostId, host => {
|
||||
expect(host.power_state).to.be.equal('Halted')
|
||||
})
|
||||
})
|
||||
|
||||
it('start the host', async () => {
|
||||
await xo.call('host.start', { id: hostId })
|
||||
await waitObjectState(xo, hostId, host => {
|
||||
expect(host.power_state).to.be.equal('Running')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
describe('.stop()', () => {
|
||||
jest.setTimeout(300e3)
|
||||
let vmId
|
||||
|
||||
beforeAll(async () => {
|
||||
vmId = await getVmToMigrateId(xo)
|
||||
try {
|
||||
await xo.call('vm.start', { id: vmId })
|
||||
} catch (_) {}
|
||||
try {
|
||||
await xo.call('vm.migrate', {
|
||||
vm: vmId,
|
||||
host: hostId,
|
||||
})
|
||||
} catch (_) {}
|
||||
})
|
||||
afterEach(async () => {
|
||||
await xo.call('host.start', { id: hostId })
|
||||
})
|
||||
|
||||
it('stop the host and shutdown its VMs', async () => {
|
||||
await xo.call('host.stop', { id: hostId })
|
||||
await Promise.all([
|
||||
waitObjectState(xo, vmId, vm => {
|
||||
expect(vm.$container).not.to.be.equal(hostId)
|
||||
expect(vm.power_state).to.be.equal('Halted')
|
||||
}),
|
||||
waitObjectState(xo, hostId, host => {
|
||||
expect(host.power_state).to.be.equal('Halted')
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
describe('.detach()', () => {
|
||||
it('ejects the host of a pool')
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
describe('.disable(), ', () => {
|
||||
afterEach(async () => {
|
||||
await xo.call('host.enable', {
|
||||
id: hostId,
|
||||
})
|
||||
})
|
||||
|
||||
it('disables to create VM on the host', async () => {
|
||||
await xo.call('host.disable', { id: hostId })
|
||||
await waitObjectState(xo, hostId, host => {
|
||||
expect(host.enabled).to.be.false()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
describe('.enable()', async () => {
|
||||
beforeEach(async () => {
|
||||
await xo.call('host.disable', { id: hostId })
|
||||
})
|
||||
|
||||
it('enables to create VM on the host', async () => {
|
||||
await xo.call('host.enable', { id: hostId })
|
||||
|
||||
await waitObjectState(xo, hostId, host => {
|
||||
expect(host.enabled).to.be.true()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
describe('.createNetwork()', () => {
|
||||
it('create a network')
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
describe('.listMissingPatches()', () => {
|
||||
it('returns an array of missing patches in the host')
|
||||
it('returns a empty array if up-to-date')
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
describe('.installPatch()', () => {
|
||||
it('installs a patch patch on the host')
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
describe('.stats()', () => {
|
||||
it('returns an array with statistics of the host', async () => {
|
||||
const stats = await xo.call('host.stats', {
|
||||
host: hostId,
|
||||
})
|
||||
expect(stats).to.be.an.object()
|
||||
|
||||
forEach(stats, function(array, key) {
|
||||
expect(array).to.be.an.array()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,79 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
|
||||
import expect from 'must'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
import { getConfig, getMainConnection, waitObjectState } from './util'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import { find } from 'lodash'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('pool', () => {
|
||||
let xo
|
||||
let serverId
|
||||
let poolId
|
||||
let config
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setTimeout(10e3)
|
||||
;[xo, config] = await Promise.all([getMainConnection(), getConfig()])
|
||||
serverId = await xo.call('server.add', config.xenServer1).catch(() => {})
|
||||
await eventToPromise(xo.objects, 'finish')
|
||||
poolId = getPoolId()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
afterAll(async () => {
|
||||
await xo.call('server.remove', {
|
||||
id: serverId,
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
function getPoolId() {
|
||||
const pools = xo.objects.indexes.type.pool
|
||||
const pool = find(pools, { name_label: config.pool.name_label })
|
||||
return pool.id
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('.set()', () => {
|
||||
afterEach(async () => {
|
||||
await xo.call('pool.set', {
|
||||
id: poolId,
|
||||
name_label: config.pool.name_label,
|
||||
name_description: '',
|
||||
})
|
||||
})
|
||||
it.skip('set pool parameters', async () => {
|
||||
await xo.call('pool.set', {
|
||||
id: poolId,
|
||||
name_label: 'nameTest',
|
||||
name_description: 'description',
|
||||
})
|
||||
|
||||
await waitObjectState(xo, poolId, pool => {
|
||||
expect(pool.name_label).to.be.equal('nameTest')
|
||||
expect(pool.name_description).to.be.equal('description')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
describe('.installPatch()', () => {
|
||||
it('install a patch on the pool')
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
describe('handlePatchUpload()', () => {
|
||||
it('')
|
||||
})
|
||||
})
|
||||
@@ -1,33 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { xo } from './util'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('role', () => {
|
||||
describe('.getAll()', () => {
|
||||
it(' returns all the roles', async () => {
|
||||
const role = await xo.call('role.getAll')
|
||||
|
||||
// FIXME: use permutationOf but figure out how not to compare objects by
|
||||
// equality.
|
||||
expect(role).toEqual([
|
||||
{
|
||||
id: 'viewer',
|
||||
name: 'Viewer',
|
||||
permissions: ['view'],
|
||||
},
|
||||
{
|
||||
id: 'operator',
|
||||
name: 'Operator',
|
||||
permissions: ['view', 'operate'],
|
||||
},
|
||||
{
|
||||
id: 'admin',
|
||||
name: 'Admin',
|
||||
permissions: ['view', 'operate', 'administrate'],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,149 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
|
||||
import expect from 'must'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
import {
|
||||
getConfig,
|
||||
getMainConnection,
|
||||
getSchedule,
|
||||
jobTest,
|
||||
scheduleTest,
|
||||
} from './util'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import { map } from 'lodash'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('schedule', () => {
|
||||
let xo
|
||||
let serverId
|
||||
let scheduleIds = []
|
||||
let jobId
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setTimeout(10e3)
|
||||
let config
|
||||
;[xo, config] = await Promise.all([getMainConnection(), getConfig()])
|
||||
|
||||
serverId = await xo.call('server.add', config.xenServer1).catch(() => {})
|
||||
await eventToPromise(xo.objects, 'finish')
|
||||
|
||||
jobId = await jobTest(xo)
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
afterAll(async () => {
|
||||
await Promise.all([
|
||||
xo.call('job.delete', { id: jobId }),
|
||||
xo.call('server.remove', { id: serverId }),
|
||||
])
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
map(scheduleIds, scheduleId =>
|
||||
xo.call('schedule.delete', { id: scheduleId })
|
||||
)
|
||||
)
|
||||
scheduleIds = []
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async function createSchedule(params) {
|
||||
const schedule = await xo.call('schedule.create', params)
|
||||
scheduleIds.push(schedule.id)
|
||||
return schedule
|
||||
}
|
||||
|
||||
async function createScheduleTest() {
|
||||
const schedule = await scheduleTest(xo, jobId)
|
||||
scheduleIds.push(schedule.id)
|
||||
return schedule
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
describe('.getAll()', () => {
|
||||
it('gets all existing schedules', async () => {
|
||||
const schedules = await xo.call('schedule.getAll')
|
||||
expect(schedules).to.be.an.array()
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
describe('.get()', () => {
|
||||
let scheduleId
|
||||
beforeAll(async () => {
|
||||
scheduleId = (await createScheduleTest()).id
|
||||
})
|
||||
|
||||
it('gets an existing schedule', async () => {
|
||||
const schedule = await xo.call('schedule.get', { id: scheduleId })
|
||||
expect(schedule.job).to.be.equal(jobId)
|
||||
expect(schedule.cron).to.be.equal('* * * * * *')
|
||||
expect(schedule.enabled).to.be.false()
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
describe('.create()', () => {
|
||||
it('creates a new schedule', async () => {
|
||||
const schedule = await createSchedule({
|
||||
jobId: jobId,
|
||||
cron: '* * * * * *',
|
||||
enabled: true,
|
||||
})
|
||||
expect(schedule.job).to.be.equal(jobId)
|
||||
expect(schedule.cron).to.be.equal('* * * * * *')
|
||||
expect(schedule.enabled).to.be.true()
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
describe('.set()', () => {
|
||||
let scheduleId
|
||||
beforeAll(async () => {
|
||||
scheduleId = (await createScheduleTest()).id
|
||||
})
|
||||
it('modifies an existing schedule', async () => {
|
||||
await xo.call('schedule.set', {
|
||||
id: scheduleId,
|
||||
cron: '2 * * * * *',
|
||||
})
|
||||
|
||||
const schedule = await getSchedule(xo, scheduleId)
|
||||
expect(schedule.cron).to.be.equal('2 * * * * *')
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
describe('.delete()', () => {
|
||||
let scheduleId
|
||||
beforeEach(async () => {
|
||||
scheduleId = (await createScheduleTest()).id
|
||||
})
|
||||
it('deletes an existing schedule', async () => {
|
||||
await xo.call('schedule.delete', { id: scheduleId })
|
||||
await getSchedule(xo, scheduleId).then(
|
||||
() => {
|
||||
throw new Error('getSchedule() should have thrown')
|
||||
},
|
||||
function(error) {
|
||||
expect(error.message).to.match(/no such object/)
|
||||
}
|
||||
)
|
||||
scheduleIds = []
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,82 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
|
||||
import expect from 'must'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
import {
|
||||
jobTest,
|
||||
scheduleTest,
|
||||
getConfig,
|
||||
getMainConnection,
|
||||
getSchedule,
|
||||
} from './util'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('scheduler', () => {
|
||||
let xo
|
||||
let serverId
|
||||
let jobId
|
||||
let scheduleId
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setTimeout(10e3)
|
||||
let config
|
||||
;[xo, config] = await Promise.all([getMainConnection(), getConfig()])
|
||||
|
||||
serverId = await xo.call('server.add', config.xenServer1).catch(() => {})
|
||||
await eventToPromise(xo.objects, 'finish')
|
||||
|
||||
jobId = await jobTest(xo)
|
||||
scheduleId = (await scheduleTest(xo, jobId)).id
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
afterAll(async () => {
|
||||
await Promise.all([
|
||||
xo.call('schedule.delete', { id: scheduleId }),
|
||||
xo.call('job.delete', { id: jobId }),
|
||||
xo.call('server.remove', { id: serverId }),
|
||||
])
|
||||
})
|
||||
|
||||
// =================================================================
|
||||
|
||||
describe('.enable()', () => {
|
||||
afterEach(async () => {
|
||||
await xo.call('scheduler.disable', { id: scheduleId })
|
||||
})
|
||||
it.skip("enables a schedule to run it's job as scheduled", async () => {
|
||||
await xo.call('scheduler.enable', { id: scheduleId })
|
||||
const schedule = await getSchedule(xo, scheduleId)
|
||||
expect(schedule.enabled).to.be.true()
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
describe('.disable()', () => {
|
||||
beforeEach(async () => {
|
||||
await xo.call('schedule.enable', { id: scheduleId })
|
||||
})
|
||||
it.skip('disables a schedule', async () => {
|
||||
await xo.call('schedule.disable', { id: scheduleId })
|
||||
const schedule = await getSchedule(xo, scheduleId)
|
||||
expect(schedule.enabled).to.be.false()
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
describe('.getScheduleTable()', () => {
|
||||
it('get a map of existing schedules', async () => {
|
||||
const table = await xo.call('scheduler.getScheduleTable')
|
||||
expect(table).to.be.an.object()
|
||||
expect(table).to.match(scheduleId)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,208 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { assign, find, map } from 'lodash'
|
||||
|
||||
import { config, rejectionOf, xo } from './util'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('server', () => {
|
||||
let serverIds = []
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
map(serverIds, serverId => xo.call('server.remove', { id: serverId }))
|
||||
)
|
||||
serverIds = []
|
||||
})
|
||||
|
||||
async function addServer(params) {
|
||||
const serverId = await xo.call('server.add', params)
|
||||
serverIds.push(serverId)
|
||||
return serverId
|
||||
}
|
||||
|
||||
function getAllServers() {
|
||||
return xo.call('server.getAll')
|
||||
}
|
||||
|
||||
async function getServer(id) {
|
||||
const servers = await getAllServers()
|
||||
return find(servers, { id: id })
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
|
||||
describe('.add()', () => {
|
||||
it('add a Xen server and return its id', async () => {
|
||||
const serverId = await addServer({
|
||||
host: 'xen1.example.org',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
autoConnect: false,
|
||||
})
|
||||
|
||||
const server = await getServer(serverId)
|
||||
expect(typeof server.id).toBe('string')
|
||||
expect(server).toEqual({
|
||||
id: serverId,
|
||||
host: 'xen1.example.org',
|
||||
username: 'root',
|
||||
status: 'disconnected',
|
||||
})
|
||||
})
|
||||
|
||||
it('does not add two servers with the same host', async () => {
|
||||
await addServer({
|
||||
host: 'xen1.example.org',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
autoConnect: false,
|
||||
})
|
||||
expect(
|
||||
(await rejectionOf(
|
||||
addServer({
|
||||
host: 'xen1.example.org',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
autoConnect: false,
|
||||
})
|
||||
)).message
|
||||
).toBe('unknown error from the peer')
|
||||
})
|
||||
|
||||
it('set autoConnect true by default', async () => {
|
||||
const serverId = await addServer(config.xenServer1)
|
||||
const server = await getServer(serverId)
|
||||
|
||||
expect(server.id).toBe(serverId)
|
||||
expect(server.host).toBe('192.168.100.3')
|
||||
expect(server.username).toBe('root')
|
||||
expect(server.status).toMatch(/^connect(?:ed|ing)$/)
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
describe('.remove()', () => {
|
||||
let serverId
|
||||
beforeEach(async () => {
|
||||
serverId = await addServer({
|
||||
host: 'xen1.example.org',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
autoConnect: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('remove a Xen server', async () => {
|
||||
await xo.call('server.remove', {
|
||||
id: serverId,
|
||||
})
|
||||
|
||||
const server = await getServer(serverId)
|
||||
expect(server).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
describe('.getAll()', () => {
|
||||
it('returns an array', async () => {
|
||||
const servers = await xo.call('server.getAll')
|
||||
|
||||
expect(servers).toBeInstanceOf(Array)
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
describe('.set()', () => {
|
||||
let serverId
|
||||
beforeEach(async () => {
|
||||
serverId = await addServer({
|
||||
host: 'xen1.example.org',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
autoConnect: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('changes attributes of an existing server', async () => {
|
||||
await xo.call('server.set', {
|
||||
id: serverId,
|
||||
username: 'root2',
|
||||
})
|
||||
|
||||
const server = await getServer(serverId)
|
||||
expect(server).toEqual({
|
||||
id: serverId,
|
||||
host: 'xen1.example.org',
|
||||
username: 'root2',
|
||||
status: 'disconnected',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
describe('.connect()', () => {
|
||||
jest.setTimeout(5e3)
|
||||
|
||||
it('connects to a Xen server', async () => {
|
||||
const serverId = await addServer(
|
||||
assign({ autoConnect: false }, config.xenServer1)
|
||||
)
|
||||
|
||||
await xo.call('server.connect', {
|
||||
id: serverId,
|
||||
})
|
||||
|
||||
const server = await getServer(serverId)
|
||||
expect(server).toEqual({
|
||||
enabled: 'true',
|
||||
id: serverId,
|
||||
host: '192.168.100.3',
|
||||
username: 'root',
|
||||
status: 'connected',
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('connect to a Xen server on a slave host', async () => {
|
||||
const serverId = await addServer(config.slaveServer)
|
||||
await xo.call('server.connect', { id: serverId })
|
||||
|
||||
const server = await getServer(serverId)
|
||||
expect(server.status).toBe('connected')
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
describe('.disconnect()', () => {
|
||||
jest.setTimeout(5e3)
|
||||
let serverId
|
||||
beforeEach(async () => {
|
||||
serverId = await addServer(
|
||||
assign({ autoConnect: false }, config.xenServer1)
|
||||
)
|
||||
await xo.call('server.connect', {
|
||||
id: serverId,
|
||||
})
|
||||
})
|
||||
|
||||
it('disconnects to a Xen server', async () => {
|
||||
await xo.call('server.disconnect', {
|
||||
id: serverId,
|
||||
})
|
||||
|
||||
const server = await getServer(serverId)
|
||||
expect(server).toEqual({
|
||||
id: serverId,
|
||||
host: '192.168.100.3',
|
||||
username: 'root',
|
||||
status: 'disconnected',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,53 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import defer from 'golike-defer'
|
||||
import { map } from 'lodash'
|
||||
|
||||
import { getConnection, rejectionOf, testConnection, xo } from './util.js'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('token', () => {
|
||||
const tokens = []
|
||||
|
||||
afterAll(async () => {
|
||||
await Promise.all(map(tokens, token => xo.call('token.delete', { token })))
|
||||
})
|
||||
|
||||
async function createToken() {
|
||||
const token = await xo.call('token.create')
|
||||
tokens.push(token)
|
||||
return token
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
describe('.create()', () => {
|
||||
it('creates a token string which can be used to sign in', async () => {
|
||||
const token = await createToken()
|
||||
|
||||
await testConnection({ credentials: { token } })
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('.delete()', () => {
|
||||
it(
|
||||
'deletes a token',
|
||||
defer(async $defer => {
|
||||
const token = await createToken()
|
||||
const xo2 = await getConnection({ credentials: { token } })
|
||||
$defer(() => xo2.close())
|
||||
|
||||
await xo2.call('token.delete', {
|
||||
token,
|
||||
})
|
||||
|
||||
expect(
|
||||
(await rejectionOf(testConnection({ credentials: { token } }))).code
|
||||
).toBe(3)
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,169 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
|
||||
import expect from 'must'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
import {
|
||||
getConfig,
|
||||
getMainConnection,
|
||||
getVmXoTestPvId,
|
||||
getOneHost,
|
||||
waitObjectState,
|
||||
} from './util'
|
||||
import { assign, map } from 'lodash'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('vbd', () => {
|
||||
let xo
|
||||
let vbdId
|
||||
let diskIds = []
|
||||
let serverId
|
||||
let vmId
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setTimeout(10e3)
|
||||
let config
|
||||
;[xo, config] = await Promise.all([getMainConnection(), getConfig()])
|
||||
|
||||
serverId = await xo.call(
|
||||
'server.add',
|
||||
assign({ autoConnect: false }, config.xenServer1)
|
||||
)
|
||||
await xo.call('server.connect', { id: serverId })
|
||||
await eventToPromise(xo.objects, 'finish')
|
||||
|
||||
vmId = await getVmXoTestPvId(xo)
|
||||
try {
|
||||
await xo.call('vm.start', { id: vmId })
|
||||
} catch (_) {}
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.setTimeout(10e3)
|
||||
vbdId = await createVbd()
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
map(diskIds, diskId => xo.call('vdi.delete', { id: diskId }))
|
||||
)
|
||||
diskIds = []
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
afterAll(async () => {
|
||||
jest.setTimeout(5e3)
|
||||
await Promise.all([
|
||||
xo.call('vm.stop', { id: vmId }),
|
||||
xo.call('server.remove', { id: serverId }),
|
||||
])
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
async function createVbd() {
|
||||
// Create disk
|
||||
const pool = await xo.getOrWaitObject(getOneHost(xo).$poolId)
|
||||
const diskId = await xo.call('disk.create', {
|
||||
name: 'diskTest',
|
||||
size: '1MB',
|
||||
sr: pool.default_SR,
|
||||
})
|
||||
diskIds.push(diskId)
|
||||
|
||||
// Create VBD
|
||||
await xo.call('vm.attachDisk', {
|
||||
vm: vmId,
|
||||
vdi: diskId,
|
||||
})
|
||||
const disk = await xo.waitObject(diskId)
|
||||
return disk.$VBDs[0]
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
|
||||
describe('.delete()', () => {
|
||||
it('delete the VBD', async () => {
|
||||
await xo.call('vbd.disconnect', { id: vbdId })
|
||||
await xo.call('vbd.delete', { id: vbdId })
|
||||
|
||||
await waitObjectState(xo, vbdId, vbd => {
|
||||
expect(vbd).to.be.undefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('deletes the VBD only if it is deconnected', async () => {
|
||||
await xo.call('vbd.delete', { id: vbdId }).then(
|
||||
() => {
|
||||
throw new Error('vbd.delete() should have thrown')
|
||||
},
|
||||
function(error) {
|
||||
// TODO: check with Julien if it is ok
|
||||
expect(error.message).to.match('unknown error from the peer')
|
||||
}
|
||||
)
|
||||
await xo.call('vbd.disconnect', { id: vbdId })
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
describe('.disconnect()', () => {
|
||||
it('disconnect the VBD', async () => {
|
||||
await xo.call('vbd.disconnect', { id: vbdId })
|
||||
await waitObjectState(xo, vbdId, vbd => {
|
||||
expect(vbd.attached).to.be.false()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('.connect()', () => {
|
||||
beforeEach(async () => {
|
||||
await xo.call('vbd.disconnect', { id: vbdId })
|
||||
})
|
||||
afterEach(async () => {
|
||||
await xo.call('vbd.disconnect', { id: vbdId })
|
||||
})
|
||||
|
||||
it('connect the VBD', async () => {
|
||||
await xo.call('vbd.connect', { id: vbdId })
|
||||
|
||||
await waitObjectState(xo, vbdId, vbd => {
|
||||
expect(vbd.attached).to.be.true()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
describe('.set()', () => {
|
||||
afterEach(async () => {
|
||||
await xo.call('vbd.disconnect', { id: vbdId })
|
||||
})
|
||||
|
||||
// TODO: resolve problem with disconnect
|
||||
it.skip('set the position of the VBD', async () => {
|
||||
await xo.call('vbd.set', {
|
||||
id: vbdId,
|
||||
position: '10',
|
||||
})
|
||||
|
||||
await waitObjectState(xo, vbdId, vbd => {
|
||||
expect(vbd.position).to.be.equal('10')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,133 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
|
||||
import expect from 'must'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
import {
|
||||
getConfig,
|
||||
getMainConnection,
|
||||
getNetworkId,
|
||||
waitObjectState,
|
||||
getVmXoTestPvId,
|
||||
} from './util'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import { map } from 'lodash'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('vif', () => {
|
||||
let xo
|
||||
let serverId
|
||||
let vifIds = []
|
||||
let vmId
|
||||
let vifId
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setTimeout(10e3)
|
||||
let config
|
||||
;[xo, config] = await Promise.all([getMainConnection(), getConfig()])
|
||||
|
||||
serverId = await xo.call('server.add', config.xenServer1).catch(() => {})
|
||||
await eventToPromise(xo.objects, 'finish')
|
||||
|
||||
vmId = await getVmXoTestPvId(xo)
|
||||
try {
|
||||
await xo.call('vm.start', { id: vmId })
|
||||
} catch (_) {}
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
beforeEach(async () => {
|
||||
vifId = await createVif()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
map(vifIds, vifId => xo.call('vif.delete', { id: vifId }))
|
||||
)
|
||||
vifIds = []
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
afterAll(async () => {
|
||||
jest.setTimeout(5e3)
|
||||
await xo.call('vm.stop', { id: vmId, force: true })
|
||||
await xo.call('server.remove', { id: serverId })
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
async function createVif() {
|
||||
const networkId = await getNetworkId(xo)
|
||||
|
||||
const vifId = await xo.call('vm.createInterface', {
|
||||
vm: vmId,
|
||||
network: networkId,
|
||||
position: '1',
|
||||
})
|
||||
vifIds.push(vifId)
|
||||
|
||||
return vifId
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('.delete()', () => {
|
||||
it('deletes a VIF', async () => {
|
||||
await xo.call('vif.disconnect', { id: vifId })
|
||||
await xo.call('vif.delete', { id: vifId })
|
||||
|
||||
await waitObjectState(xo, vifId, vif => {
|
||||
expect(vif).to.be.undefined()
|
||||
})
|
||||
|
||||
vifIds = []
|
||||
})
|
||||
|
||||
it('can not delete a VIF if it is connected', async () => {
|
||||
await xo.call('vif.delete', { id: vifId }).then(
|
||||
() => {
|
||||
throw new Error('vif.delete() should have thrown')
|
||||
},
|
||||
function(error) {
|
||||
expect(error.message).to.be.equal('unknown error from the peer')
|
||||
}
|
||||
)
|
||||
await xo.call('vif.disconnect', { id: vifId })
|
||||
})
|
||||
})
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
describe('.disconnect()', () => {
|
||||
it('disconnects a VIF', async () => {
|
||||
await xo.call('vif.disconnect', { id: vifId })
|
||||
await waitObjectState(xo, vifId, vif => {
|
||||
expect(vif.attached).to.be.false()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
describe('.connect()', () => {
|
||||
beforeEach(async () => {
|
||||
await xo.call('vif.disconnect', { id: vifId })
|
||||
})
|
||||
afterEach(async () => {
|
||||
await xo.call('vif.disconnect', { id: vifId })
|
||||
})
|
||||
it('connects a VIF', async () => {
|
||||
await xo.call('vif.connect', { id: vifId })
|
||||
await waitObjectState(xo, vifId, vif => {
|
||||
expect(vif.attached).to.be.true()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,666 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
|
||||
import expect from 'must'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
import {
|
||||
almostEqual,
|
||||
getAllHosts,
|
||||
getConfig,
|
||||
getMainConnection,
|
||||
getNetworkId,
|
||||
getOneHost,
|
||||
getSrId,
|
||||
getVmToMigrateId,
|
||||
getVmXoTestPvId,
|
||||
waitObjectState,
|
||||
} from './util'
|
||||
import { map, find } from 'lodash'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('vm', () => {
|
||||
let xo
|
||||
let vmId
|
||||
let vmIds = []
|
||||
let serverId
|
||||
let config
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setTimeout(10e3)
|
||||
;[xo, config] = await Promise.all([getMainConnection(), getConfig()])
|
||||
serverId = await xo.call('server.add', config.xenServer1).catch(() => {})
|
||||
await eventToPromise(xo.objects, 'finish')
|
||||
})
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
afterEach(async () => {
|
||||
jest.setTimeout(15e3)
|
||||
await Promise.all(
|
||||
map(vmIds, vmId => xo.call('vm.delete', { id: vmId, delete_disks: true }))
|
||||
)
|
||||
vmIds = []
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
afterAll(async () => {
|
||||
await xo.call('server.remove', {
|
||||
id: serverId,
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
async function createVm(params) {
|
||||
const vmId = await xo.call('vm.create', params)
|
||||
vmIds.push(vmId)
|
||||
return vmId
|
||||
}
|
||||
|
||||
async function createVmTest() {
|
||||
const templateId = getTemplateId(config.templates.debian)
|
||||
const vmId = await createVm({
|
||||
name_label: 'vmTest',
|
||||
template: templateId,
|
||||
VIFs: [],
|
||||
})
|
||||
return vmId
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async function getCdVbdPosition(vmId) {
|
||||
const vm = await xo.getOrWaitObject(vmId)
|
||||
for (let i = 0; i < vm.$VBDs.length; i++) {
|
||||
const vbd = await xo.getOrWaitObject(vm.$VBDs[i])
|
||||
if (vbd.is_cd_drive === true) {
|
||||
return vbd.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getHostOtherPool(vm) {
|
||||
const hosts = getAllHosts(xo)
|
||||
for (const id in hosts) {
|
||||
if (hosts[id].$poolId !== vm.$poolId) {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function getIsoId() {
|
||||
const vdis = xo.objects.indexes.type.VDI
|
||||
const iso = find(vdis, { name_label: config.iso })
|
||||
return iso.id
|
||||
}
|
||||
|
||||
function getOtherHost(vm) {
|
||||
const hosts = getAllHosts(xo)
|
||||
for (const id in hosts) {
|
||||
if (hosts[id].$poolId === vm.poolId) {
|
||||
if (id !== vm.$container) {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getTemplateId(nameTemplate) {
|
||||
const templates = xo.objects.indexes.type['VM-template']
|
||||
const template = find(templates, { name_label: nameTemplate })
|
||||
return template.id
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
describe('.create()', () => {
|
||||
it('creates a VM with only a name and a template', async () => {
|
||||
const templateId = getTemplateId(config.templates.debian)
|
||||
|
||||
vmId = await createVm({
|
||||
name_label: 'vmTest',
|
||||
template: templateId,
|
||||
VIFs: [],
|
||||
})
|
||||
await waitObjectState(xo, vmId, vm => {
|
||||
expect(vm.id).to.be.a.string()
|
||||
expect(vm).to.be.an.object()
|
||||
})
|
||||
})
|
||||
|
||||
describe('.createHVM()', () => {
|
||||
let srId
|
||||
let templateId
|
||||
|
||||
beforeAll(async () => {
|
||||
srId = await getSrId(xo)
|
||||
templateId = getTemplateId(config.templates.otherConfig)
|
||||
})
|
||||
|
||||
it.skip('creates a VM with the Other Config template, three disks, two interfaces and a ISO mounted', async () => {
|
||||
jest.setTimeout(30e3)
|
||||
|
||||
const networkId = await getNetworkId(xo)
|
||||
vmId = await createVm({
|
||||
name_label: 'vmTest',
|
||||
template: templateId,
|
||||
VIFs: [{ network: networkId }, { network: networkId }],
|
||||
VDIs: [
|
||||
{ device: '0', size: 1, SR: srId, type: 'user' },
|
||||
{ device: '1', size: 1, SR: srId, type: 'user' },
|
||||
{ device: '2', size: 1, SR: srId, type: 'user' },
|
||||
],
|
||||
})
|
||||
|
||||
await waitObjectState(xo, vmId, vm => {
|
||||
expect(vm.name_label).to.be.equal('vmTest')
|
||||
expect(vm.other.base_template_name).to.be.equal(
|
||||
config.templates.otherConfig
|
||||
)
|
||||
expect(vm.VIFs).to.have.length(2)
|
||||
expect(vm.$VBDs).to.have.length(3)
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('creates a VM with the Other Config template, no disk, no network and a ISO mounted', async () => {
|
||||
vmId = await createVm({
|
||||
name_label: 'vmTest',
|
||||
template: templateId,
|
||||
VIFs: [],
|
||||
})
|
||||
|
||||
await waitObjectState(xo, vmId, vm => {
|
||||
expect(vm.other.base_template_name).to.be.equal(
|
||||
config.templates.otherConfig
|
||||
)
|
||||
expect(vm.VIFs).to.have.length(0)
|
||||
expect(vm.$VBDs).to.have.length(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('.createPV()', () => {
|
||||
let srId
|
||||
let templateId
|
||||
let networkId
|
||||
|
||||
beforeAll(async () => {
|
||||
;[networkId, srId] = await Promise.all([getNetworkId(xo), getSrId(xo)])
|
||||
})
|
||||
|
||||
it.skip('creates a VM with the Debian 7 64 bits template, network install, one disk, one network', async () => {
|
||||
templateId = getTemplateId(config.templates.debian)
|
||||
|
||||
vmId = await createVm({
|
||||
name_label: 'vmTest',
|
||||
template: templateId,
|
||||
VIFs: [{ network: networkId }],
|
||||
VDIs: [
|
||||
{
|
||||
device: '0',
|
||||
size: 1,
|
||||
SR: srId,
|
||||
type: 'user',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await waitObjectState(xo, vmId, vm => {
|
||||
expect(vm.other.base_template_name).to.be.equal(
|
||||
config.templates.debian
|
||||
)
|
||||
expect(vm.VIFs).to.have.length(1)
|
||||
expect(vm.$VBDs).to.have.length(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('creates a VM with the CentOS 7 64 bits template, two disks, two networks and a ISO mounted', async () => {
|
||||
jest.setTimeout(10e3)
|
||||
|
||||
templateId = getTemplateId(config.templates.centOS)
|
||||
vmId = await createVm({
|
||||
name_label: 'vmTest',
|
||||
template: templateId,
|
||||
VIFs: [{ network: networkId }, { network: networkId }],
|
||||
VDIs: [
|
||||
{ device: '0', size: 1, SR: srId, type: 'user' },
|
||||
{ device: '1', size: 1, SR: srId, type: 'user' },
|
||||
],
|
||||
})
|
||||
|
||||
await waitObjectState(xo, vmId, vm => {
|
||||
expect(vm.other.base_template_name).to.be.equal(
|
||||
config.templates.centOS
|
||||
)
|
||||
expect(vm.VIFs).to.have.length(2)
|
||||
expect(vm.$VBDs).to.have.length(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
describe('.delete()', () => {
|
||||
let snapshotIds = []
|
||||
let diskIds = []
|
||||
|
||||
beforeEach(async () => {
|
||||
vmId = await createVmTest()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await Promise.all(
|
||||
map(snapshotIds, snapshotId =>
|
||||
xo.call('vm.delete', { id: snapshotId })
|
||||
),
|
||||
map(diskIds, diskId => xo.call('vdi.delete', { id: diskId }))
|
||||
)
|
||||
})
|
||||
|
||||
it('deletes a VM', async () => {
|
||||
await xo.call('vm.delete', {
|
||||
id: vmId,
|
||||
delete_disks: true,
|
||||
})
|
||||
|
||||
await waitObjectState(xo, vmId, vm => {
|
||||
expect(vm).to.be.undefined()
|
||||
})
|
||||
vmIds = []
|
||||
})
|
||||
|
||||
it('deletes a VM and its snapshots', async () => {
|
||||
const snapshotId = await xo.call('vm.snapshot', {
|
||||
id: vmId,
|
||||
name: 'snapshot',
|
||||
})
|
||||
snapshotIds.push(snapshotId)
|
||||
|
||||
await xo.call('vm.delete', {
|
||||
id: vmId,
|
||||
delete_disks: true,
|
||||
})
|
||||
vmIds = []
|
||||
await waitObjectState(xo, snapshotId, snapshot => {
|
||||
expect(snapshot).to.be.undefined()
|
||||
})
|
||||
snapshotIds = []
|
||||
})
|
||||
|
||||
it('deletes a VM and its disks', async () => {
|
||||
jest.setTimeout(5e3)
|
||||
// create disk
|
||||
const host = getOneHost(xo)
|
||||
const pool = await xo.getOrWaitObject(host.$poolId)
|
||||
|
||||
const diskId = await xo.call('disk.create', {
|
||||
name: 'diskTest',
|
||||
size: '1GB',
|
||||
sr: pool.default_SR,
|
||||
})
|
||||
diskIds.push(diskId)
|
||||
|
||||
// attach the disk on the VM
|
||||
await xo.call('vm.attachDisk', {
|
||||
vm: vmId,
|
||||
vdi: diskId,
|
||||
})
|
||||
|
||||
// delete the VM
|
||||
await xo.call('vm.delete', {
|
||||
id: vmId,
|
||||
delete_disks: true,
|
||||
})
|
||||
vmIds = []
|
||||
await waitObjectState(xo, diskId, disk => {
|
||||
expect(disk).to.be.undefined()
|
||||
})
|
||||
diskIds = []
|
||||
})
|
||||
|
||||
// TODO: do a copy of the ISO
|
||||
it.skip('deletes a vm but not delete its ISO', async () => {
|
||||
vmId = await createVmTest()
|
||||
|
||||
await xo.call('vm.insertCd', {
|
||||
id: vmId,
|
||||
cd_id: '1169eb8a-d43f-4daf-a0ca-f3434a4bf301',
|
||||
force: false,
|
||||
})
|
||||
|
||||
await xo.call('vm.delete', {
|
||||
id: vmId,
|
||||
delete_disks: true,
|
||||
})
|
||||
|
||||
waitObjectState(xo, '1169eb8a-d43f-4daf-a0ca-f3434a4bf301', iso => {
|
||||
expect(iso).not.to.be.undefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('.migrate', () => {
|
||||
jest.setTimeout(15e3)
|
||||
|
||||
let secondServerId
|
||||
let startHostId
|
||||
let hostId
|
||||
|
||||
beforeAll(async () => {
|
||||
secondServerId = await xo
|
||||
.call('server.add', config.xenServer2)
|
||||
.catch(() => {})
|
||||
await eventToPromise(xo.objects, 'finish')
|
||||
|
||||
vmId = await getVmToMigrateId(xo)
|
||||
|
||||
try {
|
||||
await xo.call('vm.start', { id: vmId })
|
||||
} catch (_) {}
|
||||
})
|
||||
beforeEach(async () => {
|
||||
const vm = await xo.getOrWaitObject(vmId)
|
||||
startHostId = vm.$container
|
||||
hostId = getOtherHost(vm)
|
||||
})
|
||||
afterEach(async () => {
|
||||
await xo.call('vm.migrate', {
|
||||
id: vmId,
|
||||
host_id: startHostId,
|
||||
})
|
||||
})
|
||||
afterAll(async () => {
|
||||
await xo.call('server.remove', {
|
||||
id: secondServerId,
|
||||
})
|
||||
})
|
||||
|
||||
it('migrates the VM on an other host', async () => {
|
||||
await xo.call('vm.migrate', {
|
||||
id: vmId,
|
||||
host_id: hostId,
|
||||
})
|
||||
await waitObjectState(xo, vmId, vm => {
|
||||
expect(vm.$container).to.be.equal(hostId)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('.migratePool()', () => {
|
||||
jest.setTimeout(100e3)
|
||||
let hostId
|
||||
let secondServerId
|
||||
let startHostId
|
||||
|
||||
beforeAll(async () => {
|
||||
secondServerId = await xo
|
||||
.call('server.add', config.xenServer2)
|
||||
.catch(() => {})
|
||||
await eventToPromise(xo.objects, 'finish')
|
||||
|
||||
vmId = await getVmToMigrateId(xo)
|
||||
|
||||
try {
|
||||
await xo.call('vm.start', { id: vmId })
|
||||
} catch (_) {}
|
||||
})
|
||||
afterAll(async () => {
|
||||
await xo.call('server.remove', { id: secondServerId })
|
||||
})
|
||||
beforeEach(async () => {
|
||||
const vm = await xo.getOrWaitObject(vmId)
|
||||
startHostId = vm.$container
|
||||
hostId = getHostOtherPool(xo, vm)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// TODO: try to get the vmId
|
||||
vmId = await getVmToMigrateId(xo)
|
||||
await xo.call('vm.migrate_pool', {
|
||||
id: vmId,
|
||||
target_host_id: startHostId,
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('migrates the VM on an other host which is in an other pool', async () => {
|
||||
await xo.call('vm.migrate_pool', {
|
||||
id: vmId,
|
||||
target_host_id: hostId,
|
||||
})
|
||||
await waitObjectState(xo, vmId, vm => {
|
||||
expect(vm).to.be.undefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
describe('.clone()', () => {
|
||||
beforeEach(async () => {
|
||||
vmId = await createVmTest()
|
||||
})
|
||||
it('clones a VM', async () => {
|
||||
const cloneId = await xo.call('vm.clone', {
|
||||
id: vmId,
|
||||
name: 'clone',
|
||||
full_copy: true,
|
||||
})
|
||||
// push cloneId in vmIds array to delete the VM after test
|
||||
vmIds.push(cloneId)
|
||||
|
||||
const [vm, clone] = await Promise.all([
|
||||
xo.getOrWaitObject(vmId),
|
||||
xo.getOrWaitObject(cloneId),
|
||||
])
|
||||
expect(clone.type).to.be.equal('VM')
|
||||
expect(clone.name_label).to.be.equal('clone')
|
||||
|
||||
almostEqual(clone, vm, ['name_label', 'ref', 'id', 'other.mac_seed'])
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
describe('.convert()', () => {
|
||||
beforeEach(async () => {
|
||||
vmId = await createVmTest()
|
||||
})
|
||||
|
||||
it('converts a VM', async () => {
|
||||
await xo.call('vm.convert', { id: vmId })
|
||||
await waitObjectState(xo, vmId, vm => {
|
||||
expect(vm.type).to.be.equal('VM-template')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
describe('.revert()', () => {
|
||||
jest.setTimeout(5e3)
|
||||
let snapshotId
|
||||
beforeEach(async () => {
|
||||
vmId = await createVmTest()
|
||||
snapshotId = await xo.call('vm.snapshot', {
|
||||
id: vmId,
|
||||
name: 'snapshot',
|
||||
})
|
||||
})
|
||||
afterEach(async () => {
|
||||
await xo.call('vm.delete', { id: snapshotId })
|
||||
})
|
||||
it('reverts a snapshot to its parent VM', async () => {
|
||||
const revert = await xo.call('vm.revert', { id: snapshotId })
|
||||
expect(revert).to.be.true()
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
describe('.handleExport()', () => {
|
||||
it('')
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
describe('.import()', () => {
|
||||
it('')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
describe('.attachDisk()', () => {
|
||||
jest.setTimeout(5e3)
|
||||
let diskId
|
||||
beforeEach(async () => {
|
||||
vmId = await createVmTest()
|
||||
const srId = await getSrId(xo)
|
||||
diskId = await xo.call('disk.create', {
|
||||
name: 'diskTest',
|
||||
size: '1GB',
|
||||
sr: srId,
|
||||
})
|
||||
})
|
||||
afterEach(async () => {
|
||||
await xo.call('vdi.delete', { id: diskId })
|
||||
})
|
||||
|
||||
it('attaches the disk to the VM with attributes by default', async () => {
|
||||
await xo.call('vm.attachDisk', {
|
||||
vm: vmId,
|
||||
vdi: diskId,
|
||||
})
|
||||
const vm = await xo.waitObject(vmId)
|
||||
await waitObjectState(xo, diskId, disk => {
|
||||
expect(disk.$VBDs).to.be.eql(vm.$VBDs)
|
||||
})
|
||||
|
||||
await waitObjectState(xo, vm.$VBDs, vbd => {
|
||||
expect(vbd.type).to.be.equal('VBD')
|
||||
// expect(vbd.attached).to.be.true()
|
||||
expect(vbd.bootable).to.be.false()
|
||||
expect(vbd.is_cd_drive).to.be.false()
|
||||
expect(vbd.position).to.be.equal('0')
|
||||
expect(vbd.read_only).to.be.false()
|
||||
expect(vbd.VDI).to.be.equal(diskId)
|
||||
expect(vbd.VM).to.be.equal(vmId)
|
||||
expect(vbd.$poolId).to.be.equal(vm.$poolId)
|
||||
})
|
||||
})
|
||||
|
||||
it('attaches the disk to the VM with specified attributes', async () => {
|
||||
await xo.call('vm.attachDisk', {
|
||||
vm: vmId,
|
||||
vdi: diskId,
|
||||
bootable: true,
|
||||
mode: 'RO',
|
||||
position: '2',
|
||||
})
|
||||
const vm = await xo.waitObject(vmId)
|
||||
await waitObjectState(xo, vm.$VBDs, vbd => {
|
||||
expect(vbd.type).to.be.equal('VBD')
|
||||
// expect(vbd.attached).to.be.true()
|
||||
expect(vbd.bootable).to.be.true()
|
||||
expect(vbd.is_cd_drive).to.be.false()
|
||||
expect(vbd.position).to.be.equal('2')
|
||||
expect(vbd.read_only).to.be.true()
|
||||
expect(vbd.VDI).to.be.equal(diskId)
|
||||
expect(vbd.VM).to.be.equal(vmId)
|
||||
expect(vbd.$poolId).to.be.equal(vm.$poolId)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
describe('.createInterface()', () => {
|
||||
let vifId
|
||||
let networkId
|
||||
beforeAll(async () => {
|
||||
vmId = await getVmXoTestPvId(xo)
|
||||
networkId = await getNetworkId(xo)
|
||||
})
|
||||
afterEach(async () => {
|
||||
await xo.call('vif.delete', { id: vifId })
|
||||
})
|
||||
|
||||
it('create a VIF between the VM and the network', async () => {
|
||||
vifId = await xo.call('vm.createInterface', {
|
||||
vm: vmId,
|
||||
network: networkId,
|
||||
position: '1',
|
||||
})
|
||||
|
||||
await waitObjectState(xo, vifId, vif => {
|
||||
expect(vif.type).to.be.equal('VIF')
|
||||
// expect(vif.attached).to.be.true()
|
||||
expect(vif.$network).to.be.equal(networkId)
|
||||
expect(vif.$VM).to.be.equal(vmId)
|
||||
expect(vif.device).to.be.equal('1')
|
||||
})
|
||||
})
|
||||
|
||||
it('can not create two interfaces on the same device', async () => {
|
||||
vifId = await xo.call('vm.createInterface', {
|
||||
vm: vmId,
|
||||
network: networkId,
|
||||
position: '1',
|
||||
})
|
||||
await xo
|
||||
.call('vm.createInterface', {
|
||||
vm: vmId,
|
||||
network: networkId,
|
||||
position: '1',
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
throw new Error('createInterface() sould have trown')
|
||||
},
|
||||
function(error) {
|
||||
expect(error.message).to.be.equal('unknown error from the peer')
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
describe('.stats()', () => {
|
||||
jest.setTimeout(20e3)
|
||||
beforeAll(async () => {
|
||||
vmId = await getVmXoTestPvId(xo)
|
||||
})
|
||||
beforeEach(async () => {
|
||||
await xo.call('vm.start', { id: vmId })
|
||||
})
|
||||
afterEach(async () => {
|
||||
await xo.call('vm.stop', {
|
||||
id: vmId,
|
||||
force: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('returns an array with statistics of the VM', async () => {
|
||||
const stats = await xo.call('vm.stats', { id: vmId })
|
||||
expect(stats).to.be.an.object()
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
describe('.bootOrder()', () => {
|
||||
it('')
|
||||
})
|
||||
})
|
||||
@@ -1,126 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import {
|
||||
config,
|
||||
getOrWaitCdVbdPosition,
|
||||
rejectionOf,
|
||||
waitObjectState,
|
||||
xo,
|
||||
} from './../util'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setTimeout(20e3)
|
||||
})
|
||||
|
||||
describe('cd', () => {
|
||||
let vmId
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
beforeAll(async () => {
|
||||
vmId = await xo.call('vm.create', {
|
||||
name_label: 'vmTest',
|
||||
template: config.templatesId.debian,
|
||||
})
|
||||
await waitObjectState(xo, vmId, vm => {
|
||||
if (vm.type !== 'VM') throw new Error('retry')
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => xo.call('vm.delete', { id: vmId }))
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('.insertCd()', () => {
|
||||
afterEach(() => xo.call('vm.ejectCd', { id: vmId }))
|
||||
|
||||
it('mount an ISO on the VM (force: false)', async () => {
|
||||
await xo.call('vm.insertCd', {
|
||||
id: vmId,
|
||||
cd_id: config.windowsIsoId,
|
||||
force: false,
|
||||
})
|
||||
const vbdId = await getOrWaitCdVbdPosition(vmId)
|
||||
|
||||
await waitObjectState(xo, vbdId, vbd => {
|
||||
expect(vbd.VDI).toBe(config.windowsIsoId)
|
||||
expect(vbd.is_cd_drive).toBeTruthy()
|
||||
expect(vbd.position).toBe('3')
|
||||
})
|
||||
})
|
||||
|
||||
it('mount an ISO on the VM (force: false) which has already a CD in the VBD', async () => {
|
||||
await xo.call('vm.insertCd', {
|
||||
id: vmId,
|
||||
cd_id: config.windowsIsoId,
|
||||
force: false,
|
||||
})
|
||||
await getOrWaitCdVbdPosition(vmId)
|
||||
|
||||
expect(
|
||||
(await rejectionOf(
|
||||
xo.call('vm.insertCd', {
|
||||
id: vmId,
|
||||
cd_id: config.ubuntuIsoId,
|
||||
force: false,
|
||||
})
|
||||
)).message
|
||||
).toBe('unknown error from the peer')
|
||||
})
|
||||
|
||||
it('mount an ISO on the VM (force: true) which has already a CD in the VBD', async () => {
|
||||
await xo.call('vm.insertCd', {
|
||||
id: vmId,
|
||||
cd_id: config.windowsIsoId,
|
||||
force: true,
|
||||
})
|
||||
const vbdId = await getOrWaitCdVbdPosition(vmId)
|
||||
|
||||
await xo.call('vm.insertCd', {
|
||||
id: vmId,
|
||||
cd_id: config.ubuntuIsoId,
|
||||
force: true,
|
||||
})
|
||||
|
||||
await waitObjectState(xo, vbdId, vbd => {
|
||||
expect(vbd.VDI).toBe(config.ubuntuIsoId)
|
||||
expect(vbd.is_cd_drive).toBeTruthy()
|
||||
expect(vbd.position).toBe('3')
|
||||
})
|
||||
})
|
||||
|
||||
it("mount an ISO on a VM which do not have already cd's VBD", async () => {
|
||||
await xo.call('vm.insertCd', {
|
||||
id: vmId,
|
||||
cd_id: config.windowsIsoId,
|
||||
force: false,
|
||||
})
|
||||
|
||||
await waitObjectState(xo, vmId, async vm => {
|
||||
expect(vm.$VBDs).toHaveLength(1)
|
||||
const vbd = await xo.getOrWaitObject(vm.$VBDs)
|
||||
expect(vbd.is_cd_drive).toBeTruthy()
|
||||
expect(vbd.position).toBe('3')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.ejectCd()', () => {
|
||||
it('ejects an ISO', async () => {
|
||||
await xo.call('vm.insertCd', {
|
||||
id: vmId,
|
||||
cd_id: config.windowsIsoId,
|
||||
force: false,
|
||||
})
|
||||
|
||||
const vbdId = await getOrWaitCdVbdPosition(vmId)
|
||||
|
||||
await xo.call('vm.ejectCd', { id: vmId })
|
||||
await waitObjectState(xo, vbdId, vbd => {
|
||||
expect(vbd.VDI).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,268 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { map, size } from 'lodash'
|
||||
|
||||
import { config, rejectionOf, waitObjectState, xo } from './../util'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setTimeout(150e3)
|
||||
})
|
||||
|
||||
describe('the VM life cyle', () => {
|
||||
const vmsToDelete = []
|
||||
// hvm with tools behave like pv vm
|
||||
let hvmWithToolsId
|
||||
let hvmWithoutToolsId
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
beforeAll(async () => {
|
||||
hvmWithToolsId = await xo.call('vm.create', {
|
||||
name_label: 'vmTest-updateState',
|
||||
template: config.templatesId.debianCloud,
|
||||
VIFs: [{ network: config.labPoolNetworkId }],
|
||||
VDIs: [
|
||||
{
|
||||
device: '0',
|
||||
size: 1,
|
||||
SR: config.labPoolSrId,
|
||||
type: 'user',
|
||||
},
|
||||
],
|
||||
})
|
||||
vmsToDelete.push(hvmWithToolsId)
|
||||
await waitObjectState(xo, hvmWithToolsId, vm => {
|
||||
if (vm.type !== 'VM') throw new Error('retry')
|
||||
})
|
||||
|
||||
hvmWithoutToolsId = await xo.call('vm.create', {
|
||||
name_label: 'vmTest-updateState',
|
||||
template: config.templatesId.centOS,
|
||||
VIFs: [{ network: config.labPoolNetworkId }],
|
||||
VDIs: [
|
||||
{
|
||||
device: '0',
|
||||
size: 1,
|
||||
SR: config.labPoolSrId,
|
||||
type: 'user',
|
||||
},
|
||||
],
|
||||
})
|
||||
vmsToDelete.push(hvmWithoutToolsId)
|
||||
await waitObjectState(xo, hvmWithoutToolsId, vm => {
|
||||
if (vm.type !== 'VM') throw new Error('retry')
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await Promise.all(
|
||||
map(vmsToDelete, id =>
|
||||
xo
|
||||
.call('vm.delete', { id, delete_disks: true })
|
||||
.catch(error => console.error(error))
|
||||
)
|
||||
)
|
||||
vmsToDelete.length = 0
|
||||
})
|
||||
|
||||
// =================================================================
|
||||
|
||||
describe('.start()', () => {
|
||||
it('starts a VM', async () => {
|
||||
await xo.call('vm.start', { id: hvmWithToolsId })
|
||||
|
||||
await waitObjectState(xo, hvmWithToolsId, vm => {
|
||||
expect(size(vm.current_operations)).toBe(0)
|
||||
expect(vm.power_state).toBe('Running')
|
||||
expect(vm.startTime).not.toBe(0)
|
||||
expect(vm.xenTools).not.toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.sets() on a running VM', () => {
|
||||
it('sets VM parameters', async () => {
|
||||
await xo.call('vm.set', {
|
||||
id: hvmWithToolsId,
|
||||
name_label: 'startedVmRenamed',
|
||||
name_description: 'test started vm',
|
||||
high_availability: true,
|
||||
CPUs: 1,
|
||||
memoryMin: 260e6,
|
||||
})
|
||||
|
||||
await waitObjectState(xo, hvmWithToolsId, vm => {
|
||||
expect(vm.name_label).toBe('startedVmRenamed')
|
||||
expect(vm.name_description).toBe('test started vm')
|
||||
expect(vm.high_availability).toBeTruthy()
|
||||
expect(vm.CPUs.number).toBe(1)
|
||||
expect(vm.memory.dynamic[0]).toBe(260e6)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.restart()', () => {
|
||||
it('restarts a VM (clean reboot)', async () => {
|
||||
await xo.call('vm.restart', {
|
||||
id: hvmWithToolsId,
|
||||
force: false,
|
||||
})
|
||||
|
||||
await waitObjectState(xo, hvmWithToolsId, vm => {
|
||||
expect(size(vm.current_operations)).toBe(0)
|
||||
expect(vm.power_state).toBe('Running')
|
||||
expect(vm.startTime).not.toBe(0)
|
||||
expect(vm.xenTools).not.toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
it('restarts a VM without PV drivers(clean reboot)', async () => {
|
||||
await xo.call('vm.start', { id: hvmWithoutToolsId })
|
||||
await waitObjectState(xo, hvmWithoutToolsId, vm => {
|
||||
if (size(vm.current_operations) !== 0 || vm.power_state !== 'Running')
|
||||
throw new Error('retry')
|
||||
})
|
||||
|
||||
expect(
|
||||
(await rejectionOf(
|
||||
xo.call('vm.restart', {
|
||||
id: hvmWithoutToolsId,
|
||||
force: false,
|
||||
})
|
||||
)).message
|
||||
).toBe('VM lacks feature shutdown')
|
||||
})
|
||||
|
||||
it('restarts a VM (hard reboot)', async () => {
|
||||
await xo.call('vm.restart', {
|
||||
id: hvmWithToolsId,
|
||||
force: true,
|
||||
})
|
||||
|
||||
await waitObjectState(xo, hvmWithToolsId, vm => {
|
||||
expect(size(vm.current_operations)).toBe(0)
|
||||
expect(vm.power_state).toBe('Running')
|
||||
expect(vm.startTime).not.toBe(0)
|
||||
expect(vm.xenTools).not.toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.suspend()', () => {
|
||||
it('suspends a VM', async () => {
|
||||
await xo.call('vm.suspend', { id: hvmWithToolsId })
|
||||
|
||||
await waitObjectState(xo, hvmWithToolsId, vm => {
|
||||
expect(size(vm.current_operations)).toBe(0)
|
||||
expect(vm.power_state).toBe('Suspended')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.resume()', () => {
|
||||
it('resumes a VM', async () => {
|
||||
await xo.call('vm.resume', { id: hvmWithToolsId })
|
||||
|
||||
await waitObjectState(xo, hvmWithToolsId, vm => {
|
||||
expect(size(vm.current_operations)).toBe(0)
|
||||
expect(vm.power_state).toBe('Running')
|
||||
expect(vm.startTime).not.toBe(0)
|
||||
expect(vm.xenTools).not.toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.stop()', () => {
|
||||
it('stops a VM (clean shutdown)', async () => {
|
||||
await xo.call('vm.stop', {
|
||||
id: hvmWithToolsId,
|
||||
force: false,
|
||||
})
|
||||
|
||||
await waitObjectState(xo, hvmWithToolsId, vm => {
|
||||
expect(size(vm.current_operations)).toBe(0)
|
||||
expect(vm.power_state).toBe('Halted')
|
||||
expect(vm.startTime).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
it('stops a VM without PV drivers (clean shutdown)', async () => {
|
||||
await xo.call('vm.start', { id: hvmWithoutToolsId })
|
||||
await waitObjectState(xo, hvmWithoutToolsId, vm => {
|
||||
if (size(vm.current_operations) !== 0 || vm.power_state !== 'Running')
|
||||
throw new Error('retry')
|
||||
})
|
||||
|
||||
expect(
|
||||
(await rejectionOf(
|
||||
xo.call('vm.stop', {
|
||||
id: hvmWithoutToolsId,
|
||||
force: false,
|
||||
})
|
||||
)).message
|
||||
).toBe('clean shutdown requires PV drivers')
|
||||
})
|
||||
|
||||
it('stops a VM (hard shutdown)', async () => {
|
||||
await xo.call('vm.start', { id: hvmWithToolsId })
|
||||
await waitObjectState(xo, hvmWithToolsId, vm => {
|
||||
if (size(vm.current_operations) !== 0 || vm.startTime === 0)
|
||||
throw new Error('retry')
|
||||
})
|
||||
|
||||
await xo.call('vm.stop', {
|
||||
id: hvmWithToolsId,
|
||||
force: true,
|
||||
})
|
||||
|
||||
await waitObjectState(xo, hvmWithToolsId, vm => {
|
||||
expect(size(vm.current_operations)).toBe(0)
|
||||
expect(vm.power_state).toBe('Halted')
|
||||
expect(vm.startTime).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.sets() on a halted VM', () => {
|
||||
it('sets VM parameters', async () => {
|
||||
await xo.call('vm.set', {
|
||||
id: hvmWithToolsId,
|
||||
name_label: 'haltedVmRenamed',
|
||||
name_description: 'test halted vm',
|
||||
high_availability: true,
|
||||
CPUs: 1,
|
||||
memoryMin: 20e8,
|
||||
memoryMax: 90e8,
|
||||
memoryStaticMax: 100e8,
|
||||
})
|
||||
|
||||
await waitObjectState(xo, hvmWithToolsId, vm => {
|
||||
expect(vm.name_label).toBe('haltedVmRenamed')
|
||||
expect(vm.name_description).toBe('test halted vm')
|
||||
expect(vm.high_availability).toBeTruthy()
|
||||
expect(vm.CPUs.number).toBe(1)
|
||||
expect(vm.memory.dynamic[0]).toBe(20e8)
|
||||
expect(vm.memory.dynamic[1]).toBe(90e8)
|
||||
expect(vm.memory.static[1]).toBe(100e8)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.recoveryStart()', () => {
|
||||
it('start a VM in recovery state', async () => {
|
||||
await xo.call('vm.recoveryStart', { id: hvmWithToolsId })
|
||||
|
||||
await waitObjectState(xo, hvmWithToolsId, vm => {
|
||||
expect(vm.boot.order).toBe('d')
|
||||
})
|
||||
|
||||
await waitObjectState(xo, hvmWithToolsId, vm => {
|
||||
expect(size(vm.current_operations)).toBe(0)
|
||||
expect(vm.power_state).toBe('Running')
|
||||
expect(vm.boot.order).not.toBe('d')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,57 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { config, waitObjectState, xo } from './../util'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setTimeout(30e3)
|
||||
})
|
||||
|
||||
describe('pci', () => {
|
||||
let vmId
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
beforeAll(async () => {
|
||||
vmId = await xo.call('vm.create', {
|
||||
name_label: 'vmTest',
|
||||
template: config.templatesId.debianCloud,
|
||||
VIFs: [{ network: config.labPoolNetworkId }],
|
||||
VDIs: [
|
||||
{
|
||||
device: '0',
|
||||
size: 1,
|
||||
SR: config.labPoolSrId,
|
||||
type: 'user',
|
||||
},
|
||||
],
|
||||
})
|
||||
await waitObjectState(xo, vmId, vm => {
|
||||
if (vm.type !== 'VM') throw new Error('retry')
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => xo.call('vm.delete', { id: vmId, delete_disks: true }))
|
||||
|
||||
// =================================================================
|
||||
|
||||
it('attaches the pci to the VM', async () => {
|
||||
await xo.call('vm.attachPci', {
|
||||
vm: vmId,
|
||||
pciId: config.pciId,
|
||||
})
|
||||
|
||||
await waitObjectState(xo, vmId, vm => {
|
||||
expect(vm.other.pci).toBe(config.pciId)
|
||||
})
|
||||
})
|
||||
|
||||
it('detaches the pci from the VM', async () => {
|
||||
await xo.call('vm.detachPci', { vm: vmId })
|
||||
|
||||
await waitObjectState(xo, vmId, vm => {
|
||||
expect(vm.other.pci).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,121 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { map, size } from 'lodash'
|
||||
|
||||
import { almostEqual, config, waitObjectState, xo } from './../util'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.setTimeout(100e3)
|
||||
})
|
||||
|
||||
describe('snapshotting', () => {
|
||||
let snapshotId
|
||||
let vmId
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
beforeAll(async () => {
|
||||
vmId = await xo.call('vm.create', {
|
||||
name_label: 'vmTest',
|
||||
name_description: 'creating a vm',
|
||||
template: config.templatesId.centOS,
|
||||
VIFs: [
|
||||
{ network: config.labPoolNetworkId },
|
||||
{ network: config.labPoolNetworkId },
|
||||
],
|
||||
VDIs: [
|
||||
{
|
||||
device: '0',
|
||||
size: 1,
|
||||
SR: config.labPoolSrId,
|
||||
type: 'user',
|
||||
},
|
||||
{
|
||||
device: '1',
|
||||
size: 1,
|
||||
SR: config.labPoolSrId,
|
||||
type: 'user',
|
||||
},
|
||||
{
|
||||
device: '2',
|
||||
size: 1,
|
||||
SR: config.labPoolSrId,
|
||||
type: 'user',
|
||||
},
|
||||
],
|
||||
})
|
||||
await waitObjectState(xo, vmId, vm => {
|
||||
if (vm.type !== 'VM') throw new Error('retry')
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => xo.call('vm.delete', { id: vmId, delete_disks: true }))
|
||||
|
||||
// =================================================================
|
||||
|
||||
describe('.snapshot()', () => {
|
||||
let $vm
|
||||
|
||||
it('snapshots a VM', async () => {
|
||||
snapshotId = await xo.call('vm.snapshot', {
|
||||
id: vmId,
|
||||
name: 'snapshot',
|
||||
})
|
||||
|
||||
const [, snapshot] = await Promise.all([
|
||||
waitObjectState(xo, vmId, vm => {
|
||||
$vm = vm
|
||||
expect(vm.snapshots[0]).toBe(snapshotId)
|
||||
}),
|
||||
xo.getOrWaitObject(snapshotId),
|
||||
])
|
||||
|
||||
expect(snapshot.type).toBe('VM-snapshot')
|
||||
expect(snapshot.name_label).toBe('snapshot')
|
||||
expect(snapshot.$snapshot_of).toBe(vmId)
|
||||
|
||||
almostEqual(snapshot, $vm, [
|
||||
'$snapshot_of',
|
||||
'$VBDs',
|
||||
'id',
|
||||
'installTime',
|
||||
'name_label',
|
||||
'snapshot_time',
|
||||
'snapshots',
|
||||
'type',
|
||||
'uuid',
|
||||
'VIFs',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('.revert()', () => {
|
||||
let createdSnapshotId
|
||||
|
||||
it('reverts a snapshot to its parent VM', async () => {
|
||||
await xo.call('vm.set', {
|
||||
id: vmId,
|
||||
name_label: 'vmRenamed',
|
||||
})
|
||||
await waitObjectState(xo, vmId, vm => {
|
||||
if (vm.name_label !== 'vmRenamed') throw new Error('retry')
|
||||
})
|
||||
|
||||
await xo.call('vm.revert', { id: snapshotId })
|
||||
|
||||
await waitObjectState(xo, vmId, vm => {
|
||||
expect(size(vm.current_operations)).toBe(0)
|
||||
expect(vm.name_label).toBe('vmTest')
|
||||
expect(size(vm.snapshots)).toBe(2)
|
||||
map(vm.snapshots, snapshot => {
|
||||
if (snapshot !== snapshotId) createdSnapshotId = snapshot
|
||||
})
|
||||
})
|
||||
|
||||
const createdSnapshot = await xo.getOrWaitObject(createdSnapshotId)
|
||||
expect(createdSnapshot.name_label).toBe('vmRenamed')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,114 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`user .changePassword() : changes the actual user password 1`] = `true`;
|
||||
|
||||
exports[`user .changePassword() : changes the actual user password 2`] = `[JsonRpcError: invalid credentials]`;
|
||||
|
||||
exports[`user .changePassword() : fails trying to change the password with invalid oldPassword 1`] = `[JsonRpcError: invalid credentials]`;
|
||||
|
||||
exports[`user .create() : creates a user with permission 1`] = `
|
||||
Object {
|
||||
"email": "wayne2@vates.fr",
|
||||
"groups": Array [],
|
||||
"id": Any<String>,
|
||||
"permission": "user",
|
||||
"preferences": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`user .create() : creates a user without permission 1`] = `
|
||||
Object {
|
||||
"email": "wayne1@vates.fr",
|
||||
"groups": Array [],
|
||||
"id": Any<String>,
|
||||
"preferences": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`user .create() : fails trying to create a user with an email already used 1`] = `[JsonRpcError: unknown error from the peer]`;
|
||||
|
||||
exports[`user .create() : fails trying to create a user without email 1`] = `[JsonRpcError: invalid parameters]`;
|
||||
|
||||
exports[`user .create() : fails trying to create a user without password 1`] = `[JsonRpcError: invalid parameters]`;
|
||||
|
||||
exports[`user .delete() : fails trying to delete a user with a nonexistent user 1`] = `[JsonRpcError: no such user nonexistentId]`;
|
||||
|
||||
exports[`user .delete() : fails trying to delete itself 1`] = `[JsonRpcError: a user cannot delete itself]`;
|
||||
|
||||
exports[`user .getAll() : gets all the users created 1`] = `
|
||||
Object {
|
||||
"email": "wayne4@vates.fr",
|
||||
"groups": Array [],
|
||||
"id": Any<String>,
|
||||
"permission": "user",
|
||||
"preferences": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`user .getAll() : gets all the users created 2`] = `
|
||||
Object {
|
||||
"email": "wayne5@vates.fr",
|
||||
"groups": Array [],
|
||||
"id": Any<String>,
|
||||
"permission": "user",
|
||||
"preferences": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`user .set() : fails trying to set a password with a non admin user connection 1`] = `[JsonRpcError: this properties can only changed by an administrator]`;
|
||||
|
||||
exports[`user .set() : fails trying to set a permission with a non admin user connection 1`] = `[JsonRpcError: this properties can only changed by an administrator]`;
|
||||
|
||||
exports[`user .set() : fails trying to set a property of a nonexistant user 1`] = `[JsonRpcError: no such user non-existent-id]`;
|
||||
|
||||
exports[`user .set() : fails trying to set an email with a non admin user connection 1`] = `[JsonRpcError: this properties can only changed by an administrator]`;
|
||||
|
||||
exports[`user .set() : fails trying to set its own permission as a non admin user 1`] = `[JsonRpcError: this properties can only changed by an administrator]`;
|
||||
|
||||
exports[`user .set() : fails trying to set its own permission as an admin 1`] = `[JsonRpcError: a user cannot change its own permission]`;
|
||||
|
||||
exports[`user .set() : sets a password 1`] = `
|
||||
Object {
|
||||
"email": "wayne3@vates.fr",
|
||||
"groups": Array [],
|
||||
"id": Any<String>,
|
||||
"permission": "none",
|
||||
"preferences": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`user .set() : sets a permission 1`] = `
|
||||
Object {
|
||||
"email": "wayne3@vates.fr",
|
||||
"groups": Array [],
|
||||
"id": Any<String>,
|
||||
"permission": "user",
|
||||
"preferences": Object {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`user .set() : sets a preference 1`] = `
|
||||
Object {
|
||||
"email": "wayne3@vates.fr",
|
||||
"groups": Array [],
|
||||
"id": Any<String>,
|
||||
"permission": "none",
|
||||
"preferences": Object {
|
||||
"filters": Object {
|
||||
"VM": Object {
|
||||
"test": "name_label: test",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`user .set() : sets an email 1`] = `
|
||||
Object {
|
||||
"email": "wayne_modified@vates.fr",
|
||||
"groups": Array [],
|
||||
"id": Any<String>,
|
||||
"permission": "none",
|
||||
"preferences": Object {},
|
||||
}
|
||||
`;
|
||||
@@ -1,264 +0,0 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { forOwn, keyBy } from 'lodash'
|
||||
|
||||
import xo, { testConnection, testWithOtherConnection } from '../_xoConnection'
|
||||
|
||||
const SIMPLE_USER = {
|
||||
email: 'wayne3@vates.fr',
|
||||
password: 'batman',
|
||||
}
|
||||
|
||||
const ADMIN_USER = {
|
||||
email: 'admin2@admin.net',
|
||||
password: 'admin',
|
||||
permission: 'admin',
|
||||
}
|
||||
|
||||
const withData = (data, fn) =>
|
||||
forOwn(data, (data, title) => {
|
||||
it(title, () => fn(data))
|
||||
})
|
||||
|
||||
describe('user', () => {
|
||||
describe('.create() :', () => {
|
||||
withData(
|
||||
{
|
||||
'creates a user without permission': {
|
||||
email: 'wayne1@vates.fr',
|
||||
password: 'batman1',
|
||||
},
|
||||
'creates a user with permission': {
|
||||
email: 'wayne2@vates.fr',
|
||||
password: 'batman2',
|
||||
permission: 'user',
|
||||
},
|
||||
},
|
||||
async data => {
|
||||
jest.setTimeout(6e3)
|
||||
const userId = await xo.createTempUser(data)
|
||||
expect(typeof userId).toBe('string')
|
||||
expect(await xo.getUser(userId)).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
})
|
||||
await testConnection({
|
||||
credentials: {
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
withData(
|
||||
{
|
||||
'fails trying to create a user without email': { password: 'batman' },
|
||||
'fails trying to create a user without password': {
|
||||
email: 'wayne@vates.fr',
|
||||
},
|
||||
},
|
||||
async data => {
|
||||
await expect(xo.createTempUser(data)).rejects.toMatchSnapshot()
|
||||
}
|
||||
)
|
||||
|
||||
it('fails trying to create a user with an email already used', async () => {
|
||||
await xo.createTempUser(SIMPLE_USER)
|
||||
await expect(xo.createTempUser(SIMPLE_USER)).rejects.toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('.changePassword() :', () => {
|
||||
it('changes the actual user password', async () => {
|
||||
jest.setTimeout(7e3)
|
||||
const user = {
|
||||
email: 'wayne7@vates.fr',
|
||||
password: 'batman',
|
||||
}
|
||||
const newPassword = 'newpwd'
|
||||
|
||||
await xo.createTempUser(user)
|
||||
await testWithOtherConnection(user, xo =>
|
||||
expect(
|
||||
xo.call('user.changePassword', {
|
||||
oldPassword: user.password,
|
||||
newPassword,
|
||||
})
|
||||
).resolves.toMatchSnapshot()
|
||||
)
|
||||
|
||||
await testConnection({
|
||||
credentials: {
|
||||
email: user.email,
|
||||
password: newPassword,
|
||||
},
|
||||
})
|
||||
|
||||
await expect(
|
||||
testConnection({
|
||||
credentials: user,
|
||||
})
|
||||
).rejects.toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('fails trying to change the password with invalid oldPassword', async () => {
|
||||
await xo.createTempUser(SIMPLE_USER)
|
||||
await testWithOtherConnection(SIMPLE_USER, xo =>
|
||||
expect(
|
||||
xo.call('user.changePassword', {
|
||||
oldPassword: 'falsepwd',
|
||||
newPassword: 'newpwd',
|
||||
})
|
||||
).rejects.toMatchSnapshot()
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.getAll() :', () => {
|
||||
it('gets all the users created', async () => {
|
||||
const userId1 = await xo.createTempUser({
|
||||
email: 'wayne4@vates.fr',
|
||||
password: 'batman',
|
||||
permission: 'user',
|
||||
})
|
||||
const userId2 = await xo.createTempUser({
|
||||
email: 'wayne5@vates.fr',
|
||||
password: 'batman',
|
||||
permission: 'user',
|
||||
})
|
||||
let users = await xo.call('user.getAll')
|
||||
expect(Array.isArray(users)).toBe(true)
|
||||
users = keyBy(users, 'id')
|
||||
expect(users[userId1]).toMatchSnapshot({ id: expect.any(String) })
|
||||
expect(users[userId2]).toMatchSnapshot({ id: expect.any(String) })
|
||||
})
|
||||
})
|
||||
|
||||
describe('.set() :', () => {
|
||||
withData(
|
||||
{
|
||||
'sets an email': { email: 'wayne_modified@vates.fr' },
|
||||
'sets a password': { password: 'newPassword' },
|
||||
'sets a permission': { permission: 'user' },
|
||||
'sets a preference': {
|
||||
preferences: {
|
||||
filters: {
|
||||
VM: {
|
||||
test: 'name_label: test',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async data => {
|
||||
jest.setTimeout(6e3)
|
||||
data.id = await xo.createTempUser(SIMPLE_USER)
|
||||
expect(await xo.call('user.set', data)).toBe(true)
|
||||
expect(await xo.getUser(data.id)).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
})
|
||||
|
||||
await testConnection({
|
||||
credentials: {
|
||||
email: data.email === undefined ? SIMPLE_USER.email : data.email,
|
||||
password:
|
||||
data.password === undefined
|
||||
? SIMPLE_USER.password
|
||||
: data.password,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
withData(
|
||||
{
|
||||
'fails trying to set an email with a non admin user connection': {
|
||||
email: 'wayne_modified@vates.fr',
|
||||
},
|
||||
'fails trying to set a password with a non admin user connection': {
|
||||
password: 'newPassword',
|
||||
},
|
||||
'fails trying to set a permission with a non admin user connection': {
|
||||
permission: 'user',
|
||||
},
|
||||
},
|
||||
async data => {
|
||||
data.id = await xo.createTempUser({
|
||||
email: 'wayne8@vates.fr',
|
||||
password: 'batman8',
|
||||
})
|
||||
await xo.createTempUser(SIMPLE_USER)
|
||||
|
||||
await testWithOtherConnection(SIMPLE_USER, xo =>
|
||||
expect(xo.call('user.set', data)).rejects.toMatchSnapshot()
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
withData(
|
||||
{
|
||||
'fails trying to set its own permission as a non admin user': SIMPLE_USER,
|
||||
'fails trying to set its own permission as an admin': {
|
||||
email: 'admin2@admin.net',
|
||||
password: 'batman',
|
||||
permission: 'admin',
|
||||
},
|
||||
},
|
||||
async data => {
|
||||
const id = await xo.createTempUser(data)
|
||||
const { email, password } = data
|
||||
await testWithOtherConnection({ email, password }, xo =>
|
||||
expect(
|
||||
xo.call('user.set', { id, permission: 'user' })
|
||||
).rejects.toMatchSnapshot()
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it('fails trying to set a property of a nonexistant user', async () => {
|
||||
await expect(
|
||||
xo.call('user.set', {
|
||||
id: 'non-existent-id',
|
||||
password: SIMPLE_USER.password,
|
||||
})
|
||||
).rejects.toMatchSnapshot()
|
||||
})
|
||||
|
||||
it.skip('fails trying to set an email already used', async () => {
|
||||
await xo.createTempUser(SIMPLE_USER)
|
||||
const userId2 = await xo.createTempUser({
|
||||
email: 'wayne6@vates.fr',
|
||||
password: 'batman',
|
||||
})
|
||||
|
||||
await expect(
|
||||
xo.call('user.set', {
|
||||
id: userId2,
|
||||
email: SIMPLE_USER.email,
|
||||
})
|
||||
).rejects.toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('.delete() :', () => {
|
||||
it('deletes a user successfully with id', async () => {
|
||||
const userId = await xo.call('user.create', SIMPLE_USER)
|
||||
expect(await xo.call('user.delete', { id: userId })).toBe(true)
|
||||
expect(await xo.getUser(userId)).toBe(undefined)
|
||||
})
|
||||
|
||||
it('fails trying to delete a user with a nonexistent user', async () => {
|
||||
await expect(
|
||||
xo.call('user.delete', { id: 'nonexistentId' })
|
||||
).rejects.toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('fails trying to delete itself', async () => {
|
||||
const id = await xo.createTempUser(ADMIN_USER)
|
||||
const { email, password } = ADMIN_USER
|
||||
await testWithOtherConnection({ email, password }, xo =>
|
||||
expect(xo.call('user.delete', { id })).rejects.toMatchSnapshot()
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,146 +0,0 @@
|
||||
import expect from 'must'
|
||||
import { find, forEach, map, cloneDeep } from 'lodash'
|
||||
|
||||
import config from './_config'
|
||||
|
||||
export const rejectionOf = promise =>
|
||||
promise.then(
|
||||
value => {
|
||||
throw value
|
||||
},
|
||||
reason => reason
|
||||
)
|
||||
|
||||
// =================================================================
|
||||
|
||||
async function getAllUsers(xo) {
|
||||
return xo.call('user.getAll')
|
||||
}
|
||||
|
||||
export async function getUser(xo, id) {
|
||||
const users = await getAllUsers(xo)
|
||||
return find(users, { id })
|
||||
}
|
||||
|
||||
export async function createUser(xo, userIds, params) {
|
||||
const userId = await xo.call('user.create', params)
|
||||
userIds.push(userId)
|
||||
return userId
|
||||
}
|
||||
|
||||
export async function deleteUsers(xo, userIds) {
|
||||
await Promise.all(
|
||||
map(userIds, userId => xo.call('user.delete', { id: userId }))
|
||||
)
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
|
||||
export function getAllHosts(xo) {
|
||||
return xo.objects.indexes.type.host
|
||||
}
|
||||
|
||||
export function getOneHost(xo) {
|
||||
const hosts = getAllHosts(xo)
|
||||
for (const id in hosts) {
|
||||
return hosts[id]
|
||||
}
|
||||
|
||||
throw new Error('no hosts found')
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
|
||||
export async function getNetworkId(xo) {
|
||||
const networks = xo.objects.indexes.type.network
|
||||
const network = find(networks, { name_label: config.network })
|
||||
return network.id
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
|
||||
export async function getVmXoTestPvId(xo) {
|
||||
const vms = xo.objects.indexes.type.VM
|
||||
const vm = find(vms, { name_label: config.pvVm })
|
||||
return vm.id
|
||||
}
|
||||
|
||||
export async function getVmToMigrateId(xo) {
|
||||
const vms = xo.objects.indexes.type.VM
|
||||
const vm = find(vms, { name_label: config.vmToMigrate })
|
||||
return vm.id
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
|
||||
export async function getSrId(xo) {
|
||||
const host = getOneHost(xo)
|
||||
const pool = await xo.getOrWaitObject(host.$poolId)
|
||||
return pool.default_SR
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
|
||||
export async function jobTest(xo) {
|
||||
const vmId = await getVmXoTestPvId(xo)
|
||||
const jobId = await xo.call('job.create', {
|
||||
job: {
|
||||
type: 'call',
|
||||
key: 'snapshot',
|
||||
method: 'vm.snapshot',
|
||||
paramsVector: {
|
||||
type: 'cross product',
|
||||
items: [
|
||||
{
|
||||
type: 'set',
|
||||
values: [
|
||||
{
|
||||
id: vmId,
|
||||
name: 'snapshot',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
return jobId
|
||||
}
|
||||
|
||||
export async function scheduleTest(xo, jobId) {
|
||||
const schedule = await xo.call('schedule.create', {
|
||||
jobId: jobId,
|
||||
cron: '* * * * * *',
|
||||
enabled: false,
|
||||
})
|
||||
return schedule
|
||||
}
|
||||
|
||||
export async function getSchedule(xo, id) {
|
||||
const schedule = xo.call('schedule.get', { id: id })
|
||||
return schedule
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
|
||||
export function deepDelete(obj, path) {
|
||||
const lastIndex = path.length - 1
|
||||
for (let i = 0; i < lastIndex; i++) {
|
||||
obj = obj[path[i]]
|
||||
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return
|
||||
}
|
||||
}
|
||||
delete obj[path[lastIndex]]
|
||||
}
|
||||
|
||||
export function almostEqual(actual, expected, ignoredAttributes) {
|
||||
actual = cloneDeep(actual)
|
||||
expected = cloneDeep(expected)
|
||||
forEach(ignoredAttributes, ignoredAttribute => {
|
||||
deepDelete(actual, ignoredAttribute.split('.'))
|
||||
deepDelete(expected, ignoredAttribute.split('.'))
|
||||
})
|
||||
expect(actual).to.be.eql(expected)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-usage-report",
|
||||
"version": "0.7.3",
|
||||
"version": "0.7.2",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
values,
|
||||
zipObject,
|
||||
} from 'lodash'
|
||||
import { ignoreErrors, promisify } from 'promise-toolbox'
|
||||
import { promisify } from 'promise-toolbox'
|
||||
import { readFile, writeFile } from 'fs'
|
||||
|
||||
// ===================================================================
|
||||
@@ -759,22 +759,14 @@ class UsageReportPlugin {
|
||||
}
|
||||
|
||||
async _sendReport(storeData) {
|
||||
const xo = this._xo
|
||||
if (xo.sendEmail === undefined) {
|
||||
ignoreErrors.call(xo.unloadPlugin('usage-report'))
|
||||
throw new Error(
|
||||
'The plugin usage-report requires the plugin transport-email to be loaded'
|
||||
)
|
||||
}
|
||||
|
||||
const data = await dataBuilder({
|
||||
xo,
|
||||
xo: this._xo,
|
||||
storedStatsPath: this._storedStatsPath,
|
||||
all: this._conf.all,
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
xo.sendEmail({
|
||||
this._xo.sendEmail({
|
||||
to: this._conf.emails,
|
||||
subject: `[Xen Orchestra] Xo Report - ${currDate}`,
|
||||
markdown: `Hi there,
|
||||
|
||||
@@ -17,11 +17,9 @@ createUserOnFirstSignin = true
|
||||
# their size just by looking at the beginning of the stream.
|
||||
#
|
||||
# But it is a guess, not a certainty, it depends on how the VHDs are formatted
|
||||
# by XenServer.
|
||||
#
|
||||
# This has been tested for 5 months, therefore it's enabled by default but can
|
||||
# be disabled specifically for a user if necessary.
|
||||
guessVhdSizeOnImport = true
|
||||
# by XenServer, therefore it's disabled for the moment but can be enabled
|
||||
# specifically for a user if necessary.
|
||||
guessVhdSizeOnImport = false
|
||||
|
||||
# Whether API logs should contains the full request/response on
|
||||
# errors.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.48.0",
|
||||
"version": "5.45.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -38,7 +38,7 @@
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/defined": "^0.0.0",
|
||||
"@xen-orchestra/emit-async": "^0.0.0",
|
||||
"@xen-orchestra/fs": "^0.10.1",
|
||||
"@xen-orchestra/fs": "^0.10.0",
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"@xen-orchestra/mixin": "^0.0.0",
|
||||
"ajv": "^6.1.1",
|
||||
@@ -123,7 +123,7 @@
|
||||
"value-matcher": "^0.2.0",
|
||||
"vhd-lib": "^0.7.0",
|
||||
"ws": "^6.0.0",
|
||||
"xen-api": "^0.27.1",
|
||||
"xen-api": "^0.27.0",
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.4.1",
|
||||
"xo-collection": "^0.4.1",
|
||||
|
||||
@@ -5,7 +5,6 @@ import { format, JsonRpcError } from 'json-rpc-peer'
|
||||
export async function set({
|
||||
host,
|
||||
|
||||
iscsiIqn,
|
||||
multipathing,
|
||||
name_label: nameLabel,
|
||||
name_description: nameDescription,
|
||||
@@ -13,13 +12,6 @@ export async function set({
|
||||
host = this.getXapiObject(host)
|
||||
|
||||
await Promise.all([
|
||||
iscsiIqn !== undefined &&
|
||||
(host.iscsi_iqn !== undefined
|
||||
? host.set_iscsi_iqn(iscsiIqn)
|
||||
: host.update_other_config(
|
||||
'iscsi_iqn',
|
||||
iscsiIqn === '' ? null : iscsiIqn
|
||||
)),
|
||||
nameDescription !== undefined && host.set_name_description(nameDescription),
|
||||
nameLabel !== undefined && host.set_name_label(nameLabel),
|
||||
multipathing !== undefined &&
|
||||
@@ -31,7 +23,6 @@ set.description = 'changes the properties of an host'
|
||||
|
||||
set.params = {
|
||||
id: { type: 'string' },
|
||||
iscsiIqn: { type: 'string', optional: true },
|
||||
name_label: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
|
||||
@@ -162,30 +162,43 @@ getPatchesDifference.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function mergeInto({ source, sources = [source], target, force }) {
|
||||
await this.checkPermissions(
|
||||
this.user.id,
|
||||
sources.map(source => [source, 'administrate'])
|
||||
)
|
||||
return this.mergeInto({
|
||||
force,
|
||||
sources,
|
||||
target,
|
||||
})
|
||||
export async function mergeInto({ source, target, force }) {
|
||||
const sourceHost = this.getObject(source.master)
|
||||
const targetHost = this.getObject(target.master)
|
||||
|
||||
if (sourceHost.productBrand !== targetHost.productBrand) {
|
||||
throw new Error(
|
||||
`a ${sourceHost.productBrand} pool cannot be merged into a ${targetHost.productBrand} pool`
|
||||
)
|
||||
}
|
||||
|
||||
const counterDiff = this.getPatchesDifference(source.master, target.master)
|
||||
if (counterDiff.length > 0) {
|
||||
const targetXapi = this.getXapi(target)
|
||||
await targetXapi.installPatches({
|
||||
patches: await targetXapi.findPatches(counterDiff),
|
||||
})
|
||||
}
|
||||
|
||||
const diff = this.getPatchesDifference(target.master, source.master)
|
||||
if (diff.length > 0) {
|
||||
const sourceXapi = this.getXapi(source)
|
||||
await sourceXapi.installPatches({
|
||||
patches: await sourceXapi.findPatches(diff),
|
||||
})
|
||||
}
|
||||
|
||||
await this.mergeXenPools(source._xapiId, target._xapiId, force)
|
||||
}
|
||||
|
||||
mergeInto.params = {
|
||||
force: { type: 'boolean', optional: true },
|
||||
source: { type: 'string', optional: true },
|
||||
sources: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
optional: true,
|
||||
},
|
||||
source: { type: 'string' },
|
||||
target: { type: 'string' },
|
||||
}
|
||||
|
||||
mergeInto.resolve = {
|
||||
source: ['source', 'pool', 'administrate'],
|
||||
target: ['target', 'pool', 'administrate'],
|
||||
}
|
||||
|
||||
|
||||
@@ -631,8 +631,6 @@ set.params = {
|
||||
|
||||
// set the VM boot firmware mode
|
||||
hvmBootFirmware: { type: ['string', 'null'], optional: true },
|
||||
|
||||
virtualizationMode: { type: 'string', optional: true },
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
@@ -1137,15 +1135,10 @@ resume.resolve = {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function revert({ snapshot, snapshotBefore }) {
|
||||
const { id: userId, permission } = this.user
|
||||
await this.checkPermissions(userId, [[snapshot.$snapshot_of, 'operate']])
|
||||
const newSnapshot = await this.getXapi(snapshot).revertVm(
|
||||
snapshot._xapiId,
|
||||
snapshotBefore
|
||||
)
|
||||
if (snapshotBefore && permission !== 'admin') {
|
||||
await this.addAcl(userId, newSnapshot.$id, 'admin')
|
||||
}
|
||||
await this.checkPermissions(this.user.id, [
|
||||
[snapshot.$snapshot_of, 'operate'],
|
||||
])
|
||||
return this.getXapi(snapshot).revertVm(snapshot._xapiId, snapshotBefore)
|
||||
}
|
||||
|
||||
revert.params = {
|
||||
|
||||
@@ -26,12 +26,7 @@ export const merge = (newValue, oldValue) => {
|
||||
|
||||
export const obfuscate = value => replace(value, OBFUSCATED_VALUE)
|
||||
|
||||
const SENSITIVE_PARAMS = {
|
||||
__proto__: null,
|
||||
cifspassword: true,
|
||||
password: true,
|
||||
token: true,
|
||||
}
|
||||
const SENSITIVE_PARAMS = { __proto__: null, password: true, token: true }
|
||||
|
||||
export function replace(value, replacement) {
|
||||
function helper(value, name) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as sensitiveValues from './sensitive-values'
|
||||
import ensureArray from './_ensureArray'
|
||||
import {
|
||||
extractProperty,
|
||||
@@ -77,7 +76,6 @@ const TRANSFORMS = {
|
||||
cores: cpuInfo && +cpuInfo.cpu_count,
|
||||
sockets: cpuInfo && +cpuInfo.socket_count,
|
||||
},
|
||||
zstdSupported: obj.restrictions.restrict_zstd_export === 'false',
|
||||
|
||||
// TODO
|
||||
// - ? networks = networksByPool.items[pool.id] (network.$pool.id)
|
||||
@@ -143,8 +141,7 @@ const TRANSFORMS = {
|
||||
},
|
||||
current_operations: obj.current_operations,
|
||||
hostname: obj.hostname,
|
||||
iscsiIqn: obj.iscsi_iqn ?? otherConfig.iscsi_iqn ?? '',
|
||||
zstdSupported: obj.license_params.restrict_zstd_export === 'false',
|
||||
iSCSI_name: otherConfig.iscsi_iqn || null,
|
||||
license_params: obj.license_params,
|
||||
license_server: obj.license_server,
|
||||
license_expiry: toTimestamp(obj.license_params.expiry),
|
||||
@@ -486,10 +483,7 @@ const TRANSFORMS = {
|
||||
attached: Boolean(obj.currently_attached),
|
||||
host: link(obj, 'host'),
|
||||
SR: link(obj, 'SR'),
|
||||
deviceConfig: sensitiveValues.replace(
|
||||
obj.device_config,
|
||||
'* obfuscated *'
|
||||
),
|
||||
device_config: obj.device_config,
|
||||
otherConfig: obj.other_config,
|
||||
}
|
||||
},
|
||||
@@ -534,7 +528,6 @@ const TRANSFORMS = {
|
||||
|
||||
name_description: obj.name_description,
|
||||
name_label: obj.name_label,
|
||||
parent: obj.sm_config['vhd-parent'],
|
||||
size: +obj.virtual_size,
|
||||
snapshots: link(obj, 'snapshots'),
|
||||
tags: obj.tags,
|
||||
|
||||
@@ -1158,9 +1158,6 @@ export default class Xapi extends XapiBase {
|
||||
{
|
||||
force: 'true',
|
||||
}
|
||||
// FIXME: missing param `vgu_map`, it does not cause issues ATM but it
|
||||
// might need to be changed one day.
|
||||
// {},
|
||||
)::pCatch({ code: 'TOO_MANY_STORAGE_MIGRATES' }, () =>
|
||||
pDelay(1e4).then(loop)
|
||||
)
|
||||
|
||||
@@ -255,7 +255,7 @@ export default {
|
||||
)) !== undefined
|
||||
) {
|
||||
if (getAll) {
|
||||
log.debug(
|
||||
log(
|
||||
`patch ${patch.name} (${id}) conflicts with installed patch ${conflictId}`
|
||||
)
|
||||
return
|
||||
@@ -271,7 +271,7 @@ export default {
|
||||
)) !== undefined
|
||||
) {
|
||||
if (getAll) {
|
||||
log.debug(`patches ${id} and ${conflictId} conflict with eachother`)
|
||||
log(`patches ${id} and ${conflictId} conflict with eachother`)
|
||||
return
|
||||
}
|
||||
throw new Error(
|
||||
|
||||
@@ -259,7 +259,7 @@ export default {
|
||||
affinityHost: {
|
||||
get: 'affinity',
|
||||
set: (value, vm) =>
|
||||
vm.set_affinity(value ? vm.$xapi.getObject(value).$ref : NULL_REF),
|
||||
vm.set_affinity(value ? this.getObject(value).$ref : NULL_REF),
|
||||
},
|
||||
|
||||
autoPoweron: {
|
||||
@@ -306,9 +306,7 @@ export default {
|
||||
get: vm => +vm.VCPUs_at_startup,
|
||||
set: [
|
||||
'VCPUs_at_startup',
|
||||
(value, vm) =>
|
||||
isVmRunning(vm) &&
|
||||
vm.$xapi.call('VM.set_VCPUs_number_live', vm.$ref, String(value)),
|
||||
(value, vm) => isVmRunning(vm) && vm.set_VCPUs_number_live(value),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -463,9 +461,8 @@ export default {
|
||||
|
||||
async revertVm(snapshotId, snapshotBefore = true) {
|
||||
const snapshot = this.getObject(snapshotId)
|
||||
let newSnapshot
|
||||
if (snapshotBefore) {
|
||||
newSnapshot = await this._snapshotVm(snapshot.$snapshot_of)
|
||||
await this._snapshotVm(snapshot.$snapshot_of)
|
||||
}
|
||||
await this.callAsync('VM.revert', snapshot.$ref)
|
||||
if (snapshot.snapshot_info['power-state-at-snapshot'] === 'Running') {
|
||||
@@ -476,7 +473,6 @@ export default {
|
||||
this.resumeVm(vm.$id)::ignoreErrors()
|
||||
}
|
||||
}
|
||||
return newSnapshot
|
||||
},
|
||||
|
||||
async resumeVm(vmId) {
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import { difference, flatten, isEmpty, uniq } from 'lodash'
|
||||
|
||||
export default class Pools {
|
||||
constructor(xo) {
|
||||
this._xo = xo
|
||||
}
|
||||
|
||||
async mergeInto({ sources: sourceIds, target, force }) {
|
||||
const { _xo } = this
|
||||
const targetHost = _xo.getObject(target.master)
|
||||
const sources = []
|
||||
const sourcePatches = {}
|
||||
|
||||
// Check hosts compatibility.
|
||||
for (const sourceId of sourceIds) {
|
||||
const source = _xo.getObject(sourceId)
|
||||
const sourceHost = _xo.getObject(source.master)
|
||||
if (sourceHost.productBrand !== targetHost.productBrand) {
|
||||
throw new Error(
|
||||
`a ${sourceHost.productBrand} pool cannot be merged into a ${targetHost.productBrand} pool`
|
||||
)
|
||||
}
|
||||
if (sourceHost.version !== targetHost.version) {
|
||||
throw new Error('The hosts are not compatible')
|
||||
}
|
||||
sources.push(source)
|
||||
sourcePatches[sourceId] = sourceHost.patches
|
||||
}
|
||||
|
||||
// Find missing patches on the target.
|
||||
const targetRequiredPatches = uniq(
|
||||
flatten(
|
||||
await Promise.all(
|
||||
sources.map(({ master }) =>
|
||||
_xo.getPatchesDifference(master, target.master)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Find missing patches on the sources.
|
||||
const allRequiredPatches = targetRequiredPatches.concat(
|
||||
targetHost.patches.map(patchId => _xo.getObject(patchId).name)
|
||||
)
|
||||
const sourceRequiredPatches = {}
|
||||
for (const sourceId of sourceIds) {
|
||||
const _sourcePatches = sourcePatches[sourceId].map(
|
||||
patchId => _xo.getObject(patchId).name
|
||||
)
|
||||
const requiredPatches = difference(allRequiredPatches, _sourcePatches)
|
||||
if (requiredPatches.length > 0) {
|
||||
sourceRequiredPatches[sourceId] = requiredPatches
|
||||
}
|
||||
}
|
||||
|
||||
// On XCP-ng, "installPatches" installs *all* the patches
|
||||
// whatever the patches argument is.
|
||||
// So we must not call it if there are no patches to install.
|
||||
if (targetRequiredPatches.length > 0 || !isEmpty(sourceRequiredPatches)) {
|
||||
// Find patches in parallel.
|
||||
const findPatchesPromises = []
|
||||
const sourceXapis = {}
|
||||
const targetXapi = _xo.getXapi(target)
|
||||
for (const sourceId of sourceIds) {
|
||||
const sourceXapi = (sourceXapis[sourceId] = _xo.getXapi(sourceId))
|
||||
findPatchesPromises.push(
|
||||
sourceXapi.findPatches(sourceRequiredPatches[sourceId] ?? [])
|
||||
)
|
||||
}
|
||||
const patchesName = await Promise.all([
|
||||
targetXapi.findPatches(targetRequiredPatches),
|
||||
...findPatchesPromises,
|
||||
])
|
||||
|
||||
// Install patches in parallel.
|
||||
const installPatchesPromises = []
|
||||
installPatchesPromises.push(
|
||||
targetXapi.installPatches({
|
||||
patches: patchesName[0],
|
||||
})
|
||||
)
|
||||
let i = 1
|
||||
for (const sourceId of sourceIds) {
|
||||
installPatchesPromises.push(
|
||||
sourceXapis[sourceId].installPatches({
|
||||
patches: patchesName[i++],
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(installPatchesPromises)
|
||||
}
|
||||
|
||||
// Merge the sources into the target sequentially to be safe.
|
||||
for (const source of sources) {
|
||||
await _xo.mergeXenPools(source._xapiId, target._xapiId, force)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,10 +293,6 @@ export default class {
|
||||
async connectXenServer(id) {
|
||||
const server = (await this._getXenServer(id)).properties
|
||||
|
||||
if (this._getXenServerStatus(id) !== 'disconnected') {
|
||||
throw new Error('the server is already connected')
|
||||
}
|
||||
|
||||
const xapi = (this._xapis[server.id] = new Xapi({
|
||||
allowUnauthorized: server.allowUnauthorized,
|
||||
readOnly: server.readOnly,
|
||||
|
||||
@@ -166,16 +166,20 @@ export default class Xo extends EventEmitter {
|
||||
|
||||
async registerHttpRequest(fn, data, { suffix = '' } = {}) {
|
||||
const { _httpRequestWatchers: watchers } = this
|
||||
let url
|
||||
|
||||
do {
|
||||
url = `/api/${await generateToken()}${suffix}`
|
||||
} while (url in watchers)
|
||||
const url = await (function generateUniqueUrl() {
|
||||
return generateToken().then(token => {
|
||||
const url = `/api/${token}${suffix}`
|
||||
|
||||
return url in watchers ? generateUniqueUrl() : url
|
||||
})
|
||||
})()
|
||||
|
||||
watchers[url] = {
|
||||
data,
|
||||
fn,
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.48.1",
|
||||
"version": "5.45.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import * as CM from 'complex-matcher'
|
||||
import { escapeRegExp } from 'lodash'
|
||||
|
||||
const valueToComplexMatcher = pattern => {
|
||||
if (typeof pattern === 'string') {
|
||||
return new CM.RegExpNode(`^${escapeRegExp(pattern)}$`, 'i')
|
||||
}
|
||||
|
||||
if (Array.isArray(pattern)) {
|
||||
return new CM.And(pattern.map(valueToComplexMatcher))
|
||||
}
|
||||
|
||||
if (pattern !== null && typeof pattern === 'object') {
|
||||
const keys = Object.keys(pattern)
|
||||
const { length } = keys
|
||||
|
||||
if (length === 1) {
|
||||
const [key] = keys
|
||||
if (key === '__and') {
|
||||
return new CM.And(pattern.__and.map(valueToComplexMatcher))
|
||||
}
|
||||
if (key === '__or') {
|
||||
return new CM.Or(pattern.__or.map(valueToComplexMatcher))
|
||||
}
|
||||
if (key === '__not') {
|
||||
return new CM.Not(valueToComplexMatcher(pattern.__not))
|
||||
}
|
||||
}
|
||||
|
||||
const children = []
|
||||
Object.keys(pattern).forEach(property => {
|
||||
const subpattern = pattern[property]
|
||||
if (subpattern !== undefined) {
|
||||
children.push(
|
||||
new CM.Property(property, valueToComplexMatcher(subpattern))
|
||||
)
|
||||
}
|
||||
})
|
||||
return children.length === 0 ? new CM.Null() : new CM.And(children)
|
||||
}
|
||||
|
||||
throw new Error('could not transform this pattern')
|
||||
}
|
||||
|
||||
export default pattern => {
|
||||
try {
|
||||
return valueToComplexMatcher(pattern).toString()
|
||||
} catch (error) {
|
||||
console.warn('constructQueryString', pattern, error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@@ -1827,7 +1827,7 @@ export default {
|
||||
vdiAction: 'Acción',
|
||||
|
||||
// Original text: "Attach disk"
|
||||
vdiAttachDevice: 'Adjuntar disco',
|
||||
vdiAttachDeviceButton: 'Adjuntar disco',
|
||||
|
||||
// Original text: "New disk"
|
||||
vbdCreateDeviceButton: 'Nuevo disco',
|
||||
|
||||
@@ -1865,7 +1865,7 @@ export default {
|
||||
vdiAction: 'Action',
|
||||
|
||||
// Original text: "Attach disk"
|
||||
vdiAttachDevice: 'Attacher un disque',
|
||||
vdiAttachDeviceButton: 'Attacher un disque',
|
||||
|
||||
// Original text: "New disk"
|
||||
vbdCreateDeviceButton: 'Nouveau disque',
|
||||
|
||||
@@ -1557,7 +1557,7 @@ export default {
|
||||
vdiAction: undefined,
|
||||
|
||||
// Original text: 'Attach disk'
|
||||
vdiAttachDevice: undefined,
|
||||
vdiAttachDeviceButton: undefined,
|
||||
|
||||
// Original text: 'New disk'
|
||||
vbdCreateDeviceButton: undefined,
|
||||
|
||||
@@ -1773,7 +1773,7 @@ export default {
|
||||
vdiAction: 'Művelet',
|
||||
|
||||
// Original text: "Attach disk"
|
||||
vdiAttachDevice: 'Diszk Hozzácsatolás',
|
||||
vdiAttachDeviceButton: 'Diszk Hozzácsatolás',
|
||||
|
||||
// Original text: "New disk"
|
||||
vbdCreateDeviceButton: 'Új diszk',
|
||||
|
||||
@@ -1567,7 +1567,7 @@ export default {
|
||||
vdiAction: 'Akcja',
|
||||
|
||||
// Original text: "Attach disk"
|
||||
vdiAttachDevice: 'Dołącz dysk',
|
||||
vdiAttachDeviceButton: 'Dołącz dysk',
|
||||
|
||||
// Original text: "New disk"
|
||||
vbdCreateDeviceButton: 'Nowy dysk',
|
||||
|
||||
@@ -1564,7 +1564,7 @@ export default {
|
||||
vdiAction: 'Ação',
|
||||
|
||||
// Original text: "Attach disk"
|
||||
vdiAttachDevice: 'Anexar disco',
|
||||
vdiAttachDeviceButton: 'Anexar disco',
|
||||
|
||||
// Original text: "New disk"
|
||||
vbdCreateDeviceButton: 'Novo disco',
|
||||
|
||||
@@ -2303,7 +2303,7 @@ export default {
|
||||
vdiAction: 'Aksiyon',
|
||||
|
||||
// Original text: "Attach disk"
|
||||
vdiAttachDevice: 'Disk tak',
|
||||
vdiAttachDeviceButton: 'Disk tak',
|
||||
|
||||
// Original text: "New disk"
|
||||
vbdCreateDeviceButton: 'Yeni disk',
|
||||
|
||||
@@ -1176,7 +1176,7 @@ export default {
|
||||
vdiAction: '操作',
|
||||
|
||||
// Original text: "Attach disk"
|
||||
vdiAttachDevice: '附加磁盘',
|
||||
vdiAttachDeviceButton: '附加磁盘',
|
||||
|
||||
// Original text: "New disk"
|
||||
vbdCreateDeviceButton: '新建磁盘',
|
||||
|
||||
@@ -21,7 +21,6 @@ const messages = {
|
||||
messageSubject: 'Subject',
|
||||
messageFrom: 'From',
|
||||
messageReply: 'Reply',
|
||||
tryXoa: 'Try XOA for free and deploy it here.',
|
||||
|
||||
editableLongClickPlaceholder: 'Long click to edit',
|
||||
editableClickPlaceholder: 'Click to edit',
|
||||
@@ -685,15 +684,6 @@ const messages = {
|
||||
vmConsoleLabel: 'Console',
|
||||
backupLabel: 'Backup',
|
||||
|
||||
// ----- SR general tab -----
|
||||
baseCopyTooltip:
|
||||
'{n, number} base cop{n, plural, one {y} other {ies}} ({usage})',
|
||||
diskTooltip: '{name} ({usage})',
|
||||
snapshotsTooltip:
|
||||
'{n, number} snapshot{n, plural, one {} other {s}} ({usage})',
|
||||
vdiOnVmTooltip: '{name} ({usage}) on {vmName}',
|
||||
vdisTooltip: '{n, number} VDI{n, plural, one {} other {s}} ({usage})',
|
||||
|
||||
// ----- SR advanced tab -----
|
||||
|
||||
srUnhealthyVdiDepth: 'Depth',
|
||||
@@ -771,14 +761,14 @@ const messages = {
|
||||
// ----- Pool actions ------
|
||||
addSrLabel: 'Add SR',
|
||||
addVmLabel: 'Add VM',
|
||||
addHostsLabel: 'Add hosts',
|
||||
addHostLabel: 'Add Host',
|
||||
missingPatchesPool:
|
||||
'The pool needs to install {nMissingPatches, number} patch{nMissingPatches, plural, one {} other {es}}. This operation may take a while.',
|
||||
missingPatchesHost:
|
||||
'The selected host{nHosts, plural, one {} other {s}} need{nHosts, plural, one {s} other {}} to install {nMissingPatches, number} patch{nMissingPatches, plural, one {} other {es}}. This operation may take a while.',
|
||||
'This host needs to install {nMissingPatches, number} patch{nMissingPatches, plural, one {} other {es}}. This operation may take a while.',
|
||||
patchUpdateNoInstall:
|
||||
'The selected host{nHosts, plural, one {} other {s}} cannot be added to the pool because the patches are not homogeneous.',
|
||||
addHostsErrorTitle: 'Adding host{nHosts, plural, one {} other {s}} failed',
|
||||
'This host cannot be added to the pool because the patches are not homogeneous.',
|
||||
addHostErrorTitle: 'Adding host failed',
|
||||
addHostNotHomogeneousErrorMessage: 'Host patches could not be homogenized.',
|
||||
disconnectServer: 'Disconnect',
|
||||
|
||||
@@ -804,9 +794,6 @@ const messages = {
|
||||
// ----- host stat tab -----
|
||||
statLoad: 'Load average',
|
||||
// ----- host advanced tab -----
|
||||
editHostIscsiIqnTitle: 'Edit iSCSI IQN',
|
||||
editHostIscsiIqnMessage:
|
||||
'Are you sure you want to edit the iSCSI IQN? This may result in failures connecting to existing SRs if the host is attached to iSCSI SRs.',
|
||||
hostTitleRamUsage: 'Host RAM usage:',
|
||||
memoryHostState:
|
||||
'RAM: {memoryUsed} used on {memoryTotal} ({memoryFree} free)',
|
||||
@@ -817,7 +804,7 @@ const messages = {
|
||||
hostAddress: 'Address',
|
||||
hostStatus: 'Status',
|
||||
hostBuildNumber: 'Build number',
|
||||
hostIscsiIqn: 'iSCSI IQN',
|
||||
hostIscsiName: 'iSCSI name',
|
||||
hostNoIscsiSr: 'Not connected to an iSCSI SR',
|
||||
hostMultipathingSrs: 'Click to see concerned SRs',
|
||||
hostMultipathingPaths:
|
||||
@@ -854,7 +841,6 @@ const messages = {
|
||||
supplementalPackInstallSuccessTitle: 'Installation success',
|
||||
supplementalPackInstallSuccessMessage:
|
||||
'Supplemental pack successfully installed.',
|
||||
uniqueHostIscsiIqnInfo: 'The iSCSI IQN must be unique. ',
|
||||
// ----- Host net tabs -----
|
||||
networkCreateButton: 'Add a network',
|
||||
pifDeviceLabel: 'Device',
|
||||
@@ -947,8 +933,6 @@ const messages = {
|
||||
powerStateRunning: 'Running',
|
||||
powerStateSuspended: 'Suspended',
|
||||
powerStatePaused: 'Paused',
|
||||
powerStateDisabled: 'Disabled',
|
||||
powerStateBusy: 'Busy',
|
||||
|
||||
// ----- VM home -----
|
||||
vmCurrentStatus: 'Current status:',
|
||||
@@ -982,11 +966,9 @@ const messages = {
|
||||
// ----- VM console tab -----
|
||||
copyToClipboardLabel: 'Copy',
|
||||
ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
|
||||
multilineCopyToClipboard: 'Multiline copy',
|
||||
tipLabel: 'Tip:',
|
||||
hideHeaderTooltip: 'Hide info',
|
||||
showHeaderTooltip: 'Show info',
|
||||
sendToClipboard: 'Send to clipboard',
|
||||
|
||||
// ----- VM container tab -----
|
||||
containerName: 'Name',
|
||||
@@ -1002,10 +984,8 @@ const messages = {
|
||||
containerRestart: 'Restart this container',
|
||||
|
||||
// ----- VM disk tab -----
|
||||
vdiAttachDeviceButton: 'Attach disk',
|
||||
vbdCreateDeviceButton: 'New disk',
|
||||
vdiAttachDevice: 'Attach disk',
|
||||
vdiAttachDeviceConfirm:
|
||||
'The selected VDI is already attached to this VM. Are you sure you want to continue?',
|
||||
vdiBootOrder: 'Boot order',
|
||||
vdiNameLabel: 'Name',
|
||||
vdiNameDescription: 'Description',
|
||||
@@ -1709,9 +1689,6 @@ const messages = {
|
||||
copyVmSelectSr: 'Select SR',
|
||||
copyVmsNoTargetSr: 'No target SR',
|
||||
copyVmsNoTargetSrMessage: 'A target SR is required to copy a VM',
|
||||
notSupportedZstdWarning:
|
||||
'Zstd is not supported on {nVms, number} VM{nVms, plural, one {} other {s}}',
|
||||
notSupportedZstdTooltip: 'Click to see the concerned VMs',
|
||||
fastCloneMode: 'Fast clone',
|
||||
fullCopyMode: 'Full copy',
|
||||
|
||||
@@ -1752,10 +1729,9 @@ const messages = {
|
||||
pillBonded: 'Bonded',
|
||||
bondedNetwork: 'Bonded network',
|
||||
privateNetwork: 'Private network',
|
||||
addPool: 'Add pool',
|
||||
|
||||
// ----- Add host -----
|
||||
hosts: 'Hosts',
|
||||
addHostSelectHost: 'Host',
|
||||
addHostNoHost: 'No host',
|
||||
addHostNoHostMessage: 'No host selected to be added',
|
||||
|
||||
@@ -1763,8 +1739,8 @@ const messages = {
|
||||
xenOrchestraServer: 'Xen Orchestra server',
|
||||
xenOrchestraWeb: 'Xen Orchestra web client',
|
||||
noProSupport: 'Professional support missing!',
|
||||
productionUse: 'Want to use in production?',
|
||||
getSupport: 'Get pro support with the Xen Orchestra Appliance at {website}',
|
||||
noProductionUse: 'Use in production at your own risk',
|
||||
downloadXoaFromWebsite: 'You can download the turnkey appliance at {website}',
|
||||
bugTracker: 'Bug Tracker',
|
||||
bugTrackerText: 'Issues? Report it!',
|
||||
community: 'Community',
|
||||
@@ -1804,8 +1780,11 @@ const messages = {
|
||||
refresh: 'Refresh',
|
||||
upgrade: 'Upgrade',
|
||||
downgrade: 'Downgrade',
|
||||
noUpdaterCommunity: 'No updater available for Community Edition',
|
||||
considerSubscribe:
|
||||
'Please consider subscribing and trying it with all the features for free during 15 days on {link}.',
|
||||
noUpdaterWarning:
|
||||
'Manual update could break your current installation due to dependencies issues, do it with caution',
|
||||
currentVersion: 'Current version:',
|
||||
register: 'Register',
|
||||
editRegistration: 'Edit registration',
|
||||
@@ -1835,8 +1814,6 @@ const messages = {
|
||||
unlistedChannelName: 'Unlisted channel name',
|
||||
selectChannel: 'Select channel',
|
||||
changeChannel: 'Change channel',
|
||||
updaterCommunity:
|
||||
'The Web updater, the release channels and the proxy settings are available in XOA.',
|
||||
|
||||
// ----- OS Disclaimer -----
|
||||
disclaimerTitle: 'Xen Orchestra from the sources',
|
||||
@@ -1911,7 +1888,7 @@ const messages = {
|
||||
OtpAuthentication: 'OTP authentication',
|
||||
|
||||
// ----- Usage -----
|
||||
others: '{nOthers, number} other{nOthers, plural, one {} other {s}}',
|
||||
others: 'Others',
|
||||
|
||||
// ----- Logs -----
|
||||
logUser: 'User',
|
||||
@@ -1941,9 +1918,7 @@ const messages = {
|
||||
reportBug: 'Report a bug',
|
||||
unhealthyVdiChainError: 'Job canceled to protect the VDI chain',
|
||||
backupRestartVm: "Restart VM's backup",
|
||||
backupForceRestartVm: "Force restart VM's backup",
|
||||
backupRestartFailedVms: "Restart failed VMs' backup",
|
||||
backupForceRestartFailedVms: "Force restart failed VMs' backup",
|
||||
clickForMoreInformation: 'Click for more information',
|
||||
|
||||
// ----- IPs ------
|
||||
@@ -2007,6 +1982,7 @@ const messages = {
|
||||
importConfigError: 'Error while importing config file',
|
||||
exportConfig: 'Export',
|
||||
downloadConfig: 'Download current config',
|
||||
noConfigImportCommunity: 'No config import available for Community Edition',
|
||||
|
||||
// ----- SR -----
|
||||
srReconnectAllModalTitle: 'Reconnect all hosts',
|
||||
@@ -2052,7 +2028,7 @@ const messages = {
|
||||
xosanAvailableSpace: 'Available space',
|
||||
xosanDiskLossLegend: '* Can fail without data loss',
|
||||
xosanCreate: 'Create',
|
||||
xosanCommunity: 'XOSAN is available in XOA',
|
||||
xosanCommunity: 'No XOSAN available for Community Edition',
|
||||
xosanNew: 'New',
|
||||
xosanAdvanced: 'Advanced',
|
||||
xosanRemoveSubvolumes: 'Remove subvolumes',
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import _ from 'intl'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
import decorate from './apply-decorators'
|
||||
import { Select } from './form'
|
||||
import Select from 'form/select'
|
||||
|
||||
const OPTIONS = [
|
||||
{
|
||||
@@ -16,40 +12,27 @@ const OPTIONS = [
|
||||
label: _('chooseCompressionGzipOption'),
|
||||
value: 'native',
|
||||
},
|
||||
]
|
||||
|
||||
const OPTIONS_WITH_ZSTD = [
|
||||
...OPTIONS,
|
||||
{
|
||||
label: _('chooseCompressionZstdOption'),
|
||||
value: 'zstd',
|
||||
},
|
||||
]
|
||||
|
||||
const SELECT_COMPRESSION_PROP_TYPES = {
|
||||
showZstd: PropTypes.bool,
|
||||
const SelectCompression = ({ onChange, value, ...props }) => (
|
||||
<Select
|
||||
labelKey='label'
|
||||
onChange={onChange}
|
||||
options={OPTIONS}
|
||||
required
|
||||
simpleValue
|
||||
value={value}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
SelectCompression.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
}
|
||||
|
||||
const SelectCompression = decorate([
|
||||
provideState({
|
||||
computed: {
|
||||
options: (_, { showZstd }) => (showZstd ? OPTIONS_WITH_ZSTD : OPTIONS),
|
||||
selectProps: (_, props) =>
|
||||
omit(props, Object.keys(SELECT_COMPRESSION_PROP_TYPES)),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ onChange, state, value }) => (
|
||||
<Select
|
||||
labelKey='label'
|
||||
options={state.options}
|
||||
required
|
||||
simpleValue
|
||||
{...state.selectProps}
|
||||
/>
|
||||
),
|
||||
])
|
||||
|
||||
SelectCompression.defaultProps = { showZstd: true }
|
||||
SelectCompression.propTypes = SELECT_COMPRESSION_PROP_TYPES
|
||||
export { SelectCompression as default }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { get, identity, isEmpty } from 'lodash'
|
||||
import * as CM from 'complex-matcher'
|
||||
import { escapeRegExp, get, identity, isEmpty } from 'lodash'
|
||||
|
||||
import { EMPTY_OBJECT } from './../utils'
|
||||
|
||||
@@ -56,4 +57,56 @@ export const constructSmartPattern = (
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const valueToComplexMatcher = pattern => {
|
||||
if (typeof pattern === 'string') {
|
||||
return new CM.RegExpNode(`^${escapeRegExp(pattern)}$`, 'i')
|
||||
}
|
||||
|
||||
if (Array.isArray(pattern)) {
|
||||
return new CM.And(pattern.map(valueToComplexMatcher))
|
||||
}
|
||||
|
||||
if (pattern !== null && typeof pattern === 'object') {
|
||||
const keys = Object.keys(pattern)
|
||||
const { length } = keys
|
||||
|
||||
if (length === 1) {
|
||||
const [key] = keys
|
||||
if (key === '__and') {
|
||||
return new CM.And(pattern.__and.map(valueToComplexMatcher))
|
||||
}
|
||||
if (key === '__or') {
|
||||
return new CM.Or(pattern.__or.map(valueToComplexMatcher))
|
||||
}
|
||||
if (key === '__not') {
|
||||
return new CM.Not(valueToComplexMatcher(pattern.__not))
|
||||
}
|
||||
}
|
||||
|
||||
const children = []
|
||||
Object.keys(pattern).forEach(property => {
|
||||
const subpattern = pattern[property]
|
||||
if (subpattern !== undefined) {
|
||||
children.push(
|
||||
new CM.Property(property, valueToComplexMatcher(subpattern))
|
||||
)
|
||||
}
|
||||
})
|
||||
return children.length === 0 ? new CM.Null() : new CM.And(children)
|
||||
}
|
||||
|
||||
throw new Error('could not transform this pattern')
|
||||
}
|
||||
|
||||
export const constructQueryString = pattern => {
|
||||
try {
|
||||
return valueToComplexMatcher(pattern).toString()
|
||||
} catch (error) {
|
||||
console.warn('constructQueryString', pattern, error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default from './preview'
|
||||
|
||||
@@ -6,12 +6,12 @@ import { createSelector } from 'reselect'
|
||||
import { filter, map, pickBy } from 'lodash'
|
||||
|
||||
import Component from './../base-component'
|
||||
import constructQueryString from '../construct-query-string'
|
||||
import Icon from './../icon'
|
||||
import Link from './../link'
|
||||
import renderXoItem from './../render-xo-item'
|
||||
import Tooltip from './../tooltip'
|
||||
import { Card, CardBlock, CardHeader } from './../card'
|
||||
import { constructQueryString } from './index'
|
||||
|
||||
const SAMPLE_SIZE_OF_MATCHING_VMS = 3
|
||||
|
||||
|
||||
@@ -2,41 +2,25 @@ import _ from 'intl'
|
||||
import classNames from 'classnames'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { cloneElement } from 'react'
|
||||
import { compact, sum } from 'lodash'
|
||||
import sum from 'lodash/sum'
|
||||
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
const Usage = ({ total, children }) => {
|
||||
const limit = total / 400
|
||||
const othersValues = compact(
|
||||
React.Children.map(children, child => {
|
||||
const { value } = child.props
|
||||
return value < limit && value
|
||||
})
|
||||
)
|
||||
const othersValues = React.Children.map(children, child => {
|
||||
const { value } = child.props
|
||||
return value < limit && value
|
||||
})
|
||||
const othersTotal = sum(othersValues)
|
||||
const nOthers = othersValues.length
|
||||
return (
|
||||
<span className='usage'>
|
||||
{nOthers > 1 ? (
|
||||
<span>
|
||||
{React.Children.map(
|
||||
children,
|
||||
(child, index) =>
|
||||
child.props.value > limit && cloneElement(child, { total })
|
||||
)}
|
||||
<Element
|
||||
others
|
||||
tooltip={_('others', { nOthers })}
|
||||
total={total}
|
||||
value={othersTotal}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
React.Children.map(children, (child, index) =>
|
||||
cloneElement(child, { total })
|
||||
)
|
||||
{React.Children.map(
|
||||
children,
|
||||
(child, index) =>
|
||||
child.props.value > limit && cloneElement(child, { total })
|
||||
)}
|
||||
<Element others tooltip={_('others')} total={total} value={othersTotal} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -662,13 +662,3 @@ export const adminOnly = Component =>
|
||||
})(({ _isAdmin, ...props }) =>
|
||||
_isAdmin ? <Component {...props} /> : <_NotFound />
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const TryXoa = ({ page }) => (
|
||||
<a
|
||||
href={`https://xen-orchestra.com/#/xoa?pk_campaign=xoa_source_upgrade&pk_kwd=${page}`}
|
||||
>
|
||||
{_('tryXoa')}
|
||||
</a>
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
createGetObjectsOfType,
|
||||
createSelector,
|
||||
} from 'selectors'
|
||||
import { flatten, forEach, isEmpty, map, uniq } from 'lodash'
|
||||
import { forEach } from 'lodash'
|
||||
import { getPatchesDifference } from 'xo'
|
||||
import { SelectHost } from 'select-objects'
|
||||
|
||||
@@ -26,7 +26,7 @@ import { SelectHost } from 'select-objects'
|
||||
const { $pool } = host
|
||||
if ($pool !== poolId) {
|
||||
const previousHost = visitedPools[$pool]
|
||||
if (previousHost !== undefined) {
|
||||
if (previousHost) {
|
||||
delete singleHosts[previousHost]
|
||||
} else {
|
||||
const { id } = host
|
||||
@@ -41,17 +41,17 @@ import { SelectHost } from 'select-objects'
|
||||
}),
|
||||
{ withRef: true }
|
||||
)
|
||||
export default class AddHostsModal extends BaseComponent {
|
||||
export default class AddHostModal extends BaseComponent {
|
||||
get value() {
|
||||
const { nHostsMissingPatches, nPoolMissingPatches } = this.state
|
||||
const { nHostMissingPatches, nPoolMissingPatches } = this.state
|
||||
if (
|
||||
process.env.XOA_PLAN < 2 &&
|
||||
(nHostsMissingPatches > 0 || nPoolMissingPatches > 0)
|
||||
(nHostMissingPatches > 0 || nPoolMissingPatches > 0)
|
||||
) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return { hosts: this.state.hosts }
|
||||
return { host: this.state.host }
|
||||
}
|
||||
|
||||
_getHostPredicate = createSelector(
|
||||
@@ -59,61 +59,44 @@ export default class AddHostsModal extends BaseComponent {
|
||||
singleHosts => host => singleHosts[host.id]
|
||||
)
|
||||
|
||||
_onChangeHosts = async hosts => {
|
||||
if (isEmpty(hosts)) {
|
||||
_onChangeHost = async host => {
|
||||
if (host === null) {
|
||||
this.setState({
|
||||
hosts,
|
||||
nHostsMissingPatches: undefined,
|
||||
host,
|
||||
nHostMissingPatches: undefined,
|
||||
nPoolMissingPatches: undefined,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const { master } = this.props.pool
|
||||
const hostMissingPatches = await getPatchesDifference(host.id, master)
|
||||
const poolMissingPatches = await getPatchesDifference(master, host.id)
|
||||
|
||||
this.setState({
|
||||
hosts,
|
||||
nHostsMissingPatches: uniq(
|
||||
flatten(
|
||||
await Promise.all(
|
||||
map(hosts, ({ id: hostId }) => getPatchesDifference(hostId, master))
|
||||
)
|
||||
)
|
||||
).length,
|
||||
nPoolMissingPatches: uniq(
|
||||
flatten(
|
||||
await Promise.all(
|
||||
map(hosts, ({ id: hostId }) => getPatchesDifference(master, hostId))
|
||||
)
|
||||
)
|
||||
).length,
|
||||
host,
|
||||
nHostMissingPatches: hostMissingPatches.length,
|
||||
nPoolMissingPatches: poolMissingPatches.length,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hosts, nHostsMissingPatches, nPoolMissingPatches } = this.state
|
||||
const canMulti = +process.env.XOA_PLAN > 3
|
||||
const { nHostMissingPatches, nPoolMissingPatches } = this.state
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('hosts')}</Col>
|
||||
<Col size={6}>{_('addHostSelectHost')}</Col>
|
||||
<Col size={6}>
|
||||
<SelectHost
|
||||
multi={canMulti}
|
||||
onChange={
|
||||
canMulti
|
||||
? this._onChangeHosts
|
||||
: host =>
|
||||
this._onChangeHosts(host !== null ? [host] : undefined)
|
||||
}
|
||||
onChange={this._onChangeHost}
|
||||
predicate={this._getHostPredicate()}
|
||||
value={
|
||||
canMulti ? hosts : hosts !== undefined ? hosts[0] : undefined
|
||||
}
|
||||
value={this.state.host}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
<br />
|
||||
{(nHostsMissingPatches > 0 || nPoolMissingPatches > 0) && (
|
||||
{(nHostMissingPatches > 0 || nPoolMissingPatches > 0) && (
|
||||
<div>
|
||||
{process.env.XOA_PLAN > 1 ? (
|
||||
<div>
|
||||
@@ -129,14 +112,13 @@ export default class AddHostsModal extends BaseComponent {
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
)}
|
||||
{nHostsMissingPatches > 0 && (
|
||||
{nHostMissingPatches > 0 && (
|
||||
<SingleLineRow>
|
||||
<Col>
|
||||
<span className='text-danger'>
|
||||
<Icon icon='error' />{' '}
|
||||
{_('missingPatchesHost', {
|
||||
nHosts: hosts.length,
|
||||
nMissingPatches: nHostsMissingPatches,
|
||||
nMissingPatches: nHostMissingPatches,
|
||||
})}
|
||||
</span>
|
||||
</Col>
|
||||
@@ -144,9 +126,7 @@ export default class AddHostsModal extends BaseComponent {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
_('patchUpdateNoInstall', {
|
||||
nHosts: hosts.length,
|
||||
})
|
||||
_('patchUpdateNoInstall')
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -1,25 +1,14 @@
|
||||
import BaseComponent from 'base-component'
|
||||
import React from 'react'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { injectIntl } from 'react-intl'
|
||||
|
||||
import _, { messages } from '../../intl'
|
||||
import SelectCompression from '../../select-compression'
|
||||
import SingleLineRow from '../../single-line-row'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Col } from '../../grid'
|
||||
import { connectStore } from '../../utils'
|
||||
import { createGetObject, createSelector } from '../../selectors'
|
||||
import { SelectSr } from '../../select-objects'
|
||||
import { injectIntl } from 'react-intl'
|
||||
|
||||
@connectStore(
|
||||
{
|
||||
isZstdSupported: createSelector(
|
||||
createGetObject((_, { vm }) => vm.$container),
|
||||
container => container === undefined || container.zstdSupported
|
||||
),
|
||||
},
|
||||
{ withRef: true }
|
||||
)
|
||||
class CopyVmModalBody extends BaseComponent {
|
||||
state = {
|
||||
compression: '',
|
||||
@@ -38,10 +27,7 @@ class CopyVmModalBody extends BaseComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl: { formatMessage },
|
||||
isZstdSupported,
|
||||
} = this.props
|
||||
const { formatMessage } = this.props.intl
|
||||
const { compression, copyMode, name, sr } = this.state
|
||||
|
||||
return process.env.XOA_PLAN > 2 ? (
|
||||
@@ -95,7 +81,6 @@ class CopyVmModalBody extends BaseComponent {
|
||||
<SelectCompression
|
||||
disabled={copyMode !== 'fullCopy'}
|
||||
onChange={this.linkState('compression')}
|
||||
showZstd={isZstdSupported}
|
||||
value={compression}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@@ -7,29 +7,16 @@ import BaseComponent from 'base-component'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Col } from 'grid'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { SelectSr } from 'select-objects'
|
||||
import { buildTemplate, connectStore } from 'utils'
|
||||
|
||||
import constructQueryString from '../../construct-query-string'
|
||||
import Icon from '../../icon'
|
||||
import Link from '../../link'
|
||||
import SelectCompression from '../../select-compression'
|
||||
import Tooltip from '../../tooltip'
|
||||
import {
|
||||
createCollectionWrapper,
|
||||
createGetObjectsOfType,
|
||||
createSelector,
|
||||
} from '../../selectors'
|
||||
|
||||
@connectStore(
|
||||
() => {
|
||||
const getVms = createGetObjectsOfType('VM').pick((_, props) => props.vms)
|
||||
return {
|
||||
containers: createSelector(
|
||||
createGetObjectsOfType('pool'),
|
||||
createGetObjectsOfType('host'),
|
||||
(pools, hosts) => ({ ...pools, ...hosts })
|
||||
),
|
||||
vms: getVms,
|
||||
}
|
||||
},
|
||||
@@ -72,41 +59,9 @@ class CopyVmsModalBody extends BaseComponent {
|
||||
_onChangeNamePattern = event =>
|
||||
this.setState({ namePattern: event.target.value })
|
||||
|
||||
_getVmsWithoutZstd = createSelector(
|
||||
() => this.props.vms,
|
||||
() => this.props.containers,
|
||||
createCollectionWrapper((vms, containers) => {
|
||||
const vmIds = []
|
||||
for (const id in vms) {
|
||||
const container = containers[vms[id].$container]
|
||||
if (container !== undefined && !container.zstdSupported) {
|
||||
vmIds.push(id)
|
||||
}
|
||||
}
|
||||
return vmIds
|
||||
})
|
||||
)
|
||||
|
||||
_getVmsWithoutZstdLink = createSelector(
|
||||
this._getVmsWithoutZstd,
|
||||
vms => ({
|
||||
pathname: '/home',
|
||||
query: {
|
||||
t: 'VM',
|
||||
s: constructQueryString({
|
||||
id: {
|
||||
__or: vms,
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
render() {
|
||||
const { formatMessage } = this.props.intl
|
||||
const { compression, namePattern, sr } = this.state
|
||||
const nVmsWithoutZstd =
|
||||
compression === 'zstd' ? this._getVmsWithoutZstd().length : 0
|
||||
return process.env.XOA_PLAN > 2 ? (
|
||||
<div>
|
||||
<SingleLineRow>
|
||||
@@ -136,20 +91,6 @@ class CopyVmsModalBody extends BaseComponent {
|
||||
onChange={this.linkState('compression')}
|
||||
value={compression}
|
||||
/>
|
||||
{compression === 'zstd' && nVmsWithoutZstd > 0 && (
|
||||
<Tooltip content={_('notSupportedZstdTooltip')}>
|
||||
<Link
|
||||
className='text-warning'
|
||||
target='_blank'
|
||||
to={this._getVmsWithoutZstdLink()}
|
||||
>
|
||||
<Icon icon='alarm' />{' '}
|
||||
{_('notSupportedZstdWarning', {
|
||||
nVms: nVmsWithoutZstd,
|
||||
})}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
import BaseComponent from 'base-component'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import _ from '../../intl'
|
||||
import SelectCompression from '../../select-compression'
|
||||
import { connectStore } from '../../utils'
|
||||
import { Container, Row, Col } from '../../grid'
|
||||
import { createGetObject, createSelector } from '../../selectors'
|
||||
|
||||
@connectStore(
|
||||
{
|
||||
isZstdSupported: createSelector(
|
||||
createGetObject((_, { vm }) => vm.$container),
|
||||
container => container === undefined || container.zstdSupported
|
||||
),
|
||||
},
|
||||
{ withRef: true }
|
||||
)
|
||||
export default class ExportVmModalBody extends BaseComponent {
|
||||
state = {
|
||||
compression: '',
|
||||
@@ -37,7 +25,6 @@ export default class ExportVmModalBody extends BaseComponent {
|
||||
<Col mediumSize={6}>
|
||||
<SelectCompression
|
||||
onChange={this.linkState('compression')}
|
||||
showZstd={this.props.isZstdSupported}
|
||||
value={this.state.compression}
|
||||
/>
|
||||
</Col>
|
||||
@@ -46,7 +33,3 @@ export default class ExportVmModalBody extends BaseComponent {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ExportVmModalBody.propTypes = {
|
||||
vm: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
@@ -562,6 +562,7 @@ export const getPatchesDifference = (source, target) =>
|
||||
target: resolveId(target),
|
||||
})
|
||||
|
||||
import AddHostModalBody from './add-host-modal' // eslint-disable-line import/first
|
||||
export const addHostToPool = (pool, host) => {
|
||||
if (host) {
|
||||
return confirm({
|
||||
@@ -579,23 +580,18 @@ export const addHostToPool = (pool, host) => {
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
import AddHostsModalBody from './add-hosts-modal' // eslint-disable-line import/first
|
||||
export const addHostsToPool = pool =>
|
||||
confirm({
|
||||
return confirm({
|
||||
icon: 'add',
|
||||
title: _('addHostsLabel'),
|
||||
body: <AddHostsModalBody pool={pool} />,
|
||||
title: _('addHostModalTitle'),
|
||||
body: <AddHostModalBody pool={pool} />,
|
||||
}).then(params => {
|
||||
const { hosts } = params
|
||||
if (isEmpty(hosts)) {
|
||||
if (!params.host) {
|
||||
error(_('addHostNoHost'), _('addHostNoHostMessage'))
|
||||
return
|
||||
}
|
||||
|
||||
return _call('pool.mergeInto', {
|
||||
sources: map(hosts, '$pool'),
|
||||
source: params.host.$pool,
|
||||
target: pool.id,
|
||||
force: true,
|
||||
}).catch(error => {
|
||||
@@ -603,12 +599,10 @@ export const addHostsToPool = pool =>
|
||||
throw error
|
||||
}
|
||||
|
||||
error(
|
||||
_('addHostsErrorTitle', { nHosts: hosts.length }),
|
||||
_('addHostNotHomogeneousErrorMessage')
|
||||
)
|
||||
error(_('addHostErrorTitle'), _('addHostNotHomogeneousErrorMessage'))
|
||||
})
|
||||
})
|
||||
}, noop)
|
||||
}
|
||||
|
||||
export const detachHost = host =>
|
||||
confirm({
|
||||
@@ -1457,7 +1451,7 @@ export const importVms = (vms, sr) =>
|
||||
import ExportVmModalBody from './export-vm-modal' // eslint-disable-line import/first
|
||||
export const exportVm = vm =>
|
||||
confirm({
|
||||
body: <ExportVmModalBody vm={vm} />,
|
||||
body: <ExportVmModalBody />,
|
||||
icon: 'export',
|
||||
title: _('exportVmLabel'),
|
||||
}).then(compress => {
|
||||
@@ -1655,9 +1649,7 @@ export const createNetwork = params => _call('network.create', params)
|
||||
export const createBondedNetwork = params =>
|
||||
_call('network.createBonded', params)
|
||||
export const createPrivateNetwork = params =>
|
||||
_call('sdnController.createPrivateNetwork', params)
|
||||
export const createCrossPoolPrivateNetwork = params =>
|
||||
_call('sdnController.createCrossPoolPrivateNetwork', params)
|
||||
_call('plugin.SDNController.createPrivateNetwork', params)
|
||||
|
||||
export const deleteNetwork = network =>
|
||||
confirm({
|
||||
@@ -2008,16 +2000,7 @@ export const editBackupNgJob = props =>
|
||||
|
||||
export const getBackupNgJob = id => _call('backupNg.getJob', { id })
|
||||
|
||||
export const runBackupNgJob = ({ force, ...params }) => {
|
||||
if (force) {
|
||||
params.settings = {
|
||||
'': {
|
||||
bypassVdiChainsCheck: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
return _call('backupNg.runJob', params)
|
||||
}
|
||||
export const runBackupNgJob = params => _call('backupNg.runJob', params)
|
||||
|
||||
export const listVmBackups = remotes =>
|
||||
_call('backupNg.listVmBackups', { remotes: resolveIds(remotes) })
|
||||
|
||||
@@ -140,10 +140,6 @@
|
||||
@extend .fa;
|
||||
@extend .fa-play;
|
||||
}
|
||||
&-force-restart {
|
||||
@extend .fa;
|
||||
@extend .fa-forward;
|
||||
}
|
||||
&-ssh-key {
|
||||
@extend .fa;
|
||||
@extend .fa-key;
|
||||
@@ -328,27 +324,27 @@
|
||||
&-running {
|
||||
@extend .fa;
|
||||
@extend .fa-desktop;
|
||||
@extend .xo-status-running;
|
||||
@extend .text-success;
|
||||
}
|
||||
&-suspended {
|
||||
@extend .fa;
|
||||
@extend .fa-desktop;
|
||||
@extend .xo-status-suspended;
|
||||
@extend .text-primary;
|
||||
}
|
||||
&-paused {
|
||||
@extend .fa;
|
||||
@extend .fa-desktop;
|
||||
@extend .xo-status-paused;
|
||||
@extend .text-muted;
|
||||
}
|
||||
&-halted {
|
||||
@extend .fa;
|
||||
@extend .fa-desktop;
|
||||
@extend .xo-status-halted;
|
||||
@extend .text-danger;
|
||||
}
|
||||
&-busy {
|
||||
@extend .fa;
|
||||
@extend .fa-desktop;
|
||||
@extend .xo-status-busy;
|
||||
@extend .text-warning;
|
||||
}
|
||||
|
||||
// Actions
|
||||
@@ -457,7 +453,7 @@
|
||||
&-disabled {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
@extend .xo-status-disabled;
|
||||
@extend .xo-status-busy;
|
||||
}
|
||||
|
||||
&-all-connected {
|
||||
@@ -530,27 +526,27 @@
|
||||
&-running {
|
||||
@extend .fa;
|
||||
@extend .fa-server;
|
||||
@extend .xo-status-running;
|
||||
@extend .text-success;
|
||||
}
|
||||
&-halted {
|
||||
@extend .fa;
|
||||
@extend .fa-server;
|
||||
@extend .xo-status-halted;
|
||||
@extend .text-danger;
|
||||
}
|
||||
&-disabled {
|
||||
@extend .fa;
|
||||
@extend .fa-server;
|
||||
@extend .xo-status-disabled;
|
||||
}
|
||||
&-busy {
|
||||
@extend .fa;
|
||||
@extend .fa-server;
|
||||
@extend .xo-status-busy;
|
||||
@extend .text-warning;
|
||||
}
|
||||
&-forget {
|
||||
@extend .fa;
|
||||
@extend .fa-ban;
|
||||
}
|
||||
&-working {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
@extend .text-warning;
|
||||
}
|
||||
|
||||
// Actions
|
||||
&-enable {
|
||||
@@ -732,11 +728,6 @@
|
||||
@extend .fa-sign-out;
|
||||
}
|
||||
|
||||
&-multiline-clipboard {
|
||||
@extend .fa;
|
||||
@extend .fa-file-text-o;
|
||||
}
|
||||
|
||||
// Menu
|
||||
&-menu-collapse {
|
||||
@extend .fa;
|
||||
|
||||
@@ -109,7 +109,7 @@ $select-input-height: 40px; // Bootstrap input height
|
||||
@extend .text-info;
|
||||
}
|
||||
|
||||
.xo-status-unknown, .xo-status-paused, .xo-status-disabled {
|
||||
.xo-status-unknown, .xo-status-paused {
|
||||
@extend .text-muted;
|
||||
}
|
||||
|
||||
@@ -213,12 +213,6 @@ $select-input-height: 40px; // Bootstrap input height
|
||||
border: 1px solid black;
|
||||
margin: 5px 10px;
|
||||
width: 250px;
|
||||
// Workaround to prevent some bootstrap elements from hiding the notifications.
|
||||
// In bootstrap, ".input-group .form-control" and ".input-group > .input-group-btn > .btn"
|
||||
// have "z-index: 2" and "z-index: 3" if they are hovered, focused or active.
|
||||
// (https://github.com/twbs/bootstrap/blob/v4.0.0-alpha.5/scss/_input-group.scss#L18-L37)
|
||||
// (https://github.com/twbs/bootstrap/blob/v4.0.0-alpha.5/scss/_input-group.scss#L177-L187)
|
||||
z-index: 3;
|
||||
&.success {
|
||||
background: $alert-success-bg;
|
||||
border-color: $alert-success-border;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
|
||||
.usage-element-others {
|
||||
background-color: #5cb85c75;
|
||||
background-color: $brand-info;
|
||||
}
|
||||
|
||||
.usage-element:hover {
|
||||
|
||||
@@ -66,16 +66,17 @@ export default class About extends Component {
|
||||
<div>
|
||||
<Row>
|
||||
<Col>
|
||||
<h2 className='text-info'>{_('productionUse')}</h2>
|
||||
<h4 className='text-info'>
|
||||
{_('getSupport', {
|
||||
<h2 className='text-danger'>{_('noProSupport')}</h2>
|
||||
<h4 className='text-danger'>{_('noProductionUse')}</h4>
|
||||
<p className='text-muted'>
|
||||
{_('downloadXoaFromWebsite', {
|
||||
website: (
|
||||
<a href='https://xen-orchestra.com/#!/pricing?pk_campaign=xoa_source_upgrade&pk_kwd=about'>
|
||||
https://xen-orchestra.com
|
||||
<a href='https://xen-orchestra.com/#!/?pk_campaign=xoa_source_upgrade&pk_kwd=about'>
|
||||
http://xen-orchestra.com
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</h4>
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
|
||||
@@ -3,7 +3,6 @@ import ActionButton from 'action-button'
|
||||
import addSubscriptions from 'add-subscriptions'
|
||||
import Button from 'button'
|
||||
import ButtonLink from 'button-link'
|
||||
import constructQueryString from 'construct-query-string'
|
||||
import Copiable from 'copiable'
|
||||
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import decorate from 'apply-decorators'
|
||||
@@ -16,6 +15,7 @@ import Tooltip from 'tooltip'
|
||||
import { adminOnly, connectStore, routes } from 'utils'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { confirm } from 'modal'
|
||||
import { constructQueryString } from 'smart-backup'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createGetLoneSnapshots, createSelector } from 'selectors'
|
||||
import { get } from '@xen-orchestra/defined'
|
||||
|
||||
@@ -186,40 +186,20 @@ export default decorate([
|
||||
}
|
||||
}
|
||||
|
||||
let schedules, settings
|
||||
if (!isEmpty(state.schedules)) {
|
||||
schedules = mapValues(
|
||||
state.schedules,
|
||||
({ id, ...schedule }) => schedule
|
||||
)
|
||||
settings = normalizeSettings({
|
||||
settings: state.settings,
|
||||
exportMode: state.exportMode,
|
||||
copyMode: state.copyMode,
|
||||
snapshotMode: state.snapshotMode,
|
||||
}).toObject()
|
||||
} else {
|
||||
const id = generateId()
|
||||
schedules = {
|
||||
[id]: DEFAULT_SCHEDULE,
|
||||
}
|
||||
settings = {
|
||||
[id]: {
|
||||
copyRetention: state.copyMode ? DEFAULT_RETENTION : undefined,
|
||||
exportRetention: state.exportMode ? DEFAULT_RETENTION : undefined,
|
||||
snapshotRetention: state.snapshotMode
|
||||
? DEFAULT_RETENTION
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
await createBackupNgJob({
|
||||
name: state.name,
|
||||
mode: state.isDelta ? 'delta' : 'full',
|
||||
compression: state.compression,
|
||||
schedules,
|
||||
settings,
|
||||
schedules: mapValues(
|
||||
state.schedules,
|
||||
({ id, ...schedule }) => schedule
|
||||
),
|
||||
settings: normalizeSettings({
|
||||
settings: state.settings,
|
||||
exportMode: state.exportMode,
|
||||
copyMode: state.copyMode,
|
||||
snapshotMode: state.snapshotMode,
|
||||
}).toObject(),
|
||||
remotes:
|
||||
state.deltaMode || state.backupMode
|
||||
? constructPattern(state.remotes)
|
||||
@@ -395,7 +375,7 @@ export default decorate([
|
||||
{ saveSchedule },
|
||||
storedSchedule = DEFAULT_SCHEDULE
|
||||
) => async (
|
||||
{ copyMode, exportMode, missingBackupMode, snapshotMode },
|
||||
{ copyMode, exportMode, snapshotMode },
|
||||
{ intl: { formatMessage } }
|
||||
) => {
|
||||
const schedule = await form({
|
||||
@@ -415,7 +395,6 @@ export default decorate([
|
||||
handler: value => {
|
||||
if (
|
||||
!(
|
||||
missingBackupMode ||
|
||||
(exportMode && value.exportRetention > 0) ||
|
||||
(copyMode && value.copyRetention > 0) ||
|
||||
(snapshotMode && value.snapshotRetention > 0)
|
||||
@@ -578,16 +557,13 @@ export default decorate([
|
||||
missingRemotes: state =>
|
||||
(state.backupMode || state.deltaMode) && isEmpty(state.remotes),
|
||||
missingSrs: state => (state.drMode || state.crMode) && isEmpty(state.srs),
|
||||
missingSchedules: (state, { job }) =>
|
||||
job !== undefined && isEmpty(state.schedules),
|
||||
missingExportRetention: (state, { job }) =>
|
||||
job !== undefined && state.exportMode && !state.exportRetentionExists,
|
||||
missingCopyRetention: (state, { job }) =>
|
||||
job !== undefined && state.copyMode && !state.copyRetentionExists,
|
||||
missingSnapshotRetention: (state, { job }) =>
|
||||
job !== undefined &&
|
||||
state.snapshotMode &&
|
||||
!state.snapshotRetentionExists,
|
||||
missingSchedules: state => isEmpty(state.schedules),
|
||||
missingExportRetention: state =>
|
||||
state.exportMode && !state.exportRetentionExists,
|
||||
missingCopyRetention: state =>
|
||||
state.copyMode && !state.copyRetentionExists,
|
||||
missingSnapshotRetention: state =>
|
||||
state.snapshotMode && !state.snapshotRetentionExists,
|
||||
exportMode: state => state.backupMode || state.deltaMode,
|
||||
copyMode: state => state.drMode || state.crMode,
|
||||
exportRetentionExists: createDoesRetentionExist('exportRetention'),
|
||||
|
||||
@@ -6,7 +6,7 @@ import SortedTable from 'sorted-table'
|
||||
import StateButton from 'state-button'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { isEmpty, find } from 'lodash'
|
||||
import { isEmpty, find, size } from 'lodash'
|
||||
|
||||
import { FormFeedback } from './../utils'
|
||||
|
||||
@@ -23,9 +23,10 @@ export default decorate([
|
||||
injectState,
|
||||
provideState({
|
||||
computed: {
|
||||
disabledDeletion: state => size(state.schedules) <= 1,
|
||||
error: state => find(FEEDBACK_ERRORS, error => state[error]),
|
||||
individualActions: (
|
||||
{ disabledEdition },
|
||||
{ disabledDeletion, disabledEdition },
|
||||
{ effects: { deleteSchedule, showScheduleModal } }
|
||||
) => [
|
||||
{
|
||||
@@ -36,6 +37,7 @@ export default decorate([
|
||||
level: 'primary',
|
||||
},
|
||||
{
|
||||
disabled: disabledDeletion,
|
||||
handler: deleteSchedule,
|
||||
icon: 'delete',
|
||||
label: _('scheduleDelete'),
|
||||
|
||||
@@ -2,7 +2,6 @@ import _ from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import ButtonGroup from 'button-group'
|
||||
import Component from 'base-component'
|
||||
import constructQueryString from 'construct-query-string'
|
||||
import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
import LogList from '../../logs'
|
||||
@@ -14,6 +13,7 @@ import StateButton from 'state-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { confirm } from 'modal'
|
||||
import { addSubscriptions } from 'utils'
|
||||
import { constructQueryString } from 'smart-backup'
|
||||
import { createSelector } from 'selectors'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { filter, find, forEach, get, keyBy, map, orderBy } from 'lodash'
|
||||
|
||||
@@ -65,22 +65,12 @@ export default class HostItem extends Component {
|
||||
_toggleExpanded = () => this.setState({ expanded: !this.state.expanded })
|
||||
_onSelect = () => this.props.onSelect(this.props.item.id)
|
||||
|
||||
_getHostState = createSelector(
|
||||
() => this.props.item.power_state,
|
||||
() => this.props.item.enabled,
|
||||
() => this.props.item.current_operations,
|
||||
(powerState, enabled, operations) =>
|
||||
!isEmpty(operations)
|
||||
? 'Busy'
|
||||
: powerState === 'Running' && !enabled
|
||||
? 'Disabled'
|
||||
: powerState
|
||||
)
|
||||
|
||||
render() {
|
||||
const { item: host, container, expandAll, selected, nVms } = this.props
|
||||
const state = this._getHostState()
|
||||
|
||||
const toolTipContent =
|
||||
host.power_state === `Running` && !host.enabled
|
||||
? `disabled`
|
||||
: _(`powerState${host.power_state}`)
|
||||
return (
|
||||
<div className={styles.item}>
|
||||
<BlockLink to={`/hosts/${host.id}`}>
|
||||
@@ -96,19 +86,25 @@ export default class HostItem extends Component {
|
||||
|
||||
<Tooltip
|
||||
content={
|
||||
<span>
|
||||
{_(`powerState${state}`)}
|
||||
{state === 'Busy' && (
|
||||
<span>
|
||||
{' ('}
|
||||
{map(host.current_operations)[0]}
|
||||
{')'}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
isEmpty(host.current_operations) ? (
|
||||
toolTipContent
|
||||
) : (
|
||||
<div>
|
||||
{toolTipContent}
|
||||
{' ('}
|
||||
{map(host.current_operations)[0]}
|
||||
{')'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon icon={state.toLowerCase()} />
|
||||
{!isEmpty(host.current_operations) ? (
|
||||
<Icon icon='busy' />
|
||||
) : host.power_state === 'Running' && !host.enabled ? (
|
||||
<Icon icon='disabled' />
|
||||
) : (
|
||||
<Icon icon={`${host.power_state.toLowerCase()}`} />
|
||||
)}
|
||||
</Tooltip>
|
||||
|
||||
<Ellipsis>
|
||||
|
||||
@@ -80,16 +80,9 @@ export default class VmItem extends Component {
|
||||
_toggleExpanded = () => this.setState({ expanded: !this.state.expanded })
|
||||
_onSelect = () => this.props.onSelect(this.props.item.id)
|
||||
|
||||
_getVmState = createSelector(
|
||||
() => this.props.item.power_state,
|
||||
() => this.props.item.current_operations,
|
||||
(powerState, operations) => (!isEmpty(operations) ? 'Busy' : powerState)
|
||||
)
|
||||
|
||||
render() {
|
||||
const { item: vm, container, expandAll, selected } = this.props
|
||||
const resourceSet = this._getResourceSet()
|
||||
const state = this._getVmState()
|
||||
|
||||
return (
|
||||
<div className={styles.item}>
|
||||
@@ -106,19 +99,23 @@ export default class VmItem extends Component {
|
||||
|
||||
<Tooltip
|
||||
content={
|
||||
<span>
|
||||
{_(`powerState${state}`)}
|
||||
{state === 'Busy' && (
|
||||
<span>
|
||||
{' ('}
|
||||
{map(vm.current_operations)[0]}
|
||||
{')'}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
isEmpty(vm.current_operations) ? (
|
||||
_(`powerState${vm.power_state}`)
|
||||
) : (
|
||||
<div>
|
||||
{_(`powerState${vm.power_state}`)}
|
||||
{' ('}
|
||||
{map(vm.current_operations)[0]}
|
||||
{')'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon icon={state.toLowerCase()} />
|
||||
{isEmpty(vm.current_operations) ? (
|
||||
<Icon icon={`${vm.power_state.toLowerCase()}`} />
|
||||
) : (
|
||||
<Icon icon='busy' />
|
||||
)}
|
||||
</Tooltip>
|
||||
|
||||
<Ellipsis>
|
||||
|
||||
@@ -228,22 +228,9 @@ export default class Host extends Component {
|
||||
_setNameLabel = nameLabel =>
|
||||
editHost(this.props.host, { name_label: nameLabel })
|
||||
|
||||
_getHostState = createSelector(
|
||||
() => this.props.host.power_state,
|
||||
() => this.props.host.enabled,
|
||||
() => this.props.host.current_operations,
|
||||
(powerState, enabled, operations) =>
|
||||
!isEmpty(operations)
|
||||
? 'Busy'
|
||||
: powerState === 'Running' && !enabled
|
||||
? 'Disabled'
|
||||
: powerState
|
||||
)
|
||||
|
||||
header() {
|
||||
const { host, pool } = this.props
|
||||
const { missingPatches } = this.state || {}
|
||||
const state = this._getHostState()
|
||||
if (!host) {
|
||||
return <Icon icon='loading' />
|
||||
}
|
||||
@@ -253,22 +240,13 @@ export default class Host extends Component {
|
||||
<Col mediumSize={6} className='header-title'>
|
||||
{pool !== undefined && <Pool id={pool.id} link />}
|
||||
<h2>
|
||||
<Tooltip
|
||||
content={
|
||||
<span>
|
||||
{_(`powerState${state}`)}
|
||||
{state === 'Busy' && (
|
||||
<span>
|
||||
{' ('}
|
||||
{map(host.current_operations)[0]}
|
||||
{')'}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<Icon
|
||||
icon={
|
||||
host.power_state === 'Running' && !host.enabled
|
||||
? 'host-disabled'
|
||||
: `host-${host.power_state.toLowerCase()}`
|
||||
}
|
||||
>
|
||||
<Icon icon={`host-${state.toLowerCase()}`} />
|
||||
</Tooltip>{' '}
|
||||
/>{' '}
|
||||
<Text value={host.name_label} onChange={this._setNameLabel} />
|
||||
{this.props.needsRestart && (
|
||||
<Tooltip content={_('rebootUpdateHostLabel')}>
|
||||
|
||||
@@ -2,7 +2,6 @@ import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Copiable from 'copiable'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import SelectFiles from 'select-files'
|
||||
@@ -10,7 +9,6 @@ import StateButton from 'state-button'
|
||||
import TabButton from 'tab-button'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { compareVersions, connectStore, getIscsiPaths } from 'utils'
|
||||
import { confirm } from 'modal'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { forEach, map, noop, isEmpty } from 'lodash'
|
||||
@@ -21,7 +19,6 @@ import { Toggle } from 'form'
|
||||
import {
|
||||
detachHost,
|
||||
disableHost,
|
||||
editHost,
|
||||
enableHost,
|
||||
forgetHost,
|
||||
isHyperThreadingEnabledHost,
|
||||
@@ -124,20 +121,6 @@ export default class extends Component {
|
||||
}
|
||||
)
|
||||
|
||||
_setHostIscsiIqn = iscsiIqn =>
|
||||
confirm({
|
||||
icon: 'alarm',
|
||||
title: _('editHostIscsiIqnTitle'),
|
||||
body: (
|
||||
<div>
|
||||
<p>{_('editHostIscsiIqnMessage')}</p>
|
||||
<p className='text-muted'>
|
||||
<Icon icon='info' /> {_('uniqueHostIscsiIqnInfo')}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
}).then(() => editHost(this.props.host, { iscsiIqn }), noop)
|
||||
|
||||
_setRemoteSyslogHost = value => setRemoteSyslogHost(this.props.host, value)
|
||||
|
||||
render() {
|
||||
@@ -248,13 +231,8 @@ export default class extends Component {
|
||||
<Copiable tagName='td'>{host.build}</Copiable>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{_('hostIscsiIqn')}</th>
|
||||
<td>
|
||||
<Text
|
||||
onChange={this._setHostIscsiIqn}
|
||||
value={host.iscsiIqn}
|
||||
/>
|
||||
</td>
|
||||
<th>{_('hostIscsiName')}</th>
|
||||
<Copiable tagName='td'>{host.iSCSI_name}</Copiable>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{_('multipathing')}</th>
|
||||
|
||||
@@ -11,7 +11,7 @@ import Shortcuts from 'shortcuts'
|
||||
import themes from 'themes'
|
||||
import _, { IntlProvider } from 'intl'
|
||||
import { blockXoaAccess } from 'xoa-updater'
|
||||
import { connectStore, getXoaPlan, routes } from 'utils'
|
||||
import { connectStore, routes } from 'utils'
|
||||
import { Notification } from 'notification'
|
||||
import { ShortcutManager } from 'react-shortcuts'
|
||||
import { ThemeProvider } from 'styled-components'
|
||||
@@ -132,8 +132,6 @@ export default class XoApp extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
dismissSourceBanner = () => this.setState({ dismissedSourceBanner: true })
|
||||
|
||||
componentDidMount() {
|
||||
this.refs.bodyWrapper.style.minHeight =
|
||||
this.refs.menu.getWrappedInstance().height + 'px'
|
||||
@@ -203,14 +201,13 @@ export default class XoApp extends Component {
|
||||
render() {
|
||||
const { signedUp, trial, registerNeeded } = this.props
|
||||
const blocked = signedUp && blockXoaAccess(trial) // If we are under expired or unstable trial (signed up only)
|
||||
const plan = getXoaPlan()
|
||||
|
||||
return (
|
||||
<IntlProvider>
|
||||
<ThemeProvider theme={themes.base}>
|
||||
<DocumentTitle title='Xen Orchestra'>
|
||||
<div>
|
||||
{plan !== 'Community' && registerNeeded && (
|
||||
{process.env.XOA_PLAN < 5 && registerNeeded && (
|
||||
<div className='alert alert-danger mb-0'>
|
||||
{_('notRegisteredDisclaimerInfo')}{' '}
|
||||
<a
|
||||
@@ -225,7 +222,7 @@ export default class XoApp extends Component {
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{plan === 'Community' && !this.state.dismissedSourceBanner && (
|
||||
{+process.env.XOA_PLAN === 5 && (
|
||||
<div className='alert alert-danger mb-0'>
|
||||
<a
|
||||
href='https://xen-orchestra.com/#!/xoa?pk_campaign=xo_source_banner'
|
||||
@@ -234,9 +231,6 @@ export default class XoApp extends Component {
|
||||
>
|
||||
{_('disclaimerText3')}
|
||||
</a>
|
||||
<button className='close' onClick={this.dismissSourceBanner}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div style={CONTAINER_STYLE}>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import _, { FormattedDuration } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ButtonGroup from 'button-group'
|
||||
import decorate from 'apply-decorators'
|
||||
import defined, { get } from '@xen-orchestra/defined'
|
||||
import Icon from 'icon'
|
||||
@@ -137,24 +136,13 @@ const VmTask = ({ children, restartVmJob, task }) => (
|
||||
<div>
|
||||
<Vm id={task.data.id} link newTab /> <TaskStateInfos status={task.status} />{' '}
|
||||
{restartVmJob !== undefined && hasTaskFailed(task) && (
|
||||
<ButtonGroup>
|
||||
<ActionButton
|
||||
data-vm={task.data.id}
|
||||
handler={restartVmJob}
|
||||
icon='run'
|
||||
size='small'
|
||||
tooltip={_('backupRestartVm')}
|
||||
/>
|
||||
<ActionButton
|
||||
btnStyle='warning'
|
||||
data-force
|
||||
data-vm={task.data.id}
|
||||
handler={restartVmJob}
|
||||
icon='force-restart'
|
||||
size='small'
|
||||
tooltip={_('backupForceRestartVm')}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<ActionButton
|
||||
handler={restartVmJob}
|
||||
icon='run'
|
||||
size='small'
|
||||
tooltip={_('backupRestartVm')}
|
||||
data-vm={task.data.id}
|
||||
/>
|
||||
)}
|
||||
<Warnings warnings={task.warnings} />
|
||||
{children}
|
||||
@@ -328,15 +316,14 @@ export default decorate([
|
||||
setFilter: (_, filter) => () => ({
|
||||
filter,
|
||||
}),
|
||||
restartVmJob: (_, params) => async (
|
||||
restartVmJob: (_, { vm }) => async (
|
||||
_,
|
||||
{ log: { scheduleId, jobId } }
|
||||
) => {
|
||||
await runBackupNgJob({
|
||||
force: get(() => params.force),
|
||||
id: jobId,
|
||||
vm,
|
||||
schedule: scheduleId,
|
||||
vm: get(() => params.vm),
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -31,7 +31,7 @@ export default decorate([
|
||||
effects: {
|
||||
_downloadLog: () => ({ formattedLog }, { log }) =>
|
||||
downloadLog({ log: formattedLog, date: log.start, type: 'backup NG' }),
|
||||
restartFailedVms: (_, params) => async (
|
||||
restartFailedVms: () => async (
|
||||
_,
|
||||
{ log: { jobId: id, scheduleId: schedule, tasks, infos } }
|
||||
) => {
|
||||
@@ -54,8 +54,8 @@ export default decorate([
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await runBackupNgJob({
|
||||
force: get(() => params.force),
|
||||
id,
|
||||
schedule,
|
||||
vms,
|
||||
@@ -97,22 +97,12 @@ export default decorate([
|
||||
/>
|
||||
)}
|
||||
{state.jobFailed && log.scheduleId !== undefined && (
|
||||
<ButtonGroup>
|
||||
<ActionButton
|
||||
handler={effects.restartFailedVms}
|
||||
icon='run'
|
||||
size='small'
|
||||
tooltip={_('backupRestartFailedVms')}
|
||||
/>
|
||||
<ActionButton
|
||||
btnStyle='warning'
|
||||
data-force
|
||||
handler={effects.restartFailedVms}
|
||||
icon='force-restart'
|
||||
size='small'
|
||||
tooltip={_('backupForceRestartFailedVms')}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<ActionButton
|
||||
handler={effects.restartFailedVms}
|
||||
icon='run'
|
||||
size='small'
|
||||
tooltip={_('backupRestartFailedVms')}
|
||||
/>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</span>
|
||||
|
||||
@@ -3,20 +3,3 @@
|
||||
font-size: 1rem;
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
.lineItem {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.item {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
margin: 0.5em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1 0 20rem;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user