Compare commits
62 Commits
xo-server-
...
better-sor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af55e0341b | ||
|
|
3a4f1f78ce | ||
|
|
1d765ebf49 | ||
|
|
3d416d75e9 | ||
|
|
f8666ba367 | ||
|
|
9e80f76dd8 | ||
|
|
c76a5eaf67 | ||
|
|
cd378f0168 | ||
|
|
7d51ff0cf5 | ||
|
|
47819ea956 | ||
|
|
c7e3560c98 | ||
|
|
b24400b21d | ||
|
|
6c1d651687 | ||
|
|
e7757b53e7 | ||
|
|
a6d182e92d | ||
|
|
925eca1463 | ||
|
|
8b454f0d39 | ||
|
|
7c4d110353 | ||
|
|
6df55523b6 | ||
|
|
3ec6a24634 | ||
|
|
164b4218c4 | ||
|
|
56df8a6477 | ||
|
|
47a83b312d | ||
|
|
41a28ae088 | ||
|
|
436a8755ae | ||
|
|
960b179d95 | ||
|
|
0f0d0e1076 | ||
|
|
a8bd0d8075 | ||
|
|
986d3af685 | ||
|
|
1833f9ffdf | ||
|
|
30a6877f8a | ||
|
|
aaae2583c7 | ||
|
|
7f24afc2e7 | ||
|
|
0040923e12 | ||
|
|
844efb88d8 | ||
|
|
9efc3dd1fb | ||
|
|
67853bad8e | ||
|
|
faa8e1441a | ||
|
|
5c54611d1b | ||
|
|
dcf55e4385 | ||
|
|
2b0f1b6aab | ||
|
|
ae6cc8eea3 | ||
|
|
5279fa49a7 | ||
|
|
dcd8a62784 | ||
|
|
8c197b0e1a | ||
|
|
aed824b200 | ||
|
|
036b30212e | ||
|
|
3451ab3f50 | ||
|
|
0d0a92c2b1 | ||
|
|
aa19bc7bf5 | ||
|
|
347759b2e7 | ||
|
|
352230446c | ||
|
|
3eff8102e1 | ||
|
|
6693d845d9 | ||
|
|
4d79c462db | ||
|
|
c44ef6a1dc | ||
|
|
f0996fcfa7 | ||
|
|
54bc384d37 | ||
|
|
504fc1efe8 | ||
|
|
f4179b93fb | ||
|
|
564252c198 | ||
|
|
802a7a4463 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/cron",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"license": "ISC",
|
||||
"description": "Focused, well maintained, cron parser/scheduler",
|
||||
"keywords": [
|
||||
|
||||
@@ -5,9 +5,16 @@ import parse from './parse'
|
||||
|
||||
const MAX_DELAY = 2 ** 31 - 1
|
||||
|
||||
function nextDelay(schedule) {
|
||||
const now = schedule._createDate()
|
||||
return next(schedule._schedule, now) - now
|
||||
}
|
||||
|
||||
class Job {
|
||||
constructor(schedule, fn) {
|
||||
const wrapper = () => {
|
||||
this._isRunning = true
|
||||
|
||||
let result
|
||||
try {
|
||||
result = fn()
|
||||
@@ -22,23 +29,34 @@ class Job {
|
||||
}
|
||||
}
|
||||
const scheduleNext = () => {
|
||||
const delay = schedule._nextDelay()
|
||||
this._timeout =
|
||||
delay < MAX_DELAY
|
||||
? setTimeout(wrapper, delay)
|
||||
: setTimeout(scheduleNext, MAX_DELAY)
|
||||
this._isRunning = false
|
||||
|
||||
if (this._isEnabled) {
|
||||
const delay = nextDelay(schedule)
|
||||
this._timeout =
|
||||
delay < MAX_DELAY
|
||||
? setTimeout(wrapper, delay)
|
||||
: setTimeout(scheduleNext, MAX_DELAY)
|
||||
}
|
||||
}
|
||||
|
||||
this._isEnabled = false
|
||||
this._isRunning = false
|
||||
this._scheduleNext = scheduleNext
|
||||
this._timeout = undefined
|
||||
}
|
||||
|
||||
start() {
|
||||
this.stop()
|
||||
this._scheduleNext()
|
||||
|
||||
this._isEnabled = true
|
||||
if (!this._isRunning) {
|
||||
this._scheduleNext()
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._isEnabled = false
|
||||
clearTimeout(this._timeout)
|
||||
}
|
||||
}
|
||||
@@ -68,11 +86,6 @@ class Schedule {
|
||||
return dates
|
||||
}
|
||||
|
||||
_nextDelay() {
|
||||
const now = this._createDate()
|
||||
return next(this._schedule, now) - now
|
||||
}
|
||||
|
||||
startJob(fn) {
|
||||
const job = this.createJob(fn)
|
||||
job.start()
|
||||
|
||||
62
@xen-orchestra/cron/src/index.spec.js
Normal file
62
@xen-orchestra/cron/src/index.spec.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { createSchedule } from './'
|
||||
|
||||
describe('issues', () => {
|
||||
test('stop during async execution', async () => {
|
||||
let nCalls = 0
|
||||
let resolve, promise
|
||||
|
||||
const job = createSchedule('* * * * *').createJob(() => {
|
||||
++nCalls
|
||||
|
||||
// eslint-disable-next-line promise/param-names
|
||||
promise = new Promise(r => {
|
||||
resolve = r
|
||||
})
|
||||
return promise
|
||||
})
|
||||
|
||||
job.start()
|
||||
jest.runAllTimers()
|
||||
|
||||
expect(nCalls).toBe(1)
|
||||
|
||||
job.stop()
|
||||
|
||||
resolve()
|
||||
await promise
|
||||
|
||||
jest.runAllTimers()
|
||||
expect(nCalls).toBe(1)
|
||||
})
|
||||
|
||||
test('stop then start during async job execution', async () => {
|
||||
let nCalls = 0
|
||||
let resolve, promise
|
||||
|
||||
const job = createSchedule('* * * * *').createJob(() => {
|
||||
++nCalls
|
||||
|
||||
// eslint-disable-next-line promise/param-names
|
||||
promise = new Promise(r => {
|
||||
resolve = r
|
||||
})
|
||||
return promise
|
||||
})
|
||||
|
||||
job.start()
|
||||
jest.runAllTimers()
|
||||
|
||||
expect(nCalls).toBe(1)
|
||||
|
||||
job.stop()
|
||||
job.start()
|
||||
|
||||
resolve()
|
||||
await promise
|
||||
|
||||
jest.runAllTimers()
|
||||
expect(nCalls).toBe(2)
|
||||
})
|
||||
})
|
||||
3
@xen-orchestra/template/.babelrc.js
Normal file
3
@xen-orchestra/template/.babelrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
62
@xen-orchestra/template/README.md
Normal file
62
@xen-orchestra/template/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# @xen-orchestra/template [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/template):
|
||||
|
||||
```
|
||||
> npm install --save @xen-orchestra/template
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Create a string replacer based on a pattern and a list of rules.
|
||||
|
||||
```js
|
||||
const myReplacer = compileTemplate('{name}_COPY_\{name}_{id}_%\%', {
|
||||
'{name}': vm => vm.name_label,
|
||||
'{id}': vm => vm.id,
|
||||
'%': (_, i) => i
|
||||
})
|
||||
|
||||
const newString = myReplacer({
|
||||
name_label: 'foo',
|
||||
id: 42,
|
||||
}, 32)
|
||||
|
||||
newString === 'foo_COPY_{name}_42_32%' // true
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> yarn
|
||||
|
||||
# Run the tests
|
||||
> yarn test
|
||||
|
||||
# Continuously compile
|
||||
> yarn dev
|
||||
|
||||
# Continuously run the tests
|
||||
> yarn dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> yarn build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
||||
46
@xen-orchestra/template/package.json
Normal file
46
@xen-orchestra/template/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@xen-orchestra/template",
|
||||
"version": "0.1.0",
|
||||
"license": "ISC",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/template",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@xen-orchestra/template",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"browserslist": [
|
||||
">2%"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"postversion": "npm publish --access public"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
}
|
||||
19
@xen-orchestra/template/src/index.js
Normal file
19
@xen-orchestra/template/src/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import escapeRegExp from 'lodash/escapeRegExp'
|
||||
|
||||
const compareLengthDesc = (a, b) => b.length - a.length
|
||||
|
||||
export function compileTemplate(pattern, rules) {
|
||||
const matches = Object.keys(rules)
|
||||
.sort(compareLengthDesc)
|
||||
.map(escapeRegExp)
|
||||
.join('|')
|
||||
const regExp = new RegExp(`\\\\(?:\\\\|${matches})|${matches}`, 'g')
|
||||
return (...params) =>
|
||||
pattern.replace(regExp, match => {
|
||||
if (match[0] === '\\') {
|
||||
return match.slice(1)
|
||||
}
|
||||
const rule = rules[match]
|
||||
return typeof rule === 'function' ? rule(...params) : rule
|
||||
})
|
||||
}
|
||||
14
@xen-orchestra/template/src/index.spec.js
Normal file
14
@xen-orchestra/template/src/index.spec.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/* eslint-env jest */
|
||||
import { compileTemplate } from '.'
|
||||
|
||||
it("correctly replaces the template's variables", () => {
|
||||
const replacer = compileTemplate(
|
||||
'{property}_\\{property}_\\\\{property}_{constant}_%_FOO',
|
||||
{
|
||||
'{property}': obj => obj.name,
|
||||
'{constant}': 1235,
|
||||
'%': (_, i) => i,
|
||||
}
|
||||
)
|
||||
expect(replacer({ name: 'bar' }, 5)).toBe('bar_{property}_\\bar_1235_5_FOO')
|
||||
})
|
||||
34
CHANGELOG.md
34
CHANGELOG.md
@@ -4,12 +4,42 @@
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [SR/new] Clarify address formats [#4450](https://github.com/vatesfr/xen-orchestra/issues/4450) (PR [#4460](https://github.com/vatesfr/xen-orchestra/pull/4460))
|
||||
- [Backup NG/New] Show warning if zstd compression is not supported on a VM [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PRs [#4411](https://github.com/vatesfr/xen-orchestra/pull/4411))
|
||||
- [VM/disks] Don't hide disks that are attached to the same VM twice [#4400](https://github.com/vatesfr/xen-orchestra/issues/4400) (PR [#4414](https://github.com/vatesfr/xen-orchestra/pull/4414))
|
||||
- [VM/console] Add a button to connect to the VM via the local SSH client (PR [#4415](https://github.com/vatesfr/xen-orchestra/pull/4415))
|
||||
- [SDN Controller] Add possibility to encrypt private networks (PR [#4441](https://github.com/vatesfr/xen-orchestra/pull/4441))
|
||||
- [SDN Controller] Ability to configure MTU for private networks (PR [#4491](https://github.com/vatesfr/xen-orchestra/pull/4491))
|
||||
- [VM Export] Filenames are now prefixed with datetime [#4503](https://github.com/vatesfr/xen-orchestra/issues/4503)
|
||||
- [Backups] Improve performance by caching VM backups listing (PR [#4509](https://github.com/vatesfr/xen-orchestra/pull/4509))
|
||||
|
||||
### 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))
|
||||
- [XOSAN] Fix "invalid parameters" error on creating a SR (PR [#4478](https://github.com/vatesfr/xen-orchestra/pull/4478))
|
||||
- [Patching] Avoid overloading XCP-ng by reducing the frequency of yum update checks [#4358](https://github.com/vatesfr/xen-orchestra/issues/4358) (PR [#4477](https://github.com/vatesfr/xen-orchestra/pull/4477))
|
||||
- [Network] Fix inability to create a bonded network (PR [#4489](https://github.com/vatesfr/xen-orchestra/pull/4489))
|
||||
- [Backup restore & Replication] Don't copy `sm_config` to new VDIs which might leads to useless coalesces [#4482](https://github.com/vatesfr/xen-orchestra/issues/4482) (PR [#4484](https://github.com/vatesfr/xen-orchestra/pull/4484))
|
||||
- [Home] Fix intermediary "no results" display showed on filtering items [#4420](https://github.com/vatesfr/xen-orchestra/issues/4420) (PR [#4456](https://github.com/vatesfr/xen-orchestra/pull/4456)
|
||||
- [Backup NG/New schedule] Properly show user errors in the form [#3831](https://github.com/vatesfr/xen-orchestra/issues/3831) (PR [#4131](https://github.com/vatesfr/xen-orchestra/pull/4131))
|
||||
- [VM/Advanced] Fix `"vm.set_domain_type" is not a function` error on switching virtualization mode (PV/HVM) [#4348](https://github.com/vatesfr/xen-orchestra/issues/4348) (PR [#4504](https://github.com/vatesfr/xen-orchestra/pull/4504))
|
||||
- [Backup NG/logs] Show warning when zstd compression is selected but not supported [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PR [#4375](https://github.com/vatesfr/xen-orchestra/pull/4375)
|
||||
- [Patches] Fix patches installation for CH 8.0 (PR [#4511](https://github.com/vatesfr/xen-orchestra/pull/4511))
|
||||
- [Network] Fix inability to set a network name [#4514](https://github.com/vatesfr/xen-orchestra/issues/4514) (PR [4510](https://github.com/vatesfr/xen-orchestra/pull/4510))
|
||||
- [Backup NG] Fix race conditions that could lead to disabled jobs still running (PR [4510](https://github.com/vatesfr/xen-orchestra/pull/4510))
|
||||
- [XOA] Remove "Updates" and "Licenses" tabs for non admin users (PR [#4526](https://github.com/vatesfr/xen-orchestra/pull/4526))
|
||||
- [New VM] Ability to escape [cloud config template](https://xen-orchestra.com/blog/xen-orchestra-5-21/#cloudconfigtemplates) variables [#4486](https://github.com/vatesfr/xen-orchestra/issues/4486) (PR [#4501](https://github.com/vatesfr/xen-orchestra/pull/4501))
|
||||
- [Backup NG] Properly log and report if job is already running [#4497](https://github.com/vatesfr/xen-orchestra/issues/4497) (PR [4534](https://github.com/vatesfr/xen-orchestra/pull/4534))
|
||||
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server v5.49.0
|
||||
- xo-web v5.49.0
|
||||
- @xen-orchestra/cron v1.0.4
|
||||
- xo-server-sdn-controller v0.3.0
|
||||
- @xen-orchestra/template v0.1.0
|
||||
- xo-server v5.50.0
|
||||
- xo-web v5.50.0
|
||||
|
||||
|
||||
## **5.38.0** (2019-08-29)
|
||||
|
||||
|
||||
@@ -7,20 +7,20 @@
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [SR/new] Clarify address formats [#4450](https://github.com/vatesfr/xen-orchestra/issues/4450) (PR [#4460](https://github.com/vatesfr/xen-orchestra/pull/4460))
|
||||
- [Backup NG/New] Show warning if zstd compression is not supported on a VM [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PRs [#4411](https://github.com/vatesfr/xen-orchestra/pull/4411))
|
||||
- [Settings/Logs] Differenciate XS/XCP-ng errors from XO errors [#4101](https://github.com/vatesfr/xen-orchestra/issues/4101) (PR [#4385](https://github.com/vatesfr/xen-orchestra/pull/4385))
|
||||
- [Backups] Improve performance by caching logs consolidation (PR [#4541](https://github.com/vatesfr/xen-orchestra/pull/4541))
|
||||
- [New VM] Cloud Init available for all plans (PR [#4543](https://github.com/vatesfr/xen-orchestra/pull/4543))
|
||||
- [Servers] IPv6 addresses can be used [#4520](https://github.com/vatesfr/xen-orchestra/issues/4520) (PR [#4521](https://github.com/vatesfr/xen-orchestra/pull/4521)) \
|
||||
Note: They must enclosed in brackets to differentiate with the port, e.g.: `[2001:db8::7334]` or `[ 2001:db8::7334]:4343`
|
||||
- [HUB] VM template store [#1918](https://github.com/vatesfr/xen-orchestra/issues/1918) (PR [#4442](https://github.com/vatesfr/xen-orchestra/pull/4442))
|
||||
- [Home/bulk vm migration] Display same-pool hosts first in the selector [#4462](https://github.com/vatesfr/xen-orchestra/issues/4462) (PR [#4474](https://github.com/vatesfr/xen-orchestra/pull/4474))
|
||||
|
||||
### 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))
|
||||
- [XOSAN] Fix "invalid parameters" error on creating a SR (PR [#4478](https://github.com/vatesfr/xen-orchestra/pull/4478))
|
||||
- [Patching] Avoid overloading XCP-ng by reducing the frequency of yum update checks [#4358](https://github.com/vatesfr/xen-orchestra/issues/4358) (PR [#4477](https://github.com/vatesfr/xen-orchestra/pull/4477))
|
||||
- [Network] Fix inability to create a bonded network (PR [#4489](https://github.com/vatesfr/xen-orchestra/pull/4489))
|
||||
- [Backup restore & Replication] Don't copy `sm_config` to new VDIs which might leads to useless coalesces [#4482](https://github.com/vatesfr/xen-orchestra/issues/4482) (PR [#4484](https://github.com/vatesfr/xen-orchestra/pull/4484))
|
||||
- [Home] Fix intermediary "no results" display showed on filtering items [#4420](https://github.com/vatesfr/xen-orchestra/issues/4420) (PR [#4456](https://github.com/vatesfr/xen-orchestra/pull/4456)
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [Host] Fix an issue where host was wrongly reporting time inconsistency (PR [#4540](https://github.com/vatesfr/xen-orchestra/pull/4540))
|
||||
|
||||
### Released packages
|
||||
|
||||
> Packages will be released in the order they are here, therefore, they should
|
||||
@@ -28,6 +28,7 @@
|
||||
>
|
||||
> Rule of thumb: add packages on top.
|
||||
|
||||
- xo-server-sdn-controller v0.2.2
|
||||
- xo-server v5.49.0
|
||||
- xo-web v5.49.0
|
||||
- xen-api v0.27.2
|
||||
- xo-server-cloud v0.3.0
|
||||
- xo-server v5.51.0
|
||||
- xo-web v5.51.0
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
### Check list
|
||||
|
||||
> Check items when done or if not relevant
|
||||
> Check if done.
|
||||
>
|
||||
> Strikethrough if not relevant: ~~example~~ ([doc](https://help.github.com/en/articles/basic-writing-and-formatting-syntax)).
|
||||
|
||||
- [ ] PR reference the relevant issue (e.g. `Fixes #007`)
|
||||
- [ ] PR reference the relevant issue (e.g. `Fixes #007` or `See xoa-support#42`)
|
||||
- [ ] 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}`)
|
||||
- [ ] documentation updated
|
||||
- [ ] **I have tested added/updated features** (and impacted code)
|
||||
- `CHANGELOG.unreleased.md`:
|
||||
- [ ] enhancement/bug fix entry added
|
||||
- [ ] list of packages to release updated (`${name} v${new version}`)
|
||||
- **I have tested added/updated features** (and impacted code)
|
||||
- [ ] unit tests (e.g. [`cron/parse.spec.js`](https://github.com/vatesfr/xen-orchestra/blob/b24400b21de1ebafa1099c56bac1de5c988d9202/%40xen-orchestra/cron/src/parse.spec.js))
|
||||
- [ ] 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)
|
||||
- [ ] at least manual testing
|
||||
|
||||
### Process
|
||||
|
||||
@@ -17,3 +21,10 @@
|
||||
1. mark it as `WiP:` (Work in Progress) if not ready to be merged
|
||||
1. when you want a review, add a reviewer (and only one)
|
||||
1. if necessary, update your PR, and re- add a reviewer
|
||||
|
||||
From [_the Four Agreements_](https://en.wikipedia.org/wiki/Don_Miguel_Ruiz#The_Four_Agreements):
|
||||
|
||||
1. Be impeccable with your word.
|
||||
1. Don't take anything personally.
|
||||
1. Don't make assumptions.
|
||||
1. Always do your best.
|
||||
|
||||
@@ -13,11 +13,11 @@ It aims to be easy to use on any device supporting modern web technologies (HTML
|
||||
|
||||
## XOA quick deploy
|
||||
|
||||
SSH to your XenServer, and execute the following:
|
||||
Log in to your account and use the deploy form available on [this page](https://xen-orchestra.com/#!/xoa)
|
||||
|
||||
```
|
||||
bash -c "$(curl -s http://xoa.io/deploy)"
|
||||
```
|
||||
> **Note:** no data will be sent to our servers, it's running only between your browser and your host!
|
||||
|
||||
[](https://xen-orchestra.com/#!/xoa)
|
||||
|
||||
### XOA credentials
|
||||
|
||||
|
||||
BIN
docs/assets/deploy_form.png
Normal file
BIN
docs/assets/deploy_form.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 85 KiB |
@@ -1,14 +1,10 @@
|
||||
|
||||
# Installation
|
||||
|
||||
SSH to your XenServer/XCP-ng host and execute the following:
|
||||
Log in to your account and use the deploy form available on [this page](https://xen-orchestra.com/#!/xoa)
|
||||
|
||||
```
|
||||
bash -c "$(curl -s http://xoa.io/deploy)"
|
||||
```
|
||||

|
||||
|
||||
This will automatically download/import/start the XOA appliance. Nothing is changed on your host itself, it's 100% safe.
|
||||
|
||||
## [More on XOA](xoa.md)
|
||||
## [More on XOA and alternate deploy](xoa.md)
|
||||
|
||||

|
||||
|
||||
@@ -18,12 +18,18 @@ In the network creation view:
|
||||
- Select a `pool`
|
||||
- Select `Private network`
|
||||
- Select an interface on which to create the network's tunnels
|
||||
- Select the encapsulation: a choice is offered between `GRE` and `VxLAN`, if `VxLAN` is chosen, then port 4789 must be open for UDP traffic on all the network's hosts (see [the requirements](#requirements))
|
||||
- Select the encapsulation: a choice is offered between `GRE` and `VxLAN`, if `VxLAN` is chosen, then port 4789 must be open for UDP traffic on all the network's hosts (see [the requirements](#vxlan))
|
||||
- Choose if the network should be encrypted or not (see [the requirements](#encryption) to use encryption)
|
||||
- Select other `pool`s to add them to the network if desired
|
||||
- For each added `pool`: select an interface on which to create the tunnels
|
||||
- Create the network
|
||||
- Have fun! ☺
|
||||
|
||||
***NB:***
|
||||
- All hosts in a private network must be able to reach the other hosts' management interface.
|
||||
> The term ‘management interface’ is used to indicate the IP-enabled NIC that carries the management traffic.
|
||||
- Only 1 encrypted GRE network and 1 encrypted VxLAN network per pool can exist at a time due to Open vSwitch limitation.
|
||||
|
||||
### Configuration
|
||||
|
||||
Like all other xo-server plugins, it can be configured directly via
|
||||
@@ -36,9 +42,19 @@ If none is provided, the plugin will create its own self-signed certificates.
|
||||
|
||||
## Requirements
|
||||
|
||||
> All requirements are met by running up to date XCP-ng hosts.
|
||||
>
|
||||
> On older XCP-ng hosts, or hosts running Citrix Hypervisor, changes might have to be done manually.
|
||||
### VxLAN
|
||||
|
||||
To be able to use `VxLAN`, the following line needs to be added, if not already present, in `/etc/sysconfig/iptables` of all the hosts where `VxLAN` is wanted:
|
||||
- `-A xapi-INPUT -p udp -m conntrack --ctstate NEW -m udp --dport 4789 -j ACCEPT`
|
||||
- On XCP-ng prior to 7.6:
|
||||
- To be able to use `VxLAN`, the following line needs to be added, if not already present, in `/etc/sysconfig/iptables` of all the hosts where `VxLAN` is wanted: `-A xapi-INPUT -p udp -m conntrack --ctstate NEW -m udp --dport 4789 -j ACCEPT`
|
||||
|
||||
### Encryption
|
||||
|
||||
> Encryption is not available prior to 8.0.
|
||||
|
||||
- On XCP-ng 8.0:
|
||||
- To be able to encrypt the networks, `openvswitch-ipsec` package must be installed on all the hosts:
|
||||
- `yum install openvswitch-ipsec --enablerepo=xcp-ng-testing`
|
||||
- `systemctl enable ipsec`
|
||||
- `systemctl enable openvswitch-ipsec`
|
||||
- `systemctl start ipsec`
|
||||
- `systemctl start openvswitch-ipsec`
|
||||
@@ -26,6 +26,8 @@ The **fastest and most secure way** to install Xen Orchestra is to use our web d
|
||||
|
||||
> **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:
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"/xo-web/"
|
||||
],
|
||||
"testRegex": "\\.spec\\.js$",
|
||||
"timers": "fake",
|
||||
"transform": {
|
||||
"\\.jsx?$": "babel-jest"
|
||||
}
|
||||
|
||||
189
packages/xen-api/examples/package-lock.json
generated
Normal file
189
packages/xen-api/examples/package-lock.json
generated
Normal file
@@ -0,0 +1,189 @@
|
||||
{
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"event-loop-delay": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/event-loop-delay/-/event-loop-delay-1.0.0.tgz",
|
||||
"integrity": "sha512-8YtyeIWHXrvTqlAhv+fmtaGGARmgStbvocERYzrZ3pwhnQULe5PuvMUTjIWw/emxssoaftfHZsJtkeY8xjiXCg==",
|
||||
"requires": {
|
||||
"napi-macros": "^1.8.2",
|
||||
"node-gyp-build": "^3.7.0"
|
||||
}
|
||||
},
|
||||
"getopts": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/getopts/-/getopts-2.2.5.tgz",
|
||||
"integrity": "sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA=="
|
||||
},
|
||||
"golike-defer": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/golike-defer/-/golike-defer-0.4.1.tgz",
|
||||
"integrity": "sha512-x8cq/Fvu32T8cnco3CBDRF+/M2LFmfSIysKfecX09uIK3cFdHcEKBTPlPnEO6lwrdxfjkOIU6dIw3EIlEJeS1A=="
|
||||
},
|
||||
"human-format": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/human-format/-/human-format-0.10.1.tgz",
|
||||
"integrity": "sha512-UzCHToSw3HI9MxH9tYzMr1JbHJbgzr6o0hZCun7sruv59S1leps21bmgpBkkwEvQon5n/2OWKH1iU7BEko02cg=="
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"make-error": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
|
||||
"integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"napi-macros": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-1.8.2.tgz",
|
||||
"integrity": "sha512-Tr0DNY4RzTaBG2W2m3l7ZtFuJChTH6VZhXVhkGGjF/4cZTt+i8GcM9ozD+30Lmr4mDoZ5Xx34t2o4GJqYWDGcg=="
|
||||
},
|
||||
"node-gyp-build": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.9.0.tgz",
|
||||
"integrity": "sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A=="
|
||||
},
|
||||
"prettier-bytes": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier-bytes/-/prettier-bytes-1.0.4.tgz",
|
||||
"integrity": "sha1-mUsCqkb2mcULYle1+qp/4lV+YtY="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"process-top": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-top/-/process-top-1.0.0.tgz",
|
||||
"integrity": "sha512-er8iSmBMslOt5cgIHg9m6zilTPsuUqpEb1yfQ4bDmO80zr/e/5hNn+Tay3CJM/FOBnJo8Bt3fFiDDH6GvIgeAg==",
|
||||
"requires": {
|
||||
"event-loop-delay": "^1.0.0",
|
||||
"prettier-bytes": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"progress-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-2.0.0.tgz",
|
||||
"integrity": "sha1-+sY6Cz0R3qy7CWmrzJOyFLzhntU=",
|
||||
"requires": {
|
||||
"speedometer": "~1.0.0",
|
||||
"through2": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"promise-toolbox": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/promise-toolbox/-/promise-toolbox-0.13.0.tgz",
|
||||
"integrity": "sha512-Z6u7EL9/QyY1zZqeqpEiKS7ygKwZyl0JL0ouno/en6vMliZZc4AmM0aFCrDAVxEyKqj2f3SpkW0lXEfAZsNWiQ==",
|
||||
"requires": {
|
||||
"make-error": "^1.3.2"
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
|
||||
"integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"speedometer": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/speedometer/-/speedometer-1.0.0.tgz",
|
||||
"integrity": "sha1-zWccsGdSwivKM3Di8zREC+T8YuI="
|
||||
},
|
||||
"stream-parser": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz",
|
||||
"integrity": "sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M=",
|
||||
"requires": {
|
||||
"debug": "2"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"throttle": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/throttle/-/throttle-1.0.3.tgz",
|
||||
"integrity": "sha1-ijLkoV8XY9mXlIMXxevjrYpB5Lc=",
|
||||
"requires": {
|
||||
"readable-stream": ">= 0.3.0",
|
||||
"stream-parser": ">= 0.0.2"
|
||||
}
|
||||
},
|
||||
"through2": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
|
||||
"integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
|
||||
"requires": {
|
||||
"readable-stream": "~2.3.6",
|
||||
"xtend": "~4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"human-format": "^0.10.1",
|
||||
"process-top": "^1.0.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"promise-toolbox": "^0.11.0",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"readable-stream": "^3.1.1",
|
||||
"throttle": "^1.0.3"
|
||||
}
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
|
||||
|
||||
debug@2:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
event-loop-delay@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/event-loop-delay/-/event-loop-delay-1.0.0.tgz#5af6282549494fd0d868c499cbdd33e027978b8c"
|
||||
integrity sha512-8YtyeIWHXrvTqlAhv+fmtaGGARmgStbvocERYzrZ3pwhnQULe5PuvMUTjIWw/emxssoaftfHZsJtkeY8xjiXCg==
|
||||
dependencies:
|
||||
napi-macros "^1.8.2"
|
||||
node-gyp-build "^3.7.0"
|
||||
|
||||
getopts@^2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.3.tgz#11d229775e2ec2067ed8be6fcc39d9b4bf39cf7d"
|
||||
integrity sha512-viEcb8TpgeG05+Nqo5EzZ8QR0hxdyrYDp6ZSTZqe2M/h53Bk036NmqG38Vhf5RGirC/Of9Xql+v66B2gp256SQ==
|
||||
|
||||
golike-defer@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/golike-defer/-/golike-defer-0.4.1.tgz#7a1cd435d61e461305805d980b133a0f3db4e1cc"
|
||||
|
||||
human-format@^0.10.1:
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/human-format/-/human-format-0.10.1.tgz#107793f355912e256148d5b5dcf66a0230187ee9"
|
||||
integrity sha512-UzCHToSw3HI9MxH9tYzMr1JbHJbgzr6o0hZCun7sruv59S1leps21bmgpBkkwEvQon5n/2OWKH1iU7BEko02cg==
|
||||
|
||||
inherits@^2.0.3, inherits@~2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
||||
|
||||
isarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
|
||||
|
||||
make-error@^1.3.2:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8"
|
||||
integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
|
||||
|
||||
napi-macros@^1.8.2:
|
||||
version "1.8.2"
|
||||
resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-1.8.2.tgz#299265c1d8aa401351ad0675107d751228c03eda"
|
||||
integrity sha512-Tr0DNY4RzTaBG2W2m3l7ZtFuJChTH6VZhXVhkGGjF/4cZTt+i8GcM9ozD+30Lmr4mDoZ5Xx34t2o4GJqYWDGcg==
|
||||
|
||||
node-gyp-build@^3.7.0:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.7.0.tgz#daa77a4f547b9aed3e2aac779eaf151afd60ec8d"
|
||||
integrity sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==
|
||||
|
||||
prettier-bytes@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/prettier-bytes/-/prettier-bytes-1.0.4.tgz#994b02aa46f699c50b6257b5faaa7fe2557e62d6"
|
||||
integrity sha1-mUsCqkb2mcULYle1+qp/4lV+YtY=
|
||||
|
||||
process-nextick-args@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
|
||||
integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==
|
||||
|
||||
process-top@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/process-top/-/process-top-1.0.0.tgz#52892bedb581c5abf0df2d0aa5c429e34275cc7e"
|
||||
integrity sha512-er8iSmBMslOt5cgIHg9m6zilTPsuUqpEb1yfQ4bDmO80zr/e/5hNn+Tay3CJM/FOBnJo8Bt3fFiDDH6GvIgeAg==
|
||||
dependencies:
|
||||
event-loop-delay "^1.0.0"
|
||||
prettier-bytes "^1.0.4"
|
||||
|
||||
progress-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-2.0.0.tgz#fac63a0b3d11deacbb0969abcc93b214bce19ed5"
|
||||
integrity sha1-+sY6Cz0R3qy7CWmrzJOyFLzhntU=
|
||||
dependencies:
|
||||
speedometer "~1.0.0"
|
||||
through2 "~2.0.3"
|
||||
|
||||
promise-toolbox@^0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/promise-toolbox/-/promise-toolbox-0.11.0.tgz#9ed928355355395072dace3f879879504e07d1bc"
|
||||
integrity sha512-bjHk0kq+Ke3J3zbkbbJH6kXCyQZbFHwOTrE/Et7vS0uS0tluoV+PLqU/kEyxl8aARM7v04y2wFoDo/wWAEPvjA==
|
||||
dependencies:
|
||||
make-error "^1.3.2"
|
||||
|
||||
"readable-stream@>= 0.3.0", readable-stream@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.1.1.tgz#ed6bbc6c5ba58b090039ff18ce670515795aeb06"
|
||||
integrity sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==
|
||||
dependencies:
|
||||
inherits "^2.0.3"
|
||||
string_decoder "^1.1.1"
|
||||
util-deprecate "^1.0.1"
|
||||
|
||||
readable-stream@~2.3.6:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
|
||||
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.3"
|
||||
isarray "~1.0.0"
|
||||
process-nextick-args "~2.0.0"
|
||||
safe-buffer "~5.1.1"
|
||||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||
|
||||
speedometer@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-1.0.0.tgz#cd671cb06752c22bca3370e2f334440be4fc62e2"
|
||||
integrity sha1-zWccsGdSwivKM3Di8zREC+T8YuI=
|
||||
|
||||
"stream-parser@>= 0.0.2":
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773"
|
||||
integrity sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M=
|
||||
dependencies:
|
||||
debug "2"
|
||||
|
||||
string_decoder@^1.1.1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
|
||||
integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
string_decoder@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
throttle@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/throttle/-/throttle-1.0.3.tgz#8a32e4a15f1763d997948317c5ebe3ad8a41e4b7"
|
||||
integrity sha1-ijLkoV8XY9mXlIMXxevjrYpB5Lc=
|
||||
dependencies:
|
||||
readable-stream ">= 0.3.0"
|
||||
stream-parser ">= 0.0.2"
|
||||
|
||||
through2@~2.0.3:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
|
||||
integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
|
||||
dependencies:
|
||||
readable-stream "~2.3.6"
|
||||
xtend "~4.0.1"
|
||||
|
||||
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||
|
||||
xtend@~4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
|
||||
integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
|
||||
@@ -1,4 +1,4 @@
|
||||
const URL_RE = /^(?:(https?:)\/*)?(?:([^:]+):([^@]+)@)?([^/]+?)(?::([0-9]+))?\/?$/
|
||||
const URL_RE = /^(?:(https?:)\/*)?(?:([^:]+):([^@]+)@)?(?:\[([^\]]+)\]|([^:/]+))(?::([0-9]+))?\/?$/
|
||||
|
||||
export default url => {
|
||||
const matches = URL_RE.exec(url)
|
||||
@@ -6,7 +6,15 @@ export default url => {
|
||||
throw new Error('invalid URL: ' + url)
|
||||
}
|
||||
|
||||
const [, protocol = 'https:', username, password, hostname, port] = matches
|
||||
const [
|
||||
,
|
||||
protocol = 'https:',
|
||||
username,
|
||||
password,
|
||||
ipv6,
|
||||
hostname = ipv6,
|
||||
port,
|
||||
] = matches
|
||||
const parsedUrl = { protocol, hostname, port }
|
||||
if (username !== undefined) {
|
||||
parsedUrl.username = decodeURIComponent(username)
|
||||
|
||||
@@ -199,7 +199,18 @@ function main(args) {
|
||||
return exports[fnName](args.slice(1))
|
||||
}
|
||||
|
||||
return exports.call(args)
|
||||
return exports.call(args).catch(error => {
|
||||
if (!(error != null && error.code === 10 && 'errors' in error.data)) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const lines = [error.message]
|
||||
const { errors } = error.data
|
||||
errors.forEach(error => {
|
||||
lines.push(` property ${error.property}: ${error.message}`)
|
||||
})
|
||||
throw lines.join('\n')
|
||||
})
|
||||
}
|
||||
exports = module.exports = main
|
||||
|
||||
|
||||
@@ -20,9 +20,13 @@ class XoServerCloud {
|
||||
}
|
||||
|
||||
async load() {
|
||||
const getResourceCatalog = () => this._getCatalog()
|
||||
getResourceCatalog.description = 'Get the list of all available resources'
|
||||
const getResourceCatalog = this._getCatalog.bind(this)
|
||||
getResourceCatalog.description =
|
||||
"Get the list of user's available resources"
|
||||
getResourceCatalog.permission = 'admin'
|
||||
getResourceCatalog.params = {
|
||||
filters: { type: 'object', optional: true },
|
||||
}
|
||||
|
||||
const registerResource = ({ namespace }) =>
|
||||
this._registerResource(namespace)
|
||||
@@ -34,8 +38,29 @@ class XoServerCloud {
|
||||
}
|
||||
registerResource.permission = 'admin'
|
||||
|
||||
const downloadAndInstallResource = this._downloadAndInstallResource.bind(
|
||||
this
|
||||
)
|
||||
|
||||
downloadAndInstallResource.description =
|
||||
'Download and install a resource via cloud plugin'
|
||||
|
||||
downloadAndInstallResource.params = {
|
||||
id: { type: 'string' },
|
||||
namespace: { type: 'string' },
|
||||
version: { type: 'string' },
|
||||
sr: { type: 'string' },
|
||||
}
|
||||
|
||||
downloadAndInstallResource.resolve = {
|
||||
sr: ['sr', 'SR', 'administrate'],
|
||||
}
|
||||
|
||||
downloadAndInstallResource.permission = 'admin'
|
||||
|
||||
this._unsetApiMethods = this._xo.addApiMethods({
|
||||
cloud: {
|
||||
downloadAndInstallResource,
|
||||
getResourceCatalog,
|
||||
registerResource,
|
||||
},
|
||||
@@ -66,8 +91,8 @@ class XoServerCloud {
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _getCatalog() {
|
||||
const catalog = await this._updater.call('getResourceCatalog')
|
||||
async _getCatalog({ filters } = {}) {
|
||||
const catalog = await this._updater.call('getResourceCatalog', { filters })
|
||||
|
||||
if (!catalog) {
|
||||
throw new Error('cannot get catalog')
|
||||
@@ -90,6 +115,26 @@ class XoServerCloud {
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _downloadAndInstallResource({ id, namespace, sr, version }) {
|
||||
const stream = await this._requestResource({
|
||||
hub: true,
|
||||
id,
|
||||
namespace,
|
||||
version,
|
||||
})
|
||||
const vm = await this._xo.getXapi(sr.$poolId).importVm(stream, {
|
||||
srId: sr.id,
|
||||
type: 'xva',
|
||||
})
|
||||
await vm.update_other_config({
|
||||
'xo:resource:namespace': namespace,
|
||||
'xo:resource:xva:version': version,
|
||||
'xo:resource:xva:id': id,
|
||||
})
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _registerResource(namespace) {
|
||||
const _namespace = (await this._getNamespaces())[namespace]
|
||||
|
||||
@@ -106,8 +151,10 @@ class XoServerCloud {
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _getNamespaceCatalog(namespace) {
|
||||
const namespaceCatalog = (await this._getCatalog())[namespace]
|
||||
async _getNamespaceCatalog({ hub, namespace }) {
|
||||
const namespaceCatalog = (await this._getCatalog({ filters: { hub } }))[
|
||||
namespace
|
||||
]
|
||||
|
||||
if (!namespaceCatalog) {
|
||||
throw new Error(`cannot get catalog: ${namespace} not registered`)
|
||||
@@ -118,14 +165,17 @@ class XoServerCloud {
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _requestResource(namespace, id, version) {
|
||||
async _requestResource({ hub = false, id, namespace, version }) {
|
||||
const _namespace = (await this._getNamespaces())[namespace]
|
||||
|
||||
if (!_namespace || !_namespace.registered) {
|
||||
if (!hub && (!_namespace || !_namespace.registered)) {
|
||||
throw new Error(`cannot get resource: ${namespace} not registered`)
|
||||
}
|
||||
|
||||
const { _token: token } = await this._getNamespaceCatalog(namespace)
|
||||
const { _token: token } = await this._getNamespaceCatalog({
|
||||
hub,
|
||||
namespace,
|
||||
})
|
||||
|
||||
// 2018-03-20 Extra check: getResourceDownloadToken seems to be called without a token in some cases
|
||||
if (token === undefined) {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/cron": "^1.0.4",
|
||||
"lodash": "^4.16.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/cron": "^1.0.4",
|
||||
"d3-time-format": "^2.1.1",
|
||||
"json5": "^2.0.1",
|
||||
"lodash": "^4.17.4"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.0",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import NodeOpenssl from 'node-openssl-cert'
|
||||
import uuidv4 from 'uuid/v4'
|
||||
import { access, constants, readFile, writeFile } from 'fs'
|
||||
import { EventEmitter } from 'events'
|
||||
import { filter, find, forEach, forOwn, map, omitBy } from 'lodash'
|
||||
import { filter, find, forOwn, map, omitBy, sample } from 'lodash'
|
||||
import { fromCallback, fromEvent } from 'promise-toolbox'
|
||||
import { join } from 'path'
|
||||
|
||||
@@ -60,7 +60,7 @@ async function fileExists(path) {
|
||||
try {
|
||||
await fromCallback(access, path, constants.F_OK)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
if (error?.code === 'ENOENT') {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -103,6 +103,14 @@ function updateNetworkOtherConfig(network) {
|
||||
)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function createPassword() {
|
||||
const chars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789?!'
|
||||
return Array.from({ length: 16 }, _ => sample(chars)).join('')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
class SDNController extends EventEmitter {
|
||||
@@ -110,6 +118,7 @@ class SDNController extends EventEmitter {
|
||||
Attributes on created networks:
|
||||
- `other_config`:
|
||||
- `xo:sdn-controller:encapsulation` : encapsulation protocol used for tunneling (either `gre` or `vxlan`)
|
||||
- `xo:sdn-controller:encrypted` : `true` if the network is encrypted
|
||||
- `xo:sdn-controller:pif-device` : PIF device on which the tunnels are created, must be physical and have an IP configuration
|
||||
- `xo:sdn-controller:private-pool-wide`: `true` if the network is created (and so must be managed) by a SDN Controller
|
||||
- `xo:sdn-controller:vni` : VxLAN Network Identifier,
|
||||
@@ -197,7 +206,12 @@ class SDNController extends EventEmitter {
|
||||
async load() {
|
||||
// Expose method to create pool-wide private network
|
||||
const createPrivateNetwork = params =>
|
||||
this._createPrivateNetwork({ ...params, vni: ++this._prevVni })
|
||||
this._createPrivateNetwork({
|
||||
encrypted: false,
|
||||
mtu: 0,
|
||||
...params,
|
||||
vni: ++this._prevVni,
|
||||
})
|
||||
|
||||
createPrivateNetwork.description =
|
||||
'Creates a pool-wide private network on a selected pool'
|
||||
@@ -207,6 +221,8 @@ class SDNController extends EventEmitter {
|
||||
networkDescription: { type: 'string' },
|
||||
encapsulation: { type: 'string' },
|
||||
pifId: { type: 'string' },
|
||||
encrypted: { type: 'boolean', optional: true },
|
||||
mtu: { type: 'integer', optional: true },
|
||||
}
|
||||
createPrivateNetwork.resolve = {
|
||||
xoPool: ['poolId', 'pool', ''],
|
||||
@@ -214,9 +230,9 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
// Expose method to create cross-pool private network
|
||||
const createCrossPoolPrivateNetwork = this._createCrossPoolPrivateNetwork.bind(
|
||||
this
|
||||
)
|
||||
const createCrossPoolPrivateNetwork = params =>
|
||||
this._createCrossPoolPrivateNetwork({ encrypted: false, mtu: 0, ...params })
|
||||
|
||||
createCrossPoolPrivateNetwork.description =
|
||||
'Creates a cross-pool private network on selected pools'
|
||||
createCrossPoolPrivateNetwork.params = {
|
||||
@@ -235,6 +251,8 @@ class SDNController extends EventEmitter {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
encrypted: { type: 'boolean', optional: true },
|
||||
mtu: { type: 'integer', optional: true },
|
||||
}
|
||||
|
||||
this._unsetApiMethods = this._xo.addApiMethods({
|
||||
@@ -394,6 +412,8 @@ class SDNController extends EventEmitter {
|
||||
encapsulation,
|
||||
xoPif,
|
||||
vni,
|
||||
encrypted,
|
||||
mtu,
|
||||
}) {
|
||||
const pool = this._xo.getXapiObject(xoPool)
|
||||
await this._setPoolControllerIfNeeded(pool)
|
||||
@@ -404,12 +424,13 @@ class SDNController extends EventEmitter {
|
||||
const privateNetworkRef = await pool.$xapi.call('network.create', {
|
||||
name_label: networkName,
|
||||
name_description: networkDescription,
|
||||
MTU: 0,
|
||||
MTU: mtu,
|
||||
other_config: {
|
||||
// Set `automatic` to false so XenCenter does not get confused
|
||||
// See: https://citrix.github.io/xenserver-sdk/#network
|
||||
automatic: 'false',
|
||||
'xo:sdn-controller:encapsulation': encapsulation,
|
||||
'xo:sdn-controller:encrypted': encrypted ? 'true' : undefined,
|
||||
'xo:sdn-controller:pif-device': pif.device,
|
||||
'xo:sdn-controller:private-pool-wide': 'true',
|
||||
'xo:sdn-controller:vni': String(vni),
|
||||
@@ -453,6 +474,8 @@ class SDNController extends EventEmitter {
|
||||
networkDescription,
|
||||
encapsulation,
|
||||
xoPifIds,
|
||||
encrypted,
|
||||
mtu,
|
||||
}) {
|
||||
const uuid = uuidv4()
|
||||
const crossPoolNetwork = {
|
||||
@@ -481,6 +504,8 @@ class SDNController extends EventEmitter {
|
||||
encapsulation,
|
||||
xoPif,
|
||||
vni,
|
||||
encrypted,
|
||||
mtu,
|
||||
})
|
||||
|
||||
const network = pool.$xapi.getObjectByRef(poolNetwork.network)
|
||||
@@ -522,7 +547,7 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
_objectsAdded(objects) {
|
||||
forEach(objects, object => {
|
||||
forOwn(objects, object => {
|
||||
const { $type } = object
|
||||
|
||||
if ($type === 'host') {
|
||||
@@ -540,26 +565,28 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
_objectsUpdated(objects) {
|
||||
return Promise.all(
|
||||
map(objects, object => {
|
||||
forOwn(objects, async object => {
|
||||
try {
|
||||
const { $type } = object
|
||||
|
||||
if ($type === 'PIF') {
|
||||
return this._pifUpdated(object)
|
||||
await this._pifUpdated(object)
|
||||
} else if ($type === 'host') {
|
||||
await this._hostUpdated(object)
|
||||
} else if ($type === 'host_metrics') {
|
||||
await this._hostMetricsUpdated(object)
|
||||
}
|
||||
if ($type === 'host') {
|
||||
return this._hostUpdated(object)
|
||||
}
|
||||
if ($type === 'host_metrics') {
|
||||
return this._hostMetricsUpdated(object)
|
||||
}
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
log.error('Error in _objectsUpdated', {
|
||||
error,
|
||||
object,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_objectsRemoved(xapi, objects) {
|
||||
return Promise.all(
|
||||
map(objects, async (object, id) => {
|
||||
forOwn(objects, async (object, id) => {
|
||||
try {
|
||||
this._ovsdbClients = this._ovsdbClients.filter(
|
||||
client => client.host.$id !== id
|
||||
)
|
||||
@@ -601,8 +628,13 @@ class SDNController extends EventEmitter {
|
||||
crossPoolNetwork => crossPoolNetwork.networks.length === 0
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
log.error('Error in _objectsRemoved', {
|
||||
error,
|
||||
object,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async _pifUpdated(pif) {
|
||||
@@ -736,10 +768,10 @@ class SDNController extends EventEmitter {
|
||||
const network = host.$xapi.getObjectByRef(poolNetwork.network)
|
||||
const pifDevice =
|
||||
network.other_config['xo:sdn-controller:pif-device'] ?? 'eth0'
|
||||
this._createTunnel(host, network, pifDevice)
|
||||
await this._createTunnel(host, network, pifDevice)
|
||||
}
|
||||
|
||||
this._addHostToPoolNetworks(host)
|
||||
await this._addHostToPoolNetworks(host)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1127,6 +1159,11 @@ class SDNController extends EventEmitter {
|
||||
const encapsulation =
|
||||
otherConfig['xo:sdn-controller:encapsulation'] ?? 'gre'
|
||||
const vni = otherConfig['xo:sdn-controller:vni'] ?? '0'
|
||||
const password =
|
||||
otherConfig['xo:sdn-controller:encrypted'] === 'true'
|
||||
? createPassword()
|
||||
: undefined
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
client.addInterfaceAndPort(
|
||||
@@ -1135,6 +1172,7 @@ class SDNController extends EventEmitter {
|
||||
centerClient.host.address,
|
||||
encapsulation,
|
||||
vni,
|
||||
password,
|
||||
centerNetwork.uuid
|
||||
),
|
||||
centerClient.addInterfaceAndPort(
|
||||
@@ -1143,6 +1181,7 @@ class SDNController extends EventEmitter {
|
||||
client.host.address,
|
||||
encapsulation,
|
||||
vni,
|
||||
password,
|
||||
network.uuid
|
||||
),
|
||||
])
|
||||
@@ -1220,6 +1259,12 @@ class SDNController extends EventEmitter {
|
||||
const encapsulation =
|
||||
otherConfig['xo:sdn-controller:encapsulation'] ?? 'gre'
|
||||
const vni = otherConfig['xo:sdn-controller:vni'] ?? '0'
|
||||
|
||||
const password =
|
||||
otherConfig['xo:sdn-controller:encrypted'] === 'true'
|
||||
? createPassword()
|
||||
: undefined
|
||||
|
||||
let bridgeName
|
||||
try {
|
||||
;[bridgeName] = await Promise.all([
|
||||
@@ -1228,14 +1273,16 @@ class SDNController extends EventEmitter {
|
||||
network.name_label,
|
||||
starCenterClient.host.address,
|
||||
encapsulation,
|
||||
vni
|
||||
vni,
|
||||
password
|
||||
),
|
||||
starCenterClient.addInterfaceAndPort(
|
||||
network.uuid,
|
||||
network.name_label,
|
||||
hostClient.host.address,
|
||||
encapsulation,
|
||||
vni
|
||||
vni,
|
||||
password
|
||||
),
|
||||
])
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import assert from 'assert'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import forOwn from 'lodash/forOwn'
|
||||
import fromEvent from 'promise-toolbox/fromEvent'
|
||||
import { connect } from 'tls'
|
||||
import { forOwn, toPairs } from 'lodash'
|
||||
|
||||
const log = createLogger('xo:xo-server:sdn-controller:ovsdb-client')
|
||||
|
||||
@@ -10,6 +10,12 @@ const OVSDB_PORT = 6640
|
||||
|
||||
// =============================================================================
|
||||
|
||||
function toMap(object) {
|
||||
return ['map', toPairs(object)]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
export class OvsdbClient {
|
||||
/*
|
||||
Create an SSL connection to an XCP-ng host.
|
||||
@@ -18,6 +24,7 @@ export class OvsdbClient {
|
||||
See:
|
||||
- OVSDB Protocol: https://tools.ietf.org/html/rfc7047
|
||||
- OVS Tunneling : http://docs.openvswitch.org/en/latest/howto/tunneling/
|
||||
- OVS IPSEC : http://docs.openvswitch.org/en/latest/howto/ipsec/
|
||||
|
||||
Attributes on created OVS ports (corresponds to a XAPI `PIF` or `VIF`):
|
||||
- `other_config`:
|
||||
@@ -65,6 +72,7 @@ export class OvsdbClient {
|
||||
remoteAddress,
|
||||
encapsulation,
|
||||
key,
|
||||
password,
|
||||
remoteNetwork
|
||||
) {
|
||||
if (
|
||||
@@ -110,33 +118,36 @@ export class OvsdbClient {
|
||||
const portName = bridgeName + '_port' + index
|
||||
|
||||
// Add interface and port to the bridge
|
||||
const options = ['map', [['remote_ip', remoteAddress], ['key', key]]]
|
||||
const otherConfig =
|
||||
remoteNetwork !== undefined
|
||||
? ['map', [['xo:sdn-controller:cross-pool', remoteNetwork]]]
|
||||
: ['map', [['xo:sdn-controller:private-pool-wide', 'true']]]
|
||||
|
||||
const options = { remote_ip: remoteAddress, key: key }
|
||||
if (password !== undefined) {
|
||||
options.psk = password
|
||||
}
|
||||
const addInterfaceOperation = {
|
||||
op: 'insert',
|
||||
table: 'Interface',
|
||||
row: {
|
||||
type: encapsulation,
|
||||
options: options,
|
||||
options: toMap(options),
|
||||
name: interfaceName,
|
||||
other_config: otherConfig,
|
||||
},
|
||||
'uuid-name': 'new_iface',
|
||||
}
|
||||
|
||||
const addPortOperation = {
|
||||
op: 'insert',
|
||||
table: 'Port',
|
||||
row: {
|
||||
name: portName,
|
||||
interfaces: ['set', [['named-uuid', 'new_iface']]],
|
||||
other_config: otherConfig,
|
||||
other_config: toMap(
|
||||
remoteNetwork !== undefined
|
||||
? { 'xo:sdn-controller:cross-pool': remoteNetwork }
|
||||
: { 'xo:sdn-controller:private-pool-wide': 'true' }
|
||||
),
|
||||
},
|
||||
'uuid-name': 'new_port',
|
||||
}
|
||||
|
||||
const mutateBridgeOperation = {
|
||||
op: 'mutate',
|
||||
table: 'Bridge',
|
||||
@@ -326,11 +337,7 @@ export class OvsdbClient {
|
||||
|
||||
async _getBridgeUuidForNetwork(networkUuid, networkName, socket) {
|
||||
const where = [
|
||||
[
|
||||
'external_ids',
|
||||
'includes',
|
||||
['map', [['xs-network-uuids', networkUuid]]],
|
||||
],
|
||||
['external_ids', 'includes', toMap({ 'xs-network-uuids': networkUuid })],
|
||||
]
|
||||
const selectResult = await this._select(
|
||||
'Bridge',
|
||||
|
||||
@@ -15,15 +15,20 @@ src
|
||||
| | └─ index.spec.js.snap
|
||||
| └─ index.spec.js
|
||||
├─ job
|
||||
| └─ index.spec.js
|
||||
├─ issues
|
||||
¦ └─ index.spec.js
|
||||
¦
|
||||
¦
|
||||
├─ _xoConnection.js
|
||||
└─ util.js
|
||||
```
|
||||
|
||||
The tests can describe xo methods or scenarios:
|
||||
```javascript
|
||||
The tests can describe:
|
||||
|
||||
- XO methods or scenarios:
|
||||
|
||||
`src/user/index.js`
|
||||
```js
|
||||
import xo from "../_xoConnection";
|
||||
|
||||
describe("user", () => {
|
||||
@@ -46,6 +51,16 @@ describe("user", () => {
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
- issues
|
||||
|
||||
`src/issues/index.js`
|
||||
```js
|
||||
describe("issue", () => {
|
||||
test("5454", () => {
|
||||
/* some tests */
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Best practices
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
email = ''
|
||||
password = ''
|
||||
|
||||
[pools]
|
||||
default = ''
|
||||
|
||||
[servers]
|
||||
[servers.default]
|
||||
username = ''
|
||||
|
||||
@@ -87,7 +87,7 @@ class XoConnection extends Xo {
|
||||
while (true) {
|
||||
try {
|
||||
await predicate(obj)
|
||||
return
|
||||
return obj
|
||||
} catch (_) {}
|
||||
// If failed, wait for next object state/update and retry.
|
||||
obj = await this.waitObject(id)
|
||||
@@ -116,13 +116,26 @@ class XoConnection extends Xo {
|
||||
return job
|
||||
}
|
||||
|
||||
async createTempNetwork(params) {
|
||||
const id = await this.call('network.create', {
|
||||
name: 'XO Test',
|
||||
pool: config.pools.default,
|
||||
...params,
|
||||
})
|
||||
this._tempResourceDisposers.push('network.delete', { id })
|
||||
return this.getOrWaitObject(id)
|
||||
}
|
||||
|
||||
async createTempVm(params) {
|
||||
const id = await this.call('vm.create', params)
|
||||
const id = await this.call('vm.create', {
|
||||
name_label: 'XO Test',
|
||||
template: config.templates.templateWithoutDisks,
|
||||
...params,
|
||||
})
|
||||
this._tempResourceDisposers.push('vm.delete', { id })
|
||||
await this.waitObjectState(id, vm => {
|
||||
return this.waitObjectState(id, vm => {
|
||||
if (vm.type !== 'VM') throw new Error('retry')
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
async createTempRemote(params) {
|
||||
@@ -197,6 +210,10 @@ class XoConnection extends Xo {
|
||||
return backups
|
||||
}
|
||||
|
||||
getBackupLogs(filter) {
|
||||
return this.call('backupNg.getLogs', { _forceRefresh: true, ...filter })
|
||||
}
|
||||
|
||||
async _cleanDisposers(disposers) {
|
||||
for (let n = disposers.length - 1; n > 0; ) {
|
||||
const params = disposers[n--]
|
||||
|
||||
@@ -66,6 +66,10 @@ const validateOperationTask = (task, props) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Note: `bypassVdiChainsCheck` must be enabled because the XAPI might be not
|
||||
// able to coalesce VDIs as fast as the tests run.
|
||||
//
|
||||
// See https://xen-orchestra.com/docs/backup_troubleshooting.html#vdi-chain-protection
|
||||
describe('backupNg', () => {
|
||||
let defaultBackupNg
|
||||
|
||||
@@ -217,7 +221,7 @@ describe('backupNg', () => {
|
||||
expect(typeof schedule).toBe('object')
|
||||
|
||||
await xo.call('backupNg.runJob', { id: jobId, schedule: schedule.id })
|
||||
const [log] = await xo.call('backupNg.getLogs', {
|
||||
const [log] = await xo.getBackupLogs({
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
expect(log.warnings).toMatchSnapshot()
|
||||
@@ -226,7 +230,7 @@ describe('backupNg', () => {
|
||||
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({
|
||||
const { id: vmIdWithoutDisks } = await xo.createTempVm({
|
||||
name_label: 'XO Test Without Disks',
|
||||
name_description: 'Creating a vm without disks',
|
||||
template: config.templates.templateWithoutDisks,
|
||||
@@ -256,7 +260,7 @@ describe('backupNg', () => {
|
||||
tasks: [vmTask],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
] = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
@@ -315,7 +319,7 @@ describe('backupNg', () => {
|
||||
tasks: [task],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
] = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
@@ -347,7 +351,7 @@ describe('backupNg', () => {
|
||||
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({
|
||||
let vm = await xo.createTempVm({
|
||||
name_label: 'XO Test Temp',
|
||||
name_description: 'Creating a temporary vm',
|
||||
template: config.templates.default,
|
||||
@@ -364,45 +368,46 @@ describe('backupNg', () => {
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
vms: {
|
||||
id: vmId,
|
||||
id: vm.id,
|
||||
},
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
},
|
||||
settings: {
|
||||
...defaultBackupNg.settings,
|
||||
'': {
|
||||
bypassVdiChainsCheck: true,
|
||||
reportWhen: 'never',
|
||||
},
|
||||
[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 }) => {
|
||||
vm = await xo.waitObjectState(vm.id, ({ snapshots }) => {
|
||||
// Test on updating snapshots.
|
||||
expect(snapshots).not.toEqual(oldSnapshots)
|
||||
expect(snapshots).not.toEqual(vm.snapshots)
|
||||
})
|
||||
}
|
||||
|
||||
const { snapshots, videoram: oldVideoram } = xo.objects.all[vmId]
|
||||
|
||||
// Test on the retention, how many snapshots should be saved.
|
||||
expect(snapshots.length).toBe(2)
|
||||
expect(vm.snapshots.length).toBe(2)
|
||||
|
||||
const newVideoram = 16
|
||||
await xo.call('vm.set', { id: vmId, videoram: newVideoram })
|
||||
await xo.waitObjectState(vmId, ({ videoram }) => {
|
||||
await xo.call('vm.set', { id: vm.id, videoram: newVideoram })
|
||||
await xo.waitObjectState(vm.id, ({ videoram }) => {
|
||||
expect(videoram).toBe(newVideoram.toString())
|
||||
})
|
||||
|
||||
await xo.call('vm.revert', {
|
||||
snapshot: snapshots[0],
|
||||
snapshot: vm.snapshots[0],
|
||||
})
|
||||
|
||||
await xo.waitObjectState(vmId, ({ videoram }) => {
|
||||
expect(videoram).toBe(oldVideoram)
|
||||
await xo.waitObjectState(vm.id, ({ videoram }) => {
|
||||
expect(videoram).toBe(vm.videoram)
|
||||
})
|
||||
|
||||
const [
|
||||
@@ -410,7 +415,7 @@ describe('backupNg', () => {
|
||||
tasks: [{ tasks: subTasks, ...vmTask }],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
] = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
@@ -442,7 +447,7 @@ describe('backupNg', () => {
|
||||
message: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
})
|
||||
expect(vmTask.data.id).toBe(vmId)
|
||||
expect(vmTask.data.id).toBe(vm.id)
|
||||
})
|
||||
|
||||
test('execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval', async () => {
|
||||
@@ -477,8 +482,9 @@ describe('backupNg', () => {
|
||||
},
|
||||
settings: {
|
||||
'': {
|
||||
reportWhen: 'never',
|
||||
bypassVdiChainsCheck: true,
|
||||
fullInterval,
|
||||
reportWhen: 'never',
|
||||
},
|
||||
[remoteId1]: { deleteFirst: true },
|
||||
[scheduleTempId]: { exportRetention },
|
||||
@@ -500,7 +506,7 @@ describe('backupNg', () => {
|
||||
expect(backups.length).toBe(exportRetention)
|
||||
)
|
||||
|
||||
const backupLogs = await xo.call('backupNg.getLogs', {
|
||||
const backupLogs = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
|
||||
51
packages/xo-server-test/src/issues/index.spec.js
Normal file
51
packages/xo-server-test/src/issues/index.spec.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import config from '../_config'
|
||||
import xo from '../_xoConnection'
|
||||
|
||||
describe('issue', () => {
|
||||
test('4507', async () => {
|
||||
await xo.createTempServer(config.servers.default)
|
||||
|
||||
const props = {
|
||||
coresPerSocket: 1,
|
||||
cpuCap: 1,
|
||||
}
|
||||
const vm = await xo.createTempVm(props)
|
||||
expect(vm).toMatchObject(props)
|
||||
|
||||
await xo.call('vm.set', {
|
||||
coresPerSocket: null,
|
||||
cpuCap: null,
|
||||
id: vm.id,
|
||||
})
|
||||
await xo.waitObjectState(vm.id, vm => {
|
||||
expect(vm.coresPerSocket).toBe(undefined)
|
||||
expect(vm.cpuCap).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
test('4514', async () => {
|
||||
await xo.createTempServer(config.servers.default)
|
||||
|
||||
const oldName = 'Old XO Test name'
|
||||
const { id, name_label } = await xo.createTempNetwork({ name: oldName })
|
||||
expect(name_label).toBe(oldName)
|
||||
|
||||
const newName = 'New XO Test name'
|
||||
await xo.call('network.set', { id, name_label: newName })
|
||||
await xo.waitObjectState(id, ({ name_label }) => {
|
||||
expect(name_label).toBe(newName)
|
||||
})
|
||||
})
|
||||
|
||||
test('4523', async () => {
|
||||
const id = await xo.call('network.create', {
|
||||
name: 'XO Test',
|
||||
pool: config.pools.default,
|
||||
})
|
||||
expect(typeof id).toBe('string')
|
||||
|
||||
await xo.call('network.delete', { id })
|
||||
})
|
||||
})
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/async-map": "^0.0.0",
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/cron": "^1.0.4",
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"handlebars": "^4.0.6",
|
||||
"html-minifier": "^4.0.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.49.0",
|
||||
"version": "5.50.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -35,7 +35,7 @@
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.1",
|
||||
"@xen-orchestra/async-map": "^0.0.0",
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/cron": "^1.0.4",
|
||||
"@xen-orchestra/defined": "^0.0.0",
|
||||
"@xen-orchestra/emit-async": "^0.0.0",
|
||||
"@xen-orchestra/fs": "^0.10.1",
|
||||
@@ -46,6 +46,7 @@
|
||||
"archiver": "^3.0.0",
|
||||
"async-iterator-to-stream": "^1.0.1",
|
||||
"base64url": "^3.0.0",
|
||||
"bind-property-descriptor": "^1.0.0",
|
||||
"blocked": "^1.2.1",
|
||||
"bluebird": "^3.5.1",
|
||||
"body-parser": "^1.18.2",
|
||||
|
||||
34
packages/xo-server/src/_MultiKeyMap.spec.js
Normal file
34
packages/xo-server/src/_MultiKeyMap.spec.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import MultiKeyMap from './_MultiKeyMap'
|
||||
|
||||
describe('MultiKeyMap', () => {
|
||||
it('works', () => {
|
||||
const map = new MultiKeyMap()
|
||||
|
||||
const keys = [
|
||||
// null key
|
||||
[],
|
||||
// simple key
|
||||
['foo'],
|
||||
// composite key
|
||||
['foo', 'bar'],
|
||||
// reverse composite key
|
||||
['bar', 'foo'],
|
||||
]
|
||||
const values = keys.map(() => ({}))
|
||||
|
||||
// set all values first to make sure they are all stored and not only the
|
||||
// last one
|
||||
keys.forEach((key, i) => {
|
||||
map.set(key, values[i])
|
||||
})
|
||||
|
||||
keys.forEach((key, i) => {
|
||||
// copy the key to make sure the array itself is not the key
|
||||
expect(map.get(key.slice())).toBe(values[i])
|
||||
map.delete(key.slice())
|
||||
expect(map.get(key.slice())).toBe(undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -16,13 +16,28 @@ function scheduleRemoveCacheEntry(keys, expires) {
|
||||
|
||||
const defaultKeyFn = () => []
|
||||
|
||||
const { slice } = Array.prototype
|
||||
|
||||
export const REMOVE_CACHE_ENTRY = {}
|
||||
|
||||
// debounce an async function so that all subsequent calls in a delay receive
|
||||
// the same result
|
||||
//
|
||||
// similar to `p-debounce` with `leading` set to `true` but with key support
|
||||
export default (fn, delay, keyFn = defaultKeyFn) => {
|
||||
//
|
||||
// - `delay`: number of milliseconds to cache the response, a function can be
|
||||
// passed to use a custom delay for a call based on its parameters
|
||||
export const debounceWithKey = (fn, delay, keyFn = defaultKeyFn) => {
|
||||
const cache = new MultiKeyMap()
|
||||
return function() {
|
||||
const delayFn = typeof delay === 'number' ? () => delay : delay
|
||||
return function(arg) {
|
||||
if (arg === REMOVE_CACHE_ENTRY) {
|
||||
return removeCacheEntry(
|
||||
cache,
|
||||
ensureArray(keyFn.apply(this, slice.call(arguments, 1)))
|
||||
)
|
||||
}
|
||||
|
||||
const keys = ensureArray(keyFn.apply(this, arguments))
|
||||
let promise = cache.get(keys)
|
||||
if (promise === undefined) {
|
||||
@@ -30,10 +45,15 @@ export default (fn, delay, keyFn = defaultKeyFn) => {
|
||||
const remove = scheduleRemoveCacheEntry.bind(
|
||||
cache,
|
||||
keys,
|
||||
Date.now() + delay
|
||||
Date.now() + delayFn.apply(this, arguments)
|
||||
)
|
||||
promise.then(remove, remove)
|
||||
}
|
||||
return promise
|
||||
}
|
||||
}
|
||||
|
||||
debounceWithKey.decorate = (...params) => (target, name, descriptor) => ({
|
||||
...descriptor,
|
||||
value: debounceWithKey(descriptor.value, ...params),
|
||||
})
|
||||
|
||||
29
packages/xo-server/src/_pDebounceWithKey.spec.js
Normal file
29
packages/xo-server/src/_pDebounceWithKey.spec.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { debounceWithKey, REMOVE_CACHE_ENTRY } from './_pDebounceWithKey'
|
||||
|
||||
describe('REMOVE_CACHE_ENTRY', () => {
|
||||
it('clears the cache', async () => {
|
||||
let i = 0
|
||||
const debouncedFn = debounceWithKey(
|
||||
function() {
|
||||
return Promise.resolve(++i)
|
||||
},
|
||||
Infinity,
|
||||
id => id
|
||||
)
|
||||
|
||||
// not cached accross keys
|
||||
expect(await debouncedFn(1)).toBe(1)
|
||||
expect(await debouncedFn(2)).toBe(2)
|
||||
|
||||
// retrieve the already cached values
|
||||
expect(await debouncedFn(1)).toBe(1)
|
||||
expect(await debouncedFn(2)).toBe(2)
|
||||
|
||||
// an entry for a specific key can be removed
|
||||
debouncedFn(REMOVE_CACHE_ENTRY, 1)
|
||||
expect(await debouncedFn(1)).toBe(3)
|
||||
expect(await debouncedFn(2)).toBe(2)
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,7 @@ import { fromCallback } from 'promise-toolbox'
|
||||
import { pipeline } from 'readable-stream'
|
||||
|
||||
import createNdJsonStream from '../_createNdJsonStream'
|
||||
import { REMOVE_CACHE_ENTRY } from '../_pDebounceWithKey'
|
||||
import { safeDateFormat } from '../utils'
|
||||
|
||||
export function createJob({ schedules, ...job }) {
|
||||
@@ -184,7 +185,20 @@ getAllLogs.params = {
|
||||
ndjson: { type: 'boolean', optional: true },
|
||||
}
|
||||
|
||||
export function getLogs({ after, before, limit, ...filter }) {
|
||||
export function getLogs({
|
||||
after,
|
||||
before,
|
||||
limit,
|
||||
|
||||
// TODO: it's a temporary work-around which should be removed
|
||||
// when the consolidated logs will be stored in the DB
|
||||
_forceRefresh = false,
|
||||
|
||||
...filter
|
||||
}) {
|
||||
if (_forceRefresh) {
|
||||
this.getBackupNgLogs(REMOVE_CACHE_ENTRY)
|
||||
}
|
||||
return this.getBackupNgLogsSorted({ after, before, limit, filter })
|
||||
}
|
||||
|
||||
@@ -302,7 +316,7 @@ export async function fetchFiles(params) {
|
||||
filename += '.zip'
|
||||
|
||||
return this.registerHttpRequest(handleFetchFiles, params, {
|
||||
suffix: encodeURI(`/${filename}`),
|
||||
suffix: '/' + encodeURIComponent(filename),
|
||||
}).then(url => ({ $getFrom: url }))
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ export async function fetchFiles({ format = 'zip', ...params }) {
|
||||
handleFetchFiles,
|
||||
{ ...params, format },
|
||||
{
|
||||
suffix: encodeURI(`/${fileName}`),
|
||||
suffix: '/' + encodeURIComponent(fileName),
|
||||
}
|
||||
).then(url => ({ $getFrom: url }))
|
||||
}
|
||||
|
||||
@@ -221,12 +221,7 @@ emergencyShutdownHost.resolve = {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function isHostServerTimeConsistent({ host }) {
|
||||
try {
|
||||
await this.getXapi(host).assertConsistentHostServerTime(host._xapiRef)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
return this.getXapi(host).isHostServerTimeConsistent(host._xapiRef)
|
||||
}
|
||||
|
||||
isHostServerTimeConsistent.params = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import xapiObjectToXo from '../xapi-object-to-xo'
|
||||
import { mapToArray } from '../utils'
|
||||
|
||||
export function getBondModes() {
|
||||
@@ -12,13 +13,15 @@ export async function create({
|
||||
mtu = 1500,
|
||||
vlan = 0,
|
||||
}) {
|
||||
return this.getXapi(pool).createNetwork({
|
||||
name,
|
||||
description,
|
||||
pifId: pif && this.getObject(pif, 'PIF')._xapiId,
|
||||
mtu: +mtu,
|
||||
vlan: +vlan,
|
||||
})
|
||||
return xapiObjectToXo(
|
||||
await this.getXapi(pool).createNetwork({
|
||||
name,
|
||||
description,
|
||||
pifId: pif && this.getObject(pif, 'PIF')._xapiId,
|
||||
mtu: +mtu,
|
||||
vlan: +vlan,
|
||||
})
|
||||
).id
|
||||
}
|
||||
|
||||
create.params = {
|
||||
@@ -116,6 +119,9 @@ set.params = {
|
||||
type: 'boolean',
|
||||
optional: true,
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
},
|
||||
name_description: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
unauthorized,
|
||||
} from 'xo-common/api-errors'
|
||||
|
||||
import { forEach, map, mapFilter, parseSize } from '../utils'
|
||||
import { forEach, map, mapFilter, parseSize, safeDateFormat } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -1189,7 +1189,11 @@ async function export_({ vm, compress }) {
|
||||
|
||||
return {
|
||||
$getFrom: await this.registerHttpRequest(handleExport, data, {
|
||||
suffix: encodeURI(`/${vm.name_label}.xva`),
|
||||
suffix:
|
||||
'/' +
|
||||
encodeURIComponent(
|
||||
`${safeDateFormat(new Date())} - ${vm.name_label}.xva`
|
||||
),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1164,11 +1164,11 @@ async function _prepareGlusterVm(
|
||||
}
|
||||
|
||||
async function _importGlusterVM(xapi, template, lvmsrId) {
|
||||
const templateStream = await this.requestResource(
|
||||
'xosan',
|
||||
template.id,
|
||||
template.version
|
||||
)
|
||||
const templateStream = await this.requestResource({
|
||||
id: template.id,
|
||||
namespace: 'xosan',
|
||||
version: template.version,
|
||||
})
|
||||
const newVM = await xapi.importVm(templateStream, {
|
||||
srId: lvmsrId,
|
||||
type: 'xva',
|
||||
@@ -1535,8 +1535,11 @@ export async function downloadAndInstallXosanPack({ id, version, pool }) {
|
||||
}
|
||||
|
||||
const xapi = this.getXapi(pool.id)
|
||||
const res = await this.requestResource('xosan', id, version)
|
||||
|
||||
const res = await this.requestResource({
|
||||
id,
|
||||
namespace: 'xosan',
|
||||
version,
|
||||
})
|
||||
await xapi.installSupplementalPackOnAllHosts(res)
|
||||
await xapi.pool.update_other_config(
|
||||
'xosan_pack_installation_time',
|
||||
|
||||
@@ -2041,6 +2041,7 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@deferrable
|
||||
async createNetwork(
|
||||
$defer,
|
||||
@@ -2358,14 +2359,22 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
async assertConsistentHostServerTime(hostRef) {
|
||||
const delta =
|
||||
async _getHostServerTimeShift(hostRef) {
|
||||
return Math.abs(
|
||||
parseDateTime(await this.call('host.get_servertime', hostRef)).getTime() -
|
||||
Date.now()
|
||||
if (Math.abs(delta) > 30e3) {
|
||||
Date.now()
|
||||
)
|
||||
}
|
||||
|
||||
async isHostServerTimeConsistent(hostRef) {
|
||||
return (await this._getHostServerTimeShift(hostRef)) < 30e3
|
||||
}
|
||||
|
||||
async assertConsistentHostServerTime(hostRef) {
|
||||
if (!(await this.isHostServerTimeConsistent(hostRef))) {
|
||||
throw new Error(
|
||||
`host server time and XOA date are not consistent with each other (${ms(
|
||||
delta
|
||||
await this._getHostServerTimeShift(hostRef)
|
||||
)})`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { filter, find, pickBy, some } from 'lodash'
|
||||
|
||||
import ensureArray from '../../_ensureArray'
|
||||
import { debounce } from '../../decorators'
|
||||
import debounceWithKey from '../../_pDebounceWithKey'
|
||||
import { debounceWithKey } from '../../_pDebounceWithKey'
|
||||
import { forEach, mapFilter, mapToArray, parseXml } from '../../utils'
|
||||
|
||||
import { extractOpaqueRef, useUpdateSystem } from '../utils'
|
||||
|
||||
@@ -276,19 +276,20 @@ export default {
|
||||
if (virtualizationMode !== 'pv' && virtualizationMode !== 'hvm') {
|
||||
throw new Error(`The virtualization mode must be 'pv' or 'hvm'`)
|
||||
}
|
||||
return vm
|
||||
.set_domain_type(virtualizationMode)
|
||||
::pCatch({ code: 'MESSAGE_METHOD_UNKNOWN' }, () =>
|
||||
vm.set_HVM_boot_policy(
|
||||
return vm.set_domain_type !== undefined
|
||||
? vm.set_domain_type(virtualizationMode)
|
||||
: vm.set_HVM_boot_policy(
|
||||
virtualizationMode === 'hvm' ? 'Boot order' : ''
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
coresPerSocket: {
|
||||
set: (coresPerSocket, vm) =>
|
||||
vm.update_platform('cores-per-socket', String(coresPerSocket)),
|
||||
vm.update_platform(
|
||||
'cores-per-socket',
|
||||
coresPerSocket !== null ? String(coresPerSocket) : null
|
||||
),
|
||||
},
|
||||
|
||||
CPUs: 'cpus',
|
||||
@@ -314,7 +315,8 @@ export default {
|
||||
|
||||
cpuCap: {
|
||||
get: vm => vm.VCPUs_params.cap && +vm.VCPUs_params.cap,
|
||||
set: (cap, vm) => vm.update_VCPUs_params('cap', String(cap)),
|
||||
set: (cap, vm) =>
|
||||
vm.update_VCPUs_params('cap', cap !== null ? String(cap) : null),
|
||||
},
|
||||
|
||||
cpuMask: {
|
||||
|
||||
@@ -332,7 +332,7 @@ export const makeEditObject = specs => {
|
||||
|
||||
export const useUpdateSystem = host => {
|
||||
// Match Xen Center's condition: https://github.com/xenserver/xenadmin/blob/f3a64fc54bbff239ca6f285406d9034f57537d64/XenModel/Utils/Helpers.cs#L420
|
||||
return versionSatisfies(host.software_version.platform_version, '^2.1.1')
|
||||
return versionSatisfies(host.software_version.platform_version, '>=2.1.1')
|
||||
}
|
||||
|
||||
export const canSrHaveNewVdiOfSize = (sr, minSize) =>
|
||||
|
||||
@@ -3,6 +3,7 @@ import kindOf from 'kindof'
|
||||
import ms from 'ms'
|
||||
import schemaInspector from 'schema-inspector'
|
||||
import { forEach, isFunction } from 'lodash'
|
||||
import { getBoundPropertyDescriptor } from 'bind-property-descriptor'
|
||||
import { MethodNotFound } from 'json-rpc-peer'
|
||||
|
||||
import * as methods from '../api'
|
||||
@@ -219,17 +220,29 @@ export default class Api {
|
||||
throw new MethodNotFound(name)
|
||||
}
|
||||
|
||||
// FIXME: it can cause issues if there any property assignments in
|
||||
// XO methods called from the API.
|
||||
const context = Object.create(xo, {
|
||||
api: {
|
||||
// Used by system.*().
|
||||
value: this,
|
||||
},
|
||||
session: {
|
||||
value: session,
|
||||
},
|
||||
})
|
||||
// create the context which is an augmented XO
|
||||
const context = (() => {
|
||||
const descriptors = {
|
||||
api: {
|
||||
// Used by system.*().
|
||||
value: this,
|
||||
},
|
||||
session: {
|
||||
value: session,
|
||||
},
|
||||
}
|
||||
|
||||
let obj = xo
|
||||
do {
|
||||
Object.getOwnPropertyNames(obj).forEach(name => {
|
||||
if (!(name in descriptors)) {
|
||||
descriptors[name] = getBoundPropertyDescriptor(obj, name, xo)
|
||||
}
|
||||
})
|
||||
} while ((obj = Reflect.getPrototypeOf(obj)) !== null)
|
||||
|
||||
return Object.create(null, descriptors)
|
||||
})()
|
||||
|
||||
// Fetch and inject the current user.
|
||||
const userId = session.get('user_id', undefined)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import ms from 'ms'
|
||||
import { forEach, isEmpty, iteratee, sortedIndexBy } from 'lodash'
|
||||
|
||||
import { debounceWithKey } from '../_pDebounceWithKey'
|
||||
|
||||
const isSkippedError = error =>
|
||||
error.message === 'no disks found' ||
|
||||
error.message === 'no VMs match this pattern' ||
|
||||
@@ -64,131 +66,138 @@ const taskTimeComparator = ({ start: s1, end: e1 }, { start: s2, end: e2 }) => {
|
||||
// tasks?: Task[],
|
||||
// }
|
||||
export default {
|
||||
async getBackupNgLogs(runId?: string) {
|
||||
const [jobLogs, restoreLogs, restoreMetadataLogs] = await Promise.all([
|
||||
this.getLogs('jobs'),
|
||||
this.getLogs('restore'),
|
||||
this.getLogs('metadataRestore'),
|
||||
])
|
||||
getBackupNgLogs: debounceWithKey(
|
||||
async function getBackupNgLogs(runId?: string) {
|
||||
const [jobLogs, restoreLogs, restoreMetadataLogs] = await Promise.all([
|
||||
this.getLogs('jobs'),
|
||||
this.getLogs('restore'),
|
||||
this.getLogs('metadataRestore'),
|
||||
])
|
||||
|
||||
const { runningJobs, runningRestores, runningMetadataRestores } = this
|
||||
const consolidated = {}
|
||||
const started = {}
|
||||
const { runningJobs, runningRestores, runningMetadataRestores } = this
|
||||
const consolidated = {}
|
||||
const started = {}
|
||||
|
||||
const handleLog = ({ data, time, message }, id) => {
|
||||
const { event } = data
|
||||
if (event === 'job.start') {
|
||||
if (
|
||||
(data.type === 'backup' || data.key === undefined) &&
|
||||
(runId === undefined || runId === id)
|
||||
) {
|
||||
const { scheduleId, jobId } = data
|
||||
consolidated[id] = started[id] = {
|
||||
const handleLog = ({ data, time, message }, id) => {
|
||||
const { event } = data
|
||||
if (event === 'job.start') {
|
||||
if (
|
||||
(data.type === 'backup' || data.key === undefined) &&
|
||||
(runId === undefined || runId === id)
|
||||
) {
|
||||
const { scheduleId, jobId } = data
|
||||
consolidated[id] = started[id] = {
|
||||
data: data.data,
|
||||
id,
|
||||
jobId,
|
||||
jobName: data.jobName,
|
||||
message: 'backup',
|
||||
scheduleId,
|
||||
start: time,
|
||||
status: runningJobs[jobId] === id ? 'pending' : 'interrupted',
|
||||
}
|
||||
}
|
||||
} else if (event === 'job.end') {
|
||||
const { runJobId } = data
|
||||
const log = started[runJobId]
|
||||
if (log !== undefined) {
|
||||
delete started[runJobId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.error)),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
} else if (event === 'task.start') {
|
||||
const task = {
|
||||
data: data.data,
|
||||
id,
|
||||
jobId,
|
||||
jobName: data.jobName,
|
||||
message: 'backup',
|
||||
scheduleId,
|
||||
message,
|
||||
start: time,
|
||||
status: runningJobs[jobId] === id ? 'pending' : 'interrupted',
|
||||
}
|
||||
const { parentId } = data
|
||||
let parent
|
||||
if (parentId === undefined && (runId === undefined || runId === id)) {
|
||||
// top level task
|
||||
task.status =
|
||||
(message === 'restore' && !runningRestores.has(id)) ||
|
||||
(message === 'metadataRestore' &&
|
||||
!runningMetadataRestores.has(id))
|
||||
? 'interrupted'
|
||||
: 'pending'
|
||||
consolidated[id] = started[id] = task
|
||||
} else if ((parent = started[parentId]) !== undefined) {
|
||||
// sub-task for which the parent exists
|
||||
task.status = parent.status
|
||||
started[id] = task
|
||||
;(parent.tasks || (parent.tasks = [])).push(task)
|
||||
}
|
||||
} else if (event === 'task.end') {
|
||||
const { taskId } = data
|
||||
const log = started[taskId]
|
||||
if (log !== undefined) {
|
||||
// TODO: merge/transfer work-around
|
||||
delete started[taskId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.result), data.status),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
} else if (event === 'task.warning') {
|
||||
const parent = started[data.taskId]
|
||||
parent !== undefined &&
|
||||
(parent.warnings || (parent.warnings = [])).push({
|
||||
data: data.data,
|
||||
message,
|
||||
})
|
||||
} else if (event === 'task.info') {
|
||||
const parent = started[data.taskId]
|
||||
parent !== undefined &&
|
||||
(parent.infos || (parent.infos = [])).push({
|
||||
data: data.data,
|
||||
message,
|
||||
})
|
||||
} else if (event === 'jobCall.start') {
|
||||
const parent = started[data.runJobId]
|
||||
if (parent !== undefined) {
|
||||
;(parent.tasks || (parent.tasks = [])).push(
|
||||
(started[id] = {
|
||||
data: {
|
||||
type: 'VM',
|
||||
id: data.params.id,
|
||||
},
|
||||
id,
|
||||
start: time,
|
||||
status: parent.status,
|
||||
})
|
||||
)
|
||||
}
|
||||
} else if (event === 'jobCall.end') {
|
||||
const { runCallId } = data
|
||||
const log = started[runCallId]
|
||||
if (log !== undefined) {
|
||||
delete started[runCallId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.error)),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (event === 'job.end') {
|
||||
const { runJobId } = data
|
||||
const log = started[runJobId]
|
||||
if (log !== undefined) {
|
||||
delete started[runJobId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.error)),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
} else if (event === 'task.start') {
|
||||
const task = {
|
||||
data: data.data,
|
||||
id,
|
||||
message,
|
||||
start: time,
|
||||
}
|
||||
const { parentId } = data
|
||||
let parent
|
||||
if (parentId === undefined && (runId === undefined || runId === id)) {
|
||||
// top level task
|
||||
task.status =
|
||||
(message === 'restore' && !runningRestores.has(id)) ||
|
||||
(message === 'metadataRestore' && !runningMetadataRestores.has(id))
|
||||
? 'interrupted'
|
||||
: 'pending'
|
||||
consolidated[id] = started[id] = task
|
||||
} else if ((parent = started[parentId]) !== undefined) {
|
||||
// sub-task for which the parent exists
|
||||
task.status = parent.status
|
||||
started[id] = task
|
||||
;(parent.tasks || (parent.tasks = [])).push(task)
|
||||
}
|
||||
} else if (event === 'task.end') {
|
||||
const { taskId } = data
|
||||
const log = started[taskId]
|
||||
if (log !== undefined) {
|
||||
// TODO: merge/transfer work-around
|
||||
delete started[taskId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.result), data.status),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
} else if (event === 'task.warning') {
|
||||
const parent = started[data.taskId]
|
||||
parent !== undefined &&
|
||||
(parent.warnings || (parent.warnings = [])).push({
|
||||
data: data.data,
|
||||
message,
|
||||
})
|
||||
} else if (event === 'task.info') {
|
||||
const parent = started[data.taskId]
|
||||
parent !== undefined &&
|
||||
(parent.infos || (parent.infos = [])).push({
|
||||
data: data.data,
|
||||
message,
|
||||
})
|
||||
} else if (event === 'jobCall.start') {
|
||||
const parent = started[data.runJobId]
|
||||
if (parent !== undefined) {
|
||||
;(parent.tasks || (parent.tasks = [])).push(
|
||||
(started[id] = {
|
||||
data: {
|
||||
type: 'VM',
|
||||
id: data.params.id,
|
||||
},
|
||||
id,
|
||||
start: time,
|
||||
status: parent.status,
|
||||
})
|
||||
)
|
||||
}
|
||||
} else if (event === 'jobCall.end') {
|
||||
const { runCallId } = data
|
||||
const log = started[runCallId]
|
||||
if (log !== undefined) {
|
||||
delete started[runCallId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.error)),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
forEach(jobLogs, handleLog)
|
||||
forEach(restoreLogs, handleLog)
|
||||
forEach(restoreMetadataLogs, handleLog)
|
||||
|
||||
return runId === undefined ? consolidated : consolidated[runId]
|
||||
},
|
||||
10e3,
|
||||
function keyFn(runId) {
|
||||
return [this, runId]
|
||||
}
|
||||
|
||||
forEach(jobLogs, handleLog)
|
||||
forEach(restoreLogs, handleLog)
|
||||
forEach(restoreMetadataLogs, handleLog)
|
||||
|
||||
return runId === undefined ? consolidated : consolidated[runId]
|
||||
},
|
||||
),
|
||||
|
||||
async getBackupNgLogsSorted({ after, before, filter, limit }) {
|
||||
let logs = await this.getBackupNgLogs()
|
||||
|
||||
@@ -44,6 +44,7 @@ import { type Schedule } from '../scheduling'
|
||||
|
||||
import createSizeStream from '../../size-stream'
|
||||
import parseDuration from '../../_parseDuration'
|
||||
import { debounceWithKey } from '../../_pDebounceWithKey'
|
||||
import {
|
||||
type DeltaVmExport,
|
||||
type DeltaVmImport,
|
||||
@@ -821,56 +822,66 @@ export default class BackupNg {
|
||||
)()
|
||||
}
|
||||
|
||||
@debounceWithKey.decorate(10e3, function keyFn(remoteId) {
|
||||
return [this, remoteId]
|
||||
})
|
||||
async _listVmBackupsOnRemote(remoteId: string) {
|
||||
const app = this._app
|
||||
const backupsByVm = {}
|
||||
try {
|
||||
const handler = await app.getRemoteHandler(remoteId)
|
||||
|
||||
const entries = (await handler.list(BACKUP_DIR).catch(error => {
|
||||
if (error == null || error.code !== 'ENOENT') {
|
||||
throw error
|
||||
}
|
||||
return []
|
||||
})).filter(name => name !== 'index.json')
|
||||
|
||||
await Promise.all(
|
||||
entries.map(async vmUuid => {
|
||||
// $FlowFixMe don't know what is the problem (JFT)
|
||||
const backups = await this._listVmBackups(handler, vmUuid)
|
||||
|
||||
if (backups.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// inject an id usable by importVmBackupNg()
|
||||
backups.forEach(backup => {
|
||||
backup.id = `${remoteId}/${backup._filename}`
|
||||
|
||||
const { vdis, vhds } = backup
|
||||
backup.disks =
|
||||
vhds === undefined
|
||||
? []
|
||||
: Object.keys(vhds).map(vdiId => {
|
||||
const vdi = vdis[vdiId]
|
||||
return {
|
||||
id: `${dirname(backup._filename)}/${vhds[vdiId]}`,
|
||||
name: vdi.name_label,
|
||||
uuid: vdi.uuid,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
backupsByVm[vmUuid] = backups
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
log.warn(`listVmBackups for remote ${remoteId}:`, { error })
|
||||
}
|
||||
return backupsByVm
|
||||
}
|
||||
|
||||
async listVmBackupsNg(remotes: string[]) {
|
||||
const backupsByVmByRemote: $Dict<$Dict<Metadata[]>> = {}
|
||||
|
||||
const app = this._app
|
||||
await Promise.all(
|
||||
remotes.map(async remoteId => {
|
||||
try {
|
||||
const handler = await app.getRemoteHandler(remoteId)
|
||||
|
||||
const entries = (await handler.list(BACKUP_DIR).catch(error => {
|
||||
if (error == null || error.code !== 'ENOENT') {
|
||||
throw error
|
||||
}
|
||||
return []
|
||||
})).filter(name => name !== 'index.json')
|
||||
|
||||
const backupsByVm = (backupsByVmByRemote[remoteId] = {})
|
||||
await Promise.all(
|
||||
entries.map(async vmUuid => {
|
||||
// $FlowFixMe don't know what is the problem (JFT)
|
||||
const backups = await this._listVmBackups(handler, vmUuid)
|
||||
|
||||
if (backups.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// inject an id usable by importVmBackupNg()
|
||||
backups.forEach(backup => {
|
||||
backup.id = `${remoteId}/${backup._filename}`
|
||||
|
||||
const { vdis, vhds } = backup
|
||||
backup.disks =
|
||||
vhds === undefined
|
||||
? []
|
||||
: Object.keys(vhds).map(vdiId => {
|
||||
const vdi = vdis[vdiId]
|
||||
return {
|
||||
id: `${dirname(backup._filename)}/${vhds[vdiId]}`,
|
||||
name: vdi.name_label,
|
||||
uuid: vdi.uuid,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
backupsByVm[vmUuid] = backups
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
log.warn(`listVmBackups for remote ${remoteId}:`, { error })
|
||||
}
|
||||
backupsByVmByRemote[remoteId] = await this._listVmBackupsOnRemote(
|
||||
remoteId
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1146,6 +1157,21 @@ export default class BackupNg {
|
||||
$defer.call(xapi, 'deleteVm', snapshot)
|
||||
}
|
||||
|
||||
let compress = getJobCompression(job)
|
||||
const pool = snapshot.$pool
|
||||
if (
|
||||
compress === 'zstd' &&
|
||||
pool.restrictions.restrict_zstd_export !== 'false'
|
||||
) {
|
||||
compress = false
|
||||
logger.warning(
|
||||
`Zstd is not supported on the pool ${pool.name_label}, the VM will be exported without compression`,
|
||||
{
|
||||
event: 'task.warning',
|
||||
taskId,
|
||||
}
|
||||
)
|
||||
}
|
||||
let xva: any = await wrapTask(
|
||||
{
|
||||
logger,
|
||||
@@ -1153,7 +1179,7 @@ export default class BackupNg {
|
||||
parentId: taskId,
|
||||
},
|
||||
xapi.exportVm($cancelToken, snapshot, {
|
||||
compress: getJobCompression(job),
|
||||
compress,
|
||||
})
|
||||
)
|
||||
const exportTask = xva.task
|
||||
|
||||
@@ -243,38 +243,17 @@ export default class Jobs {
|
||||
}
|
||||
|
||||
async _runJob(job: Job, schedule?: Schedule, data_?: any) {
|
||||
const { id } = job
|
||||
|
||||
const runningJobs = this._runningJobs
|
||||
if (id in runningJobs) {
|
||||
throw new Error(`job ${id} is already running`)
|
||||
}
|
||||
|
||||
const { type } = job
|
||||
const executor = this._executors[type]
|
||||
if (executor === undefined) {
|
||||
throw new Error(`cannot run job ${id}: no executor for type ${type}`)
|
||||
}
|
||||
|
||||
let data
|
||||
if (type === 'backup') {
|
||||
// $FlowFixMe only defined for BackupJob
|
||||
const settings = job.settings['']
|
||||
data = {
|
||||
// $FlowFixMe only defined for BackupJob
|
||||
mode: job.mode,
|
||||
reportWhen: (settings && settings.reportWhen) || 'failure',
|
||||
}
|
||||
}
|
||||
if (type === 'metadataBackup') {
|
||||
data = {
|
||||
reportWhen: job.settings['']?.reportWhen ?? 'failure',
|
||||
}
|
||||
}
|
||||
|
||||
const logger = this._logger
|
||||
const { id, type } = job
|
||||
const runJobId = logger.notice(`Starting execution of ${id}.`, {
|
||||
data,
|
||||
data:
|
||||
type === 'backup' || type === 'metadataBackup'
|
||||
? {
|
||||
// $FlowFixMe only defined for BackupJob
|
||||
mode: job.mode,
|
||||
reportWhen: job.settings['']?.reportWhen ?? 'failure',
|
||||
}
|
||||
: undefined,
|
||||
event: 'job.start',
|
||||
userId: job.userId,
|
||||
jobId: id,
|
||||
@@ -285,44 +264,64 @@ export default class Jobs {
|
||||
type,
|
||||
})
|
||||
|
||||
// runId is a temporary property used to check if the report is sent after the server interruption
|
||||
this.updateJob({ id, runId: runJobId })::ignoreErrors()
|
||||
runningJobs[id] = runJobId
|
||||
|
||||
const runs = this._runs
|
||||
|
||||
const { cancel, token } = CancelToken.source()
|
||||
runs[runJobId] = { cancel }
|
||||
|
||||
let session
|
||||
const app = this._app
|
||||
try {
|
||||
session = app.createUserConnection()
|
||||
session.set('user_id', job.userId)
|
||||
const runningJobs = this._runningJobs
|
||||
|
||||
const status = await executor({
|
||||
app,
|
||||
cancelToken: token,
|
||||
data: data_,
|
||||
job,
|
||||
logger,
|
||||
runJobId,
|
||||
schedule,
|
||||
session,
|
||||
})
|
||||
await logger.notice(
|
||||
`Execution terminated for ${job.id}.`,
|
||||
{
|
||||
event: 'job.end',
|
||||
if (id in runningJobs) {
|
||||
throw new Error(`the job (${id}) is already running`)
|
||||
}
|
||||
|
||||
const executor = this._executors[type]
|
||||
if (executor === undefined) {
|
||||
throw new Error(`cannot run job (${id}): no executor for type ${type}`)
|
||||
}
|
||||
|
||||
// runId is a temporary property used to check if the report is sent after the server interruption
|
||||
this.updateJob({ id, runId: runJobId })::ignoreErrors()
|
||||
runningJobs[id] = runJobId
|
||||
|
||||
const runs = this._runs
|
||||
let session
|
||||
try {
|
||||
const { cancel, token } = CancelToken.source()
|
||||
runs[runJobId] = { cancel }
|
||||
|
||||
session = app.createUserConnection()
|
||||
session.set('user_id', job.userId)
|
||||
|
||||
const status = await executor({
|
||||
app,
|
||||
cancelToken: token,
|
||||
data: data_,
|
||||
job,
|
||||
logger,
|
||||
runJobId,
|
||||
},
|
||||
true
|
||||
)
|
||||
schedule,
|
||||
session,
|
||||
})
|
||||
|
||||
app.emit('job:terminated', runJobId, {
|
||||
type: job.type,
|
||||
status,
|
||||
})
|
||||
await logger.notice(
|
||||
`Execution terminated for ${job.id}.`,
|
||||
{
|
||||
event: 'job.end',
|
||||
runJobId,
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
app.emit('job:terminated', runJobId, {
|
||||
type: job.type,
|
||||
status,
|
||||
})
|
||||
} finally {
|
||||
this.updateJob({ id, runId: null })::ignoreErrors()
|
||||
delete runningJobs[id]
|
||||
delete runs[runJobId]
|
||||
if (session !== undefined) {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
await logger.error(
|
||||
`The execution of ${id} has failed.`,
|
||||
@@ -337,13 +336,6 @@ export default class Jobs {
|
||||
type: job.type,
|
||||
})
|
||||
throw error
|
||||
} finally {
|
||||
this.updateJob({ id, runId: null })::ignoreErrors()
|
||||
delete runningJobs[id]
|
||||
delete runs[runJobId]
|
||||
if (session !== undefined) {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import asyncMap from '@xen-orchestra/async-map'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import { fromEvent, ignoreErrors } from 'promise-toolbox'
|
||||
|
||||
import debounceWithKey from '../_pDebounceWithKey'
|
||||
import { debounceWithKey } from '../_pDebounceWithKey'
|
||||
import parseDuration from '../_parseDuration'
|
||||
import { type Xapi } from '../xapi'
|
||||
import {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import { createSchedule } from '@xen-orchestra/cron'
|
||||
import { ignoreErrors } from 'promise-toolbox'
|
||||
import { keyBy } from 'lodash'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
@@ -155,7 +156,9 @@ export default class Scheduling {
|
||||
this._runs[id] = createSchedule(
|
||||
schedule.cron,
|
||||
schedule.timezone
|
||||
).startJob(() => this._app.runJobSequence([schedule.jobId], schedule))
|
||||
).startJob(() => {
|
||||
ignoreErrors.call(this._app.runJobSequence([schedule.jobId], schedule))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.48.1",
|
||||
"version": "5.50.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -32,8 +32,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nraynaud/novnc": "0.6.1",
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/cron": "^1.0.4",
|
||||
"@xen-orchestra/defined": "^0.0.0",
|
||||
"@xen-orchestra/template": "^0.1.0",
|
||||
"ansi_up": "^4.0.3",
|
||||
"asap": "^2.0.6",
|
||||
"babel-core": "^6.26.0",
|
||||
|
||||
@@ -14,11 +14,16 @@ const AVAILABLE_TEMPLATE_VARS = {
|
||||
const showAvailableTemplateVars = () =>
|
||||
alert(
|
||||
_('availableTemplateVarsTitle'),
|
||||
<ul>
|
||||
{map(AVAILABLE_TEMPLATE_VARS, (value, key) => (
|
||||
<li key={key}>{_.keyValue(key, _(value))}</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
<ul>
|
||||
{map(AVAILABLE_TEMPLATE_VARS, (value, key) => (
|
||||
<li key={key}>{_.keyValue(key, _(value))}</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className='text-info'>
|
||||
<Icon icon='info' /> {_('templateEscape')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const showNetworkConfigInfo = () =>
|
||||
@@ -87,5 +92,3 @@ export const DEFAULT_NETWORK_CONFIG_TEMPLATE = `#network:
|
||||
# name: eth0
|
||||
# subnets:
|
||||
# - type: dhcp`
|
||||
|
||||
export const CAN_CLOUD_INIT = +process.env.XOA_PLAN > 3
|
||||
|
||||
@@ -984,11 +984,18 @@ const messages = {
|
||||
// ----- VM console tab -----
|
||||
copyToClipboardLabel: 'Copy',
|
||||
ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
|
||||
ctrlAltDelConfirmation: 'Send Ctrl+Alt+Del to VM?',
|
||||
multilineCopyToClipboard: 'Multiline copy',
|
||||
tipLabel: 'Tip:',
|
||||
hideHeaderTooltip: 'Hide info',
|
||||
showHeaderTooltip: 'Show info',
|
||||
sendToClipboard: 'Send to clipboard',
|
||||
sshRootTooltip: 'Connect using external SSH tool as root',
|
||||
sshRootLabel: 'SSH',
|
||||
sshUserTooltip: 'Connect using external SSH tool as user…',
|
||||
sshUserLabel: 'SSH as…',
|
||||
sshUsernameLabel: 'SSH user name',
|
||||
sshNeedClientTools: 'No IP address reported by client tools',
|
||||
|
||||
// ----- VM container tab -----
|
||||
containerName: 'Name',
|
||||
@@ -1311,12 +1318,12 @@ const messages = {
|
||||
newVmSshKey: 'SSH key',
|
||||
noConfigDrive: 'No config drive',
|
||||
newVmCustomConfig: 'Custom config',
|
||||
premiumOnly: 'Only available in Premium',
|
||||
availableTemplateVarsInfo:
|
||||
'Click here to see the available template variables',
|
||||
availableTemplateVarsTitle: 'Available template variables',
|
||||
templateNameInfo: 'the VM\'s name. It must not contain "_"',
|
||||
templateIndexInfo: "the VM's index, it will take 0 in case of single VM",
|
||||
templateEscape: 'Tip: escape any variable with a preceding backslash (\\)',
|
||||
coreOsDefaultTemplateError:
|
||||
'Error on getting the default coreOS cloud template',
|
||||
newVmBootAfterCreate: 'Boot VM after creation',
|
||||
@@ -1748,6 +1755,9 @@ const messages = {
|
||||
newNetworkInfo: 'Info',
|
||||
newNetworkType: 'Type',
|
||||
newNetworkEncapsulation: 'Encapsulation',
|
||||
newNetworkEncrypted: 'Encrypted',
|
||||
encryptionWarning:
|
||||
'A pool can have 1 encrypted GRE network and 1 encrypted VxLAN network max',
|
||||
newNetworkSdnControllerTip:
|
||||
'Private networks work on up-to-date XCP-ng hosts, for other scenarios please see the requirements',
|
||||
deleteNetwork: 'Delete network',
|
||||
@@ -1921,6 +1931,7 @@ const messages = {
|
||||
logUser: 'User',
|
||||
logMessage: 'Message',
|
||||
logSuggestXcpNg: 'Use XCP-ng to get rid of restrictions',
|
||||
logXapiError: 'This is a XenServer/XCP-ng error',
|
||||
logError: 'Error',
|
||||
logTitle: 'Logs',
|
||||
logDisplayDetails: 'Display details',
|
||||
@@ -2132,6 +2143,21 @@ const messages = {
|
||||
xosanIssueHostNotInNetwork:
|
||||
'Will configure the host xosan network device with a static IP address and plug it in.',
|
||||
|
||||
// Hub
|
||||
hubPage: 'Hub',
|
||||
noDefaultSr: 'The selected pool has no default SR',
|
||||
successfulInstall: 'VM installed successfully',
|
||||
vmNoAvailable: 'No VMs available ',
|
||||
create: 'Create',
|
||||
hubResourceAlert: 'Resource alert',
|
||||
os: 'OS',
|
||||
version: 'Version',
|
||||
size: 'Size',
|
||||
totalDiskSize: 'Total disk size',
|
||||
hideInstalledPool: 'Already installed templates are hidden',
|
||||
hubSrErrorTitle: 'Missing property',
|
||||
hubImportNotificationTitle: 'XVA import',
|
||||
|
||||
// Licenses
|
||||
xosanUnregisteredDisclaimer:
|
||||
'You are not registered and therefore will not be able to create or manage your XOSAN SRs. {link}',
|
||||
|
||||
@@ -58,3 +58,11 @@ export const setHomeVmIdsSelection = createAction(
|
||||
'SET_HOME_VM_IDS_SELECTION',
|
||||
homeVmIdsSelection => homeVmIdsSelection
|
||||
)
|
||||
export const markHubResourceAsInstalling = createAction(
|
||||
'MARK_HUB_RESOURCE_AS_INSTALLING',
|
||||
id => id
|
||||
)
|
||||
export const markHubResourceAsInstalled = createAction(
|
||||
'MARK_HUB_RESOURCE_AS_INSTALLED',
|
||||
id => id
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import cookies from 'cookies-js'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
import invoke from '../invoke'
|
||||
|
||||
@@ -92,6 +93,19 @@ export default {
|
||||
homeVmIdsSelection,
|
||||
}),
|
||||
|
||||
// whether a resource is currently being installed: `hubInstallingResources[<template id>]`
|
||||
hubInstallingResources: combineActionHandlers(
|
||||
{},
|
||||
{
|
||||
[actions.markHubResourceAsInstalling]: (
|
||||
prevHubInstallingResources,
|
||||
id
|
||||
) => ({ ...prevHubInstallingResources, [id]: true }),
|
||||
[actions.markHubResourceAsInstalled]: (prevHubInstallingResources, id) =>
|
||||
omit(prevHubInstallingResources, id),
|
||||
}
|
||||
),
|
||||
|
||||
objects: combineActionHandlers(
|
||||
{
|
||||
all: {}, // Mutable for performance!
|
||||
|
||||
@@ -6,7 +6,6 @@ import { connect } from 'react-redux'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import {
|
||||
clone,
|
||||
escapeRegExp,
|
||||
every,
|
||||
forEach,
|
||||
isArray,
|
||||
@@ -14,12 +13,9 @@ import {
|
||||
isFunction,
|
||||
isPlainObject,
|
||||
isString,
|
||||
join,
|
||||
keys,
|
||||
map,
|
||||
mapValues,
|
||||
pick,
|
||||
replace,
|
||||
sample,
|
||||
some,
|
||||
} from 'lodash'
|
||||
@@ -355,33 +351,6 @@ export const resolveResourceSet = resourceSet => {
|
||||
export const resolveResourceSets = resourceSets =>
|
||||
map(resourceSets, resolveResourceSet)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Creates a string replacer based on a pattern and a list of rules
|
||||
//
|
||||
// ```js
|
||||
// const myReplacer = buildTemplate('{name}_COPY_{name}_{id}_%', {
|
||||
// '{name}': vm => vm.name_label,
|
||||
// '{id}': vm => vm.id,
|
||||
// '%': (_, i) => i
|
||||
// })
|
||||
//
|
||||
// const newString = myReplacer({
|
||||
// name_label: 'foo',
|
||||
// id: 42,
|
||||
// }, 32)
|
||||
//
|
||||
// newString === 'foo_COPY_foo_42_32'
|
||||
// ```
|
||||
export function buildTemplate(pattern, rules) {
|
||||
const regExp = new RegExp(join(map(keys(rules), escapeRegExp), '|'), 'g')
|
||||
return (...params) =>
|
||||
replace(pattern, regExp, match => {
|
||||
const rule = rules[match]
|
||||
return isFunction(rule) ? rule(...params) : rule
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const streamToString = getStream
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _, { messages } from 'intl'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import { compileTemplate } from '@xen-orchestra/template'
|
||||
import { injectIntl } from 'react-intl'
|
||||
|
||||
import BaseComponent from 'base-component'
|
||||
@@ -8,7 +9,7 @@ import SingleLineRow from 'single-line-row'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Col } from 'grid'
|
||||
import { SelectSr } from 'select-objects'
|
||||
import { buildTemplate, connectStore } from 'utils'
|
||||
import { connectStore } from 'utils'
|
||||
|
||||
import SelectCompression from '../../select-compression'
|
||||
import ZstdChecker from '../../zstd-checker'
|
||||
@@ -35,7 +36,7 @@ class CopyVmsModalBody extends BaseComponent {
|
||||
const names = namePattern
|
||||
? map(
|
||||
resolvedVms,
|
||||
buildTemplate(namePattern, {
|
||||
compileTemplate(namePattern, {
|
||||
'{name}': vm => vm.name_label,
|
||||
'{id}': vm => vm.id,
|
||||
})
|
||||
|
||||
@@ -349,6 +349,10 @@ export const subscribeResourceCatalog = createSubscription(() =>
|
||||
_call('cloud.getResourceCatalog')
|
||||
)
|
||||
|
||||
export const subscribeHubResourceCatalog = createSubscription(() =>
|
||||
_call('cloud.getResourceCatalog', { filters: { hub: true } })
|
||||
)
|
||||
|
||||
const getNotificationCookie = () => {
|
||||
const notificationCookie = cookies.get(
|
||||
`notifications:${store.getState().user.id}`
|
||||
@@ -2871,7 +2875,10 @@ export const fixHostNotInXosanNetwork = (xosanSr, host) =>
|
||||
|
||||
// XOSAN packs -----------------------------------------------------------------
|
||||
|
||||
export const getResourceCatalog = () => _call('cloud.getResourceCatalog')
|
||||
export const getResourceCatalog = ({ filters } = {}) =>
|
||||
_call('cloud.getResourceCatalog', { filters })
|
||||
|
||||
export const getAllResourceCatalog = () => _call('cloud.getAllResourceCatalog')
|
||||
|
||||
const downloadAndInstallXosanPack = (pack, pool, { version }) =>
|
||||
_call('xosan.downloadAndInstallXosanPack', {
|
||||
@@ -2880,6 +2887,14 @@ const downloadAndInstallXosanPack = (pack, pool, { version }) =>
|
||||
pool: resolveId(pool),
|
||||
})
|
||||
|
||||
export const downloadAndInstallResource = ({ namespace, id, version, sr }) =>
|
||||
_call('cloud.downloadAndInstallResource', {
|
||||
namespace,
|
||||
id,
|
||||
version,
|
||||
sr: resolveId(sr),
|
||||
})
|
||||
|
||||
import UpdateXosanPacksModal from './update-xosan-packs-modal' // eslint-disable-line import/first
|
||||
export const updateXosanPacks = pool =>
|
||||
confirm({
|
||||
|
||||
@@ -119,6 +119,11 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
return network => networks[network.id]
|
||||
}
|
||||
)
|
||||
|
||||
this._getSelectedVmsPoolIds = createSelector(
|
||||
() => this.props.vms,
|
||||
vms => map(vms, '$pool')
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -262,6 +267,11 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
_toggleSmartVifMapping = () =>
|
||||
this.setState({ smartVifMapping: !this.state.smartVifMapping })
|
||||
|
||||
compareContainers = (pool1, pool2) => {
|
||||
const poolIds = this._getSelectedVmsPoolIds()
|
||||
return poolIds.includes(pool1.id) ? -1 : poolIds.includes(pool2.id) ? 1 : 0
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
defaultSrConnectedToHost,
|
||||
@@ -281,6 +291,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
<Col size={6}>{_('migrateVmSelectHost')}</Col>
|
||||
<Col size={6}>
|
||||
<SelectHost
|
||||
compareContainers={this.compareContainers}
|
||||
onChange={this._selectHost}
|
||||
predicate={this._getHostPredicate()}
|
||||
value={host}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import _ from 'intl'
|
||||
import React from 'react'
|
||||
import BaseComponent from 'base-component'
|
||||
import { forEach } from 'lodash'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { buildTemplate, connectStore } from 'utils'
|
||||
import { compileTemplate } from '@xen-orchestra/template'
|
||||
import { connectStore } from 'utils'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { forEach } from 'lodash'
|
||||
|
||||
const RULES = {
|
||||
'{date}': () => new Date().toISOString(),
|
||||
@@ -30,8 +31,8 @@ export default class SnapshotVmModalBody extends BaseComponent {
|
||||
return { names: {}, descriptions: {}, saveMemory }
|
||||
}
|
||||
|
||||
const generateName = buildTemplate(namePattern, RULES)
|
||||
const generateDescription = buildTemplate(descriptionPattern, RULES)
|
||||
const generateName = compileTemplate(namePattern, RULES)
|
||||
const generateDescription = compileTemplate(descriptionPattern, RULES)
|
||||
const names = {}
|
||||
const descriptions = {}
|
||||
|
||||
|
||||
@@ -277,6 +277,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-thumbs-up;
|
||||
}
|
||||
&-deploy {
|
||||
@extend .fa;
|
||||
@extend .fa-rocket;
|
||||
}
|
||||
|
||||
// Backups
|
||||
&-backup {
|
||||
@@ -886,6 +890,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-database;
|
||||
}
|
||||
&-menu-hub {
|
||||
@extend .fa;
|
||||
@extend .fa-cubes;
|
||||
}
|
||||
// New VM
|
||||
&-new-vm {
|
||||
&-infos {
|
||||
|
||||
@@ -116,6 +116,12 @@ const destructVmsPattern = pattern =>
|
||||
vms: destructPattern(pattern),
|
||||
}
|
||||
|
||||
const checkRetentions = (schedule, { copyMode, exportMode, snapshotMode }) =>
|
||||
(!copyMode && !exportMode && !snapshotMode) ||
|
||||
(copyMode && schedule.copyRetention > 0) ||
|
||||
(exportMode && schedule.exportRetention > 0) ||
|
||||
(snapshotMode && schedule.snapshotRetention > 0)
|
||||
|
||||
const createDoesRetentionExist = name => {
|
||||
const predicate = setting => setting[name] > 0
|
||||
return ({ propSettings, settings = propSettings }) => settings.some(predicate)
|
||||
@@ -396,14 +402,16 @@ export default decorate([
|
||||
{ saveSchedule },
|
||||
storedSchedule = DEFAULT_SCHEDULE
|
||||
) => async (
|
||||
{ copyMode, exportMode, missingBackupMode, snapshotMode },
|
||||
{ copyMode, exportMode, snapshotMode },
|
||||
{ intl: { formatMessage } }
|
||||
) => {
|
||||
const modes = { copyMode, exportMode, snapshotMode }
|
||||
const schedule = await form({
|
||||
defaultValue: storedSchedule,
|
||||
render: props => (
|
||||
<NewSchedule
|
||||
modes={{ copyMode, exportMode, snapshotMode }}
|
||||
missingRetentions={!checkRetentions(props.value, modes)}
|
||||
modes={modes}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
@@ -414,14 +422,7 @@ export default decorate([
|
||||
),
|
||||
size: 'large',
|
||||
handler: value => {
|
||||
if (
|
||||
!(
|
||||
missingBackupMode ||
|
||||
(exportMode && value.exportRetention > 0) ||
|
||||
(copyMode && value.copyRetention > 0) ||
|
||||
(snapshotMode && value.snapshotRetention > 0)
|
||||
)
|
||||
) {
|
||||
if (!checkRetentions(value, modes)) {
|
||||
throw new UserError(_('newScheduleError'), _('retentionNeeded'))
|
||||
}
|
||||
return value
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import _ from 'intl'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import Scheduler, { SchedulePreview } from 'scheduling'
|
||||
import { Card, CardBlock } from 'card'
|
||||
@@ -9,7 +11,7 @@ import { Number } from 'form'
|
||||
|
||||
import { FormGroup, Input } from './../utils'
|
||||
|
||||
export default decorate([
|
||||
const New = decorate([
|
||||
provideState({
|
||||
computed: {
|
||||
formId: generateId,
|
||||
@@ -54,9 +56,14 @@ export default decorate([
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ effects, state, modes, value: schedule }) => (
|
||||
({ effects, missingRetentions, modes, state, value: schedule }) => (
|
||||
<Card>
|
||||
<CardBlock>
|
||||
{missingRetentions && (
|
||||
<div className='text-danger text-md-center'>
|
||||
<Icon icon='alarm' /> {_('retentionNeeded')}
|
||||
</div>
|
||||
)}
|
||||
<FormGroup>
|
||||
<label htmlFor={state.idInputName}>
|
||||
<strong>{_('formName')}</strong>
|
||||
@@ -119,3 +126,12 @@ export default decorate([
|
||||
</Card>
|
||||
),
|
||||
])
|
||||
|
||||
New.propTypes = {
|
||||
missingRetentions: PropTypes.bool,
|
||||
modes: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
export { New as default }
|
||||
|
||||
63
packages/xo-web/src/xo-app/hub/index.js
Normal file
63
packages/xo-web/src/xo-app/hub/index.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import _ from 'intl'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import { addSubscriptions, adminOnly } from 'utils'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { isEmpty, map, omit, orderBy } from 'lodash'
|
||||
import { subscribeHubResourceCatalog } from 'xo'
|
||||
|
||||
import Page from '../page'
|
||||
import Resource from './resource'
|
||||
|
||||
// ==================================================================
|
||||
|
||||
const HEADER = (
|
||||
<h2>
|
||||
<Icon icon='menu-hub' /> {_('hubPage')}
|
||||
</h2>
|
||||
)
|
||||
|
||||
export default decorate([
|
||||
adminOnly,
|
||||
addSubscriptions({
|
||||
catalog: subscribeHubResourceCatalog,
|
||||
}),
|
||||
provideState({
|
||||
computed: {
|
||||
resources: (_, { catalog }) =>
|
||||
orderBy(
|
||||
map(omit(catalog, '_namespaces'), (entry, namespace) => ({
|
||||
namespace,
|
||||
...entry.xva,
|
||||
})),
|
||||
'name',
|
||||
'asc'
|
||||
),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ state: { resources } }) => (
|
||||
<Page header={HEADER} title='hubPage' formatTitle>
|
||||
<Container>
|
||||
<Row>
|
||||
{isEmpty(resources) ? (
|
||||
<Col>
|
||||
<h2 className='text-muted'>
|
||||
{_('vmNoAvailable')}
|
||||
<Icon icon='alarm' color='yellow' />
|
||||
</h2>
|
||||
</Col>
|
||||
) : (
|
||||
resources.map(data => (
|
||||
<Col key={data.namespace} mediumSize={3}>
|
||||
<Resource {...data} />
|
||||
</Col>
|
||||
))
|
||||
)}
|
||||
</Row>
|
||||
</Container>
|
||||
</Page>
|
||||
),
|
||||
])
|
||||
60
packages/xo-web/src/xo-app/hub/resource-form.js
Normal file
60
packages/xo-web/src/xo-app/hub/resource-form.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as FormGrid from 'form-grid'
|
||||
import _ from 'intl'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Container } from 'grid'
|
||||
import { SelectPool } from 'select-objects'
|
||||
import { error } from 'notification'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
|
||||
export default decorate([
|
||||
provideState({
|
||||
initialState: ({ multi }) => ({
|
||||
pools: multi ? [] : undefined,
|
||||
}),
|
||||
effects: {
|
||||
onChangePool(__, pools) {
|
||||
const noDefaultSr = Array.isArray(pools)
|
||||
? pools.some(pool => pool.default_SR === undefined)
|
||||
: pools.default_SR === undefined
|
||||
if (noDefaultSr) {
|
||||
error(_('hubSrErrorTitle'), _('noDefaultSr'))
|
||||
} else {
|
||||
this.props.onChange({
|
||||
pools,
|
||||
pool: pools,
|
||||
})
|
||||
return {
|
||||
pools,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ effects, install, multi, state, poolPredicate }) => (
|
||||
<Container>
|
||||
<FormGrid.Row>
|
||||
<label>
|
||||
{_('vmImportToPool')}
|
||||
|
||||
{install && (
|
||||
<Tooltip content={_('hideInstalledPool')}>
|
||||
<Icon icon='info' />
|
||||
</Tooltip>
|
||||
)}
|
||||
</label>
|
||||
<SelectPool
|
||||
className='mb-1'
|
||||
multi={multi}
|
||||
onChange={effects.onChangePool}
|
||||
predicate={poolPredicate}
|
||||
required
|
||||
value={state.pools}
|
||||
/>
|
||||
</FormGrid.Row>
|
||||
</Container>
|
||||
),
|
||||
])
|
||||
255
packages/xo-web/src/xo-app/hub/resource.js
Normal file
255
packages/xo-web/src/xo-app/hub/resource.js
Normal file
@@ -0,0 +1,255 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { Col, Row } from 'grid'
|
||||
import { alert, form } from 'modal'
|
||||
import { connectStore, formatSize, getXoaPlan } from 'utils'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { downloadAndInstallResource, deleteTemplates } from 'xo'
|
||||
import { error, success } from 'notification'
|
||||
import { find, filter } from 'lodash'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { withRouter } from 'react-router'
|
||||
|
||||
import ResourceForm from './resource-form'
|
||||
|
||||
const subscribeAlert = () =>
|
||||
alert(
|
||||
_('hubResourceAlert'),
|
||||
<div>
|
||||
<p>
|
||||
{_('considerSubscribe', {
|
||||
link: 'https://xen-orchestra.com',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default decorate([
|
||||
withRouter,
|
||||
connectStore(() => {
|
||||
const getTemplates = createGetObjectsOfType('VM-template').sort()
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
return {
|
||||
templates: getTemplates,
|
||||
pools: getPools,
|
||||
hubInstallingResources: state => state.hubInstallingResources,
|
||||
}
|
||||
}),
|
||||
provideState({
|
||||
initialState: () => ({
|
||||
selectedInstallPools: [],
|
||||
}),
|
||||
effects: {
|
||||
async install() {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
namespace,
|
||||
markHubResourceAsInstalled,
|
||||
markHubResourceAsInstalling,
|
||||
version,
|
||||
} = this.props
|
||||
const { isTemplateInstalled } = this.state
|
||||
if (getXoaPlan() === 'Community') {
|
||||
subscribeAlert()
|
||||
return
|
||||
}
|
||||
const resourceParams = await form({
|
||||
render: props => (
|
||||
<ResourceForm
|
||||
install
|
||||
multi
|
||||
poolPredicate={isTemplateInstalled}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
header: (
|
||||
<span>
|
||||
<Icon icon='add-vm' /> {name}
|
||||
</span>
|
||||
),
|
||||
size: 'medium',
|
||||
})
|
||||
|
||||
markHubResourceAsInstalling(id)
|
||||
try {
|
||||
await Promise.all(
|
||||
resourceParams.pools.map(pool =>
|
||||
downloadAndInstallResource({
|
||||
namespace,
|
||||
id,
|
||||
version,
|
||||
sr: pool.default_SR,
|
||||
})
|
||||
)
|
||||
)
|
||||
success(_('hubImportNotificationTitle'), _('successfulInstall'))
|
||||
} catch (_error) {
|
||||
error(_('hubImportNotificationTitle'), _error.message)
|
||||
}
|
||||
markHubResourceAsInstalled(id)
|
||||
},
|
||||
async create() {
|
||||
const { isPoolCreated, installedTemplates } = this.state
|
||||
const { name } = this.props
|
||||
if (getXoaPlan() === 'Community') {
|
||||
subscribeAlert()
|
||||
return
|
||||
}
|
||||
const resourceParams = await form({
|
||||
render: props => (
|
||||
<ResourceForm poolPredicate={isPoolCreated} {...props} />
|
||||
),
|
||||
header: (
|
||||
<span>
|
||||
<Icon icon='add-vm' /> {name}
|
||||
</span>
|
||||
),
|
||||
size: 'medium',
|
||||
})
|
||||
const { $pool } = resourceParams.pool
|
||||
const template = find(installedTemplates, { $pool })
|
||||
if (template !== undefined) {
|
||||
this.props.router.push(
|
||||
`/vms/new?pool=${$pool}&template=${template.id}`
|
||||
)
|
||||
} else {
|
||||
throw new Error(`can't find template for pool: ${$pool}`)
|
||||
}
|
||||
},
|
||||
async deleteTemplates(__, { name }) {
|
||||
const { isPoolCreated } = this.state
|
||||
const resourceParams = await form({
|
||||
render: props => (
|
||||
<ResourceForm
|
||||
delete
|
||||
multi
|
||||
poolPredicate={isPoolCreated}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
header: (
|
||||
<span>
|
||||
<Icon icon='vm-delete' /> {name}
|
||||
</span>
|
||||
),
|
||||
size: 'medium',
|
||||
})
|
||||
const _templates = filter(this.state.installedTemplates, template =>
|
||||
find(resourceParams.pools, { $pool: template.$pool })
|
||||
)
|
||||
await deleteTemplates(_templates)
|
||||
},
|
||||
updateSelectedInstallPools(_, selectedInstallPools) {
|
||||
return {
|
||||
selectedInstallPools,
|
||||
}
|
||||
},
|
||||
updateSelectedCreatePool(_, selectedCreatePool) {
|
||||
return {
|
||||
selectedCreatePool,
|
||||
}
|
||||
},
|
||||
redirectToTaskPage() {
|
||||
this.props.router.push('/tasks')
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
installedTemplates: (_, { id, templates }) =>
|
||||
filter(templates, ['other.xo:resource:xva:id', id]),
|
||||
isTemplateInstalledOnAllPools: ({ installedTemplates }, { pools }) =>
|
||||
installedTemplates.length > 0 &&
|
||||
pools.every(
|
||||
pool =>
|
||||
installedTemplates.find(template => template.$pool === pool.id) !==
|
||||
undefined
|
||||
),
|
||||
isTemplateInstalled: ({ installedTemplates }) => pool =>
|
||||
installedTemplates.find(template => template.$pool === pool.id) ===
|
||||
undefined,
|
||||
isPoolCreated: ({ installedTemplates }) => pool =>
|
||||
installedTemplates.find(template => template.$pool === pool.id) !==
|
||||
undefined,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({
|
||||
effects,
|
||||
hubInstallingResources,
|
||||
id,
|
||||
name,
|
||||
os,
|
||||
size,
|
||||
state,
|
||||
totalDiskSize,
|
||||
version,
|
||||
}) => (
|
||||
<Card shadow>
|
||||
<CardHeader>
|
||||
{name}
|
||||
<ActionButton
|
||||
className='pull-right'
|
||||
color='light'
|
||||
data-name={name}
|
||||
disabled={state.installedTemplates.length === 0}
|
||||
handler={effects.deleteTemplates}
|
||||
size='small'
|
||||
style={{ border: 'none' }}
|
||||
>
|
||||
<Icon icon='delete' size='xs' />
|
||||
</ActionButton>
|
||||
<br />
|
||||
</CardHeader>
|
||||
<CardBlock className='text-center'>
|
||||
<div>
|
||||
<span className='text-muted'>{_('os')}</span> <strong>{os}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span className='text-muted'>{_('version')}</span>
|
||||
{' '}
|
||||
<strong>{version}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span className='text-muted'>{_('size')}</span>
|
||||
{' '}
|
||||
<strong>{formatSize(size)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span className='text-muted'>{_('totalDiskSize')}</span>
|
||||
{' '}
|
||||
<strong>{formatSize(totalDiskSize)}</strong>
|
||||
</div>
|
||||
<hr />
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<ActionButton
|
||||
block
|
||||
disabled={state.isTemplateInstalledOnAllPools}
|
||||
form={state.idInstallForm}
|
||||
handler={effects.install}
|
||||
icon='add'
|
||||
pending={hubInstallingResources[id]}
|
||||
>
|
||||
{_('install')}
|
||||
</ActionButton>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<ActionButton
|
||||
block
|
||||
disabled={state.installedTemplates.length === 0}
|
||||
form={state.idCreateForm}
|
||||
handler={effects.create}
|
||||
icon='deploy'
|
||||
>
|
||||
{_('create')}
|
||||
</ActionButton>
|
||||
</Col>
|
||||
</Row>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
),
|
||||
])
|
||||
@@ -27,6 +27,7 @@ import BackupNg from './backup-ng'
|
||||
import Dashboard from './dashboard'
|
||||
import Home from './home'
|
||||
import Host from './host'
|
||||
import Hub from './hub'
|
||||
import Jobs from './jobs'
|
||||
import Menu from './menu'
|
||||
import Modal, { alert, FormModal } from 'modal'
|
||||
@@ -93,6 +94,7 @@ const BODY_STYLE = {
|
||||
'vms/:id': Vm,
|
||||
xoa: Xoa,
|
||||
xosan: Xosan,
|
||||
hub: Hub,
|
||||
})
|
||||
@connectStore(state => {
|
||||
return {
|
||||
|
||||
@@ -354,6 +354,11 @@ export default class Menu extends Component {
|
||||
},
|
||||
],
|
||||
},
|
||||
isAdmin && {
|
||||
to: '/hub',
|
||||
icon: 'menu-hub',
|
||||
label: 'hubPage',
|
||||
},
|
||||
isAdmin && { to: '/about', icon: 'menu-about', label: 'aboutPage' },
|
||||
!noOperatablePools && {
|
||||
to: '/tasks',
|
||||
|
||||
@@ -15,16 +15,16 @@ import store from 'store'
|
||||
import Tags from 'tags'
|
||||
import Tooltip from 'tooltip'
|
||||
import Wizard, { Section } from 'wizard'
|
||||
import { compileTemplate } from '@xen-orchestra/template'
|
||||
import { confirm } from 'modal'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import {
|
||||
AvailableTemplateVars,
|
||||
CAN_CLOUD_INIT,
|
||||
DEFAULT_CLOUD_CONFIG_TEMPLATE,
|
||||
DEFAULT_NETWORK_CONFIG_TEMPLATE,
|
||||
NetworkConfigInfo,
|
||||
} from 'cloud-config'
|
||||
import { confirm } from 'modal'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import {
|
||||
Input as DebounceInput,
|
||||
Textarea as DebounceTextarea,
|
||||
@@ -78,7 +78,6 @@ import {
|
||||
import { SizeInput, Toggle } from 'form'
|
||||
import {
|
||||
addSubscriptions,
|
||||
buildTemplate,
|
||||
connectStore,
|
||||
formatSize,
|
||||
getCoresPerSocketPossibilities,
|
||||
@@ -237,6 +236,9 @@ class Vif extends BaseComponent {
|
||||
const getPool = createGetObject((_, props) => props.location.query.pool)
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
const getSrs = createGetObjectsOfType('SR')
|
||||
const getTemplate = createGetObject(
|
||||
(_, props) => props.location.query.template
|
||||
)
|
||||
const getTemplates = createGetObjectsOfType('VM-template').sort()
|
||||
const getUserSshKeys = createSelector(
|
||||
(_, props) => {
|
||||
@@ -257,6 +259,7 @@ class Vif extends BaseComponent {
|
||||
props.pool === undefined // to get objects as a self user
|
||||
),
|
||||
srs: getSrs(state, props),
|
||||
template: getTemplate(state, props),
|
||||
templates: getTemplates(state, props),
|
||||
userSshKeys: getUserSshKeys(state, props),
|
||||
})
|
||||
@@ -277,7 +280,20 @@ export default class NewVm extends BaseComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._reset()
|
||||
this._reset(() => {
|
||||
const { template } = this.props
|
||||
if (template !== undefined) {
|
||||
this._initTemplate(this.props.template)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
get(() => prevProps.template.id) !== get(() => this.props.template.id)
|
||||
) {
|
||||
this._initTemplate(this.props.template)
|
||||
}
|
||||
}
|
||||
|
||||
_getResourceSet = createFinder(
|
||||
@@ -303,7 +319,7 @@ export default class NewVm extends BaseComponent {
|
||||
// Utils -----------------------------------------------------------------------
|
||||
|
||||
get _isDiskTemplate() {
|
||||
const { template } = this.state.state
|
||||
const { template } = this.props
|
||||
return (
|
||||
template &&
|
||||
template.template_info.disks.length === 0 &&
|
||||
@@ -327,38 +343,36 @@ export default class NewVm extends BaseComponent {
|
||||
|
||||
// Actions ---------------------------------------------------------------------
|
||||
|
||||
_reset = () => {
|
||||
this._replaceState({
|
||||
bootAfterCreate: true,
|
||||
CPUs: '',
|
||||
cpuCap: '',
|
||||
cpuWeight: '',
|
||||
existingDisks: {},
|
||||
fastClone: true,
|
||||
hvmBootFirmware: '',
|
||||
installMethod: 'noConfigDrive',
|
||||
multipleVms: false,
|
||||
name_label: '',
|
||||
name_description: '',
|
||||
nameLabels: map(Array(NB_VMS_MIN), (_, index) => `VM_${index + 1}`),
|
||||
namePattern: '{name}%',
|
||||
nbVms: NB_VMS_MIN,
|
||||
VDIs: [],
|
||||
VIFs: [],
|
||||
seqStart: 1,
|
||||
share: false,
|
||||
tags: [],
|
||||
})
|
||||
_reset = callback => {
|
||||
this._replaceState(
|
||||
{
|
||||
bootAfterCreate: true,
|
||||
CPUs: '',
|
||||
cpuCap: '',
|
||||
cpuWeight: '',
|
||||
existingDisks: {},
|
||||
fastClone: true,
|
||||
hvmBootFirmware: '',
|
||||
installMethod: 'noConfigDrive',
|
||||
multipleVms: false,
|
||||
name_label: '',
|
||||
name_description: '',
|
||||
nameLabels: map(Array(NB_VMS_MIN), (_, index) => `VM_${index + 1}`),
|
||||
namePattern: '{name}%',
|
||||
nbVms: NB_VMS_MIN,
|
||||
VDIs: [],
|
||||
VIFs: [],
|
||||
seqStart: 1,
|
||||
share: false,
|
||||
tags: [],
|
||||
},
|
||||
callback
|
||||
)
|
||||
}
|
||||
|
||||
_selfCreate = () => {
|
||||
const {
|
||||
CPUs,
|
||||
VDIs,
|
||||
existingDisks,
|
||||
memoryDynamicMax,
|
||||
template,
|
||||
} = this.state.state
|
||||
const { CPUs, VDIs, existingDisks, memoryDynamicMax } = this.state.state
|
||||
const { template } = this.props
|
||||
const disksSize = sumBy(VDIs, 'size') + sumBy(existingDisks, 'size')
|
||||
const templateDisksSize = sumBy(template.template_info.disks, 'size')
|
||||
const templateMemoryDynamicMax = template.memory.dynamic[1]
|
||||
@@ -473,6 +487,7 @@ export default class NewVm extends BaseComponent {
|
||||
})
|
||||
|
||||
const resourceSet = this._getResourceSet()
|
||||
const { template } = this.props
|
||||
|
||||
const data = {
|
||||
affinityHost: state.affinityHost && state.affinityHost.id,
|
||||
@@ -480,7 +495,7 @@ export default class NewVm extends BaseComponent {
|
||||
existingDisks: state.existingDisks,
|
||||
installation,
|
||||
name_label: state.name_label,
|
||||
template: state.template.id,
|
||||
template: template.id,
|
||||
VDIs: state.VDIs,
|
||||
VIFs: _VIFs,
|
||||
resourceSet: resourceSet && resourceSet.id,
|
||||
@@ -512,13 +527,19 @@ export default class NewVm extends BaseComponent {
|
||||
: createVm(data)
|
||||
}
|
||||
|
||||
_onChangeTemplate = template => {
|
||||
const { pathname, query } = this.props.location
|
||||
this.context.router.push({
|
||||
pathname,
|
||||
query: { ...query, template: template && template.id },
|
||||
})
|
||||
}
|
||||
|
||||
_initTemplate = template => {
|
||||
if (!template) {
|
||||
return this._reset()
|
||||
}
|
||||
|
||||
this._setState({ template })
|
||||
|
||||
const storeState = store.getState()
|
||||
const isInResourceSet = this._getIsInResourceSet()
|
||||
const { state } = this.state
|
||||
@@ -571,7 +592,6 @@ export default class NewVm extends BaseComponent {
|
||||
this._setState({
|
||||
// infos
|
||||
name_label,
|
||||
template,
|
||||
name_description,
|
||||
nameLabels: map(Array(+state.nbVms), (_, index) =>
|
||||
replacer({ name_label, name_description, template }, index + 1)
|
||||
@@ -636,7 +656,7 @@ export default class NewVm extends BaseComponent {
|
||||
_getSrPredicate = createSelector(
|
||||
this._getIsInPool,
|
||||
this._getIsInResourceSet,
|
||||
() => this.state.state.template,
|
||||
() => this.props.template,
|
||||
() => this.props.pool === undefined,
|
||||
(isInPool, isInResourceSet, template, self) => disk =>
|
||||
(self ? isInResourceSet(disk.id) : isInPool(disk)) &&
|
||||
@@ -654,7 +674,7 @@ export default class NewVm extends BaseComponent {
|
||||
this._getIsInPool,
|
||||
this._getIsInResourceSet,
|
||||
() => this.props.pool === undefined,
|
||||
() => this.state.state.template,
|
||||
() => this.props.template,
|
||||
(isInPool, isInResourceSet, self, template) => network =>
|
||||
(self ? isInResourceSet(network.id) : isInPool(network)) &&
|
||||
template !== undefined &&
|
||||
@@ -731,7 +751,7 @@ export default class NewVm extends BaseComponent {
|
||||
)
|
||||
|
||||
_buildTemplate = pattern =>
|
||||
buildTemplate(pattern, {
|
||||
compileTemplate(pattern, {
|
||||
'{name}': state => state.name_label || '',
|
||||
'%': (_, i) => i,
|
||||
})
|
||||
@@ -753,12 +773,12 @@ export default class NewVm extends BaseComponent {
|
||||
)
|
||||
|
||||
_isCoreOs = createSelector(
|
||||
() => this.state.template,
|
||||
() => this.props.template,
|
||||
template => template && template.name_label === 'CoreOS'
|
||||
)
|
||||
|
||||
_isHvm = createSelector(
|
||||
() => this.state.template,
|
||||
() => this.props.template,
|
||||
template => template && template.virtualizationMode === 'hvm'
|
||||
)
|
||||
|
||||
@@ -846,6 +866,7 @@ export default class NewVm extends BaseComponent {
|
||||
}
|
||||
_addVdi = () => {
|
||||
const { state } = this.state
|
||||
const { template } = this.props
|
||||
|
||||
this._setState({
|
||||
VDIs: [
|
||||
@@ -856,7 +877,7 @@ export default class NewVm extends BaseComponent {
|
||||
(state.name_label || 'disk') +
|
||||
'_' +
|
||||
generateReadableRandomString(5),
|
||||
SR: this._getDefaultSr(state.template),
|
||||
SR: this._getDefaultSr(template),
|
||||
type: 'system',
|
||||
},
|
||||
],
|
||||
@@ -870,12 +891,13 @@ export default class NewVm extends BaseComponent {
|
||||
})
|
||||
}
|
||||
_addInterface = () => {
|
||||
const { template } = this.props
|
||||
const { state } = this.state
|
||||
|
||||
this._setState({
|
||||
VIFs: [
|
||||
...state.VIFs,
|
||||
{ network: this._getDefaultNetworkIds(state.template)[0] },
|
||||
{ network: this._getDefaultNetworkIds(template)[0] },
|
||||
],
|
||||
})
|
||||
}
|
||||
@@ -1002,7 +1024,8 @@ export default class NewVm extends BaseComponent {
|
||||
// INFO ------------------------------------------------------------------------
|
||||
|
||||
_renderInfo = () => {
|
||||
const { name_description, name_label, template } = this.state.state
|
||||
const { name_description, name_label } = this.state.state
|
||||
const { template } = this.props
|
||||
return (
|
||||
<Section
|
||||
icon='new-vm-infos'
|
||||
@@ -1014,14 +1037,14 @@ export default class NewVm extends BaseComponent {
|
||||
<span className={styles.inlineSelect}>
|
||||
{this.props.pool ? (
|
||||
<SelectVmTemplate
|
||||
onChange={this._initTemplate}
|
||||
onChange={this._onChangeTemplate}
|
||||
placeholder={_('newVmSelectTemplate')}
|
||||
predicate={this._getVmPredicate()}
|
||||
value={template}
|
||||
/>
|
||||
) : (
|
||||
<SelectResourceSetsVmTemplate
|
||||
onChange={this._initTemplate}
|
||||
onChange={this._onChangeTemplate}
|
||||
placeholder={_('newVmSelectTemplate')}
|
||||
resourceSet={this._getResolvedResourceSet()}
|
||||
value={template}
|
||||
@@ -1048,17 +1071,14 @@ export default class NewVm extends BaseComponent {
|
||||
)
|
||||
}
|
||||
_isInfoDone = () => {
|
||||
const { template, name_label } = this.state.state
|
||||
const { name_label } = this.state.state
|
||||
const { template } = this.props
|
||||
return name_label && template
|
||||
}
|
||||
|
||||
_renderPerformances = () => {
|
||||
const {
|
||||
coresPerSocket,
|
||||
CPUs,
|
||||
memoryDynamicMax,
|
||||
template,
|
||||
} = this.state.state
|
||||
const { coresPerSocket, CPUs, memoryDynamicMax } = this.state.state
|
||||
const { template } = this.props
|
||||
const memoryThreshold = get(() => template.memory.static[0])
|
||||
|
||||
return (
|
||||
@@ -1136,7 +1156,8 @@ export default class NewVm extends BaseComponent {
|
||||
}
|
||||
|
||||
_renderInstallSettings = () => {
|
||||
const { template, coreOsDefaultTemplateError } = this.state.state
|
||||
const { coreOsDefaultTemplateError } = this.state.state
|
||||
const { template } = this.props
|
||||
if (!template) {
|
||||
return
|
||||
}
|
||||
@@ -1175,20 +1196,17 @@ export default class NewVm extends BaseComponent {
|
||||
</LineItem>
|
||||
<br />
|
||||
<LineItem>
|
||||
<Tooltip content={CAN_CLOUD_INIT ? undefined : _('premiumOnly')}>
|
||||
<label>
|
||||
<input
|
||||
checked={installMethod === 'SSH'}
|
||||
disabled={!CAN_CLOUD_INIT}
|
||||
name='installMethod'
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='SSH'
|
||||
/>
|
||||
|
||||
{_('newVmSshKey')}
|
||||
</label>
|
||||
</Tooltip>
|
||||
<label>
|
||||
<input
|
||||
checked={installMethod === 'SSH'}
|
||||
name='installMethod'
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='SSH'
|
||||
/>
|
||||
|
||||
{_('newVmSshKey')}
|
||||
</label>
|
||||
|
||||
<span className={classNames('input-group', styles.fixedWidth)}>
|
||||
<DebounceInput
|
||||
@@ -1216,20 +1234,17 @@ export default class NewVm extends BaseComponent {
|
||||
</LineItem>
|
||||
<br />
|
||||
<LineItem>
|
||||
<Tooltip content={CAN_CLOUD_INIT ? undefined : _('premiumOnly')}>
|
||||
<label>
|
||||
<input
|
||||
checked={installMethod === 'customConfig'}
|
||||
disabled={!CAN_CLOUD_INIT}
|
||||
name='installMethod'
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='customConfig'
|
||||
/>
|
||||
|
||||
{_('newVmCustomConfig')}
|
||||
</label>
|
||||
</Tooltip>
|
||||
<label>
|
||||
<input
|
||||
checked={installMethod === 'customConfig'}
|
||||
name='installMethod'
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='customConfig'
|
||||
/>
|
||||
|
||||
{_('newVmCustomConfig')}
|
||||
</label>
|
||||
|
||||
<AvailableTemplateVars />
|
||||
|
||||
@@ -1380,8 +1395,8 @@ export default class NewVm extends BaseComponent {
|
||||
installMethod,
|
||||
installNetwork,
|
||||
sshKeys,
|
||||
template,
|
||||
} = this.state.state
|
||||
const { template } = this.props
|
||||
switch (installMethod) {
|
||||
case 'customConfig':
|
||||
return (
|
||||
|
||||
@@ -36,6 +36,7 @@ const EMPTY = {
|
||||
bondMode: undefined,
|
||||
description: '',
|
||||
encapsulation: 'gre',
|
||||
encrypted: false,
|
||||
isPrivate: false,
|
||||
mtu: '',
|
||||
name: '',
|
||||
@@ -127,6 +128,9 @@ const NewNetwork = decorate([
|
||||
bonded: isPrivate ? bonded : false,
|
||||
}
|
||||
},
|
||||
toggleEncrypted() {
|
||||
return { encrypted: !this.state.encrypted }
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
disableAddPool: ({ networks }, { nPools }) =>
|
||||
@@ -181,6 +185,7 @@ const NewNetwork = decorate([
|
||||
isPrivate,
|
||||
description,
|
||||
encapsulation,
|
||||
encrypted,
|
||||
mtu,
|
||||
name,
|
||||
networks,
|
||||
@@ -212,6 +217,8 @@ const NewNetwork = decorate([
|
||||
networkDescription: description,
|
||||
encapsulation: encapsulation,
|
||||
xoPifIds: pifIds,
|
||||
encrypted,
|
||||
mtu: mtu !== '' ? +mtu : undefined,
|
||||
})
|
||||
})()
|
||||
: createPrivateNetwork({
|
||||
@@ -220,6 +227,8 @@ const NewNetwork = decorate([
|
||||
networkDescription: description,
|
||||
encapsulation: encapsulation,
|
||||
pifId: pif.id,
|
||||
encrypted,
|
||||
mtu: mtu !== '' ? +mtu : undefined,
|
||||
})
|
||||
: createNetwork({
|
||||
description,
|
||||
@@ -268,6 +277,7 @@ const NewNetwork = decorate([
|
||||
isPrivate,
|
||||
description,
|
||||
encapsulation,
|
||||
encrypted,
|
||||
modeOptions,
|
||||
mtu,
|
||||
name,
|
||||
@@ -335,6 +345,15 @@ const NewNetwork = decorate([
|
||||
type='text'
|
||||
value={description}
|
||||
/>
|
||||
<label>{_('newNetworkMtu')}</label>
|
||||
<input
|
||||
className='form-control'
|
||||
name='mtu'
|
||||
onChange={effects.linkState}
|
||||
placeholder={formatMessage(messages.newNetworkDefaultMtu)}
|
||||
type='text'
|
||||
value={mtu}
|
||||
/>
|
||||
{isPrivate ? (
|
||||
<div>
|
||||
<label>{_('newNetworkEncapsulation')}</label>
|
||||
@@ -347,6 +366,16 @@ const NewNetwork = decorate([
|
||||
]}
|
||||
value={encapsulation}
|
||||
/>
|
||||
<Toggle
|
||||
onChange={effects.toggleEncrypted}
|
||||
value={encrypted}
|
||||
/>{' '}
|
||||
<label>{_('newNetworkEncrypted')}</label>
|
||||
<div>
|
||||
<em>
|
||||
<Icon icon='info' /> {_('encryptionWarning')}
|
||||
</em>
|
||||
</div>
|
||||
<div className='mt-1'>
|
||||
{state.networks.map(({ pool, pif }, key) => (
|
||||
<div key={key}>
|
||||
@@ -405,17 +434,6 @@ const NewNetwork = decorate([
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<label>{_('newNetworkMtu')}</label>
|
||||
<input
|
||||
className='form-control'
|
||||
name='mtu'
|
||||
onChange={effects.linkState}
|
||||
placeholder={formatMessage(
|
||||
messages.newNetworkDefaultMtu
|
||||
)}
|
||||
type='text'
|
||||
value={mtu}
|
||||
/>
|
||||
{bonded ? (
|
||||
<div>
|
||||
<label>{_('newNetworkBondMode')}</label>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { addSubscriptions, downloadLog } from 'utils'
|
||||
import { alert } from 'modal'
|
||||
import { createSelector } from 'selectors'
|
||||
import { CAN_REPORT_BUG, reportBug } from 'report-bug-button'
|
||||
import { get } from '@xen-orchestra/defined'
|
||||
import {
|
||||
deleteApiLog,
|
||||
deleteApiLogs,
|
||||
@@ -33,6 +34,22 @@ const formatLog = log =>
|
||||
2
|
||||
)}\n${JSON.stringify(log.data.error, null, 2).replace(/\\n/g, '\n')}`
|
||||
|
||||
const LogMessage = ({ item: log }) => {
|
||||
const { error } = log.data
|
||||
return (
|
||||
<span>
|
||||
<pre className={styles.widthLimit}>{get(() => error.message)}</pre>
|
||||
{get(() => error.code) === 'LICENCE_RESTRICTION' ? (
|
||||
<a href='https://xcp-ng.org/' rel='noopener noreferrer' target='_blank'>
|
||||
{_('logSuggestXcpNg')}
|
||||
</a>
|
||||
) : get(() => error.name) === 'XapiError' ? (
|
||||
_('logXapiError')
|
||||
) : null}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
name: _('logUser'),
|
||||
@@ -50,22 +67,7 @@ const COLUMNS = [
|
||||
},
|
||||
{
|
||||
name: _('logMessage'),
|
||||
itemRenderer: log => (
|
||||
<span>
|
||||
<pre className={styles.widthLimit}>
|
||||
{log.data.error && log.data.error.message}
|
||||
</pre>
|
||||
{log.data.error && log.data.error.code === 'LICENCE_RESTRICTION' && (
|
||||
<a
|
||||
href='https://xcp-ng.org/'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
{_('logSuggestXcpNg')}
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
component: LogMessage,
|
||||
sortCriteria: log => log.data.error && log.data.error.message,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ import ActionButton from 'action-button'
|
||||
import Button from 'button'
|
||||
import Component from 'base-component'
|
||||
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import cookies from 'cookies-js'
|
||||
import debounce from 'lodash/debounce'
|
||||
import getEventValue from 'get-event-value'
|
||||
import Icon from 'icon'
|
||||
@@ -13,7 +14,7 @@ import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import { isVmRunning, resolveUrl } from 'xo'
|
||||
import { Col, Container, Row } from 'grid'
|
||||
import { confirm } from 'modal'
|
||||
import { confirm, form } from 'modal'
|
||||
import {
|
||||
CpuSparkLines,
|
||||
MemorySparkLines,
|
||||
@@ -61,7 +62,13 @@ export default class TabConsole extends Component {
|
||||
this._toggleMinimalLayout()
|
||||
}
|
||||
}
|
||||
_sendCtrlAltDel = () => {
|
||||
|
||||
_sendCtrlAltDel = async () => {
|
||||
await confirm({
|
||||
icon: 'vm-keyboard',
|
||||
title: _('ctrlAltDelButtonLabel'),
|
||||
body: _('ctrlAltDelConfirmation'),
|
||||
})
|
||||
this.refs.noVnc.sendCtrlAltDel()
|
||||
}
|
||||
|
||||
@@ -91,9 +98,40 @@ export default class TabConsole extends Component {
|
||||
this.setState({ minimalLayout: !this.state.minimalLayout })
|
||||
}
|
||||
|
||||
_openSsh = (username = 'root') => {
|
||||
window.location = `ssh://${encodeURIComponent(username)}@${
|
||||
this.props.vm.addresses['0/ip']
|
||||
}`
|
||||
}
|
||||
|
||||
_openSshMore = async () => {
|
||||
const cookieKey = `${this.props.vm.uuid}/ssh-user-name`
|
||||
const username = await form({
|
||||
defaultValue: cookies.get(cookieKey) || 'root',
|
||||
header: _('sshUsernameLabel'),
|
||||
render: ({ value, onChange }) => (
|
||||
<div>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
if (username !== (cookies.get(cookieKey) || 'root')) {
|
||||
// seems to be seconds
|
||||
const expires = 31 * 3600 * 24
|
||||
cookies.set(cookieKey, username, { expires })
|
||||
}
|
||||
this._openSsh(username)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { statsOverview, vm } = this.props
|
||||
const { minimalLayout, scale } = this.state
|
||||
const canSsh = vm.addresses && vm.addresses['0/ip']
|
||||
|
||||
if (!isVmRunning(vm)) {
|
||||
return (
|
||||
@@ -161,12 +199,42 @@ export default class TabConsole extends Component {
|
||||
</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={2}>
|
||||
<Button onClick={this._sendCtrlAltDel}>
|
||||
<Icon icon='vm-keyboard' /> {_('ctrlAltDelButtonLabel')}
|
||||
</Button>
|
||||
<Col mediumSize={5} largeSize={3}>
|
||||
<div className='btn-group'>
|
||||
<span className='input-group-btn'>
|
||||
<ActionButton
|
||||
handler={this._openSsh}
|
||||
tooltip={
|
||||
canSsh ? _('sshRootTooltip') : _('sshNeedClientTools')
|
||||
}
|
||||
disabled={!canSsh}
|
||||
icon='remote'
|
||||
>
|
||||
{_('sshRootLabel')}
|
||||
</ActionButton>
|
||||
</span>
|
||||
<span className='input-group-btn'>
|
||||
<ActionButton
|
||||
handler={this._openSshMore}
|
||||
tooltip={
|
||||
canSsh ? _('sshUserTooltip') : _('sshNeedClientTools')
|
||||
}
|
||||
disabled={!canSsh}
|
||||
icon='remote'
|
||||
>
|
||||
{_('sshUserLabel')}
|
||||
</ActionButton>
|
||||
</span>
|
||||
<span className='input-group-btn'>
|
||||
<ActionButton
|
||||
handler={this._sendCtrlAltDel}
|
||||
tooltip={_('ctrlAltDelButtonLabel')}
|
||||
icon='vm-keyboard'
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<Col mediumSize={2} className='hidden-lg-down'>
|
||||
<input
|
||||
className='form-control'
|
||||
max={3}
|
||||
|
||||
@@ -37,17 +37,15 @@ import { XoSelect, Size, Text } from 'editable'
|
||||
import { confirm } from 'modal'
|
||||
import { error } from 'notification'
|
||||
import {
|
||||
compact,
|
||||
every,
|
||||
filter,
|
||||
find,
|
||||
forEach,
|
||||
get,
|
||||
groupBy,
|
||||
map,
|
||||
mapValues,
|
||||
pick,
|
||||
some,
|
||||
sortedUniq,
|
||||
uniq,
|
||||
} from 'lodash'
|
||||
import {
|
||||
attachDiskToVm,
|
||||
@@ -85,11 +83,10 @@ class VdiSr extends Component {
|
||||
poolId => sr => sr.$pool === poolId && isSrWritable(sr)
|
||||
)
|
||||
|
||||
_onChangeSr = sr => migrateVdi(this.props.item, sr)
|
||||
_onChangeSr = sr => migrateVdi(this.props.item.vdi, sr)
|
||||
|
||||
render() {
|
||||
const { item: vdi, userData } = this.props
|
||||
const sr = userData.srs[vdi.$SR]
|
||||
const sr = this.props.item.vdiSr
|
||||
return (
|
||||
sr !== undefined && (
|
||||
<XoSelect
|
||||
@@ -111,81 +108,70 @@ class VdiSr extends Component {
|
||||
|
||||
const COLUMNS_VM_PV = [
|
||||
{
|
||||
itemRenderer: vdi => (
|
||||
itemRenderer: ({ vdi }) => (
|
||||
<Text
|
||||
value={vdi.name_label}
|
||||
onChange={value => editVdi(vdi, { name_label: value })}
|
||||
/>
|
||||
),
|
||||
name: _('vdiNameLabel'),
|
||||
sortCriteria: 'name_label',
|
||||
sortCriteria: 'vdi.name_label',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
itemRenderer: vdi => (
|
||||
itemRenderer: ({ vdi }) => (
|
||||
<Text
|
||||
value={vdi.name_description}
|
||||
onChange={value => editVdi(vdi, { name_description: value })}
|
||||
/>
|
||||
),
|
||||
name: _('vdiNameDescription'),
|
||||
sortCriteria: 'name_description',
|
||||
sortCriteria: 'vdi.name_description',
|
||||
},
|
||||
{
|
||||
itemRenderer: vdi => (
|
||||
itemRenderer: ({ vdi }) => (
|
||||
<Size
|
||||
value={vdi.size || null}
|
||||
onChange={size => editVdi(vdi, { size })}
|
||||
/>
|
||||
),
|
||||
name: _('vdiSize'),
|
||||
sortCriteria: 'size',
|
||||
sortCriteria: 'vdi.size',
|
||||
},
|
||||
{
|
||||
component: VdiSr,
|
||||
name: _('vdiSr'),
|
||||
sortCriteria: (vdi, userData) => {
|
||||
const sr = userData.srs[vdi.$SR]
|
||||
return sr !== undefined && sr.name_label
|
||||
},
|
||||
sortCriteria: ({ vdiSr }) => vdiSr !== undefined && vdiSr.name_label,
|
||||
},
|
||||
{
|
||||
itemRenderer: ({ id }, userData) => (
|
||||
<span>{userData.vbdsByVdi[id].device}</span>
|
||||
),
|
||||
itemRenderer: vbd => <span>{vbd.device}</span>,
|
||||
name: _('vbdDevice'),
|
||||
sortCriteria: ({ id }, userData) => userData.vbdsByVdi[id].device,
|
||||
sortCriteria: 'device',
|
||||
},
|
||||
{
|
||||
itemRenderer: (vdi, userData) => {
|
||||
const vbd = userData.vbdsByVdi[vdi.id]
|
||||
return (
|
||||
<Toggle
|
||||
onChange={bootable => setBootableVbd(vbd, bootable)}
|
||||
value={vbd.bootable}
|
||||
/>
|
||||
)
|
||||
},
|
||||
itemRenderer: vbd => (
|
||||
<Toggle
|
||||
onChange={bootable => setBootableVbd(vbd, bootable)}
|
||||
value={vbd.bootable}
|
||||
/>
|
||||
),
|
||||
name: _('vbdBootableStatus'),
|
||||
id: 'vbdBootableStatus',
|
||||
},
|
||||
{
|
||||
itemRenderer: (vdi, userData) => {
|
||||
const vbd = userData.vbdsByVdi[vdi.id]
|
||||
return (
|
||||
<StateButton
|
||||
disabledLabel={_('vbdStatusDisconnected')}
|
||||
disabledHandler={connectVbd}
|
||||
disabledTooltip={_('vbdConnect')}
|
||||
enabledLabel={_('vbdStatusConnected')}
|
||||
enabledHandler={disconnectVbd}
|
||||
enabledTooltip={_('vbdDisconnect')}
|
||||
disabled={!(vbd.attached || isVmRunning(userData.vm))}
|
||||
handlerParam={vbd}
|
||||
state={vbd.attached}
|
||||
/>
|
||||
)
|
||||
},
|
||||
itemRenderer: (vbd, { vm }) => (
|
||||
<StateButton
|
||||
disabledLabel={_('vbdStatusDisconnected')}
|
||||
disabledHandler={connectVbd}
|
||||
disabledTooltip={_('vbdConnect')}
|
||||
enabledLabel={_('vbdStatusConnected')}
|
||||
enabledHandler={disconnectVbd}
|
||||
enabledTooltip={_('vbdDisconnect')}
|
||||
disabled={!(vbd.attached || isVmRunning(vm))}
|
||||
handlerParam={vbd}
|
||||
state={vbd.attached}
|
||||
/>
|
||||
),
|
||||
name: _('vbdStatus'),
|
||||
},
|
||||
]
|
||||
@@ -194,31 +180,19 @@ const COLUMNS = filter(COLUMNS_VM_PV, col => col.id !== 'vbdBootableStatus')
|
||||
|
||||
const ACTIONS = [
|
||||
{
|
||||
disabled: (selectedItems, userData) =>
|
||||
some(map(selectedItems, vdi => userData.vbdsByVdi[vdi.id]), 'attached'),
|
||||
handler: (selectedItems, userData) =>
|
||||
deleteVbds(map(selectedItems, vdi => userData.vbdsByVdi[vdi.id])),
|
||||
individualDisabled: (vdi, userData) => {
|
||||
const vbd = userData.vbdsByVdi[vdi.id]
|
||||
return vbd !== undefined && vbd.attached
|
||||
},
|
||||
individualHandler: (vdi, userData) => {
|
||||
const vbd = userData.vbdsByVdi[vdi.id]
|
||||
return vbd !== undefined && deleteVbd(vbd)
|
||||
},
|
||||
disabled: selectedVbds => some(selectedVbds, 'attached'),
|
||||
handler: deleteVbds,
|
||||
individualDisabled: vbd => vbd.attached,
|
||||
individualHandler: deleteVbd,
|
||||
icon: 'vdi-forget',
|
||||
label: _('vdiForget'),
|
||||
level: 'danger',
|
||||
},
|
||||
{
|
||||
disabled: (selectedItems, userData) =>
|
||||
some(map(selectedItems, vdi => userData.vbdsByVdi[vdi.id]), 'attached'),
|
||||
handler: deleteVdis,
|
||||
individualDisabled: (vdi, userData) => {
|
||||
const vbd = userData.vbdsByVdi[vdi.id]
|
||||
return vbd !== undefined && vbd.attached
|
||||
},
|
||||
individualHandler: deleteVdi,
|
||||
disabled: selectedVbds => some(selectedVbds, 'attached'),
|
||||
handler: selectedVbds => deleteVdis(uniq(map(selectedVbds, 'vdi'))),
|
||||
individualDisabled: vbd => vbd.attached,
|
||||
individualHandler: vbd => deleteVdi(vbd.vdi),
|
||||
individualLabel: _('vdiRemove'),
|
||||
icon: 'vdi-remove',
|
||||
label: _('deleteSelectedVdis'),
|
||||
@@ -833,52 +807,53 @@ export default class TabDisks extends Component {
|
||||
sr.$container === requiredHost
|
||||
)
|
||||
|
||||
_getVbdsByVdi = createSelector(
|
||||
() => this.props.vdis,
|
||||
_getVbds = createSelector(
|
||||
() => this.props.vbds,
|
||||
() => this.props.vm,
|
||||
(vdis, vbds, vm) => mapValues(vdis, vdi => find(vbds, { VDI: vdi.id }))
|
||||
)
|
||||
|
||||
_getIsVdiAttached = createSelector(
|
||||
createSelector(
|
||||
() => this.props.allVbds,
|
||||
() => Object.keys(this.props.vdis),
|
||||
(vbds, vdis) => pick(groupBy(vbds, 'VDI'), vdis)
|
||||
),
|
||||
vbdsByVdi => mapValues(vbdsByVdi, vbds => some(vbds, 'attached'))
|
||||
() => this.props.vdis,
|
||||
() => this.props.srs,
|
||||
(vbds, vdis, srs) =>
|
||||
compact(
|
||||
map(vbds, vbd => {
|
||||
let vdi
|
||||
return (
|
||||
!vbd.is_cd_drive &&
|
||||
((vdi = vdis[vbd.VDI]),
|
||||
vdi !== undefined && { ...vbd, vdi, vdiSr: srs[vdi.$SR] })
|
||||
)
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
individualActions = [
|
||||
...(process.env.XOA_PLAN > 1
|
||||
? [
|
||||
{
|
||||
handler: exportVdi,
|
||||
handler: vbd => exportVdi(vbd.vdi),
|
||||
icon: 'export',
|
||||
label: _('exportVdi'),
|
||||
},
|
||||
{
|
||||
disabled: ({ id }, { isVdiAttached }) => isVdiAttached[id],
|
||||
handler: importVdi,
|
||||
disabled: vbd => vbd.attached,
|
||||
handler: vbd => importVdi(vbd.vdi),
|
||||
icon: 'import',
|
||||
label: _('importVdi'),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
handler: this._migrateVdi,
|
||||
handler: vbd => this._migrateVdi(vbd.vdi),
|
||||
icon: 'vdi-migrate',
|
||||
label: _('vdiMigrate'),
|
||||
},
|
||||
{
|
||||
handler: vdi => copy(vdi.uuid),
|
||||
handler: vbd => copy(vbd.vdi.uuid),
|
||||
icon: 'clipboard',
|
||||
label: vdi => _('copyUuid', { uuid: vdi.uuid }),
|
||||
label: vbd => _('copyUuid', { uuid: vbd.vdi.uuid }),
|
||||
},
|
||||
]
|
||||
|
||||
render() {
|
||||
const { allVbds, srs, vdis, vm } = this.props
|
||||
const { allVbds, vm } = this.props
|
||||
|
||||
const { attachDisk, bootOrder, newDisk } = this.state
|
||||
|
||||
@@ -952,11 +927,8 @@ export default class TabDisks extends Component {
|
||||
<Col>
|
||||
<SortedTable
|
||||
actions={ACTIONS}
|
||||
collection={vdis}
|
||||
collection={this._getVbds()}
|
||||
columns={vm.virtualizationMode === 'pv' ? COLUMNS_VM_PV : COLUMNS}
|
||||
data-isVdiAttached={this._getIsVdiAttached()}
|
||||
data-srs={srs}
|
||||
data-vbdsByVdi={this._getVbdsByVdi()}
|
||||
data-vm={vm}
|
||||
individualActions={this.individualActions}
|
||||
shortcutsTarget='body'
|
||||
|
||||
@@ -2,15 +2,16 @@ import _ from 'intl'
|
||||
import Icon from 'icon'
|
||||
import Page from '../page'
|
||||
import React from 'react'
|
||||
import { routes } from 'utils'
|
||||
import { connectStore, routes } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { isAdmin } from 'selectors'
|
||||
import { NavLink, NavTabs } from 'nav'
|
||||
|
||||
import Update from './update'
|
||||
import Licenses from './licenses'
|
||||
import Notifications, { NotificationTag } from './notifications'
|
||||
|
||||
const HEADER = (
|
||||
const Header = ({ isAdmin }) => (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col mediumSize={3}>
|
||||
@@ -20,12 +21,16 @@ const HEADER = (
|
||||
</Col>
|
||||
<Col mediumSize={9}>
|
||||
<NavTabs className='pull-right'>
|
||||
<NavLink to='/xoa/update'>
|
||||
<Icon icon='menu-update' /> {_('updatePage')}
|
||||
</NavLink>
|
||||
<NavLink to='/xoa/licenses'>
|
||||
<Icon icon='menu-license' /> {_('licensesPage')}
|
||||
</NavLink>
|
||||
{isAdmin && (
|
||||
<NavLink to='/xoa/update'>
|
||||
<Icon icon='menu-update' /> {_('updatePage')}
|
||||
</NavLink>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<NavLink to='/xoa/licenses'>
|
||||
<Icon icon='menu-license' /> {_('licensesPage')}
|
||||
</NavLink>
|
||||
)}
|
||||
<NavLink to='/xoa/notifications'>
|
||||
<Icon icon='menu-notification' /> {_('notificationsPage')}{' '}
|
||||
<NotificationTag />
|
||||
@@ -40,10 +45,14 @@ const Xoa = routes('xoa', {
|
||||
update: Update,
|
||||
licenses: Licenses,
|
||||
notifications: Notifications,
|
||||
})(({ children }) => (
|
||||
<Page header={HEADER} title='xoaPage' formatTitle>
|
||||
{children}
|
||||
</Page>
|
||||
))
|
||||
})(
|
||||
connectStore({
|
||||
isAdmin,
|
||||
})(({ children, isAdmin }) => (
|
||||
<Page header={<Header isAdmin={isAdmin} />} title='xoaPage' formatTitle>
|
||||
{children}
|
||||
</Page>
|
||||
))
|
||||
)
|
||||
|
||||
export default Xoa
|
||||
|
||||
@@ -156,7 +156,9 @@ const Updates = decorate([
|
||||
const { email = xoaRegisterState.email, password } = state
|
||||
await xoaUpdater.register(email, password, isRegistered)
|
||||
|
||||
return initialRegistrationState()
|
||||
if (state.isRegistered) {
|
||||
return initialRegistrationState()
|
||||
}
|
||||
},
|
||||
resetChannel: initialChannelState,
|
||||
resetProxyConfig: initialProxyState,
|
||||
@@ -219,11 +221,12 @@ const Updates = decorate([
|
||||
? () => ({ 'xen-orchestra': 'sources' })
|
||||
: async function() {
|
||||
const {
|
||||
engine,
|
||||
installer,
|
||||
updater,
|
||||
npm,
|
||||
} = await xoaUpdater.getLocalManifest()
|
||||
return { ...installer, ...updater, ...npm }
|
||||
return { ...engine, ...installer, ...updater, ...npm }
|
||||
},
|
||||
isDisconnected: (_, { xoaUpdaterState }) =>
|
||||
xoaUpdater === 'disconnected' || xoaUpdaterState === 'error',
|
||||
@@ -250,6 +253,7 @@ const Updates = decorate([
|
||||
.sort()
|
||||
.map(name => `- ${name}: ${installedPackages[name]}`)
|
||||
.join('\n'),
|
||||
proxyFormId: generateId,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
@@ -410,7 +414,7 @@ const Updates = decorate([
|
||||
{_('proxySettings')} {state.isProxyConfigEdited ? '*' : ''}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<form>
|
||||
<form id={state.proxyFormId} className='form'>
|
||||
<fieldset disabled={COMMUNITY}>
|
||||
<div className='form-group'>
|
||||
<input
|
||||
@@ -461,6 +465,7 @@ const Updates = decorate([
|
||||
<ActionButton
|
||||
icon='save'
|
||||
btnStyle='primary'
|
||||
form={state.proxyFormId}
|
||||
handler={effects.configure}
|
||||
>
|
||||
{_('formSave')}
|
||||
|
||||
@@ -65,7 +65,7 @@ require('exec-promise')(() =>
|
||||
const originalScripts = scripts
|
||||
|
||||
if (!pkg.private && !('postversion' in scripts)) {
|
||||
scripts = { ...scripts, postversion: 'npm publish' }
|
||||
scripts = { ...scripts, postversion: 'npm publish --access public' }
|
||||
}
|
||||
|
||||
const prepublish = scripts.prepublish
|
||||
|
||||
Reference in New Issue
Block a user