Compare commits

...

62 Commits

Author SHA1 Message Date
Mohamedox
af55e0341b memorize selected vms pool ids 2019-09-27 10:03:54 +02:00
Mohamedox
3a4f1f78ce adapt PR to comments 2019-09-27 10:03:53 +02:00
Mohamedox
1d765ebf49 update changelog 2019-09-27 10:03:21 +02:00
Mohamedox
3d416d75e9 feat(xo-web/home): enhance sort of bulk VM Migrate selector
Fixes #4462
2019-09-27 10:01:33 +02:00
HamadaBrest
f8666ba367 fix(xo-web/hub): properly display community info (#4552) 2019-09-26 16:57:53 +02:00
badrAZ
9e80f76dd8 fix(xo-server-test/backup-ng): force fetching uptodate logs (#4551)
See a6d182e

The issue:

xo-server-test execute sequentially multiple backups and validate their execution using logs. The issue is that the logs are not up-to-date in the second fetch because the method returns the previous cached response.

The solution:

Add _forceRefresh param to backupNg.getLogs to be able to clear the method cache. It's just a work-around which will be removed once the consolidated logs will be stored in the DB.
2019-09-26 16:42:12 +02:00
HamadaBrest
c76a5eaf67 feat: hub: XVA templates store (#4442)
Fixes #1918
2019-09-26 16:36:32 +02:00
Julien Fontanet
cd378f0168 chore(PULL_REQUEST_TEMPLATE): strikethrough not relevant items 2019-09-26 14:24:54 +02:00
badrAZ
7d51ff0cf5 feat(xo-server/_pDebounceWithKey): ability to clear cache entry (#4549)
Fixes #4548
2019-09-26 14:17:33 +02:00
Julien Fontanet
47819ea956 chore(PULL_REQUEST_TEMPLATE): better checklist + 4 agreements (#4547) 2019-09-25 17:41:32 +02:00
Julien Fontanet
c7e3560c98 chore(normalize-packages): add --access public to npm publish
Only for non-private packages without existing `postversion` script.
2019-09-25 17:36:36 +02:00
Julien Fontanet
b24400b21d feat(xen-api): support IPv6 addresses (#4521)
Fixes #4520
2019-09-24 13:59:49 +02:00
Pierre Donias
6c1d651687 fix(xo-server/host.isHostServerTimeConsistent): dont return false on check failure (#4540) 2019-09-24 10:46:43 +02:00
Pierre Donias
e7757b53e7 feat(xo-web/new-vm): remove cloud init plan limitation (#4543) 2019-09-24 10:24:27 +02:00
Julien Fontanet
a6d182e92d feat(xo-server/getBackupNgLogs): implement debounce (#4509) (#4541)
Similar to #4509

Fixes xoa-support#1676

For now, the delay is set to 10s which is the duration used by xo-web's
subscription, which makes it enough to make it independent of the number of
clients.

In the future, this could be configurable, but we may simply do the
consolidation only once during the backup execution.
2019-09-23 16:20:17 +02:00
Julien Fontanet
925eca1463 chore(xo-server/api): context has methods bound to XO
This makes sure the correct context is used, which is necessary for properties write and for debouncing.
2019-09-23 15:59:45 +02:00
Julien Fontanet
8b454f0d39 chore(xo-server/MultiKeyMap): add basic tests 2019-09-23 15:14:54 +02:00
Pierre Donias
7c4d110353 feat(xo-web/settings/logs): identify XAPI errors (#4385)
Fixes #4101
2019-09-23 10:31:34 +02:00
Julien Fontanet
6df55523b6 feat(xo-web): display node in list of packages 2019-09-20 17:22:21 +02:00
badrAZ
3ec6a24634 chore(CHANGELOG): update next 2019-09-20 16:36:04 +02:00
badrAZ
164b4218c4 feat(xo-web): 5.50.0 2019-09-20 16:21:38 +02:00
badrAZ
56df8a6477 feat(xo-server): 5.50.0 2019-09-20 16:21:22 +02:00
badrAZ
47a83b312d feat(@xen-orchestra/template): 0.1.0 2019-09-20 16:20:26 +02:00
badrAZ
41a28ae088 feat(xo-server-sdn-controller): 0.3.0 2019-09-20 16:05:27 +02:00
badrAZ
436a8755ae feat(@xen-orchestra/cron): 1.0.4 2019-09-20 16:04:31 +02:00
Julien Fontanet
960b179d95 chore(xen-api/examples): update dependencies 2019-09-20 12:22:43 +02:00
badrAZ
0f0d0e1076 fix(xo-server-test/backupNg): bypass VDI chains check (#4538) 2019-09-20 12:15:49 +02:00
Julien Fontanet
a8bd0d8075 feat(xo-server/listVmBackupsNg): implement debounce (#4509)
Fixes xoa-support#1676

For now, the delay is set to 10s which is the duration used by xo-web's
subscription, which makes it enough to make it independent of the number of
clients.

In the future, this should probably be longer and configurable, but this
requires invalidating the cache in case of changes (creation/deletion of
backups) and explicit requests from the users, which will be implemented in
another PR.
2019-09-20 12:01:58 +02:00
Julien Fontanet
986d3af685 feat(xo-server/vm.export): prefix filename with datetime
Fixes #4503
2019-09-20 11:26:12 +02:00
BenjiReis
1833f9ffdf feat(xo-server-sdn-controller): MTU configurable for private networks (#4491) 2019-09-20 11:19:18 +02:00
badrAZ
30a6877f8a fix(xo-server/jobs): report backups as failed if already running (#4534)
Fixes #4497
2019-09-19 17:27:16 +02:00
badrAZ
aaae2583c7 fix(xo-web/new-vm): ability to escape cloud config template variables (#4501)
Fixes #4486

Fixes xoa-support#1721
2019-09-19 16:19:18 +02:00
Pierre Donias
7f24afc2e7 fix(xo-web/xoa): remove "Updates" & "Licenses" tabs for non admins (#4526)
Fixes support#1753
2019-09-19 16:00:10 +02:00
BenjiReis
0040923e12 chore(xo-server-sdn-controller): move default values to public API (#4536) 2019-09-19 15:48:01 +02:00
Julien Fontanet
844efb88d8 fix(cron): fix 2 race conditions (#4533)
Should fix xoa-support#344, xoa-support#1186 & xoa-support#1755.

These could lead to:
- job not properly stopped
- job run twice
2019-09-19 13:52:29 +02:00
HamadaBrest
9efc3dd1fb feat(xo-web/new-vm): populate form with template in URL query (#4500)
Fixes #4494
2019-09-19 12:03:46 +02:00
Julien Fontanet
67853bad8e chore(cron): extract Schedule#_nextDelay() 2019-09-18 17:44:10 +02:00
BenjiReis
faa8e1441a chore(xo-server-sdn-controller): clearer maps (#4531) 2019-09-18 13:46:36 +02:00
BenjiReis
5c54611d1b feat(xo-server-sdn-controller): encryption for private networks (#4441) 2019-09-17 15:26:19 +02:00
badrAZ
dcf55e4385 fix(xo-server/network.set): missing param definition (#4510)
Fixes #4514

See https://xcp-ng.org/forum/topic/962/why-are-you-using-xcp-ng-center/96
2019-09-17 12:05:11 +02:00
Nicolas Raynaud
2b0f1b6aab feat(xo-web/vm/console): direct connection to SSH button (#4415) 2019-09-17 12:02:33 +02:00
Julien Fontanet
ae6cc8eea3 feat(xo-web/xoa/updater): true submit button for proxy
This allows using "Enter" key to submit.
2019-09-16 17:40:49 +02:00
marcpezin
5279fa49a7 chore(installation): make screenshot a link (#4524) 2019-09-16 14:49:13 +02:00
badrAZ
dcd8a62784 fix(xo-server/network.create): return id XAPI record (#4523)
It was a mistake to return the record, and it was not used.
2019-09-16 14:45:17 +02:00
marcpezin
8c197b0e1a docs(installation): use deploy form (#4522) 2019-09-16 12:12:59 +02:00
Julien Fontanet
aed824b200 fix(xo-web): dont reset state on registration failure 2019-09-13 13:59:25 +02:00
Julien Fontanet
036b30212e feat(xo-cli): special handling of invalid params error
Before:
```
✖ invalid parameters
JsonRpcError: invalid parameters
    at Peer._callee$ (/usr/local/lib/node_modules/xen-api/node_modules/json-rpc-peer/dist/index.js:137:44)
    at tryCatch (/usr/local/lib/node_modules/xen-api/node_modules/json-rpc-peer/node_modules/regenerator-runtime/runtime.js:62:40)
    at Generator.invoke [as _invoke] (/usr/local/lib/node_modules/xen-api/node_modules/json-rpc-peer/node_modules/regenerator-runtime/runtime.js:288:22)
    at Generator.prototype.(anonymous function) [as next] (/usr/local/lib/node_modules/xen-api/node_modules/json-rpc-peer/node_modules/regenerator-runtime/runtime.js:114:21)
    at asyncGeneratorStep (/usr/local/lib/node_modules/xen-api/node_modules/json-rpc-peer/node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:24)
    at _next (/usr/local/lib/node_modules/xen-api/node_modules/json-rpc-peer/node_modules/@babel/runtime/helpers/asyncToGenerator.js:25:9)
    at /usr/local/lib/node_modules/xen-api/node_modules/json-rpc-peer/node_modules/@babel/runtime/helpers/asyncToGenerator.js:32:7
    at new Promise (<anonymous>)
    at Peer.<anonymous> (/usr/local/lib/node_modules/xen-api/node_modules/json-rpc-peer/node_modules/@babel/runtime/helpers/asyncToGenerator.js:21:12)
    at Peer.exec (/usr/local/lib/node_modules/xen-api/node_modules/json-rpc-peer/dist/index.js:180:20)
    at Peer.write (/usr/local/lib/node_modules/xen-api/node_modules/json-rpc-peer/dist/index.js:284:10)
    at Xo.<anonymous> (/usr/local/lib/node_modules/xen-api/node_modules/jsonrpc-websocket-client/dist/index.js:77:12)
    at emitOne (events.js:116:13)
    at Xo.emit (events.js:211:7)
    at WebSocket.<anonymous> (/usr/local/lib/node_modules/xen-api/node_modules/jsonrpc-websocket-client/dist/websocket-client.js:206:18)
    at WebSocket.onMessage (/usr/local/lib/node_modules/xen-api/node_modules/ws/lib/event-target.js:120:16)
```

After:
```
✖ invalid parameters
  property @: should not contains property ["foo"]
  property @.id: is missing and not optional
```
2019-09-13 12:02:29 +02:00
badrAZ
3451ab3f50 fix(xo-server/editVm): null support for CoresPerSocket and cpuCap (#4507)
Introduced by 3196c7c#diff-a20130cea265330a92852ddcd3a425ebR286
2019-09-12 12:09:01 +02:00
BenjiReis
0d0a92c2b1 fix(xo-server-sdn-controller): avoid unhandled promises (#4518) 2019-09-12 11:44:11 +02:00
BenjiReis
aa19bc7bf5 chore(xo-server-sdn-controller): add missing awaits (#4516) 2019-09-12 11:06:19 +02:00
BenjiReis
347759b2e7 chore(xo-server-sdn-controller): safer error testing (#4517) 2019-09-12 09:43:09 +02:00
badrAZ
352230446c chore(xo-server-test/createTempVm): returns a record instead of an id (#4508) 2019-09-11 16:19:51 +02:00
Pierre Donias
3eff8102e1 fix(xo-server/patching): bad semver check for update system (#4511)
So far, in order to know if we needed to use the "new" patching system, we were checking that the pool master's `software_version.platform_version` satisfied `^2.1.1` (instead of `>=2.1.1` like [XenCenter does](f3a64fc54b/XenModel/Utils/Helpers.cs (L420))). This broke the installation and the display of missing patches for CH 8.0 whose `software_version.platform_version` is `3.0.0`.
2019-09-11 14:12:21 +02:00
Rajaa.BARHTAOUI
6693d845d9 feat(xo-web/vm/disks): show duplicated disks (#4414)
Fixes #4400
2019-09-10 15:34:16 +02:00
Julien Fontanet
4d79c462db fix(xo-server): use encodeURIComponent for filenames
This makes sure special characters like `?` or `#` are correctly handled.
2019-09-10 14:44:48 +02:00
badrAZ
c44ef6a1dc fix(xo-web/backup-ng): display user errors in the form (#4131)
Fixes #3831
2019-09-10 14:23:20 +02:00
badrAZ
f0996fcfa7 feat(xo-server/backup-ng): emit warning task when zstd is chosen but not supported (#4375)
See #3892
2019-09-10 11:55:57 +02:00
badrAZ
54bc384d37 fix(xo-server/vm): fix "vm.set_domain_type" is not a function on XS < 7.5 (#4504)
Fixes #4348
Introduced by 3196c7ca09 (diff-a20130cea265330a92852ddcd3a425ebR286)
2019-09-09 17:05:12 +02:00
BenjiReis
504fc1efe8 doc(xo-server-sdn-controller): hosts must be able to reach each other (#4498) 2019-09-06 11:49:10 +02:00
BenjiReis
f4179b93fb chore(sdn-controller): remove other_config from interfaces (#4479)
Only needed in ports
2019-09-06 11:32:24 +02:00
badrAZ
564252c198 chore(CHANGELOG): update next 2019-09-05 16:32:47 +02:00
badrAZ
802a7a4463 feat(xo-web): 5.49.0 2019-09-05 16:17:00 +02:00
81 changed files with 2105 additions and 896 deletions

View File

@@ -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": [

View File

@@ -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()

View 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)
})
})

View File

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

View File

@@ -0,0 +1,62 @@
# @xen-orchestra/template [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](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)

View 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"
}
}

View 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
})
}

View 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')
})

View File

@@ -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)

View File

@@ -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

View File

@@ -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.

View File

@@ -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!
[![](./assets/deploy_form.png)](https://xen-orchestra.com/#!/xoa)
### XOA credentials

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

View File

@@ -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)"
```
![](./assets/deploy_form.png)
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)
![](https://xen-orchestra.com/assets/xoa1.png)

View File

@@ -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`

View File

@@ -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!
![](./assets/deploy_form.png)
### Via a bash script
Alternatively, you can deploy it by connecting to your XenServer host and executing the following:

View File

@@ -46,6 +46,7 @@
"/xo-web/"
],
"testRegex": "\\.spec\\.js$",
"timers": "fake",
"transform": {
"\\.jsx?$": "babel-jest"
}

View 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=="
}
}
}

View File

@@ -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"
}

View File

@@ -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=

View File

@@ -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)

View File

@@ -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

View File

@@ -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) {

View File

@@ -31,7 +31,7 @@
"node": ">=6"
},
"dependencies": {
"@xen-orchestra/cron": "^1.0.3",
"@xen-orchestra/cron": "^1.0.4",
"lodash": "^4.16.2"
},
"devDependencies": {

View File

@@ -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"

View File

@@ -15,7 +15,7 @@
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"version": "0.2.1",
"version": "0.3.0",
"engines": {
"node": ">=6"
},

View File

@@ -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) {

View File

@@ -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',

View File

@@ -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

View File

@@ -3,6 +3,9 @@
email = ''
password = ''
[pools]
default = ''
[servers]
[servers.default]
username = ''

View File

@@ -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--]

View File

@@ -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,
})

View 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 })
})
})

View File

@@ -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",

View File

@@ -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",

View 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)
})
})
})

View File

@@ -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),
})

View 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)
})
})

View File

@@ -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 }))
}

View File

@@ -93,7 +93,7 @@ export async function fetchFiles({ format = 'zip', ...params }) {
handleFetchFiles,
{ ...params, format },
{
suffix: encodeURI(`/${fileName}`),
suffix: '/' + encodeURIComponent(fileName),
}
).then(url => ({ $getFrom: url }))
}

View File

@@ -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 = {

View File

@@ -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,

View File

@@ -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`
),
}),
}
}

View File

@@ -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',

View File

@@ -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)
)})`
)
}

View File

@@ -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'

View File

@@ -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: {

View File

@@ -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) =>

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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()
}
}
}

View File

@@ -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 {

View File

@@ -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))
})
}
}

View File

@@ -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",

View File

@@ -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

View File

@@ -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}',

View File

@@ -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
)

View File

@@ -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!

View File

@@ -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

View File

@@ -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,
})

View File

@@ -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({

View File

@@ -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}

View File

@@ -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 = {}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 }

View 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'>
&nbsp; {_('vmNoAvailable')}
<Icon icon='alarm' color='yellow' />
</h2>
</Col>
) : (
resources.map(data => (
<Col key={data.namespace} mediumSize={3}>
<Resource {...data} />
</Col>
))
)}
</Row>
</Container>
</Page>
),
])

View 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')}
&nbsp;
{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>
),
])

View 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>
),
])

View File

@@ -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 {

View File

@@ -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',

View File

@@ -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'
/>
&nbsp;
{_('newVmSshKey')}
</label>
</Tooltip>
<label>
<input
checked={installMethod === 'SSH'}
name='installMethod'
onChange={this._linkState('installMethod')}
type='radio'
value='SSH'
/>
&nbsp;
{_('newVmSshKey')}
</label>
&nbsp;
<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'
/>
&nbsp;
{_('newVmCustomConfig')}
</label>
</Tooltip>
<label>
<input
checked={installMethod === 'customConfig'}
name='installMethod'
onChange={this._linkState('installMethod')}
type='radio'
value='customConfig'
/>
&nbsp;
{_('newVmCustomConfig')}
</label>
&nbsp;
<AvailableTemplateVars />
&nbsp;
@@ -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 (

View File

@@ -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>

View File

@@ -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,
},
{

View File

@@ -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}

View File

@@ -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'

View File

@@ -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

View File

@@ -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')}

View File

@@ -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