Compare commits
162 Commits
xo-server/
...
xo-server/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e4924caf6 | ||
|
|
7f391a5860 | ||
|
|
5c7249c8fc | ||
|
|
932d00133d | ||
|
|
32a371bf13 | ||
|
|
5d0622d2cf | ||
|
|
9ab9155bf0 | ||
|
|
86a1ed6d46 | ||
|
|
b3c9936d74 | ||
|
|
21b4d7cf11 | ||
|
|
4ec07f9ff8 | ||
|
|
b7c89d6f64 | ||
|
|
0eb168ec70 | ||
|
|
8ac1a66e93 | ||
|
|
301da3662a | ||
|
|
e474946cb7 | ||
|
|
9a0ca1ebb2 | ||
|
|
520f7b2a77 | ||
|
|
c0b3b3aab8 | ||
|
|
d499332ce3 | ||
|
|
19ce06e0bb | ||
|
|
ea6ff4224e | ||
|
|
871d1f8632 | ||
|
|
77ce2ff6d1 | ||
|
|
6383104796 | ||
|
|
b99b4159c8 | ||
|
|
8bedb1f3b9 | ||
|
|
dc85804a27 | ||
|
|
42a31e512a | ||
|
|
2be7388696 | ||
|
|
bc5b00781b | ||
|
|
313e2b3de6 | ||
|
|
0bbd002060 | ||
|
|
5e785266a5 | ||
|
|
5870769e7d | ||
|
|
79b80dcd07 | ||
|
|
6f6e547e6c | ||
|
|
352c9357df | ||
|
|
1ba4641641 | ||
|
|
60e0047285 | ||
|
|
235e7c143c | ||
|
|
522d6eed92 | ||
|
|
9d1d6ea4c5 | ||
|
|
0afd506a41 | ||
|
|
9dfb837e3f | ||
|
|
4ab63b569f | ||
|
|
8d390d256d | ||
|
|
4eec5e06fc | ||
|
|
e4063b1ba8 | ||
|
|
0c3227cf8e | ||
|
|
7bed200bf5 | ||
|
|
4f763e2109 | ||
|
|
75167fb65b | ||
|
|
675588f780 | ||
|
|
2d6f94edd8 | ||
|
|
247c66ef4b | ||
|
|
1076fac40f | ||
|
|
14a4a415a2 | ||
|
|
524355b59c | ||
|
|
36fe49f3f5 | ||
|
|
c0c0af9b14 | ||
|
|
d1e472d482 | ||
|
|
c80e43ad0d | ||
|
|
fdd395e2b6 | ||
|
|
e094437168 | ||
|
|
2ee0be7466 | ||
|
|
2784a7cc92 | ||
|
|
b09f998d6c | ||
|
|
bdeb5895f6 | ||
|
|
3944b8aaee | ||
|
|
6e66cffb92 | ||
|
|
57092ee788 | ||
|
|
70e9e1c706 | ||
|
|
9662b8fbee | ||
|
|
9f66421ae7 | ||
|
|
50584c2e50 | ||
|
|
7be4e1901a | ||
|
|
b47146de45 | ||
|
|
97b229b2c7 | ||
|
|
6bb5bb9403 | ||
|
|
8c4b8271d8 | ||
|
|
69291c0574 | ||
|
|
2dc073dcd6 | ||
|
|
1894cb35d2 | ||
|
|
cd37420b07 | ||
|
|
55cb6b39db | ||
|
|
89d13b2285 | ||
|
|
1b64b0468a | ||
|
|
085fb83294 | ||
|
|
edd606563f | ||
|
|
fb804e99f0 | ||
|
|
1707cbcb54 | ||
|
|
6d6a630c31 | ||
|
|
ff2990e8e5 | ||
|
|
d679aff0fb | ||
|
|
603a444905 | ||
|
|
a002958448 | ||
|
|
cb4bc37424 | ||
|
|
0fc6f917e6 | ||
|
|
ec0d012b24 | ||
|
|
2cd4b171a1 | ||
|
|
0cb6906c4d | ||
|
|
4c19b93c30 | ||
|
|
6165f1b405 | ||
|
|
37a4221e43 | ||
|
|
9831b222b5 | ||
|
|
7b6f44fb74 | ||
|
|
399f4d0ea3 | ||
|
|
26a668a875 | ||
|
|
bf96262b6e | ||
|
|
1155fa1fe9 | ||
|
|
1875d31731 | ||
|
|
6f855fd14e | ||
|
|
08e392bb46 | ||
|
|
66d63e0546 | ||
|
|
7ee56fe8bc | ||
|
|
669d04ee48 | ||
|
|
cb1b37326e | ||
|
|
7bb73bee67 | ||
|
|
7286ddc338 | ||
|
|
7d1f9e33fe | ||
|
|
63c676ebfe | ||
|
|
fcaf6b7923 | ||
|
|
9f347a170a | ||
|
|
2f7cd4426d | ||
|
|
854f256470 | ||
|
|
5d0b40f752 | ||
|
|
27a2853ee8 | ||
|
|
67f6b80312 | ||
|
|
016037adc1 | ||
|
|
70d5c1034d | ||
|
|
ed6fb8754f | ||
|
|
6d08a9b11c | ||
|
|
cf6aa7cf79 | ||
|
|
6c4e57aae0 | ||
|
|
d08a04959c | ||
|
|
2762f74ce5 | ||
|
|
6ebcf6eec5 | ||
|
|
25b78fb7e1 | ||
|
|
670dd2dd96 | ||
|
|
1baf04f786 | ||
|
|
ce05b7a041 | ||
|
|
290cc146c8 | ||
|
|
db4d46a584 | ||
|
|
8ed2e51dde | ||
|
|
33702c09a6 | ||
|
|
45aeca3753 | ||
|
|
deae7dfb4d | ||
|
|
2af043ebdd | ||
|
|
e121295735 | ||
|
|
7c1c405a64 | ||
|
|
5d7c95a34d | ||
|
|
504c934fc9 | ||
|
|
81b0223f73 | ||
|
|
6d1e410bfd | ||
|
|
26c5c6152d | ||
|
|
d83bf0ebaf | ||
|
|
5adfe9a552 | ||
|
|
883f461dc7 | ||
|
|
8595ebc258 | ||
|
|
2bd31f4560 | ||
|
|
6df85ecadd |
93
.jshintrc
93
.jshintrc
@@ -1,93 +0,0 @@
|
||||
{
|
||||
// Julien Fontanet JSHint configuration
|
||||
// https://gist.github.com/julien-f/8095615
|
||||
//
|
||||
// Changes from defaults:
|
||||
// - all enforcing options (except `++` & `--`) enabled
|
||||
// - single quotes
|
||||
// - indentation set to 2 instead of 4
|
||||
// - almost all relaxing options disabled
|
||||
// - environments are set to Node.js
|
||||
//
|
||||
// See http://jshint.com/docs/ for more details
|
||||
|
||||
"maxerr" : 50, // {int} Maximum error before stopping
|
||||
|
||||
// Enforcing
|
||||
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
|
||||
"camelcase" : true, // true: Identifiers must be in camelCase
|
||||
"curly" : true, // true: Require {} for every new block or scope
|
||||
"eqeqeq" : true, // true: Require triple equals (===) for comparison
|
||||
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
|
||||
"freeze" : true, // true: Prohibit overwriting prototypes of native objects (Array, Date, ...)
|
||||
"immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
|
||||
"indent" : 2, // {int} Number of spaces to use for indentation
|
||||
"latedef" : true, // true: Require variables/functions to be defined before being used
|
||||
"newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()`
|
||||
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
|
||||
"noempty" : true, // true: Prohibit use of empty blocks
|
||||
"nonbsp" : true, // true: Prohibit use of non breakable spaces
|
||||
"nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment)
|
||||
"plusplus" : false, // true: Prohibit use of `++` & `--`
|
||||
"quotmark" : "single", // Quotation mark consistency:
|
||||
// false : do nothing (default)
|
||||
// true : ensure whatever is used is consistent
|
||||
// "single" : require single quotes
|
||||
// "double" : require double quotes
|
||||
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
|
||||
"unused" : true, // true: Require all defined variables be used
|
||||
"strict" : false, // true: Requires all functions run in ES5 Strict Mode
|
||||
"maxcomplexity" : 7, // {int} Max cyclomatic complexity per function
|
||||
"maxdepth" : 3, // {int} Max depth of nested blocks (within functions)
|
||||
"maxlen" : 80, // {int} Max number of characters per line
|
||||
"maxparams" : 4, // {int} Max number of formal params allowed per function
|
||||
"maxstatements" : 20, // {int} Max number statements per function
|
||||
|
||||
// Relaxing
|
||||
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
|
||||
"boss" : false, // true: Tolerate assignments where comparisons would be expected
|
||||
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
|
||||
"eqnull" : false, // true: Tolerate use of `== null`
|
||||
"esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
|
||||
"expr" : false, // true: Tolerate `ExpressionStatement` as Programs
|
||||
"funcscope" : false, // true: Tolerate defining variables inside control statements
|
||||
"globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
|
||||
"iterator" : false, // true: Tolerate using the `__iterator__` property
|
||||
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
|
||||
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
|
||||
"laxcomma" : false, // true: Tolerate comma-first style coding
|
||||
"loopfunc" : false, // true: Tolerate functions being defined in loops
|
||||
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
|
||||
// (ex: `for each`, multiple try/catch, function expression…)
|
||||
"multistr" : false, // true: Tolerate multi-line strings
|
||||
"notypeof" : false, // true: Tolerate typeof comparison with unknown values.
|
||||
"proto" : false, // true: Tolerate using the `__proto__` property
|
||||
"scripturl" : false, // true: Tolerate script-targeted URLs
|
||||
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
|
||||
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
|
||||
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
|
||||
"validthis" : false, // true: Tolerate using this in a non-constructor function
|
||||
"noyield" : false, // true: Tolerate generators without yields
|
||||
|
||||
// Environments
|
||||
"browser" : false, // Web Browser (window, document, etc)
|
||||
"browserify" : false, // Browserify (node.js code in the browser)
|
||||
"couch" : false, // CouchDB
|
||||
"devel" : false, // Development/debugging (alert, confirm, etc)
|
||||
"dojo" : false, // Dojo Toolkit
|
||||
"jquery" : false, // jQuery
|
||||
"mocha" : false, // mocha
|
||||
"mootools" : false, // MooTools
|
||||
"node" : true, // Node.js
|
||||
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
|
||||
"phantom" : false, // PhantomJS
|
||||
"prototypejs" : false, // Prototype and Scriptaculous
|
||||
"rhino" : false, // Rhino
|
||||
"worker" : false, // Web Workers
|
||||
"wsh" : false, // Windows Scripting Host
|
||||
"yui" : false, // Yahoo User Interface
|
||||
|
||||
// Custom Globals
|
||||
"globals" : {} // additional predefined global variables
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
# - 'stable'
|
||||
- '6'
|
||||
- '4'
|
||||
- '0.12'
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
|
||||
3
ISSUE_TEMPLATE.md
Normal file
3
ISSUE_TEMPLATE.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# ALL ISSUES SHOULD BE CREATED IN XO-WEB'S TRACKER!
|
||||
|
||||
https://github.com/vatesfr/xo-web/issues
|
||||
@@ -19,7 +19,7 @@ ___
|
||||
|
||||
## Installation
|
||||
|
||||
Manual install procedure is [available here](https://github.com/vatesfr/xo/blob/master/doc/installation/README.md#installation).
|
||||
Manual install procedure is [available here](https://xen-orchestra.com/docs/from_the_sources.html).
|
||||
|
||||
## Compilation
|
||||
|
||||
|
||||
59
package.json
59
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server",
|
||||
"version": "5.1.2",
|
||||
"version": "5.4.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -34,8 +34,8 @@
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@marsaud/smb2": "^0.7.1",
|
||||
"@marsaud/smb2-promise": "^0.2.0",
|
||||
"@marsaud/smb2-promise": "^0.2.1",
|
||||
"@nraynaud/struct-fu": "^1.0.1",
|
||||
"app-conf": "^0.4.0",
|
||||
"babel-runtime": "^6.5.0",
|
||||
"base64url": "^2.0.0",
|
||||
@@ -51,23 +51,25 @@
|
||||
"escape-string-regexp": "^1.0.3",
|
||||
"event-to-promise": "^0.7.0",
|
||||
"exec-promise": "^0.6.1",
|
||||
"execa": "^0.4.0",
|
||||
"execa": "^0.5.0",
|
||||
"express": "^4.13.3",
|
||||
"express-session": "^1.11.3",
|
||||
"fatfs": "^0.10.3",
|
||||
"fs-extra": "^0.30.0",
|
||||
"fs-promise": "^0.4.1",
|
||||
"get-stream": "^2.1.0",
|
||||
"hashy": "~0.4.2",
|
||||
"helmet": "^2.0.0",
|
||||
"fs-extra": "^1.0.0",
|
||||
"fs-promise": "^1.0.0",
|
||||
"get-stream": "^3.0.0",
|
||||
"golike-defer": "^0.0.0",
|
||||
"hashy": "~0.5.1",
|
||||
"helmet": "^3.0.0",
|
||||
"highland": "^2.5.1",
|
||||
"http-proxy": "^1.13.2",
|
||||
"http-server-plus": "^0.6.4",
|
||||
"human-format": "^0.6.0",
|
||||
"is-my-json-valid": "^2.12.2",
|
||||
"http-server-plus": "^0.8.0",
|
||||
"human-format": "^0.7.0",
|
||||
"is-my-json-valid": "^2.13.1",
|
||||
"is-redirect": "^1.0.0",
|
||||
"js-yaml": "^3.2.7",
|
||||
"json-rpc-peer": "^0.11.0",
|
||||
"json5": "^0.4.0",
|
||||
"json-rpc-peer": "^0.13.0",
|
||||
"json5": "^0.5.0",
|
||||
"julien-f-source-map-support": "0.0.0",
|
||||
"julien-f-unzip": "^0.2.1",
|
||||
"kindof": "^2.0.0",
|
||||
@@ -86,7 +88,7 @@
|
||||
"partial-stream": "0.0.0",
|
||||
"passport": "^0.3.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"promise-toolbox": "^0.3.2",
|
||||
"promise-toolbox": "^0.7.0",
|
||||
"proxy-agent": "^2.0.0",
|
||||
"pug": "^2.0.0-alpha6",
|
||||
"redis": "^2.0.1",
|
||||
@@ -94,18 +96,22 @@
|
||||
"semver": "^5.1.0",
|
||||
"serve-static": "^1.9.2",
|
||||
"stack-chain": "^1.3.3",
|
||||
"struct-fu": "^1.0.0",
|
||||
"tar-stream": "^1.5.2",
|
||||
"through2": "^2.0.0",
|
||||
"trace": "^2.0.1",
|
||||
"uuid": "^3.0.0",
|
||||
"ws": "^1.1.1",
|
||||
"xen-api": "^0.9.0",
|
||||
"xen-api": "^0.9.6",
|
||||
"xml2js": "~0.4.6",
|
||||
"xo-acl-resolver": "^0.2.1",
|
||||
"xo-acl-resolver": "^0.2.2",
|
||||
"xo-collection": "^0.4.0",
|
||||
"xo-remote-parser": "^0.3"
|
||||
"xo-common": "0.1.0",
|
||||
"xo-remote-parser": "^0.3",
|
||||
"xo-vmdk-to-vhd": "0.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^6.0.4",
|
||||
"babel-eslint": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.2.9",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-runtime": "^6.5.2",
|
||||
"babel-preset-es2015": "^6.5.0",
|
||||
@@ -117,20 +123,20 @@
|
||||
"gulp-babel": "^6",
|
||||
"gulp-coffee": "^2.3.1",
|
||||
"gulp-plumber": "^1.0.0",
|
||||
"gulp-sourcemaps": "^1.5.1",
|
||||
"gulp-sourcemaps": "^2.1.1",
|
||||
"gulp-watch": "^4.2.2",
|
||||
"index-modules": "0.1.0",
|
||||
"leche": "^2.1.1",
|
||||
"mocha": "^2.2.1",
|
||||
"mocha": "^3.0.2",
|
||||
"must": "^0.13.1",
|
||||
"nyc": "^7.0.0",
|
||||
"rimraf": "^2.5.2",
|
||||
"sinon": "^1.14.1",
|
||||
"standard": "^7.0.0"
|
||||
"standard": "^8.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build-indexes && gulp build --production",
|
||||
"depcheck": "dependency-check ./package.json",
|
||||
"build-indexes": "./tools/generate-index src/api src/xapi/mixins src/xo-mixins",
|
||||
"build-indexes": "index-modules src/api src/xapi/mixins src/xo-mixins",
|
||||
"dev": "npm run build-indexes && gulp build",
|
||||
"dev-test": "mocha --opts .mocha.opts --watch --reporter=min \"dist/**/*.spec.js\"",
|
||||
"lint": "standard",
|
||||
@@ -140,10 +146,11 @@
|
||||
"prerelease": "git checkout next-release && git pull --ff-only && git checkout stable && git pull --ff-only && git merge next-release",
|
||||
"release": "npm version",
|
||||
"start": "node bin/xo-server",
|
||||
"test": "nyc mocha --opts .mocha.opts \"dist/**/*.spec.js\""
|
||||
"test": "mocha --opts .mocha.opts \"dist/**/*.spec.js\""
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"lodash",
|
||||
"transform-decorators-legacy",
|
||||
"transform-runtime"
|
||||
],
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
# Example XO-Server configuration.
|
||||
# BE *VERY* CAREFUL WHEN EDITING!
|
||||
# YAML FILES ARE SUPER SUPER SENSITIVE TO MISTAKES IN WHITESPACE OR ALIGNMENT!
|
||||
# visit http://www.yamllint.com/ to validate this file as needed
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Example XO-Server configuration.
|
||||
#
|
||||
# This file is automatically looking for at the following places:
|
||||
# - `$HOME/.config/xo-server/config.yaml`
|
||||
# - `/etc/xo-server/config.yaml`
|
||||
#
|
||||
# The first entries have priority.
|
||||
|
||||
#
|
||||
# Note: paths are relative to the configuration file.
|
||||
|
||||
#=====================================================================
|
||||
@@ -117,10 +123,18 @@ http:
|
||||
|
||||
# Connection to the Redis server.
|
||||
redis:
|
||||
# Syntax: redis://[db[:password]@]hostname[:port]
|
||||
# Syntax: redis://[db[:password]@]hostname[:port][/db-number]
|
||||
#
|
||||
# Default: redis://localhost:6379
|
||||
#uri: ''
|
||||
# Default: redis://localhost:6379/0
|
||||
#uri: redis://redis.company.lan/42
|
||||
|
||||
# List of aliased commands.
|
||||
#
|
||||
# See http://redis.io/topics/security#disabling-of-specific-commands
|
||||
#renameCommands:
|
||||
# del: '3dda29ad-3015-44f9-b13b-fa570de92489'
|
||||
# srem: '3fd758c9-5610-4e9d-a058-dbf4cb6d8bf0'
|
||||
|
||||
|
||||
# Directory containing the database of XO.
|
||||
# Currently used for logs.
|
||||
|
||||
94
signin.pug
94
signin.pug
@@ -6,55 +6,45 @@ html
|
||||
meta(name = 'viewport' content = 'width=device-width, initial-scale=1.0')
|
||||
title Xen Orchestra
|
||||
meta(name = 'author' content = 'Vates SAS')
|
||||
link(rel = 'stylesheet' href = 'styles/main.css')
|
||||
link(rel = 'stylesheet' href = 'v4/styles/main.css')
|
||||
body
|
||||
.container
|
||||
.row-login
|
||||
.page-header
|
||||
img(src = 'images/logo_small.png')
|
||||
h2 Xen Orchestra
|
||||
form.form-horizontal(action = 'signin/local' method = 'post')
|
||||
fieldset
|
||||
legend.login
|
||||
h3 Sign in
|
||||
if error
|
||||
p.text-danger #{error}
|
||||
.form-group
|
||||
.col-sm-12
|
||||
.input-group
|
||||
span.input-group-addon
|
||||
i.xo-icon-user.fa-fw
|
||||
input.form-control.input-sm(
|
||||
name = 'username'
|
||||
type = 'text'
|
||||
placeholder = 'Username'
|
||||
required
|
||||
)
|
||||
.form-group
|
||||
.col-sm-12
|
||||
.input-group
|
||||
span.input-group-addon
|
||||
i.fa.fa-key.fa-fw
|
||||
input.form-control.input-sm(
|
||||
name = 'password'
|
||||
type = 'password'
|
||||
placeholder = 'Password'
|
||||
required
|
||||
)
|
||||
.form-group
|
||||
.col-sm-5
|
||||
.checkbox
|
||||
label
|
||||
input(
|
||||
name = 'remember-me'
|
||||
type = 'checkbox'
|
||||
)
|
||||
| Remember me
|
||||
.form-group
|
||||
.col-sm-12
|
||||
button.btn.btn-login.btn-block.btn-success
|
||||
i.fa.fa-sign-in
|
||||
| Sign in
|
||||
each label, id in strategies
|
||||
div: a(href = 'signin/' + id) Sign in with #{label}
|
||||
link(rel = 'stylesheet' href = 'index.css')
|
||||
body(style = 'display: flex; height: 100vh;')
|
||||
div(style = 'margin: auto; width: 20em;')
|
||||
div.mb-2(style = 'display: flex;')
|
||||
img(src = 'assets/logo.png' style = 'margin: auto;')
|
||||
h2.text-xs-center.mb-2 Xen Orchestra
|
||||
form(action = 'signin/local' method = 'post')
|
||||
fieldset
|
||||
if error
|
||||
p.text-danger #{error}
|
||||
.input-group.mb-1
|
||||
span.input-group-addon
|
||||
i.xo-icon-user.fa-fw
|
||||
input.form-control(
|
||||
name = 'username'
|
||||
type = 'text'
|
||||
placeholder = 'Username'
|
||||
required
|
||||
)
|
||||
.input-group.mb-1
|
||||
span.input-group-addon
|
||||
i.fa.fa-key.fa-fw
|
||||
input.form-control(
|
||||
name = 'password'
|
||||
type = 'password'
|
||||
placeholder = 'Password'
|
||||
required
|
||||
)
|
||||
.checkbox
|
||||
label
|
||||
input(
|
||||
name = 'remember-me'
|
||||
type = 'checkbox'
|
||||
)
|
||||
|
|
||||
| Remember me
|
||||
div
|
||||
button.btn.btn-block.btn-info
|
||||
i.fa.fa-sign-in
|
||||
| Sign in
|
||||
each label, id in strategies
|
||||
div: a(href = 'signin/' + id) Sign in with #{label}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import {JsonRpcError} from 'json-rpc-peer'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Export standard JSON-RPC errors.
|
||||
export { // eslint-disable-line no-duplicate-imports
|
||||
InvalidJson,
|
||||
InvalidParameters,
|
||||
InvalidRequest,
|
||||
JsonRpcError,
|
||||
MethodNotFound
|
||||
} from 'json-rpc-peer'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class NotImplemented extends JsonRpcError {
|
||||
constructor () {
|
||||
super('not implemented', 0)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class NoSuchObject extends JsonRpcError {
|
||||
constructor (id, type) {
|
||||
super('no such object', 1, {id, type})
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class Unauthorized extends JsonRpcError {
|
||||
constructor () {
|
||||
super('not authenticated or not enough permissions', 2)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class InvalidCredential extends JsonRpcError {
|
||||
constructor () {
|
||||
super('invalid credential', 3)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class AlreadyAuthenticated extends JsonRpcError {
|
||||
constructor () {
|
||||
super('already authenticated', 4)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class ForbiddenOperation extends JsonRpcError {
|
||||
constructor (operation, reason) {
|
||||
super(`forbidden operation: ${operation}`, 5, reason)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// To be used with a user-readable message.
|
||||
// The message can be destined to be displayed to the front-end user.
|
||||
export class GenericError extends JsonRpcError {
|
||||
constructor (message) {
|
||||
super(message, 6)
|
||||
}
|
||||
}
|
||||
0
src/api/.index-modules
Normal file
0
src/api/.index-modules
Normal file
@@ -7,8 +7,7 @@ startsWith = require 'lodash/startsWith'
|
||||
{coroutine: $coroutine} = require 'bluebird'
|
||||
{
|
||||
extractProperty,
|
||||
parseXml,
|
||||
promisify
|
||||
parseXml
|
||||
} = require '../utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
39
src/api/ip-pool.js
Normal file
39
src/api/ip-pool.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { unauthorized } from 'xo-common/api-errors'
|
||||
|
||||
export function create (props) {
|
||||
return this.createIpPool(props)
|
||||
}
|
||||
|
||||
create.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
function delete_ ({ id }) {
|
||||
return this.deleteIpPool(id)
|
||||
}
|
||||
export { delete_ as delete }
|
||||
|
||||
delete_.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function getAll (params) {
|
||||
const { user } = this
|
||||
|
||||
if (!user) {
|
||||
throw unauthorized()
|
||||
}
|
||||
|
||||
return this.getAllIpPools(user.permission === 'admin'
|
||||
? params && params.userId
|
||||
: user.id
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function set ({ id, ...props }) {
|
||||
return this.updateIpPool(id, props)
|
||||
}
|
||||
|
||||
set.permission = 'admin'
|
||||
@@ -18,7 +18,11 @@ get.params = {
|
||||
}
|
||||
|
||||
export async function create ({job}) {
|
||||
return (await this.createJob(this.session.get('user_id'), job)).id
|
||||
if (!job.userId) {
|
||||
job.userId = this.session.get('user_id')
|
||||
}
|
||||
|
||||
return (await this.createJob(job)).id
|
||||
}
|
||||
|
||||
create.permission = 'admin'
|
||||
@@ -27,6 +31,7 @@ create.params = {
|
||||
job: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: {type: 'string', optional: true},
|
||||
name: {type: 'string', optional: true},
|
||||
type: {type: 'string'},
|
||||
key: {type: 'string'},
|
||||
@@ -38,14 +43,7 @@ create.params = {
|
||||
items: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {type: 'string'},
|
||||
values: {
|
||||
type: 'array',
|
||||
items: {type: 'object'}
|
||||
}
|
||||
}
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -77,14 +75,7 @@ set.params = {
|
||||
items: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {type: 'string'},
|
||||
values: {
|
||||
type: 'array',
|
||||
items: {type: 'object'}
|
||||
}
|
||||
}
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { mapToArray } from '../utils'
|
||||
|
||||
export function getBondModes () {
|
||||
return ['balance-slb', 'active-backup', 'lacp']
|
||||
}
|
||||
|
||||
export async function create ({ pool, name, description, pif, mtu = 1500, vlan = 0 }) {
|
||||
return this.getXapi(pool).createNetwork({
|
||||
name,
|
||||
@@ -24,6 +30,80 @@ create.permission = 'admin'
|
||||
|
||||
// =================================================================
|
||||
|
||||
export async function createBonded ({ pool, name, description, pifs, mtu = 1500, mac, bondMode }) {
|
||||
return this.getXapi(pool).createBondedNetwork({
|
||||
name,
|
||||
description,
|
||||
pifIds: mapToArray(pifs, pif =>
|
||||
this.getObject(pif, 'PIF')._xapiId
|
||||
),
|
||||
mtu: +mtu,
|
||||
mac,
|
||||
bondMode
|
||||
})
|
||||
}
|
||||
|
||||
createBonded.params = {
|
||||
pool: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string', optional: true },
|
||||
pifs: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
mtu: { type: ['integer', 'string'], optional: true },
|
||||
// RegExp since schema-inspector does not provide a param check based on an enumeration
|
||||
bondMode: { type: 'string', pattern: new RegExp(`^(${getBondModes().join('|')})$`) }
|
||||
}
|
||||
|
||||
createBonded.resolve = {
|
||||
pool: ['pool', 'pool', 'administrate']
|
||||
}
|
||||
createBonded.permission = 'admin'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function set ({
|
||||
network,
|
||||
|
||||
name_description: nameDescription,
|
||||
name_label: nameLabel,
|
||||
defaultIsLocked,
|
||||
id
|
||||
}) {
|
||||
await this.getXapi(network).setNetworkProperties(network._xapiId, {
|
||||
nameDescription,
|
||||
nameLabel,
|
||||
defaultIsLocked
|
||||
})
|
||||
}
|
||||
|
||||
set.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
},
|
||||
name_label: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
},
|
||||
name_description: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
},
|
||||
defaultIsLocked: {
|
||||
type: 'boolean',
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
network: ['id', 'network', 'administrate']
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
export async function delete_ ({ network }) {
|
||||
return this.getXapi(network).deleteNetwork(network._xapiId)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
// TODO: too low level, move into host.
|
||||
|
||||
import { IPV4_CONFIG_MODES, IPV6_CONFIG_MODES } from '../xapi'
|
||||
|
||||
export function getIpv4ConfigurationModes () {
|
||||
return IPV4_CONFIG_MODES
|
||||
}
|
||||
|
||||
export function getIpv6ConfigurationModes () {
|
||||
return IPV6_CONFIG_MODES
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Delete
|
||||
|
||||
@@ -66,3 +76,18 @@ reconfigureIp.params = {
|
||||
reconfigureIp.resolve = {
|
||||
pif: ['id', 'PIF', 'administrate']
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function editPif ({ pif, vlan }) {
|
||||
await this.getXapi(pif).editPif(pif._xapiId, { vlan })
|
||||
}
|
||||
|
||||
editPif.params = {
|
||||
id: { type: 'string' },
|
||||
vlan: { type: ['integer', 'string'] }
|
||||
}
|
||||
|
||||
editPif.resolve = {
|
||||
pif: ['id', 'PIF', 'administrate']
|
||||
}
|
||||
|
||||
@@ -102,3 +102,24 @@ purgeConfiguration.params = {
|
||||
}
|
||||
|
||||
purgeConfiguration.permission = 'admin'
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
export async function test ({ id, data }) {
|
||||
await this.testPlugin(id, data)
|
||||
}
|
||||
|
||||
test.description = 'Test a plugin with its current configuration'
|
||||
|
||||
test.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
},
|
||||
data: {
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
|
||||
test.permission = 'admin'
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import {GenericError} from '../api-errors'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function set ({
|
||||
@@ -35,21 +33,21 @@ set.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function setDefaultSr ({pool, sr}) {
|
||||
await this.getXapi(pool).setDefaultSr(sr._xapiId)
|
||||
export async function setDefaultSr ({ sr }) {
|
||||
await this.hasPermissions(this.user.id, [ [ sr.$pool, 'administrate' ] ])
|
||||
|
||||
await this.getXapi(sr).setDefaultSr(sr._xapiId)
|
||||
}
|
||||
|
||||
setDefaultSr.permission = '' // signed in
|
||||
|
||||
setDefaultSr.params = {
|
||||
pool: {
|
||||
type: 'string'
|
||||
},
|
||||
sr: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultSr.resolve = {
|
||||
pool: ['pool', 'pool', 'administrate'],
|
||||
sr: ['sr', 'SR']
|
||||
}
|
||||
// -------------------------------------------------------------------
|
||||
@@ -67,6 +65,21 @@ installPatch.params = {
|
||||
}
|
||||
}
|
||||
|
||||
installPatch.resolve = {
|
||||
pool: ['pool', 'pool', 'administrate']
|
||||
}
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function installAllPatches ({ pool }) {
|
||||
await this.getXapi(pool).installAllPoolPatchesOnAllHosts()
|
||||
}
|
||||
|
||||
installPatch.params = {
|
||||
pool: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
installPatch.resolve = {
|
||||
pool: ['pool', 'pool', 'administrate']
|
||||
}
|
||||
@@ -106,12 +119,7 @@ export {uploadPatch as patch}
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function mergeInto ({ source, target, force }) {
|
||||
try {
|
||||
await this.mergeXenPools(source._xapiId, target._xapiId, force)
|
||||
} catch (e) {
|
||||
// FIXME: should we expose plain XAPI error messages?
|
||||
throw new GenericError(e.message)
|
||||
}
|
||||
await this.mergeXenPools(source._xapiId, target._xapiId, force)
|
||||
}
|
||||
|
||||
mergeInto.params = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
Unauthorized
|
||||
} from '../api-errors'
|
||||
unauthorized
|
||||
} from 'xo-common/api-errors'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -51,11 +51,12 @@ delete_.params = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function set ({ id, name, subjects, objects, limits }) {
|
||||
export function set ({ id, name, subjects, objects, ipPools, limits }) {
|
||||
return this.updateResourceSet(id, {
|
||||
limits,
|
||||
name,
|
||||
objects,
|
||||
ipPools,
|
||||
subjects
|
||||
})
|
||||
}
|
||||
@@ -84,6 +85,13 @@ set.params = {
|
||||
},
|
||||
optional: true
|
||||
},
|
||||
ipPools: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
optional: true
|
||||
},
|
||||
limits: {
|
||||
type: 'object',
|
||||
optional: true
|
||||
@@ -109,7 +117,7 @@ get.params = {
|
||||
export async function getAll () {
|
||||
const { user } = this
|
||||
if (!user) {
|
||||
throw new Unauthorized()
|
||||
throw unauthorized()
|
||||
}
|
||||
|
||||
return this.getAllResourceSets(user.id)
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import {deprecate} from 'util'
|
||||
|
||||
import {InvalidCredential, AlreadyAuthenticated} from '../api-errors'
|
||||
import { getUserPublicProperties } from '../utils'
|
||||
import {invalidCredentials} from 'xo-common/api-errors'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function signIn (credentials) {
|
||||
if (this.session.has('user_id')) {
|
||||
throw new AlreadyAuthenticated()
|
||||
}
|
||||
|
||||
const user = await this.authenticateUser(credentials)
|
||||
if (!user) {
|
||||
throw new InvalidCredential()
|
||||
throw invalidCredentials()
|
||||
}
|
||||
this.session.set('user_id', user.id)
|
||||
|
||||
return this.getUserPublicProperties(user)
|
||||
return getUserPublicProperties(user)
|
||||
}
|
||||
|
||||
signIn.description = 'sign in'
|
||||
@@ -55,7 +52,7 @@ export async function getUser () {
|
||||
|
||||
return userId === undefined
|
||||
? null
|
||||
: this.getUserPublicProperties(await this.getUser(userId))
|
||||
: getUserPublicProperties(await this.getUser(userId))
|
||||
}
|
||||
|
||||
getUser.description = 'return the currently connected user'
|
||||
|
||||
@@ -121,6 +121,7 @@ export async function createIso ({
|
||||
deviceConfig.legacy_mode = 'true'
|
||||
} else if (type === 'smb') {
|
||||
path = path.replace(/\\/g, '/')
|
||||
deviceConfig.type = 'cifs'
|
||||
deviceConfig.username = user
|
||||
deviceConfig.cifspassword = password
|
||||
}
|
||||
@@ -136,7 +137,7 @@ export async function createIso ({
|
||||
nameDescription,
|
||||
'iso', // SR type ISO
|
||||
'iso', // SR content type ISO
|
||||
true,
|
||||
type !== 'local',
|
||||
{}
|
||||
)
|
||||
|
||||
|
||||
67
src/api/system.js
Normal file
67
src/api/system.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import forEach from 'lodash/forEach'
|
||||
import getKeys from 'lodash/keys'
|
||||
import moment from 'moment-timezone'
|
||||
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
import { version as xoServerVersion } from '../../package.json'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export function getMethodsInfo () {
|
||||
const methods = {}
|
||||
|
||||
forEach(this.apiMethods, (method, name) => {
|
||||
methods[name] = {
|
||||
description: method.description,
|
||||
params: method.params || {},
|
||||
permission: method.permission
|
||||
}
|
||||
})
|
||||
|
||||
return methods
|
||||
}
|
||||
getMethodsInfo.description = 'returns the signatures of all available API methods'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const getServerTimezone = (tz => () => tz)(moment.tz.guess())
|
||||
getServerTimezone.description = 'return the timezone server'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const getServerVersion = () => xoServerVersion
|
||||
getServerVersion.description = 'return the version of xo-server'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const getVersion = () => '0.1'
|
||||
getVersion.description = 'API version (unstable)'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function listMethods () {
|
||||
return getKeys(this.apiMethods)
|
||||
}
|
||||
listMethods.description = 'returns the name of all available API methods'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function methodSignature ({method: name}) {
|
||||
const method = this.apiMethods[name]
|
||||
|
||||
if (!method) {
|
||||
throw noSuchObject()
|
||||
}
|
||||
|
||||
// Return an array for compatibility with XML-RPC.
|
||||
return [
|
||||
// XML-RPC require the name of the method.
|
||||
{
|
||||
name,
|
||||
description: method.description,
|
||||
params: method.params || {},
|
||||
permission: method.permission
|
||||
}
|
||||
]
|
||||
}
|
||||
methodSignature.description = 'returns the signature of an API method'
|
||||
@@ -36,9 +36,9 @@ hasPermission.params = {
|
||||
|
||||
export function wait ({duration, returnValue}) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(+duration, () => {
|
||||
setTimeout(() => {
|
||||
resolve(returnValue)
|
||||
})
|
||||
}, +duration)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {InvalidParameters} from '../api-errors'
|
||||
import { mapToArray } from '../utils'
|
||||
import {invalidParameters} from 'xo-common/api-errors'
|
||||
import { getUserPublicProperties, mapToArray } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function create ({email, password, permission}) {
|
||||
return (await this.createUser(email, {password, permission})).id
|
||||
return (await this.createUser({email, password, permission})).id
|
||||
}
|
||||
|
||||
create.description = 'creates a new user'
|
||||
@@ -22,7 +22,7 @@ create.params = {
|
||||
// Deletes an existing user.
|
||||
async function delete_ ({id}) {
|
||||
if (id === this.session.get('user_id')) {
|
||||
throw new InvalidParameters('a user cannot delete itself')
|
||||
throw invalidParameters('a user cannot delete itself')
|
||||
}
|
||||
|
||||
await this.deleteUser(id)
|
||||
@@ -48,7 +48,7 @@ export async function getAll () {
|
||||
const users = await this.getAllUsers()
|
||||
|
||||
// Filters out private properties.
|
||||
return mapToArray(users, this.getUserPublicProperties)
|
||||
return mapToArray(users, getUserPublicProperties)
|
||||
}
|
||||
|
||||
getAll.description = 'returns all the existing users'
|
||||
@@ -58,15 +58,21 @@ getAll.permission = 'admin'
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function set ({id, email, password, permission, preferences}) {
|
||||
if (permission && id === this.session.get('user_id')) {
|
||||
throw new InvalidParameters('a user cannot change its own permission')
|
||||
const isAdmin = this.user && this.user.permission === 'admin'
|
||||
if (isAdmin) {
|
||||
if (permission && id === this.session.get('user_id')) {
|
||||
throw invalidParameters('a user cannot change its own permission')
|
||||
}
|
||||
} else if (email || password || permission) {
|
||||
throw invalidParameters('this properties can only changed by an administrator')
|
||||
}
|
||||
|
||||
await this.updateUser(id, {email, password, permission, preferences})
|
||||
}
|
||||
|
||||
set.description = 'changes the properties of an existing user'
|
||||
|
||||
set.permission = 'admin'
|
||||
set.permission = ''
|
||||
|
||||
set.params = {
|
||||
id: { type: 'string' },
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
{coroutine: $coroutine} = require 'bluebird'
|
||||
|
||||
{format} = require 'json-rpc-peer'
|
||||
{InvalidParameters} = require '../api-errors'
|
||||
{invalidParameters} = require 'xo-common/api-errors'
|
||||
{isArray: $isArray, parseSize} = require '../utils'
|
||||
{JsonRpcError} = require '../api-errors'
|
||||
{JsonRpcError} = require 'json-rpc-peer'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
@@ -38,7 +38,7 @@ set = $coroutine (params) ->
|
||||
size = parseSize(params.size)
|
||||
|
||||
if size < vdi.size
|
||||
throw new InvalidParameters(
|
||||
throw invalidParameters(
|
||||
"cannot set new size (#{size}) below the current size (#{vdi.size})"
|
||||
)
|
||||
yield xapi.resizeVdi(ref, size)
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import {
|
||||
diffItems,
|
||||
noop,
|
||||
pCatch
|
||||
} from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// TODO: move into vm and rename to removeInterface
|
||||
async function delete_ ({vif}) {
|
||||
this.allocIpAddresses(
|
||||
vif.id,
|
||||
null,
|
||||
vif.allowedIpv4Addresses.concat(vif.allowedIpv6Addresses)
|
||||
)::pCatch(noop)
|
||||
|
||||
await this.getXapi(vif).deleteVif(vif._xapiId)
|
||||
}
|
||||
export {delete_ as delete}
|
||||
@@ -13,10 +27,11 @@ delete_.resolve = {
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// TODO: move into vm and rename to disconnectInterface
|
||||
export async function disconnect ({vif}) {
|
||||
// TODO: check if VIF is attached before
|
||||
await this.getXapi(vif).call('VIF.unplug_force', vif._xapiRef)
|
||||
await this.getXapi(vif).disconnectVif(vif._xapiId)
|
||||
}
|
||||
|
||||
disconnect.params = {
|
||||
@@ -31,7 +46,7 @@ disconnect.resolve = {
|
||||
// TODO: move into vm and rename to connectInterface
|
||||
export async function connect ({vif}) {
|
||||
// TODO: check if VIF is attached before
|
||||
await this.getXapi(vif).call('VIF.plug', vif._xapiRef)
|
||||
await this.getXapi(vif).connectVif(vif._xapiId)
|
||||
}
|
||||
|
||||
connect.params = {
|
||||
@@ -44,28 +59,83 @@ connect.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const set = ({ vif, allowedIpv4Addresses, allowedIpv6Addresses }) => (
|
||||
this.getXapi(vif._xapiId).editVif({
|
||||
export async function set ({
|
||||
vif,
|
||||
network,
|
||||
mac,
|
||||
allowedIpv4Addresses,
|
||||
allowedIpv6Addresses,
|
||||
attached
|
||||
}) {
|
||||
const oldIpAddresses = vif.allowedIpv4Addresses.concat(vif.allowedIpv6Addresses)
|
||||
const newIpAddresses = []
|
||||
{
|
||||
const { push } = newIpAddresses
|
||||
push.apply(newIpAddresses, allowedIpv4Addresses || vif.allowedIpv4Addresses)
|
||||
push.apply(newIpAddresses, allowedIpv6Addresses || vif.allowedIpv6Addresses)
|
||||
}
|
||||
|
||||
if (network || mac) {
|
||||
const xapi = this.getXapi(vif)
|
||||
|
||||
const vm = xapi.getObject(vif.$VM)
|
||||
mac == null && (mac = vif.MAC)
|
||||
network = xapi.getObject(network && network.id || vif.$network)
|
||||
attached == null && (attached = vif.attached)
|
||||
|
||||
await this.allocIpAddresses(vif.id, null, oldIpAddresses)
|
||||
await xapi.deleteVif(vif._xapiId)
|
||||
|
||||
// create new VIF with new parameters
|
||||
const newVif = await xapi.createVif(vm.$id, network.$id, {
|
||||
mac,
|
||||
currently_attached: attached,
|
||||
ipv4_allowed: newIpAddresses
|
||||
})
|
||||
|
||||
await this.allocIpAddresses(newVif.$id, newIpAddresses)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const [ addAddresses, removeAddresses ] = diffItems(
|
||||
newIpAddresses,
|
||||
oldIpAddresses
|
||||
)
|
||||
await this.allocIpAddresses(
|
||||
vif.id,
|
||||
addAddresses,
|
||||
removeAddresses
|
||||
)
|
||||
|
||||
return this.getXapi(vif).editVif(vif._xapiId, {
|
||||
ipv4Allowed: allowedIpv4Addresses,
|
||||
ipv6Allowed: allowedIpv6Addresses
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
set.params = {
|
||||
id: { type: 'string' },
|
||||
network: { type: 'string', optional: true },
|
||||
mac: { type: 'string', optional: true },
|
||||
allowedIpv4Addresses: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
optional: true
|
||||
},
|
||||
allowedIpv6Addresses: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
optional: true
|
||||
},
|
||||
attached: { type: 'boolean', optional: true }
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
vif: ['id', 'VIF', 'operate']
|
||||
vif: ['id', 'VIF', 'operate'],
|
||||
network: ['network', 'network', 'operate']
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ $debug = (require 'debug') 'xo:api:vm'
|
||||
$filter = require 'lodash/filter'
|
||||
$findIndex = require 'lodash/findIndex'
|
||||
$findWhere = require 'lodash/find'
|
||||
concat = require 'lodash/concat'
|
||||
endsWith = require 'lodash/endsWith'
|
||||
escapeStringRegexp = require 'escape-string-regexp'
|
||||
eventToPromise = require 'event-to-promise'
|
||||
@@ -12,9 +13,10 @@ startsWith = require 'lodash/startsWith'
|
||||
{format} = require 'json-rpc-peer'
|
||||
|
||||
{
|
||||
GenericError,
|
||||
Unauthorized
|
||||
} = require('../api-errors')
|
||||
forbiddenOperation,
|
||||
invalidParameters,
|
||||
unauthorized
|
||||
} = require('xo-common/api-errors')
|
||||
{
|
||||
forEach,
|
||||
formatXml: $js2xml,
|
||||
@@ -47,7 +49,7 @@ checkPermissionOnSrs = (vm, permission = 'operate') -> (
|
||||
)
|
||||
|
||||
return @hasPermissions(@session.get('user_id'), permissions).then((success) => (
|
||||
throw new Unauthorized() unless success
|
||||
throw unauthorized() unless success
|
||||
))
|
||||
)
|
||||
|
||||
@@ -60,6 +62,11 @@ extract = (obj, prop) ->
|
||||
|
||||
# TODO: Implement ACLs
|
||||
create = $coroutine (params) ->
|
||||
{ user } = this
|
||||
resourceSet = extract(params, 'resourceSet')
|
||||
if not resourceSet and user.permission isnt 'admin'
|
||||
throw unauthorized()
|
||||
|
||||
template = extract(params, 'template')
|
||||
params.template = template._xapiId
|
||||
|
||||
@@ -80,7 +87,7 @@ create = $coroutine (params) ->
|
||||
vbd.type is 'Disk' and
|
||||
(vdi = vbd.$VDI)
|
||||
)
|
||||
vdiSizesByDevice[vbd.device] = +vdi.virtual_size
|
||||
vdiSizesByDevice[vbd.userdevice] = +vdi.virtual_size
|
||||
|
||||
return
|
||||
)
|
||||
@@ -94,7 +101,7 @@ create = $coroutine (params) ->
|
||||
limits.disk += size
|
||||
|
||||
return $assign({}, vdi, {
|
||||
device: vdi.device ? vdi.position,
|
||||
device: vdi.userdevice ? vdi.device ? vdi.position,
|
||||
size,
|
||||
SR: sr._xapiId,
|
||||
type: vdi.type
|
||||
@@ -102,10 +109,10 @@ create = $coroutine (params) ->
|
||||
)
|
||||
|
||||
existingVdis = extract(params, 'existingDisks')
|
||||
params.existingVdis = existingVdis and map(existingVdis, (vdi, device) =>
|
||||
params.existingVdis = existingVdis and map(existingVdis, (vdi, userdevice) =>
|
||||
if vdi.size?
|
||||
size = parseSize(vdi.size)
|
||||
vdiSizesByDevice[device] = size
|
||||
vdiSizesByDevice[userdevice] = size
|
||||
|
||||
if vdi.$SR
|
||||
sr = @getObject(vdi.$SR)
|
||||
@@ -128,23 +135,24 @@ create = $coroutine (params) ->
|
||||
return {
|
||||
mac: vif.mac
|
||||
network: network._xapiId
|
||||
ipv4_allowed: vif.allowedIpv4Addresses
|
||||
ipv6_allowed: vif.allowedIpv6Addresses
|
||||
}
|
||||
)
|
||||
|
||||
installation = extract(params, 'installation')
|
||||
params.installRepository = installation && installation.repository
|
||||
|
||||
resourceSet = extract(params, 'resourceSet')
|
||||
checkLimits = null
|
||||
|
||||
xapiVm = yield xapi.createVm(template._xapiId, params)
|
||||
vm = xapi.xo.addObject(xapiVm)
|
||||
|
||||
{ user } = this
|
||||
if resourceSet
|
||||
yield this.checkResourceSetConstraints(resourceSet, user.id, objectIds)
|
||||
yield this.allocateLimitsInResourceSet(limits, resourceSet)
|
||||
else unless user.permission is 'admin'
|
||||
throw new Unauthorized()
|
||||
checkLimits = $coroutine (limits2) =>
|
||||
yield this.allocateLimitsInResourceSet(limits, resourceSet)
|
||||
yield this.allocateLimitsInResourceSet(limits2, resourceSet)
|
||||
|
||||
xapiVm = yield xapi.createVm(template._xapiId, params, checkLimits)
|
||||
vm = xapi.xo.addObject(xapiVm)
|
||||
|
||||
if resourceSet
|
||||
yield Promise.all([
|
||||
@@ -152,9 +160,23 @@ create = $coroutine (params) ->
|
||||
xapi.xo.setData(xapiVm.$id, 'resourceSet', resourceSet)
|
||||
])
|
||||
|
||||
for vifId in vm.VIFs
|
||||
vif = @getObject(vifId, 'VIF')
|
||||
yield this.allocIpAddresses(vifId, concat(vif.allowedIpv4Addresses, vif.allowedIpv6Addresses)).catch(() =>
|
||||
xapi.deleteVif(vif._xapiId)
|
||||
)
|
||||
|
||||
if params.bootAfterCreate
|
||||
pCatch.call(xapi.startVm(vm._xapiId), noop)
|
||||
|
||||
return vm.id
|
||||
|
||||
create.params = {
|
||||
bootAfterCreate: {
|
||||
type: 'boolean'
|
||||
optional: true
|
||||
}
|
||||
|
||||
cloudConfig: {
|
||||
type: 'string'
|
||||
optional: true
|
||||
@@ -211,6 +233,18 @@ create.params = {
|
||||
optional: true # Auto-generated per default.
|
||||
type: 'string'
|
||||
}
|
||||
|
||||
allowedIpv4Addresses: {
|
||||
optional: true
|
||||
type: 'array'
|
||||
items: { type: 'string' }
|
||||
}
|
||||
|
||||
allowedIpv6Addresses: {
|
||||
optional: true
|
||||
type: 'array'
|
||||
items: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,7 +287,7 @@ create.params = {
|
||||
}
|
||||
|
||||
create.resolve = {
|
||||
template: ['template', 'VM-template', 'administrate'],
|
||||
template: ['template', 'VM-template', ''],
|
||||
}
|
||||
|
||||
exports.create = create
|
||||
@@ -368,7 +402,7 @@ migrate = $coroutine ({
|
||||
])
|
||||
|
||||
unless yield @hasPermissions(@session.get('user_id'), permissions)
|
||||
throw new Unauthorized()
|
||||
throw unauthorized()
|
||||
|
||||
yield @getXapi(vm).migrateVm(vm._xapiId, @getXapi(host), host._xapiId, {
|
||||
migrationNetworkId: migrationNetwork?._xapiId
|
||||
@@ -416,7 +450,7 @@ set = (params) ->
|
||||
return @allocateLimitsInResourceSet(limits, resourceSet)
|
||||
|
||||
if (limits.cpuWeight && this.user.permission != 'admin')
|
||||
throw new Unauthorized()
|
||||
throw unauthorized()
|
||||
)
|
||||
|
||||
set.params = {
|
||||
@@ -461,7 +495,7 @@ set.params = {
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
VM: ['id', ['VM', 'VM-snapshot'], 'administrate']
|
||||
VM: ['id', ['VM', 'VM-snapshot', 'VM-template'], 'administrate']
|
||||
}
|
||||
|
||||
exports.set = set
|
||||
@@ -549,7 +583,7 @@ copy.params = {
|
||||
}
|
||||
|
||||
copy.resolve = {
|
||||
vm: [ 'vm', 'VM', 'administrate' ]
|
||||
vm: [ 'vm', ['VM', 'VM-snapshot'], 'administrate' ]
|
||||
sr: [ 'sr', 'SR', 'operate' ]
|
||||
}
|
||||
|
||||
@@ -562,7 +596,7 @@ convertToTemplate = $coroutine ({vm}) ->
|
||||
unless yield @hasPermissions(@session.get('user_id'), [
|
||||
[ vm.$pool, 'administrate' ]
|
||||
])
|
||||
throw new Unauthorized()
|
||||
throw unauthorized()
|
||||
|
||||
yield @getXapi(vm).call 'VM.set_is_a_template', vm._xapiRef, true
|
||||
|
||||
@@ -757,10 +791,10 @@ exports.rollingBackup = rollingBackup
|
||||
rollingDrCopy = ({vm, pool, sr, tag, depth}) ->
|
||||
unless sr
|
||||
unless pool
|
||||
throw new InvalidParameters('either pool or sr param should be specified')
|
||||
throw invalidParameters('either pool or sr param should be specified')
|
||||
|
||||
if vm.$pool is pool.id
|
||||
throw new GenericError('Disaster Recovery attempts to copy on the same pool')
|
||||
throw forbiddenOperation('Disaster Recovery attempts to copy on the same pool')
|
||||
|
||||
sr = @getObject(pool.default_SR, 'SR')
|
||||
|
||||
@@ -818,8 +852,7 @@ stop = $coroutine ({vm, force}) ->
|
||||
yield xapi.call 'VM.clean_shutdown', vm._xapiRef
|
||||
catch error
|
||||
if error.code is 'VM_MISSING_PV_DRIVERS' or error.code is 'VM_LACKS_FEATURE_SHUTDOWN'
|
||||
# TODO: Improve reporting: this message is unclear.
|
||||
@throw 'INVALID_PARAMS'
|
||||
throw invalidParameters('clean shutdown requires PV drivers')
|
||||
else
|
||||
throw error
|
||||
|
||||
@@ -875,15 +908,12 @@ exports.resume = resume
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# revert a snapshot to its parent VM
|
||||
revert = $coroutine ({snapshot}) ->
|
||||
# Attempts a revert from this snapshot to its parent VM
|
||||
yield @getXapi(snapshot).call 'VM.revert', snapshot._xapiRef
|
||||
|
||||
return true
|
||||
revert = ({snapshot, snapshotBefore}) ->
|
||||
return @getXapi(snapshot).revertVm(snapshot._xapiId, snapshotBefore)
|
||||
|
||||
revert.params = {
|
||||
id: { type: 'string' }
|
||||
id: { type: 'string' },
|
||||
snapshotBefore: { type: 'boolean', optional: true }
|
||||
}
|
||||
|
||||
revert.resolve = {
|
||||
@@ -943,30 +973,30 @@ exports.export = export_;
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
handleVmImport = $coroutine (req, res, { xapi, srId }) ->
|
||||
handleVmImport = $coroutine (req, res, { data, srId, type, xapi }) ->
|
||||
# Timeout seems to be broken in Node 4.
|
||||
# See https://github.com/nodejs/node/issues/3319
|
||||
req.setTimeout(43200000) # 12 hours
|
||||
|
||||
try
|
||||
vm = yield xapi.importVm(req, { srId })
|
||||
vm = yield xapi.importVm(req, { data, srId, type })
|
||||
res.end(format.response(0, vm.$id))
|
||||
catch e
|
||||
res.writeHead(500)
|
||||
res.end(format.error(0, new GenericError(e.message)))
|
||||
res.end(format.error(0, new Error(e.message)))
|
||||
|
||||
return
|
||||
|
||||
# TODO: "sr_id" can be passed in URL to target a specific SR
|
||||
import_ = $coroutine ({host, sr}) ->
|
||||
import_ = $coroutine ({ data, host, sr, type }) ->
|
||||
if not sr
|
||||
if not host
|
||||
throw new InvalidParameters('you must provide either host or SR')
|
||||
throw invalidParameters('you must provide either host or SR')
|
||||
|
||||
xapi = @getXapi(host)
|
||||
sr = xapi.pool.$default_SR
|
||||
if not sr
|
||||
throw new InvalidParameters('there is not default SR in this pool')
|
||||
throw invalidParameters('there is not default SR in this pool')
|
||||
|
||||
# FIXME: must have administrate permission on default SR.
|
||||
else
|
||||
@@ -974,13 +1004,45 @@ import_ = $coroutine ({host, sr}) ->
|
||||
|
||||
return {
|
||||
$sendTo: yield @registerHttpRequest(handleVmImport, {
|
||||
data,
|
||||
srId: sr._xapiId,
|
||||
type,
|
||||
xapi
|
||||
})
|
||||
}
|
||||
|
||||
import_.params = {
|
||||
data: {
|
||||
type: 'object',
|
||||
optional: true,
|
||||
properties: {
|
||||
descriptionLabel: { type: 'string' },
|
||||
disks: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
capacity: { type: 'integer' },
|
||||
descriptionLabel: { type: 'string' },
|
||||
nameLabel: { type: 'string' },
|
||||
path: { type: 'string' },
|
||||
position: { type: 'integer' }
|
||||
}
|
||||
},
|
||||
optional: true
|
||||
},
|
||||
memory: { type: 'integer' },
|
||||
nameLabel: { type: 'string' },
|
||||
nCpus: { type: 'integer' },
|
||||
networks: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
optional: true
|
||||
},
|
||||
}
|
||||
},
|
||||
host: { type: 'string', optional: true },
|
||||
type: { type: 'string', optional: true },
|
||||
sr: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
@@ -1022,21 +1084,47 @@ exports.attachDisk = attachDisk
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# TODO: implement resource sets
|
||||
createInterface = $coroutine ({vm, network, position, mtu, mac}) ->
|
||||
createInterface = $coroutine ({
|
||||
vm,
|
||||
network,
|
||||
position,
|
||||
mac,
|
||||
allowedIpv4Addresses,
|
||||
allowedIpv6Addresses
|
||||
}) ->
|
||||
vif = yield @getXapi(vm).createVif(vm._xapiId, network._xapiId, {
|
||||
mac,
|
||||
mtu,
|
||||
position
|
||||
position,
|
||||
ipv4_allowed: allowedIpv4Addresses,
|
||||
ipv6_allowed: allowedIpv6Addresses
|
||||
})
|
||||
|
||||
{ push } = ipAddresses = []
|
||||
push.apply(ipAddresses, allowedIpv4Addresses) if allowedIpv4Addresses
|
||||
push.apply(ipAddresses, allowedIpv6Addresses) if allowedIpv6Addresses
|
||||
pCatch.call(@allocIpAddresses(vif.$id, allo), noop) if ipAddresses.length
|
||||
|
||||
return vif.$id
|
||||
|
||||
createInterface.params = {
|
||||
vm: { type: 'string' }
|
||||
network: { type: 'string' }
|
||||
position: { type: 'string', optional: true }
|
||||
mtu: { type: 'string', optional: true }
|
||||
position: { type: ['integer', 'string'], optional: true }
|
||||
mac: { type: 'string', optional: true }
|
||||
allowedIpv4Addresses: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
optional: true
|
||||
},
|
||||
allowedIpv6Addresses: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
|
||||
createInterface.resolve = {
|
||||
@@ -1115,10 +1203,7 @@ setBootOrder = $coroutine ({vm, order}) ->
|
||||
yield xapi.call 'VM.set_HVM_boot_params', vm._xapiRef, order
|
||||
return true
|
||||
|
||||
@throw(
|
||||
'INVALID_PARAMS'
|
||||
'You can only set the boot order on a HVM guest'
|
||||
)
|
||||
throw invalidParameters('You can only set the boot order on a HVM guest')
|
||||
|
||||
setBootOrder.params = {
|
||||
vm: { type: 'string' },
|
||||
|
||||
@@ -1,5 +1,49 @@
|
||||
import { streamToBuffer } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export function clean () {
|
||||
return this.clean()
|
||||
}
|
||||
|
||||
clean.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function exportConfig () {
|
||||
return {
|
||||
$getFrom: await this.registerHttpRequest((req, res) => {
|
||||
res.writeHead(200, 'OK', {
|
||||
'content-disposition': 'attachment'
|
||||
})
|
||||
|
||||
return this.exportConfig()
|
||||
},
|
||||
undefined,
|
||||
{ suffix: '/config.json' })
|
||||
}
|
||||
}
|
||||
|
||||
exportConfig.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function getAllObjects () {
|
||||
return this.getObjects()
|
||||
}
|
||||
|
||||
getAllObjects.permission = ''
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function importConfig () {
|
||||
return {
|
||||
$sendTo: await this.registerHttpRequest(async (req, res) => {
|
||||
await this.importConfig(JSON.parse(await streamToBuffer(req)))
|
||||
|
||||
res.end('config successfully imported')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
importConfig.permission = 'admin'
|
||||
|
||||
@@ -3,6 +3,7 @@ import difference from 'lodash/difference'
|
||||
import filter from 'lodash/filter'
|
||||
import getKey from 'lodash/keys'
|
||||
import {createClient as createRedisClient} from 'redis'
|
||||
import {v4 as generateUuid} from 'uuid'
|
||||
|
||||
import {
|
||||
forEach,
|
||||
@@ -35,13 +36,13 @@ export default class Redis extends Collection {
|
||||
connection,
|
||||
indexes = [],
|
||||
prefix,
|
||||
uri = 'tcp://localhost:6379'
|
||||
uri
|
||||
}) {
|
||||
super()
|
||||
|
||||
this.indexes = indexes
|
||||
this.prefix = prefix
|
||||
this.redis = promisifyAll.call(connection || createRedisClient(uri))
|
||||
this.redis = promisifyAll(connection || createRedisClient(uri))
|
||||
}
|
||||
|
||||
_extract (ids) {
|
||||
@@ -68,12 +69,12 @@ export default class Redis extends Collection {
|
||||
// TODO: remove “replace” which is a temporary measure, implement
|
||||
// “set()” instead.
|
||||
|
||||
const {indexes, prefix, redis, idPrefix = ''} = this
|
||||
const {indexes, prefix, redis} = this
|
||||
|
||||
return Promise.all(mapToArray(models, async model => {
|
||||
// Generate a new identifier if necessary.
|
||||
if (model.id === undefined) {
|
||||
model.id = idPrefix + String(await redis.incr(prefix + '_id'))
|
||||
model.id = generateUuid()
|
||||
}
|
||||
|
||||
const success = await redis.sadd(prefix + '_ids', model.id)
|
||||
@@ -149,6 +150,10 @@ export default class Redis extends Collection {
|
||||
}
|
||||
|
||||
_remove (ids) {
|
||||
if (isEmpty(ids)) {
|
||||
return
|
||||
}
|
||||
|
||||
const {prefix, redis} = this
|
||||
|
||||
// TODO: handle indexes.
|
||||
|
||||
@@ -2,10 +2,7 @@ import bind from 'lodash/bind'
|
||||
|
||||
import {
|
||||
isArray,
|
||||
isPromise,
|
||||
isFunction,
|
||||
noop,
|
||||
pFinally
|
||||
isFunction
|
||||
} from './utils'
|
||||
|
||||
// ===================================================================
|
||||
@@ -98,117 +95,6 @@ export const debounce = duration => (target, name, descriptor) => {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const _push = Array.prototype.push
|
||||
|
||||
export const deferrable = (target, name, descriptor) => {
|
||||
let fn
|
||||
function newFn () {
|
||||
const deferreds = []
|
||||
const defer = fn => {
|
||||
deferreds.push(fn)
|
||||
}
|
||||
defer.clear = () => {
|
||||
deferreds.length = 0
|
||||
}
|
||||
|
||||
const args = [ defer ]
|
||||
_push.apply(args, arguments)
|
||||
|
||||
let executeDeferreds = () => {
|
||||
let i = deferreds.length
|
||||
while (i) {
|
||||
deferreds[--i]()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = fn.apply(this, args)
|
||||
|
||||
if (isPromise(result)) {
|
||||
result::pFinally(executeDeferreds)
|
||||
|
||||
// Do not execute the deferreds in the finally block.
|
||||
executeDeferreds = noop
|
||||
}
|
||||
|
||||
return result
|
||||
} finally {
|
||||
executeDeferreds()
|
||||
}
|
||||
}
|
||||
|
||||
if (descriptor) {
|
||||
fn = descriptor.value
|
||||
descriptor.value = newFn
|
||||
|
||||
return descriptor
|
||||
}
|
||||
|
||||
fn = target
|
||||
return newFn
|
||||
}
|
||||
|
||||
// Deferred functions are only executed on failures.
|
||||
//
|
||||
// i.e.: defer.clear() is automatically called in case of success.
|
||||
deferrable.onFailure = (target, name, descriptor) => {
|
||||
let fn
|
||||
function newFn (defer) {
|
||||
const result = fn.apply(this, arguments)
|
||||
|
||||
return isPromise(result)
|
||||
? result.then(result => {
|
||||
defer.clear()
|
||||
return result
|
||||
})
|
||||
: (defer.clear(), result)
|
||||
}
|
||||
|
||||
if (descriptor) {
|
||||
fn = descriptor.value
|
||||
descriptor.value = newFn
|
||||
} else {
|
||||
fn = target
|
||||
target = newFn
|
||||
}
|
||||
|
||||
return deferrable(target, name, descriptor)
|
||||
}
|
||||
|
||||
// Deferred functions are only executed on success.
|
||||
//
|
||||
// i.e.: defer.clear() is automatically called in case of failure.
|
||||
deferrable.onSuccess = (target, name, descriptor) => {
|
||||
let fn
|
||||
function newFn (defer) {
|
||||
try {
|
||||
const result = fn.apply(this, arguments)
|
||||
|
||||
return isPromise(result)
|
||||
? result.then(null, error => {
|
||||
defer.clear()
|
||||
throw error
|
||||
})
|
||||
: result
|
||||
} catch (error) {
|
||||
defer.clear()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
if (descriptor) {
|
||||
fn = descriptor.value
|
||||
descriptor.value = newFn
|
||||
} else {
|
||||
fn = target
|
||||
target = newFn
|
||||
}
|
||||
|
||||
return deferrable(target, name, descriptor)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const _ownKeys = (
|
||||
typeof Reflect !== 'undefined' && Reflect.ownKeys ||
|
||||
(({
|
||||
|
||||
@@ -4,7 +4,7 @@ import expect from 'must'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
import {autobind, debounce, deferrable} from './decorators'
|
||||
import {autobind, debounce} from './decorators'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -76,98 +76,3 @@ describe('debounce()', () => {
|
||||
}, 2e1)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('deferrable()', () => {
|
||||
it('works with normal termination', () => {
|
||||
let i = 0
|
||||
const fn = deferrable(defer => {
|
||||
i += 2
|
||||
defer(() => { i -= 2 })
|
||||
|
||||
i *= 2
|
||||
defer(() => { i /= 2 })
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
expect(fn()).to.equal(4)
|
||||
expect(i).to.equal(0)
|
||||
})
|
||||
|
||||
it('defer.clear() removes previous deferreds', () => {
|
||||
let i = 0
|
||||
const fn = deferrable(defer => {
|
||||
i += 2
|
||||
defer(() => { i -= 2 })
|
||||
|
||||
defer.clear()
|
||||
|
||||
i *= 2
|
||||
defer(() => { i /= 2 })
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
expect(fn()).to.equal(4)
|
||||
expect(i).to.equal(2)
|
||||
})
|
||||
|
||||
it('works with exception', () => {
|
||||
let i = 0
|
||||
const fn = deferrable(defer => {
|
||||
i += 2
|
||||
defer(() => { i -= 2 })
|
||||
|
||||
i *= 2
|
||||
defer(() => { i /= 2 })
|
||||
|
||||
throw i
|
||||
})
|
||||
|
||||
expect(() => fn()).to.throw(4)
|
||||
expect(i).to.equal(0)
|
||||
})
|
||||
|
||||
it('works with promise resolution', async () => {
|
||||
let i = 0
|
||||
const fn = deferrable(async defer => {
|
||||
i += 2
|
||||
defer(() => { i -= 2 })
|
||||
|
||||
i *= 2
|
||||
defer(() => { i /= 2 })
|
||||
|
||||
// Wait a turn of the events loop.
|
||||
await Promise.resolve()
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
await expect(fn()).to.eventually.equal(4)
|
||||
expect(i).to.equal(0)
|
||||
})
|
||||
|
||||
it('works with promise rejection', async () => {
|
||||
let i = 0
|
||||
const fn = deferrable(async defer => {
|
||||
// Wait a turn of the events loop.
|
||||
await Promise.resolve()
|
||||
|
||||
i += 2
|
||||
defer(() => { i -= 2 })
|
||||
|
||||
i *= 2
|
||||
defer(() => { i /= 2 })
|
||||
|
||||
// Wait a turn of the events loop.
|
||||
await Promise.resolve()
|
||||
|
||||
throw i
|
||||
})
|
||||
|
||||
await expect(fn()).to.reject.to.equal(4)
|
||||
expect(i).to.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
import assign from 'lodash/assign'
|
||||
import startsWith from 'lodash/startsWith'
|
||||
import { parse as parseUrl } from 'url'
|
||||
import isRedirect from 'is-redirect'
|
||||
import { assign, isString, startsWith } from 'lodash'
|
||||
import { request as httpRequest } from 'http'
|
||||
import { request as httpsRequest } from 'https'
|
||||
import { stringify as formatQueryString } from 'querystring'
|
||||
|
||||
import {
|
||||
isString,
|
||||
streamToBuffer
|
||||
} from './utils'
|
||||
format as formatUrl,
|
||||
parse as parseUrl,
|
||||
resolve as resolveUrl
|
||||
} from 'url'
|
||||
|
||||
import { streamToBuffer } from './utils'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export default (...args) => {
|
||||
const raw = opts => {
|
||||
let req
|
||||
|
||||
const pResponse = new Promise((resolve, reject) => {
|
||||
const opts = {}
|
||||
for (let i = 0, length = args.length; i < length; ++i) {
|
||||
const arg = args[i]
|
||||
assign(opts, isString(arg) ? parseUrl(arg) : arg)
|
||||
}
|
||||
|
||||
const {
|
||||
body,
|
||||
headers: { ...headers } = {},
|
||||
@@ -62,11 +57,16 @@ export default (...args) => {
|
||||
}
|
||||
}
|
||||
|
||||
req = (
|
||||
protocol && startsWith(protocol.toLowerCase(), 'https')
|
||||
? httpsRequest
|
||||
: httpRequest
|
||||
)({
|
||||
const secure = protocol && startsWith(protocol.toLowerCase(), 'https')
|
||||
let requestFn
|
||||
if (secure) {
|
||||
requestFn = httpsRequest
|
||||
} else {
|
||||
requestFn = httpRequest
|
||||
delete rest.rejectUnauthorized
|
||||
}
|
||||
|
||||
req = requestFn({
|
||||
...rest,
|
||||
headers
|
||||
})
|
||||
@@ -98,6 +98,11 @@ export default (...args) => {
|
||||
}
|
||||
|
||||
const code = response.statusCode
|
||||
const { location } = response.headers
|
||||
if (isRedirect(code) && location) {
|
||||
assign(opts, parseUrl(resolveUrl(formatUrl(opts), location)))
|
||||
return raw(opts)
|
||||
}
|
||||
if (code < 200 || code >= 300) {
|
||||
const error = new Error(response.statusMessage)
|
||||
error.code = code
|
||||
@@ -112,13 +117,27 @@ export default (...args) => {
|
||||
|
||||
return response
|
||||
})
|
||||
|
||||
pResponse.cancel = () => {
|
||||
req.emit('error', new Error('HTTP request canceled!'))
|
||||
req.abort()
|
||||
}
|
||||
pResponse.readAll = () => pResponse.then(response => response.readAll())
|
||||
pResponse.request = req
|
||||
|
||||
return pResponse
|
||||
}
|
||||
|
||||
const httpRequestPlus = (...args) => {
|
||||
const opts = {}
|
||||
for (let i = 0, length = args.length; i < length; ++i) {
|
||||
const arg = args[i]
|
||||
assign(opts, isString(arg) ? parseUrl(arg) : arg)
|
||||
}
|
||||
|
||||
const pResponse = raw(opts)
|
||||
|
||||
pResponse.cancel = () => {
|
||||
const { request } = pResponse
|
||||
request.emit('error', new Error('HTTP request canceled!'))
|
||||
request.abort()
|
||||
}
|
||||
pResponse.readAll = () => pResponse.then(response => response.readAll())
|
||||
|
||||
return pResponse
|
||||
}
|
||||
export { httpRequestPlus as default }
|
||||
|
||||
76
src/index.js
76
src/index.js
@@ -9,7 +9,6 @@ import eventToPromise from 'event-to-promise'
|
||||
import has from 'lodash/has'
|
||||
import helmet from 'helmet'
|
||||
import includes from 'lodash/includes'
|
||||
import pick from 'lodash/pick'
|
||||
import proxyConsole from './proxy-console'
|
||||
import serveStatic from 'serve-static'
|
||||
import startsWith from 'lodash/startsWith'
|
||||
@@ -18,21 +17,13 @@ import { compile as compilePug } from 'pug'
|
||||
import { createServer as createProxyServer } from 'http-proxy'
|
||||
import { join as joinPath } from 'path'
|
||||
|
||||
import {
|
||||
AlreadyAuthenticated,
|
||||
InvalidCredential,
|
||||
InvalidParameters,
|
||||
NoSuchObject,
|
||||
NotImplemented
|
||||
} from './api-errors'
|
||||
import JsonRpcPeer from 'json-rpc-peer'
|
||||
import { invalidCredentials } from 'xo-common/api-errors'
|
||||
import {
|
||||
readFile,
|
||||
readdir
|
||||
} from 'fs-promise'
|
||||
|
||||
import * as apiMethods from './api/index'
|
||||
import Api from './api'
|
||||
import WebServer from 'http-server-plus'
|
||||
import Xo from './xo'
|
||||
import {
|
||||
@@ -188,7 +179,7 @@ async function setUpPassport (express, xo) {
|
||||
next()
|
||||
} else if (req.cookies.token) {
|
||||
next()
|
||||
} else if (/favicon|fontawesome|images|styles/.test(url)) {
|
||||
} else if (/favicon|fontawesome|images|styles|\.(?:css|jpg|png)$/.test(url)) {
|
||||
next()
|
||||
} else {
|
||||
req.flash('return-url', url)
|
||||
@@ -225,7 +216,8 @@ async function registerPlugin (pluginPath, pluginName) {
|
||||
const {
|
||||
default: factory = plugin,
|
||||
configurationSchema,
|
||||
configurationPresets
|
||||
configurationPresets,
|
||||
testSchema
|
||||
} = plugin
|
||||
|
||||
// The default export can be either a factory or directly a plugin
|
||||
@@ -239,6 +231,7 @@ async function registerPlugin (pluginPath, pluginName) {
|
||||
instance,
|
||||
configurationSchema,
|
||||
configurationPresets,
|
||||
testSchema,
|
||||
version
|
||||
)
|
||||
}
|
||||
@@ -407,27 +400,6 @@ const setUpStaticFiles = (express, opts) => {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const errorClasses = {
|
||||
ALREADY_AUTHENTICATED: AlreadyAuthenticated,
|
||||
INVALID_CREDENTIAL: InvalidCredential,
|
||||
INVALID_PARAMS: InvalidParameters,
|
||||
NO_SUCH_OBJECT: NoSuchObject,
|
||||
NOT_IMPLEMENTED: NotImplemented
|
||||
}
|
||||
|
||||
const apiHelpers = {
|
||||
getUserPublicProperties (user) {
|
||||
// Handles both properties and wrapped models.
|
||||
const properties = user.properties || user
|
||||
|
||||
return pick(properties, 'id', 'email', 'groups', 'permission', 'preferences', 'provider')
|
||||
},
|
||||
|
||||
throw (errorId, data) {
|
||||
throw new (errorClasses[errorId])(data)
|
||||
}
|
||||
}
|
||||
|
||||
const setUpApi = (webServer, xo, verboseLogsOnErrors) => {
|
||||
const webSocketServer = new WebSocket.Server({
|
||||
server: webServer,
|
||||
@@ -435,18 +407,6 @@ const setUpApi = (webServer, xo, verboseLogsOnErrors) => {
|
||||
})
|
||||
xo.on('stop', () => pFromCallback(cb => webSocketServer.close(cb)))
|
||||
|
||||
// FIXME: it can cause issues if there any property assignments in
|
||||
// XO methods called from the API.
|
||||
const context = { __proto__: xo, ...apiHelpers }
|
||||
|
||||
const api = new Api({
|
||||
context,
|
||||
verboseLogsOnErrors
|
||||
})
|
||||
xo.defineProperty('api', api)
|
||||
|
||||
api.addMethods(apiMethods)
|
||||
|
||||
webSocketServer.on('connection', socket => {
|
||||
const { remoteAddress } = socket.upgradeReq.socket
|
||||
|
||||
@@ -461,7 +421,7 @@ const setUpApi = (webServer, xo, verboseLogsOnErrors) => {
|
||||
// Create the JSON-RPC server for this connection.
|
||||
const jsonRpc = new JsonRpcPeer(message => {
|
||||
if (message.type === 'request') {
|
||||
return api.call(connection, message.method, message.params)
|
||||
return xo.callApiMethod(connection, message.method, message.params)
|
||||
}
|
||||
})
|
||||
connection.notify = bind(jsonRpc.notify, jsonRpc)
|
||||
@@ -517,7 +477,7 @@ const setUpConsoleProxy = (webServer, xo) => {
|
||||
|
||||
const user = await xo.authenticateUser({ token })
|
||||
if (!await xo.hasPermissions(user.id, [ [ id, 'operate' ] ])) {
|
||||
throw new InvalidCredential()
|
||||
throw invalidCredentials()
|
||||
}
|
||||
|
||||
const { remoteAddress } = socket
|
||||
@@ -650,16 +610,24 @@ export default async function main (args) {
|
||||
await registerPlugins(xo)
|
||||
}
|
||||
|
||||
// Gracefully shutdown on signals.
|
||||
//
|
||||
// TODO: implements a timeout? (or maybe it is the services launcher
|
||||
// responsibility?)
|
||||
const shutdown = signal => {
|
||||
debug('%s caught, closing…', signal)
|
||||
xo.stop()
|
||||
}
|
||||
forEach([ 'SIGINT', 'SIGTERM' ], signal => {
|
||||
let alreadyCalled = false
|
||||
|
||||
// Gracefully shutdown on signals.
|
||||
process.on('SIGINT', () => shutdown('SIGINT'))
|
||||
process.on('SIGTERM', () => shutdown('SIGTERM'))
|
||||
process.on(signal, () => {
|
||||
if (alreadyCalled) {
|
||||
warn('forced exit')
|
||||
process.exit(1)
|
||||
}
|
||||
alreadyCalled = true
|
||||
|
||||
debug('%s caught, closing…', signal)
|
||||
xo.stop()
|
||||
})
|
||||
})
|
||||
|
||||
await eventToPromise(xo, 'stopped')
|
||||
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import assign from 'lodash/assign'
|
||||
import {BaseError} from 'make-error'
|
||||
import Bluebird from 'bluebird'
|
||||
import every from 'lodash/every'
|
||||
import filter from 'lodash/filter'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isPlainObject from 'lodash/isPlainObject'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import size from 'lodash/size'
|
||||
import some from 'lodash/some'
|
||||
import { BaseError } from 'make-error'
|
||||
|
||||
import { crossProduct } from './math'
|
||||
import {
|
||||
createRawObject,
|
||||
forEach
|
||||
serializeError,
|
||||
thunkToArray
|
||||
} from './utils'
|
||||
|
||||
export class JobExecutorError extends BaseError {}
|
||||
@@ -18,30 +28,67 @@ export class UnsupportedVectorType extends JobExecutorError {
|
||||
}
|
||||
}
|
||||
|
||||
export const productParams = (...args) => {
|
||||
let product = createRawObject()
|
||||
assign(product, ...args)
|
||||
return product
|
||||
// ===================================================================
|
||||
|
||||
const match = (pattern, value) => {
|
||||
if (isPlainObject(pattern)) {
|
||||
if (size(pattern) === 1) {
|
||||
if (pattern.__or) {
|
||||
return some(pattern.__or, subpattern => match(subpattern, value))
|
||||
}
|
||||
if (pattern.__not) {
|
||||
return !match(pattern.__not, value)
|
||||
}
|
||||
}
|
||||
|
||||
return isPlainObject(value) && every(pattern, (subpattern, key) => (
|
||||
value[key] !== undefined && match(subpattern, value[key])
|
||||
))
|
||||
}
|
||||
|
||||
if (isArray(pattern)) {
|
||||
return isArray(value) && every(pattern, subpattern =>
|
||||
some(value, subvalue => match(subpattern, subvalue))
|
||||
)
|
||||
}
|
||||
|
||||
return pattern === value
|
||||
}
|
||||
|
||||
export function _computeCrossProduct (items, productCb, extractValueMap = {}) {
|
||||
const upstreamValues = []
|
||||
const itemsCopy = items.slice()
|
||||
const item = itemsCopy.pop()
|
||||
const values = extractValueMap[item.type] && extractValueMap[item.type](item) || item
|
||||
forEach(values, value => {
|
||||
if (itemsCopy.length) {
|
||||
let downstreamValues = _computeCrossProduct(itemsCopy, productCb, extractValueMap)
|
||||
forEach(downstreamValues, downstreamValue => {
|
||||
upstreamValues.push(productCb(value, downstreamValue))
|
||||
const paramsVectorActionsMap = {
|
||||
extractProperties ({ mapping, value }) {
|
||||
return mapValues(mapping, key => value[key])
|
||||
},
|
||||
crossProduct ({ items }) {
|
||||
return thunkToArray(crossProduct(
|
||||
map(items, value => resolveParamsVector.call(this, value))
|
||||
))
|
||||
},
|
||||
fetchObjects ({ pattern }) {
|
||||
return filter(this.xo.getObjects(), object => match(pattern, object))
|
||||
},
|
||||
map ({ collection, iteratee, paramName = 'value' }) {
|
||||
return map(resolveParamsVector.call(this, collection), value => {
|
||||
return resolveParamsVector.call(this, {
|
||||
...iteratee,
|
||||
[paramName]: value
|
||||
})
|
||||
} else {
|
||||
upstreamValues.push(value)
|
||||
}
|
||||
})
|
||||
return upstreamValues
|
||||
})
|
||||
},
|
||||
set: ({ values }) => values
|
||||
}
|
||||
|
||||
export function resolveParamsVector (paramsVector) {
|
||||
const visitor = paramsVectorActionsMap[paramsVector.type]
|
||||
if (!visitor) {
|
||||
throw new Error(`Unsupported function '${paramsVector.type}'.`)
|
||||
}
|
||||
|
||||
return visitor.call(this, paramsVector)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class JobExecutor {
|
||||
constructor (xo) {
|
||||
this.xo = xo
|
||||
@@ -76,30 +123,24 @@ export default class JobExecutor {
|
||||
event: 'job.end',
|
||||
runJobId
|
||||
})
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
this._logger.error(`The execution of ${job.id} has failed.`, {
|
||||
event: 'job.end',
|
||||
runJobId,
|
||||
error: e
|
||||
error: serializeError(error)
|
||||
})
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async _execCall (job, runJobId) {
|
||||
let paramsFlatVector
|
||||
|
||||
if (job.paramsVector) {
|
||||
if (job.paramsVector.type === 'crossProduct') {
|
||||
paramsFlatVector = _computeCrossProduct(job.paramsVector.items, productParams, this._extractValueCb)
|
||||
} else {
|
||||
throw new UnsupportedVectorType(job.paramsVector)
|
||||
}
|
||||
} else {
|
||||
paramsFlatVector = [{}] // One call with no parameters
|
||||
}
|
||||
const { paramsVector } = job
|
||||
const paramsFlatVector = paramsVector
|
||||
? resolveParamsVector.call(this, paramsVector)
|
||||
: [{}] // One call with no parameters
|
||||
|
||||
const connection = this.xo.createUserConnection()
|
||||
const promises = []
|
||||
|
||||
connection.set('user_id', job.userId)
|
||||
|
||||
@@ -109,7 +150,7 @@ export default class JobExecutor {
|
||||
calls: {}
|
||||
}
|
||||
|
||||
forEach(paramsFlatVector, params => {
|
||||
await Bluebird.map(paramsFlatVector, params => {
|
||||
const runCallId = this._logger.notice(`Starting ${job.method} call. (${job.id})`, {
|
||||
event: 'jobCall.start',
|
||||
runJobId,
|
||||
@@ -123,36 +164,35 @@ export default class JobExecutor {
|
||||
start: Date.now()
|
||||
}
|
||||
|
||||
promises.push(
|
||||
this.xo.api.call(connection, job.method, assign({}, params)).then(
|
||||
value => {
|
||||
this._logger.notice(`Call ${job.method} (${runCallId}) is a success. (${job.id})`, {
|
||||
event: 'jobCall.end',
|
||||
runJobId,
|
||||
runCallId,
|
||||
returnedValue: value
|
||||
})
|
||||
return this.xo.callApiMethod(connection, job.method, assign({}, params)).then(
|
||||
value => {
|
||||
this._logger.notice(`Call ${job.method} (${runCallId}) is a success. (${job.id})`, {
|
||||
event: 'jobCall.end',
|
||||
runJobId,
|
||||
runCallId,
|
||||
returnedValue: value
|
||||
})
|
||||
|
||||
call.returnedValue = value
|
||||
call.end = Date.now()
|
||||
},
|
||||
reason => {
|
||||
this._logger.notice(`Call ${job.method} (${runCallId}) has failed. (${job.id})`, {
|
||||
event: 'jobCall.end',
|
||||
runJobId,
|
||||
runCallId,
|
||||
error: {...reason, message: reason.message}
|
||||
})
|
||||
call.returnedValue = value
|
||||
call.end = Date.now()
|
||||
},
|
||||
reason => {
|
||||
this._logger.notice(`Call ${job.method} (${runCallId}) has failed. (${job.id})`, {
|
||||
event: 'jobCall.end',
|
||||
runJobId,
|
||||
runCallId,
|
||||
error: serializeError(reason)
|
||||
})
|
||||
|
||||
call.error = reason
|
||||
call.end = Date.now()
|
||||
}
|
||||
)
|
||||
call.error = reason
|
||||
call.end = Date.now()
|
||||
}
|
||||
)
|
||||
}, {
|
||||
concurrency: 2
|
||||
})
|
||||
|
||||
connection.close()
|
||||
await Promise.all(promises)
|
||||
execStatus.end = Date.now()
|
||||
|
||||
return execStatus
|
||||
|
||||
@@ -1,71 +1,100 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import {expect} from 'chai'
|
||||
import leche from 'leche'
|
||||
import { expect } from 'chai'
|
||||
|
||||
import {
|
||||
_computeCrossProduct,
|
||||
productParams
|
||||
} from './job-executor'
|
||||
import { resolveParamsVector } from './job-executor'
|
||||
|
||||
describe('productParams', function () {
|
||||
describe('resolveParamsVector', function () {
|
||||
leche.withData({
|
||||
'Two sets of one': [
|
||||
{a: 1, b: 2}, {a: 1}, {b: 2}
|
||||
'cross product with three sets': [
|
||||
// Expected result.
|
||||
[ { id: 3, value: 'foo', remote: 'local' },
|
||||
{ id: 7, value: 'foo', remote: 'local' },
|
||||
{ id: 10, value: 'foo', remote: 'local' },
|
||||
{ id: 3, value: 'bar', remote: 'local' },
|
||||
{ id: 7, value: 'bar', remote: 'local' },
|
||||
{ id: 10, value: 'bar', remote: 'local' } ],
|
||||
// Entry.
|
||||
{
|
||||
type: 'crossProduct',
|
||||
items: [{
|
||||
type: 'set',
|
||||
values: [ { id: 3 }, { id: 7 }, { id: 10 } ]
|
||||
}, {
|
||||
type: 'set',
|
||||
values: [ { value: 'foo' }, { value: 'bar' } ]
|
||||
}, {
|
||||
type: 'set',
|
||||
values: [ { remote: 'local' } ]
|
||||
}]
|
||||
}
|
||||
],
|
||||
'Two sets of two': [
|
||||
{a: 1, b: 2, c: 3, d: 4}, {a: 1, b: 2}, {c: 3, d: 4}
|
||||
],
|
||||
'Three sets': [
|
||||
{a: 1, b: 2, c: 3, d: 4, e: 5, f: 6}, {a: 1}, {b: 2, c: 3}, {d: 4, e: 5, f: 6}
|
||||
],
|
||||
'One set': [
|
||||
{a: 1, b: 2}, {a: 1, b: 2}
|
||||
],
|
||||
'Empty set': [
|
||||
{a: 1}, {a: 1}, {}
|
||||
],
|
||||
'All empty': [
|
||||
{}, {}, {}
|
||||
],
|
||||
'No set': [
|
||||
{}
|
||||
'cross product with `set` and `map`': [
|
||||
// Expected result.
|
||||
[
|
||||
{ remote: 'local', id: 'vm:2' },
|
||||
{ remote: 'smb', id: 'vm:2' }
|
||||
],
|
||||
|
||||
// Entry.
|
||||
{
|
||||
type: 'crossProduct',
|
||||
items: [{
|
||||
type: 'set',
|
||||
values: [ { remote: 'local' }, { remote: 'smb' } ]
|
||||
}, {
|
||||
type: 'map',
|
||||
collection: {
|
||||
type: 'fetchObjects',
|
||||
pattern: {
|
||||
$pool: { __or: [ 'pool:1', 'pool:8', 'pool:12' ] },
|
||||
power_state: 'Running',
|
||||
tags: [ 'foo' ],
|
||||
type: 'VM'
|
||||
}
|
||||
},
|
||||
iteratee: {
|
||||
type: 'extractProperties',
|
||||
mapping: { id: 'id' }
|
||||
}
|
||||
}]
|
||||
},
|
||||
|
||||
// Context.
|
||||
{
|
||||
xo: {
|
||||
getObjects: function () {
|
||||
return [{
|
||||
id: 'vm:1',
|
||||
$pool: 'pool:1',
|
||||
tags: [],
|
||||
type: 'VM',
|
||||
power_state: 'Halted'
|
||||
}, {
|
||||
id: 'vm:2',
|
||||
$pool: 'pool:1',
|
||||
tags: [ 'foo' ],
|
||||
type: 'VM',
|
||||
power_state: 'Running'
|
||||
}, {
|
||||
id: 'host:1',
|
||||
type: 'host',
|
||||
power_state: 'Running'
|
||||
}, {
|
||||
id: 'vm:3',
|
||||
$pool: 'pool:8',
|
||||
tags: [ 'foo' ],
|
||||
type: 'VM',
|
||||
power_state: 'Halted'
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}, function (resultSet, ...sets) {
|
||||
it('Assembles all given param sets in on set', function () {
|
||||
expect(productParams(...sets)).to.eql(resultSet)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('_computeCrossProduct', function () {
|
||||
// Gives the sum of all args
|
||||
const addTest = (...args) => args.reduce((prev, curr) => prev + curr, 0)
|
||||
// Gives the product of all args
|
||||
const multiplyTest = (...args) => args.reduce((prev, curr) => prev * curr, 1)
|
||||
|
||||
leche.withData({
|
||||
'2 sets of 2 items to multiply': [
|
||||
[10, 14, 15, 21], [[2, 3], [5, 7]], multiplyTest
|
||||
],
|
||||
'3 sets of 2 items to multiply': [
|
||||
[110, 130, 154, 182, 165, 195, 231, 273], [[2, 3], [5, 7], [11, 13]], multiplyTest
|
||||
],
|
||||
'2 sets of 3 items to multiply': [
|
||||
[14, 22, 26, 21, 33, 39, 35, 55, 65], [[2, 3, 5], [7, 11, 13]], multiplyTest
|
||||
],
|
||||
'2 sets of 2 items to add': [
|
||||
[7, 9, 8, 10], [[2, 3], [5, 7]], addTest
|
||||
],
|
||||
'3 sets of 2 items to add': [
|
||||
[18, 20, 20, 22, 19, 21, 21, 23], [[2, 3], [5, 7], [11, 13]], addTest
|
||||
],
|
||||
'2 sets of 3 items to add': [
|
||||
[9, 13, 15, 10, 14, 16, 12, 16, 18], [[2, 3, 5], [7, 11, 13]], addTest
|
||||
]
|
||||
}, function (product, items, cb) {
|
||||
it('Crosses sets of values with a crossProduct callback', function () {
|
||||
expect(_computeCrossProduct(items, cb)).to.have.members(product)
|
||||
}, function (expectedResult, entry, context) {
|
||||
it('Resolves params vector', function () {
|
||||
expect(resolveParamsVector.call(context, entry)).to.deep.have.members(expectedResult)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
48
src/math.js
Normal file
48
src/math.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import assign from 'lodash/assign'
|
||||
|
||||
const _combine = (vectors, n, cb) => {
|
||||
if (!n) {
|
||||
return
|
||||
}
|
||||
|
||||
const nLast = n - 1
|
||||
|
||||
const vector = vectors[nLast]
|
||||
const m = vector.length
|
||||
if (n === 1) {
|
||||
for (let i = 0; i < m; ++i) {
|
||||
cb([ vector[i] ])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < m; ++i) {
|
||||
const value = vector[i]
|
||||
|
||||
_combine(vectors, nLast, (vector) => {
|
||||
vector.push(value)
|
||||
cb(vector)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Compute all combinations from vectors.
|
||||
//
|
||||
// Ex: combine([[2, 3], [5, 7]])
|
||||
// => [ [ 2, 5 ], [ 3, 5 ], [ 2, 7 ], [ 3, 7 ] ]
|
||||
export const combine = vectors => cb => _combine(vectors, vectors.length, cb)
|
||||
|
||||
// Merge the properties of an objects set in one object.
|
||||
//
|
||||
// Ex: mergeObjects([ { a: 1 }, { b: 2 } ]) => { a: 1, b: 2 }
|
||||
export const mergeObjects = objects => assign({}, ...objects)
|
||||
|
||||
// Compute a cross product between vectors.
|
||||
//
|
||||
// Ex: crossProduct([ [ { a: 2 }, { b: 3 } ], [ { c: 5 }, { d: 7 } ] ] )
|
||||
// => [ { a: 2, c: 5 }, { b: 3, c: 5 }, { a: 2, d: 7 }, { b: 3, d: 7 } ]
|
||||
export const crossProduct = (vectors, mergeFn = mergeObjects) => cb => (
|
||||
combine(vectors)(vector => {
|
||||
cb(mergeFn(vector))
|
||||
})
|
||||
)
|
||||
72
src/math.spec.js
Normal file
72
src/math.spec.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import leche from 'leche'
|
||||
|
||||
import { thunkToArray } from './utils'
|
||||
import {
|
||||
crossProduct,
|
||||
mergeObjects
|
||||
} from './math'
|
||||
|
||||
describe('mergeObjects', function () {
|
||||
leche.withData({
|
||||
'Two sets of one': [
|
||||
{a: 1, b: 2}, {a: 1}, {b: 2}
|
||||
],
|
||||
'Two sets of two': [
|
||||
{a: 1, b: 2, c: 3, d: 4}, {a: 1, b: 2}, {c: 3, d: 4}
|
||||
],
|
||||
'Three sets': [
|
||||
{a: 1, b: 2, c: 3, d: 4, e: 5, f: 6}, {a: 1}, {b: 2, c: 3}, {d: 4, e: 5, f: 6}
|
||||
],
|
||||
'One set': [
|
||||
{a: 1, b: 2}, {a: 1, b: 2}
|
||||
],
|
||||
'Empty set': [
|
||||
{a: 1}, {a: 1}, {}
|
||||
],
|
||||
'All empty': [
|
||||
{}, {}, {}
|
||||
],
|
||||
'No set': [
|
||||
{}
|
||||
]
|
||||
}, function (resultSet, ...sets) {
|
||||
it('Assembles all given param sets in on set', function () {
|
||||
expect(mergeObjects(sets)).to.eql(resultSet)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('crossProduct', function () {
|
||||
// Gives the sum of all args
|
||||
const addTest = args => args.reduce((prev, curr) => prev + curr, 0)
|
||||
// Gives the product of all args
|
||||
const multiplyTest = args => args.reduce((prev, curr) => prev * curr, 1)
|
||||
|
||||
leche.withData({
|
||||
'2 sets of 2 items to multiply': [
|
||||
[10, 14, 15, 21], [[2, 3], [5, 7]], multiplyTest
|
||||
],
|
||||
'3 sets of 2 items to multiply': [
|
||||
[110, 130, 154, 182, 165, 195, 231, 273], [[2, 3], [5, 7], [11, 13]], multiplyTest
|
||||
],
|
||||
'2 sets of 3 items to multiply': [
|
||||
[14, 22, 26, 21, 33, 39, 35, 55, 65], [[2, 3, 5], [7, 11, 13]], multiplyTest
|
||||
],
|
||||
'2 sets of 2 items to add': [
|
||||
[7, 9, 8, 10], [[2, 3], [5, 7]], addTest
|
||||
],
|
||||
'3 sets of 2 items to add': [
|
||||
[18, 20, 20, 22, 19, 21, 21, 23], [[2, 3], [5, 7], [11, 13]], addTest
|
||||
],
|
||||
'2 sets of 3 items to add': [
|
||||
[9, 13, 15, 10, 14, 16, 12, 16, 18], [[2, 3, 5], [7, 11, 13]], addTest
|
||||
]
|
||||
}, function (product, items, cb) {
|
||||
it('Crosses sets of values with a crossProduct callback', function () {
|
||||
expect(thunkToArray(crossProduct(items, cb))).to.have.members(product)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -14,10 +14,6 @@ export class Groups extends Collection {
|
||||
return Group
|
||||
}
|
||||
|
||||
get idPrefix () {
|
||||
return 'group:'
|
||||
}
|
||||
|
||||
create (name) {
|
||||
return this.add(new Group({
|
||||
name,
|
||||
|
||||
@@ -11,12 +11,7 @@ export class Jobs extends Collection {
|
||||
return Job
|
||||
}
|
||||
|
||||
get idPrefix () {
|
||||
return 'job:'
|
||||
}
|
||||
|
||||
async create (userId, job) {
|
||||
job.userId = userId
|
||||
async create (job) {
|
||||
// Serializes.
|
||||
job.paramsVector = JSON.stringify(job.paramsVector)
|
||||
return /* await */ this.add(new Job(job))
|
||||
|
||||
@@ -13,10 +13,6 @@ export class PluginsMetadata extends Collection {
|
||||
return PluginMetadata
|
||||
}
|
||||
|
||||
get idPrefix () {
|
||||
return 'plugin-metadata:'
|
||||
}
|
||||
|
||||
async save ({ id, autoload, configuration }) {
|
||||
return /* await */ this.update({
|
||||
id,
|
||||
|
||||
@@ -13,10 +13,6 @@ export class Remotes extends Collection {
|
||||
return Remote
|
||||
}
|
||||
|
||||
get idPrefix () {
|
||||
return 'remote-'
|
||||
}
|
||||
|
||||
create (name, url) {
|
||||
return this.add(new Remote({
|
||||
name,
|
||||
|
||||
@@ -11,10 +11,6 @@ export class Schedules extends Collection {
|
||||
return Schedule
|
||||
}
|
||||
|
||||
get idPrefix () {
|
||||
return 'schedule:'
|
||||
}
|
||||
|
||||
create (userId, job, cron, enabled, name = undefined, timezone = undefined) {
|
||||
return this.add(new Schedule({
|
||||
userId,
|
||||
|
||||
@@ -31,15 +31,14 @@ export class Users extends Collection {
|
||||
return User
|
||||
}
|
||||
|
||||
async create (email, properties = {}) {
|
||||
async create (properties) {
|
||||
const { email } = properties
|
||||
|
||||
// Avoid duplicates.
|
||||
if (await this.exists({email})) {
|
||||
throw new Error(`the user ${email} already exists`)
|
||||
}
|
||||
|
||||
// Adds the email to the user's properties.
|
||||
properties.email = email
|
||||
|
||||
// Create the user object.
|
||||
const user = new User(properties)
|
||||
|
||||
|
||||
@@ -154,6 +154,13 @@ export default class RemoteHandlerAbstract {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async refreshChecksum (path) {
|
||||
const stream = addChecksumToReadStream(await this.createReadStream(path))
|
||||
stream.resume() // start reading the whole file
|
||||
const checksum = await stream.checksum
|
||||
await this.outputFile(`${path}.checksum`, checksum)
|
||||
}
|
||||
|
||||
async createOutputStream (file, {
|
||||
checksum = false,
|
||||
...options
|
||||
|
||||
@@ -12,7 +12,7 @@ export default class NfsHandler extends LocalHandler {
|
||||
}
|
||||
|
||||
_getRealPath () {
|
||||
return `/tmp/xo-server/mounts/${this._remote.id}`
|
||||
return `/run/xo-server/mounts/${this._remote.id}`
|
||||
}
|
||||
|
||||
async _loadRealMounts () {
|
||||
@@ -79,6 +79,6 @@ export default class NfsHandler extends LocalHandler {
|
||||
}
|
||||
|
||||
async _umount (remote) {
|
||||
await execa('umount', [remote.path])
|
||||
await execa('umount', [this._getRealPath()])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import paramsVector from 'job/params-vector'
|
||||
|
||||
export default {
|
||||
$schema: 'http://json-schema.org/draft-04/schema#',
|
||||
type: 'object',
|
||||
@@ -27,7 +25,9 @@ export default {
|
||||
type: 'string',
|
||||
description: 'called method'
|
||||
},
|
||||
paramsVector
|
||||
paramsVector: {
|
||||
type: 'object'
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'type',
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
export default {
|
||||
$schema: 'http://json-schema.org/draft-04/schema#',
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
enum: ['crossProduct']
|
||||
},
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'vector of values to multiply with others vectors',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
enum: ['set']
|
||||
},
|
||||
values: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object'
|
||||
},
|
||||
minItems: 1
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'type',
|
||||
'values'
|
||||
]
|
||||
},
|
||||
minItems: 1
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'type',
|
||||
'items'
|
||||
]
|
||||
}
|
||||
|
||||
/* Example:
|
||||
{
|
||||
"type": "cross product",
|
||||
"items": [
|
||||
{
|
||||
"type": "set",
|
||||
"values": [
|
||||
{"id": 0, "name": "snapshost de 0"},
|
||||
{"id": 1, "name": "snapshost de 1"}
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "set",
|
||||
"values": [
|
||||
{"force": true}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -20,7 +20,7 @@ export default {
|
||||
},
|
||||
unloadable: {
|
||||
type: 'boolean',
|
||||
default: 'true',
|
||||
default: true,
|
||||
description: 'whether or not this plugin can be unloaded'
|
||||
},
|
||||
configuration: {
|
||||
@@ -30,6 +30,14 @@ export default {
|
||||
configurationSchema: {
|
||||
$ref: 'http://json-schema.org/draft-04/schema#',
|
||||
description: 'configuration schema for this plugin (not present if not configurable)'
|
||||
},
|
||||
testable: {
|
||||
type: 'boolean',
|
||||
description: 'whether or not this plugin can be tested'
|
||||
},
|
||||
testSchema: {
|
||||
$ref: 'http://json-schema.org/draft-04/schema#',
|
||||
description: 'test schema for this plugin'
|
||||
}
|
||||
},
|
||||
required: [
|
||||
|
||||
79
src/utils.js
79
src/utils.js
@@ -11,6 +11,7 @@ import isString from 'lodash/isString'
|
||||
import keys from 'lodash/keys'
|
||||
import kindOf from 'kindof'
|
||||
import multiKeyHashInt from 'multikey-hash'
|
||||
import pick from 'lodash/pick'
|
||||
import xml2js from 'xml2js'
|
||||
|
||||
// Moment timezone can be loaded only one time, it's a workaround to load
|
||||
@@ -18,7 +19,10 @@ import xml2js from 'xml2js'
|
||||
// does not implement `guess` function for example.
|
||||
import 'moment-timezone'
|
||||
|
||||
import through2 from 'through2'
|
||||
import { CronJob } from 'cron'
|
||||
import { Readable } from 'stream'
|
||||
import { utcFormat, utcParse } from 'd3-time-format'
|
||||
import {
|
||||
all as pAll,
|
||||
defer,
|
||||
@@ -29,9 +33,6 @@ import {
|
||||
createHash,
|
||||
randomBytes
|
||||
} from 'crypto'
|
||||
import { Readable } from 'stream'
|
||||
import through2 from 'through2'
|
||||
import {utcFormat as d3TimeFormat} from 'd3-time-format'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -59,7 +60,7 @@ export const streamToBuffer = getStream.buffer
|
||||
|
||||
export function camelToSnakeCase (string) {
|
||||
return string.replace(
|
||||
/([a-z])([A-Z])/g,
|
||||
/([a-z0-9])([A-Z])/g,
|
||||
(_, prevChar, currChar) => `${prevChar}_${currChar.toLowerCase()}`
|
||||
)
|
||||
}
|
||||
@@ -73,6 +74,27 @@ export const createRawObject = Object.create
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Only works with string items!
|
||||
export const diffItems = (coll1, coll2) => {
|
||||
const removed = createRawObject()
|
||||
forEach(coll2, value => {
|
||||
removed[value] = true
|
||||
})
|
||||
|
||||
const added = []
|
||||
forEach(coll1, value => {
|
||||
if (value in removed) {
|
||||
delete removed[value]
|
||||
} else {
|
||||
added.push(value)
|
||||
}
|
||||
})
|
||||
|
||||
return [ added, keys(removed) ]
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const ALGORITHM_TO_ID = {
|
||||
md5: '1',
|
||||
sha256: '5',
|
||||
@@ -177,6 +199,13 @@ export function extractProperty (obj, prop) {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const getUserPublicProperties = user => pick(
|
||||
user.properties || user,
|
||||
'id', 'email', 'groups', 'permission', 'preferences', 'provider'
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const getPseudoRandomBytes = n => {
|
||||
const bytes = new Buffer(n)
|
||||
|
||||
@@ -197,7 +226,7 @@ export const generateUnsecureToken = (n = 32) => base64url(getPseudoRandomBytes(
|
||||
// Generate a secure random Base64 string.
|
||||
export const generateToken = (randomBytes => {
|
||||
return (n = 32) => randomBytes(n).then(base64url)
|
||||
})(randomBytes::promisify())
|
||||
})(promisify(randomBytes))
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -362,7 +391,9 @@ export const popProperty = obj => {
|
||||
|
||||
// Format a date in ISO 8601 in a safe way to be used in filenames
|
||||
// (even on Windows).
|
||||
export const safeDateFormat = d3TimeFormat('%Y%m%dT%H%M%SZ')
|
||||
export const safeDateFormat = utcFormat('%Y%m%dT%H%M%SZ')
|
||||
|
||||
export const safeDateParse = utcParse('%Y%m%dT%H%M%SZ')
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -476,5 +507,41 @@ export const scheduleFn = (cronTime, fn, timeZone) => {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Create a serializable object from an error.
|
||||
export const serializeError = error => ({
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
...error // Copy enumerable properties.
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Create an array which contains the results of one thunk function.
|
||||
// Only works with synchronous thunks.
|
||||
export const thunkToArray = thunk => {
|
||||
const values = []
|
||||
thunk(::values.push)
|
||||
return values
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Creates a new function which throws an error.
|
||||
//
|
||||
// ```js
|
||||
// promise.catch(throwFn('an error has occured'))
|
||||
//
|
||||
// function foo (param = throwFn('param is required')) {}
|
||||
// ```
|
||||
export const throwFn = error => () => {
|
||||
throw (
|
||||
isString(error)
|
||||
? new Error(error)
|
||||
: error
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Wrap a value in a function.
|
||||
export const wrap = value => () => value
|
||||
|
||||
@@ -7,6 +7,7 @@ import expect from 'must'
|
||||
import {
|
||||
camelToSnakeCase,
|
||||
createRawObject,
|
||||
diffItems,
|
||||
ensureArray,
|
||||
extractProperty,
|
||||
formatXml,
|
||||
@@ -20,10 +21,12 @@ import {
|
||||
describe('camelToSnakeCase()', function () {
|
||||
it('converts a string from camelCase to snake_case', function () {
|
||||
expect(camelToSnakeCase('fooBar')).to.equal('foo_bar')
|
||||
expect(camelToSnakeCase('ipv4Allowed')).to.equal('ipv4_allowed')
|
||||
})
|
||||
|
||||
it('does not alter snake_case strings', function () {
|
||||
expect(camelToSnakeCase('foo_bar')).to.equal('foo_bar')
|
||||
expect(camelToSnakeCase('ipv4_allowed')).to.equal('ipv4_allowed')
|
||||
})
|
||||
|
||||
it('does not alter upper case letters expect those from the camelCase', function () {
|
||||
@@ -55,6 +58,20 @@ describe('createRawObject()', () => {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('diffItems', () => {
|
||||
it('computes the added/removed items between 2 iterables', () => {
|
||||
expect(diffItems(
|
||||
['foo', 'bar'],
|
||||
['baz', 'foo']
|
||||
)).to.eql([
|
||||
['bar'],
|
||||
['baz']
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('ensureArray()', function () {
|
||||
it('wrap the value in an array', function () {
|
||||
const value = 'foo'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fu from 'struct-fu'
|
||||
import fu from '@nraynaud/struct-fu'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
|
||||
import {
|
||||
noop,
|
||||
@@ -91,7 +92,7 @@ const fuHeader = fu.struct([
|
||||
fu.uint8('parentUuid', 16),
|
||||
fu.uint32('parentTimestamp'),
|
||||
fu.uint32('reserved1'),
|
||||
fu.char('parentUnicodeName', 512),
|
||||
fu.char16be('parentUnicodeName', 512),
|
||||
fu.struct('parentLocatorEntry', [
|
||||
fu.uint32('platformCode'),
|
||||
fu.uint32('platformDataSpace'),
|
||||
@@ -144,24 +145,24 @@ const unpackField = (field, buf) => {
|
||||
}
|
||||
// ===================================================================
|
||||
|
||||
// Returns the checksum of a raw footer.
|
||||
// The raw footer is altered with the new sum.
|
||||
function checksumFooter (rawFooter) {
|
||||
const checksumField = fuFooter.fields.checksum
|
||||
// Returns the checksum of a raw struct.
|
||||
// The raw struct (footer or header) is altered with the new sum.
|
||||
function checksumStruct (rawStruct, struct) {
|
||||
const checksumField = struct.fields.checksum
|
||||
|
||||
let sum = 0
|
||||
|
||||
// Reset current sum.
|
||||
packField(checksumField, 0, rawFooter)
|
||||
packField(checksumField, 0, rawStruct)
|
||||
|
||||
for (let i = 0; i < VHD_FOOTER_SIZE; i++) {
|
||||
sum = (sum + rawFooter[i]) & 0xFFFFFFFF
|
||||
for (let i = 0, n = struct.size; i < n; i++) {
|
||||
sum = (sum + rawStruct[i]) & 0xFFFFFFFF
|
||||
}
|
||||
|
||||
sum = 0xFFFFFFFF - sum
|
||||
|
||||
// Write new sum.
|
||||
packField(checksumField, sum, rawFooter)
|
||||
packField(checksumField, sum, rawStruct)
|
||||
|
||||
return sum
|
||||
}
|
||||
@@ -257,7 +258,7 @@ class Vhd {
|
||||
)
|
||||
|
||||
const sum = unpackField(fuFooter.fields.checksum, buf)
|
||||
const sumToTest = checksumFooter(buf)
|
||||
const sumToTest = checksumStruct(buf, fuFooter)
|
||||
|
||||
// Checksum child & parent.
|
||||
if (sumToTest !== sum) {
|
||||
@@ -494,25 +495,36 @@ class Vhd {
|
||||
}
|
||||
}
|
||||
|
||||
// Write a context footer. (At the end and beggining of a vhd file.)
|
||||
// Write a context footer. (At the end and beginning of a vhd file.)
|
||||
async writeFooter () {
|
||||
const { footer } = this
|
||||
|
||||
const offset = this.getEndOfData()
|
||||
const rawFooter = fuFooter.pack(footer)
|
||||
|
||||
footer.checksum = checksumFooter(rawFooter)
|
||||
footer.checksum = checksumStruct(rawFooter, fuFooter)
|
||||
debug(`Write footer at: ${offset} (checksum=${footer.checksum}). (data=${rawFooter.toString('hex')})`)
|
||||
|
||||
await this._write(rawFooter, 0)
|
||||
await this._write(rawFooter, offset)
|
||||
}
|
||||
|
||||
async writeHeader () {
|
||||
const { header } = this
|
||||
const rawHeader = fuHeader.pack(header)
|
||||
header.checksum = checksumStruct(rawHeader, fuHeader)
|
||||
const offset = VHD_FOOTER_SIZE
|
||||
debug(`Write header at: ${offset} (checksum=${header.checksum}). (data=${rawHeader.toString('hex')})`)
|
||||
await this._write(rawHeader, offset)
|
||||
}
|
||||
}
|
||||
|
||||
// Merge vhd child into vhd parent.
|
||||
//
|
||||
// Child must be a delta backup !
|
||||
// Parent must be a full backup !
|
||||
//
|
||||
// TODO: update the identifier of the parent VHD.
|
||||
export default async function vhdMerge (
|
||||
parentHandler, parentPath,
|
||||
childHandler, childPath
|
||||
@@ -564,3 +576,46 @@ export default async function vhdMerge (
|
||||
|
||||
await parentVhd.writeFooter()
|
||||
}
|
||||
|
||||
// returns true if the child was actually modified
|
||||
export async function chainVhd (
|
||||
parentHandler, parentPath,
|
||||
childHandler, childPath
|
||||
) {
|
||||
const parentVhd = new Vhd(parentHandler, parentPath)
|
||||
const childVhd = new Vhd(childHandler, childPath)
|
||||
await Promise.all([
|
||||
parentVhd.readHeaderAndFooter(),
|
||||
childVhd.readHeaderAndFooter()
|
||||
])
|
||||
|
||||
const { header } = childVhd
|
||||
|
||||
const parentName = parentPath.split('/').pop()
|
||||
const parentUuid = parentVhd.footer.uuid
|
||||
if (
|
||||
header.parentUnicodeName !== parentName ||
|
||||
!isEqual(header.parentUuid, parentUuid)
|
||||
) {
|
||||
header.parentUuid = parentUuid
|
||||
header.parentUnicodeName = parentName
|
||||
await childVhd.writeHeader()
|
||||
return true
|
||||
}
|
||||
|
||||
// The checksum was broken between xo-server v5.2.4 and v5.2.5
|
||||
//
|
||||
// Replace by a correct checksum if necessary.
|
||||
//
|
||||
// TODO: remove when enough time as passed (6 months).
|
||||
{
|
||||
const rawHeader = fuHeader.pack(header)
|
||||
const checksum = checksumStruct(rawHeader, fuHeader)
|
||||
if (checksum !== header.checksum) {
|
||||
await childVhd._write(rawHeader, VHD_FOOTER_SIZE)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
extractProperty,
|
||||
forEach,
|
||||
isArray,
|
||||
isEmpty,
|
||||
mapToArray,
|
||||
parseXml
|
||||
} from './utils'
|
||||
@@ -194,6 +195,15 @@ const TRANSFORMS = {
|
||||
: 'out of date'
|
||||
})()
|
||||
|
||||
let resourceSet = otherConfig['xo:resource_set']
|
||||
if (resourceSet) {
|
||||
try {
|
||||
resourceSet = JSON.parse(resourceSet)
|
||||
} catch (_) {
|
||||
resourceSet = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const vm = {
|
||||
// type is redefined after for controllers/, templates &
|
||||
// snapshots.
|
||||
@@ -232,7 +242,8 @@ const TRANSFORMS = {
|
||||
return {
|
||||
enabled: true,
|
||||
info: info && parseXml(info).docker_info,
|
||||
process: process && parseXml(process).docker_ps,
|
||||
containers: ensureArray(process && parseXml(process).docker_ps.item),
|
||||
process: process && parseXml(process).docker_ps, // deprecated (only used in v4)
|
||||
version: version && parseXml(version).docker_version
|
||||
}
|
||||
})(),
|
||||
@@ -271,6 +282,7 @@ const TRANSFORMS = {
|
||||
other: otherConfig,
|
||||
os_version: guestMetrics && guestMetrics.os_version || null,
|
||||
power_state: obj.power_state,
|
||||
resourceSet,
|
||||
snapshots: link(obj, 'snapshots'),
|
||||
startTime: metrics && toTimestamp(metrics.start_time),
|
||||
tags: obj.tags,
|
||||
@@ -362,6 +374,7 @@ const TRANSFORMS = {
|
||||
name_description: obj.name_description,
|
||||
name_label: obj.name_label,
|
||||
size: +obj.physical_size,
|
||||
shared: Boolean(obj.shared),
|
||||
SR_type: obj.type,
|
||||
tags: obj.tags,
|
||||
usage: +obj.virtual_allocation,
|
||||
@@ -382,7 +395,7 @@ const TRANSFORMS = {
|
||||
return {
|
||||
type: 'PBD',
|
||||
|
||||
attached: obj.currently_attached,
|
||||
attached: Boolean(obj.currently_attached),
|
||||
host: link(obj, 'host'),
|
||||
SR: link(obj, 'SR')
|
||||
}
|
||||
@@ -391,10 +404,13 @@ const TRANSFORMS = {
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
pif (obj) {
|
||||
const metrics = obj.$metrics
|
||||
|
||||
return {
|
||||
type: 'PIF',
|
||||
|
||||
attached: Boolean(obj.currently_attached),
|
||||
isBondMaster: !isEmpty(obj.bond_master_of),
|
||||
device: obj.device,
|
||||
dns: obj.DNS,
|
||||
disallowUnplug: Boolean(obj.disallow_unplug),
|
||||
@@ -402,6 +418,7 @@ const TRANSFORMS = {
|
||||
ip: obj.IP,
|
||||
mac: obj.MAC,
|
||||
management: Boolean(obj.management), // TODO: find a better name.
|
||||
carrier: Boolean(metrics && metrics.carrier),
|
||||
mode: obj.ip_configuration_mode,
|
||||
mtu: +obj.MTU,
|
||||
netmask: obj.netmask,
|
||||
@@ -483,6 +500,7 @@ const TRANSFORMS = {
|
||||
network (obj) {
|
||||
return {
|
||||
bridge: obj.bridge,
|
||||
defaultIsLocked: obj.default_locking_mode === 'disabled',
|
||||
MTU: +obj.MTU,
|
||||
name_description: obj.name_description,
|
||||
name_label: obj.name_label,
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import createDebug from 'debug'
|
||||
import deferrable from 'golike-defer'
|
||||
import every from 'lodash/every'
|
||||
import fatfs from 'fatfs'
|
||||
import find from 'lodash/find'
|
||||
import flatten from 'lodash/flatten'
|
||||
import includes from 'lodash/includes'
|
||||
import sortBy from 'lodash/sortBy'
|
||||
import unzip from 'julien-f-unzip'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import omit from 'lodash/omit'
|
||||
import tarStream from 'tar-stream'
|
||||
import uniq from 'lodash/uniq'
|
||||
import vmdkToVhd from 'xo-vmdk-to-vhd'
|
||||
import { defer } from 'promise-toolbox'
|
||||
import {
|
||||
wrapError as wrapXapiError,
|
||||
@@ -18,12 +22,7 @@ import {
|
||||
|
||||
import httpRequest from '../http-request'
|
||||
import fatfsBuffer, { init as fatfsBufferInit } from '../fatfs-buffer'
|
||||
import {
|
||||
debounce,
|
||||
deferrable,
|
||||
mixin
|
||||
} from '../decorators'
|
||||
import httpProxy from '../http-proxy'
|
||||
import { mixin } from '../decorators'
|
||||
import {
|
||||
bufferToStream,
|
||||
camelToSnakeCase,
|
||||
@@ -35,33 +34,31 @@ import {
|
||||
mapToArray,
|
||||
noop,
|
||||
pAll,
|
||||
parseXml,
|
||||
pCatch,
|
||||
pDelay,
|
||||
pFinally,
|
||||
promisifyAll,
|
||||
pSettle
|
||||
} from '../utils'
|
||||
import {
|
||||
GenericError,
|
||||
ForbiddenOperation
|
||||
} from '../api-errors'
|
||||
import { forbiddenOperation } from 'xo-common/api-errors'
|
||||
|
||||
import mixins from './mixins'
|
||||
import OTHER_CONFIG_TEMPLATE from './other-config-template'
|
||||
import {
|
||||
asBoolean,
|
||||
asInteger,
|
||||
debug,
|
||||
extractOpaqueRef,
|
||||
filterUndefineds,
|
||||
getNamespaceForType,
|
||||
isVmHvm,
|
||||
isVmRunning,
|
||||
NULL_REF,
|
||||
optional,
|
||||
prepareXapiParam
|
||||
prepareXapiParam,
|
||||
put
|
||||
} from './utils'
|
||||
|
||||
const debug = createDebug('xo:xapi')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const TAG_BASE_DELTA = 'xo:base_delta'
|
||||
@@ -69,45 +66,6 @@ const TAG_COPY_SRC = 'xo:copy_of'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// HTTP put, use an ugly hack if the length is not known because XAPI
|
||||
// does not support chunk encoding.
|
||||
const put = (stream, {
|
||||
headers: { ...headers } = {},
|
||||
...opts
|
||||
}, task) => {
|
||||
const makeRequest = () => httpRequest({
|
||||
...opts,
|
||||
body: stream,
|
||||
headers,
|
||||
method: 'put'
|
||||
})
|
||||
|
||||
// Xen API does not support chunk encoding.
|
||||
if (stream.length == null) {
|
||||
headers['transfer-encoding'] = null
|
||||
|
||||
const promise = makeRequest()
|
||||
|
||||
if (task) {
|
||||
// Some connections need the task to resolve (VDI import).
|
||||
task::pFinally(() => {
|
||||
promise.cancel()
|
||||
})
|
||||
} else {
|
||||
// Some tasks need the connection to close (VM import).
|
||||
promise.request.once('finish', () => {
|
||||
promise.cancel()
|
||||
})
|
||||
}
|
||||
|
||||
return promise.readAll()
|
||||
}
|
||||
|
||||
return makeRequest().readAll()
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// FIXME: remove this work around when fixed, https://phabricator.babeljs.io/T2877
|
||||
// export * from './utils'
|
||||
require('lodash/assign')(module.exports, require('./utils'))
|
||||
@@ -116,6 +74,9 @@ require('lodash/assign')(module.exports, require('./utils'))
|
||||
export const VDI_FORMAT_VHD = 'vhd'
|
||||
export const VDI_FORMAT_RAW = 'raw'
|
||||
|
||||
export const IPV4_CONFIG_MODES = ['None', 'DHCP', 'Static']
|
||||
export const IPV6_CONFIG_MODES = ['None', 'DHCP', 'Static', 'Autoconf']
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@mixin(mapToArray(mixins))
|
||||
@@ -379,6 +340,22 @@ export default class Xapi extends XapiBase {
|
||||
})
|
||||
}
|
||||
|
||||
async setNetworkProperties (id, {
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
defaultIsLocked
|
||||
}) {
|
||||
let defaultLockingMode
|
||||
if (defaultIsLocked != null) {
|
||||
defaultLockingMode = defaultIsLocked ? 'disabled' : 'unlocked'
|
||||
}
|
||||
await this._setObjectProperties(this.getObject(id), {
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
defaultLockingMode
|
||||
})
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
async addTag (id, tag) {
|
||||
@@ -411,82 +388,6 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
// =================================================================
|
||||
|
||||
// FIXME: should be static
|
||||
@debounce(24 * 60 * 60 * 1000)
|
||||
async _getXenUpdates () {
|
||||
const { readAll, statusCode } = await httpRequest(
|
||||
'http://updates.xensource.com/XenServer/updates.xml',
|
||||
{ agent: httpProxy }
|
||||
)
|
||||
|
||||
if (statusCode !== 200) {
|
||||
throw new GenericError('cannot fetch patches list from Citrix')
|
||||
}
|
||||
|
||||
const data = parseXml(await readAll()).patchdata
|
||||
|
||||
const patches = createRawObject()
|
||||
forEach(data.patches.patch, patch => {
|
||||
patches[patch.uuid] = {
|
||||
date: patch.timestamp,
|
||||
description: patch['name-description'],
|
||||
documentationUrl: patch.url,
|
||||
guidance: patch['after-apply-guidance'],
|
||||
name: patch['name-label'],
|
||||
url: patch['patch-url'],
|
||||
uuid: patch.uuid,
|
||||
conflicts: mapToArray(ensureArray(patch.conflictingpatches), patch => {
|
||||
return patch.conflictingpatch.uuid
|
||||
}),
|
||||
requirements: mapToArray(ensureArray(patch.requiredpatches), patch => {
|
||||
return patch.requiredpatch.uuid
|
||||
})
|
||||
// TODO: what does it mean, should we handle it?
|
||||
// version: patch.version,
|
||||
}
|
||||
if (patches[patch.uuid].conflicts[0] === undefined) {
|
||||
patches[patch.uuid].conflicts.length = 0
|
||||
}
|
||||
if (patches[patch.uuid].requirements[0] === undefined) {
|
||||
patches[patch.uuid].requirements.length = 0
|
||||
}
|
||||
})
|
||||
|
||||
const resolveVersionPatches = function (uuids) {
|
||||
const versionPatches = createRawObject()
|
||||
|
||||
forEach(ensureArray(uuids), ({uuid}) => {
|
||||
versionPatches[uuid] = patches[uuid]
|
||||
})
|
||||
|
||||
return versionPatches
|
||||
}
|
||||
|
||||
const versions = createRawObject()
|
||||
let latestVersion
|
||||
forEach(data.serverversions.version, version => {
|
||||
versions[version.value] = {
|
||||
date: version.timestamp,
|
||||
name: version.name,
|
||||
id: version.value,
|
||||
documentationUrl: version.url,
|
||||
patches: resolveVersionPatches(version.patch)
|
||||
}
|
||||
|
||||
if (version.latest) {
|
||||
latestVersion = versions[version.value]
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
patches,
|
||||
latestVersion,
|
||||
versions
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
async joinPool (masterAddress, masterUsername, masterPassword, force = false) {
|
||||
await this.call(
|
||||
force ? 'pool.join_force' : 'pool.join',
|
||||
@@ -498,194 +399,6 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
// =================================================================
|
||||
|
||||
// Returns installed and not installed patches for a given host.
|
||||
async _getPoolPatchesForHost (host) {
|
||||
const versions = (await this._getXenUpdates()).versions
|
||||
|
||||
const hostVersions = host.software_version
|
||||
const version =
|
||||
versions[hostVersions.product_version] ||
|
||||
versions[hostVersions.product_version_text]
|
||||
|
||||
return version
|
||||
? version.patches
|
||||
: []
|
||||
}
|
||||
|
||||
_getInstalledPoolPatchesOnHost (host) {
|
||||
const installed = createRawObject()
|
||||
|
||||
forEach(host.$patches, hostPatch => {
|
||||
installed[hostPatch.$pool_patch.uuid] = true
|
||||
})
|
||||
|
||||
return installed
|
||||
}
|
||||
|
||||
async _listMissingPoolPatchesOnHost (host) {
|
||||
const all = await this._getPoolPatchesForHost(host)
|
||||
const installed = this._getInstalledPoolPatchesOnHost(host)
|
||||
|
||||
const installable = createRawObject()
|
||||
forEach(all, (patch, uuid) => {
|
||||
if (installed[uuid]) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const uuid of patch.conflicts) {
|
||||
if (uuid in installed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
installable[uuid] = patch
|
||||
})
|
||||
|
||||
return installable
|
||||
}
|
||||
|
||||
async listMissingPoolPatchesOnHost (hostId) {
|
||||
// Returns an array to not break compatibility.
|
||||
return mapToArray(
|
||||
await this._listMissingPoolPatchesOnHost(this.getObject(hostId))
|
||||
)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
_isPoolPatchInstallableOnHost (patchUuid, host) {
|
||||
const installed = this._getInstalledPoolPatchesOnHost(host)
|
||||
|
||||
if (installed[patchUuid]) {
|
||||
return false
|
||||
}
|
||||
|
||||
let installable = true
|
||||
|
||||
forEach(installed, patch => {
|
||||
if (includes(patch.conflicts, patchUuid)) {
|
||||
installable = false
|
||||
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
return installable
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async uploadPoolPatch (stream, patchName = 'unknown') {
|
||||
const taskRef = await this._createTask('Patch upload', patchName)
|
||||
|
||||
const task = this._watchTask(taskRef)
|
||||
const [ patchRef ] = await Promise.all([
|
||||
task,
|
||||
put(stream, {
|
||||
hostname: this.pool.$master.address,
|
||||
path: '/pool_patch_upload',
|
||||
query: {
|
||||
session_id: this.sessionId,
|
||||
task_id: taskRef
|
||||
}
|
||||
}, task)
|
||||
])
|
||||
|
||||
return this._getOrWaitObject(patchRef)
|
||||
}
|
||||
|
||||
async _getOrUploadPoolPatch (uuid) {
|
||||
try {
|
||||
return this.getObjectByUuid(uuid)
|
||||
} catch (error) {}
|
||||
|
||||
debug('downloading patch %s', uuid)
|
||||
|
||||
const patchInfo = (await this._getXenUpdates()).patches[uuid]
|
||||
if (!patchInfo) {
|
||||
throw new Error('no such patch ' + uuid)
|
||||
}
|
||||
|
||||
let stream = await httpRequest(patchInfo.url, { agent: httpProxy })
|
||||
stream = await new Promise((resolve, reject) => {
|
||||
const PATCH_RE = /\.xsupdate$/
|
||||
stream.pipe(unzip.Parse()).on('entry', entry => {
|
||||
if (PATCH_RE.test(entry.path)) {
|
||||
entry.length = entry.size
|
||||
resolve(entry)
|
||||
} else {
|
||||
entry.autodrain()
|
||||
}
|
||||
}).on('error', reject)
|
||||
})
|
||||
|
||||
return this.uploadPoolPatch(stream, patchInfo.name)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async _installPoolPatchOnHost (patchUuid, host) {
|
||||
debug('installing patch %s', patchUuid)
|
||||
|
||||
const patch = await this._getOrUploadPoolPatch(patchUuid)
|
||||
await this.call('pool_patch.apply', patch.$ref, host.$ref)
|
||||
}
|
||||
|
||||
async installPoolPatchOnHost (patchUuid, hostId) {
|
||||
return /* await */ this._installPoolPatchOnHost(
|
||||
patchUuid,
|
||||
this.getObject(hostId)
|
||||
)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async installPoolPatchOnAllHosts (patchUuid) {
|
||||
const patch = await this._getOrUploadPoolPatch(patchUuid)
|
||||
|
||||
await this.call('pool_patch.pool_apply', patch.$ref)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async _installPoolPatchOnHostAndRequirements (patch, host, patchesByUuid) {
|
||||
const { requirements } = patch
|
||||
if (requirements.length) {
|
||||
for (const requirementUuid of requirements) {
|
||||
if (this._isPoolPatchInstallableOnHost(requirementUuid, host)) {
|
||||
const requirement = patchesByUuid[requirementUuid]
|
||||
await this._installPoolPatchOnHostAndRequirements(requirement, host, patchesByUuid)
|
||||
|
||||
host = this.getObject(host.$id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this._installPoolPatchOnHost(patch.uuid, host)
|
||||
}
|
||||
|
||||
async installAllPoolPatchesOnHost (hostId) {
|
||||
let host = this.getObject(hostId)
|
||||
|
||||
const installableByUuid = await this._listMissingPoolPatchesOnHost(host)
|
||||
|
||||
// List of all installable patches sorted from the newest to the
|
||||
// oldest.
|
||||
const installable = sortBy(
|
||||
installableByUuid,
|
||||
patch => -Date.parse(patch.date)
|
||||
)
|
||||
|
||||
for (let i = 0, n = installable.length; i < n; ++i) {
|
||||
const patch = installable[i]
|
||||
|
||||
if (this._isPoolPatchInstallableOnHost(patch.uuid, host)) {
|
||||
await this._installPoolPatchOnHostAndRequirements(patch, host, installableByUuid)
|
||||
host = this.getObject(host.$id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async emergencyShutdownHost (hostId) {
|
||||
const host = this.getObject(hostId)
|
||||
const vms = host.$resident_VMs
|
||||
@@ -714,7 +427,7 @@ export default class Xapi extends XapiBase {
|
||||
await this.call('host.evacuate', ref)
|
||||
} catch (error) {
|
||||
if (!force) {
|
||||
await this.call('host.enabled', ref)
|
||||
await this.call('host.enable', ref)
|
||||
|
||||
throw error
|
||||
}
|
||||
@@ -915,7 +628,7 @@ export default class Xapi extends XapiBase {
|
||||
actions_after_crash,
|
||||
actions_after_reboot,
|
||||
actions_after_shutdown,
|
||||
affinity: affinity == null ? 'OpaqueRef:NULL' : affinity,
|
||||
affinity: affinity == null ? NULL_REF : affinity,
|
||||
HVM_boot_params,
|
||||
HVM_boot_policy,
|
||||
is_a_template: asBoolean(is_a_template),
|
||||
@@ -1052,7 +765,8 @@ export default class Xapi extends XapiBase {
|
||||
session_id: this.sessionId,
|
||||
task_id: taskRef,
|
||||
use_compression: compress ? 'true' : 'false'
|
||||
}
|
||||
},
|
||||
rejectUnauthorized: false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1147,7 +861,10 @@ export default class Xapi extends XapiBase {
|
||||
[TAG_BASE_DELTA]: baseVm.uuid
|
||||
}
|
||||
}
|
||||
: vm
|
||||
: {
|
||||
...vm,
|
||||
other_config: omit(vm.other_config, TAG_BASE_DELTA)
|
||||
}
|
||||
}, 'streams', {
|
||||
value: await streams::pAll()
|
||||
})
|
||||
@@ -1196,7 +913,7 @@ export default class Xapi extends XapiBase {
|
||||
is_a_template: false
|
||||
})
|
||||
)
|
||||
$onFailure(() => this._deleteVm(vm))
|
||||
$onFailure(() => this._deleteVm(vm, true))
|
||||
|
||||
await Promise.all([
|
||||
this._setObjectProperties(vm, {
|
||||
@@ -1286,7 +1003,7 @@ export default class Xapi extends XapiBase {
|
||||
// Create VIFs.
|
||||
Promise.all(mapToArray(delta.vifs, vif => {
|
||||
const network =
|
||||
this.getObject(vif.$network$uuid, null) ||
|
||||
vif.$network$uuid && this.getObject(vif.$network$uuid, null) ||
|
||||
networksOnPoolMasterByDevice[vif.device] ||
|
||||
defaultNetwork
|
||||
|
||||
@@ -1326,13 +1043,13 @@ export default class Xapi extends XapiBase {
|
||||
}) {
|
||||
// VDIs/SRs mapping
|
||||
const vdis = {}
|
||||
const defaultSrRef = host.$pool.$default_SR.$ref
|
||||
const defaultSr = host.$pool.$default_SR
|
||||
for (const vbd of vm.$VBDs) {
|
||||
const vdi = vbd.$VDI
|
||||
if (vbd.type === 'Disk') {
|
||||
vdis[vdi.$ref] = mapVdisSrs && mapVdisSrs[vdi.$id]
|
||||
? hostXapi.getObject(mapVdisSrs[vdi.$id]).$ref
|
||||
: defaultSrRef
|
||||
: defaultSr.$ref // Will error if there are no default SR.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1418,16 +1135,112 @@ export default class Xapi extends XapiBase {
|
||||
return vmRef
|
||||
}
|
||||
|
||||
@deferrable.onFailure
|
||||
async _importOvaVm ($onFailure, stream, {
|
||||
descriptionLabel,
|
||||
disks,
|
||||
memory,
|
||||
nameLabel,
|
||||
networks,
|
||||
nCpus
|
||||
}, sr) {
|
||||
// 1. Create VM.
|
||||
const vm = await this._getOrWaitObject(
|
||||
await this._createVmRecord({
|
||||
...OTHER_CONFIG_TEMPLATE,
|
||||
memory_dynamic_max: memory,
|
||||
memory_dynamic_min: memory,
|
||||
memory_static_max: memory,
|
||||
name_description: descriptionLabel,
|
||||
name_label: nameLabel,
|
||||
VCPUs_at_startup: nCpus,
|
||||
VCPUs_max: nCpus
|
||||
})
|
||||
)
|
||||
$onFailure(() => this._deleteVm(vm, true))
|
||||
// Disable start and change the VM name label during import.
|
||||
await Promise.all([
|
||||
this.addForbiddenOperationToVm(vm.$id, 'start', 'OVA import in progress...'),
|
||||
this._setObjectProperties(vm, { name_label: `[Importing...] ${nameLabel}` })
|
||||
])
|
||||
|
||||
// 2. Create VDIs & Vifs.
|
||||
const vdis = {}
|
||||
const vifDevices = await this.call('VM.get_allowed_VIF_devices', vm.$ref)
|
||||
await Promise.all(
|
||||
map(disks, async disk => {
|
||||
const vdi = vdis[disk.path] = await this.createVdi(disk.capacity, {
|
||||
name_description: disk.descriptionLabel,
|
||||
name_label: disk.nameLabel,
|
||||
sr: sr.$ref
|
||||
})
|
||||
$onFailure(() => this._deleteVdi(vdi)::pCatch(noop))
|
||||
|
||||
return this._createVbd(vm, vdi, { position: disk.position })
|
||||
}).concat(map(networks, (networkId, i) => (
|
||||
this._createVif(vm, this.getObject(networkId), {
|
||||
device: vifDevices[i]
|
||||
})
|
||||
)))
|
||||
)
|
||||
|
||||
// 3. Import VDIs contents.
|
||||
await new Promise((resolve, reject) => {
|
||||
const extract = tarStream.extract()
|
||||
|
||||
stream.on('error', reject)
|
||||
|
||||
extract.on('finish', resolve)
|
||||
extract.on('error', reject)
|
||||
extract.on('entry', async (entry, stream, cb) => {
|
||||
// Not a disk to import.
|
||||
const vdi = vdis[entry.name]
|
||||
if (!vdi) {
|
||||
stream.on('end', cb)
|
||||
stream.resume()
|
||||
return
|
||||
}
|
||||
|
||||
const vhdStream = await vmdkToVhd(stream)
|
||||
await this._importVdiContent(vdi, vhdStream, VDI_FORMAT_RAW)
|
||||
|
||||
// See: https://github.com/mafintosh/tar-stream#extracting
|
||||
// No import parallelization.
|
||||
cb()
|
||||
})
|
||||
stream.pipe(extract)
|
||||
})
|
||||
|
||||
// Enable start and restore the VM name label after import.
|
||||
await Promise.all([
|
||||
this.removeForbiddenOperationFromVm(vm.$id, 'start'),
|
||||
this._setObjectProperties(vm, { name_label: nameLabel })
|
||||
])
|
||||
return vm
|
||||
}
|
||||
|
||||
// TODO: an XVA can contain multiple VMs
|
||||
async importVm (stream, {
|
||||
data,
|
||||
onlyMetadata = false,
|
||||
srId
|
||||
srId,
|
||||
type = 'xva'
|
||||
} = {}) {
|
||||
return /* await */ this._getOrWaitObject(await this._importVm(
|
||||
stream,
|
||||
srId && this.getObject(srId),
|
||||
onlyMetadata
|
||||
))
|
||||
const sr = srId && this.getObject(srId)
|
||||
|
||||
if (type === 'xva') {
|
||||
return /* await */ this._getOrWaitObject(await this._importVm(
|
||||
stream,
|
||||
sr,
|
||||
onlyMetadata
|
||||
))
|
||||
}
|
||||
|
||||
if (type === 'ova') {
|
||||
return this._getOrWaitObject(await this._importOvaVm(stream, data, sr))
|
||||
}
|
||||
|
||||
throw new Error(`unsupported type: '${type}'`)
|
||||
}
|
||||
|
||||
async migrateVm (vmId, hostXapi, hostId, {
|
||||
@@ -1534,7 +1347,7 @@ export default class Xapi extends XapiBase {
|
||||
await this._startVm(this.getObject(vmId))
|
||||
} catch (e) {
|
||||
if (e.code === 'OPERATION_BLOCKED') {
|
||||
throw new ForbiddenOperation('Start', e.params[1])
|
||||
throw forbiddenOperation('Start', e.params[1])
|
||||
}
|
||||
|
||||
throw e
|
||||
@@ -1829,9 +1642,16 @@ export default class Xapi extends XapiBase {
|
||||
await this.call('VBD.plug', vbdId)
|
||||
}
|
||||
|
||||
_disconnectVbd (vbd) {
|
||||
async _disconnectVbd (vbd) {
|
||||
// TODO: check if VBD is attached before
|
||||
return this.call('VBD.unplug_force', vbd.$ref)
|
||||
try {
|
||||
await this.call('VBD.unplug_force', vbd.$ref)
|
||||
} catch (error) {
|
||||
if (error.code === 'VBD_NOT_UNPLUGGABLE') {
|
||||
await this.call('VBD.set_unpluggable', vbd.$ref, true)
|
||||
return this.call('VBD.unplug_force', vbd.$ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async disconnectVbd (vbdId) {
|
||||
@@ -1987,15 +1807,14 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
async _createVif (vm, network, {
|
||||
mac = '',
|
||||
mtu = 1500,
|
||||
position = undefined,
|
||||
|
||||
currently_attached = true,
|
||||
device = position != null ? String(position) : undefined,
|
||||
ipv4_allowed = undefined,
|
||||
ipv6_allowed = undefined,
|
||||
locking_mode = undefined,
|
||||
MAC = mac,
|
||||
MTU = mtu,
|
||||
other_config = {},
|
||||
qos_algorithm_params = {},
|
||||
qos_algorithm_type = ''
|
||||
@@ -2012,7 +1831,7 @@ export default class Xapi extends XapiBase {
|
||||
ipv6_allowed,
|
||||
locking_mode,
|
||||
MAC,
|
||||
MTU: asInteger(MTU),
|
||||
MTU: asInteger(network.MTU),
|
||||
network: network.$ref,
|
||||
other_config,
|
||||
qos_algorithm_params,
|
||||
@@ -2020,18 +1839,13 @@ export default class Xapi extends XapiBase {
|
||||
VM: vm.$ref
|
||||
}))
|
||||
|
||||
if (isVmRunning(vm)) {
|
||||
if (currently_attached && isVmRunning(vm)) {
|
||||
await this.call('VIF.plug', vifRef)
|
||||
}
|
||||
|
||||
return vifRef
|
||||
}
|
||||
|
||||
// TODO: check whether the VIF was unplugged before.
|
||||
async _deleteVif (vif) {
|
||||
await this.call('VIF.destroy', vif.$ref)
|
||||
}
|
||||
|
||||
async createVif (vmId, networkId, opts = undefined) {
|
||||
return /* await */ this._getOrWaitObject(
|
||||
await this._createVif(
|
||||
@@ -2042,10 +1856,6 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
async deleteVif (vifId) {
|
||||
await this._deleteVif(this.getObject(vifId))
|
||||
}
|
||||
|
||||
async createNetwork ({
|
||||
name,
|
||||
description = 'Created with Xen Orchestra',
|
||||
@@ -2066,10 +1876,69 @@ export default class Xapi extends XapiBase {
|
||||
return this._getOrWaitObject(networkRef)
|
||||
}
|
||||
|
||||
async editPif (
|
||||
pifId,
|
||||
{ vlan }
|
||||
) {
|
||||
const pif = this.getObject(pifId)
|
||||
const physPif = find(this.objects.all, obj => (
|
||||
obj.$type === 'pif' &&
|
||||
(obj.physical || !isEmpty(obj.bond_master_of)) &&
|
||||
obj.$pool === pif.$pool &&
|
||||
obj.device === pif.device
|
||||
))
|
||||
|
||||
if (!physPif) {
|
||||
throw new Error('PIF not found')
|
||||
}
|
||||
|
||||
const pifs = this.getObject(pif.network).$PIFs
|
||||
|
||||
const wasAttached = {}
|
||||
forEach(pifs, pif => {
|
||||
wasAttached[pif.host] = pif.currently_attached
|
||||
})
|
||||
|
||||
const vlans = uniq(mapToArray(pifs, pif => pif.VLAN_master_of))
|
||||
await Promise.all(
|
||||
mapToArray(vlans, vlan => vlan !== NULL_REF && this.call('VLAN.destroy', vlan))
|
||||
)
|
||||
|
||||
const newPifs = await this.call('pool.create_VLAN_from_PIF', physPif.$ref, pif.network, asInteger(vlan))
|
||||
await Promise.all(
|
||||
mapToArray(newPifs, pifRef =>
|
||||
!wasAttached[this.getObject(pifRef).host] && this.call('PIF.unplug', pifRef)::pCatch(noop)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async createBondedNetwork ({
|
||||
bondMode,
|
||||
mac,
|
||||
pifIds,
|
||||
...params
|
||||
}) {
|
||||
const network = await this.createNetwork(params)
|
||||
// TODO: test and confirm:
|
||||
// Bond.create is called here with PIFs from one host but XAPI should then replicate the
|
||||
// bond on each host in the same pool with the corresponding PIFs (ie same interface names?).
|
||||
await this.call('Bond.create', network.$ref, map(pifIds, pifId => this.getObject(pifId).$ref), mac, bondMode)
|
||||
|
||||
return network
|
||||
}
|
||||
|
||||
async deleteNetwork (networkId) {
|
||||
const network = this.getObject(networkId)
|
||||
const pifs = network.$PIFs
|
||||
|
||||
const vlans = uniq(mapToArray(pifs, pif => pif.VLAN_master_of))
|
||||
await Promise.all(
|
||||
mapToArray(network.$PIFs, (pif) => this.call('VLAN.destroy', pif.$VLAN_master_of.$ref))
|
||||
mapToArray(vlans, vlan => vlan !== NULL_REF && this.call('VLAN.destroy', vlan))
|
||||
)
|
||||
|
||||
const bonds = uniq(flatten(mapToArray(pifs, pif => pif.bond_master_of)))
|
||||
await Promise.all(
|
||||
mapToArray(bonds, bond => this.call('Bond.destroy', bond))
|
||||
)
|
||||
|
||||
await this.call('network.destroy', network.$ref)
|
||||
@@ -2136,6 +2005,7 @@ export default class Xapi extends XapiBase {
|
||||
sruuid: sr.uuid,
|
||||
configuration: config
|
||||
})
|
||||
await this.registerDockerContainer(vmId)
|
||||
}
|
||||
|
||||
// Generic Config Drive
|
||||
@@ -2147,7 +2017,7 @@ export default class Xapi extends XapiBase {
|
||||
const buffer = fatfsBufferInit()
|
||||
const vdi = await this.createVdi(buffer.length, { name_label: 'XO CloudConfigDrive', name_description: undefined, sr: sr.$ref })
|
||||
// Then, generate a FAT fs
|
||||
const fs = fatfs.createFileSystem(fatfsBuffer(buffer))::promisifyAll()
|
||||
const fs = promisifyAll(fatfs.createFileSystem(fatfsBuffer(buffer)))
|
||||
// Create Cloud config folders
|
||||
await fs.mkdir('openstack')
|
||||
await fs.mkdir('openstack/latest')
|
||||
|
||||
0
src/xapi/mixins/.index-modules
Normal file
0
src/xapi/mixins/.index-modules
Normal file
@@ -1,10 +1,60 @@
|
||||
import {
|
||||
makeEditObject
|
||||
} from '../utils'
|
||||
import { isEmpty } from '../../utils'
|
||||
|
||||
import { makeEditObject } from '../utils'
|
||||
|
||||
export default {
|
||||
async _connectVif (vif) {
|
||||
await this.call('VIF.plug', vif.$ref)
|
||||
},
|
||||
async connectVif (vifId) {
|
||||
await this._connectVif(this.getObject(vifId))
|
||||
},
|
||||
async _deleteVif (vif) {
|
||||
await this.call('VIF.destroy', vif.$ref)
|
||||
},
|
||||
async deleteVif (vifId) {
|
||||
const vif = this.getObject(vifId)
|
||||
if (vif.currently_attached) {
|
||||
await this._disconnectVif(vif)
|
||||
}
|
||||
await this._deleteVif(vif)
|
||||
},
|
||||
async _disconnectVif (vif) {
|
||||
await this.call('VIF.unplug_force', vif.$ref)
|
||||
},
|
||||
async disconnectVif (vifId) {
|
||||
await this._disconnectVif(this.getObject(vifId))
|
||||
},
|
||||
editVif: makeEditObject({
|
||||
ipv4Allowed: true,
|
||||
ipv6Allowed: true
|
||||
ipv4Allowed: {
|
||||
get: true,
|
||||
set: [
|
||||
'ipv4Allowed',
|
||||
function (value, vif) {
|
||||
const lockingMode = isEmpty(value) && isEmpty(vif.ipv6_allowed)
|
||||
? 'network_default'
|
||||
: 'locked'
|
||||
|
||||
if (lockingMode !== vif.locking_mode) {
|
||||
return this._set('locking_mode', lockingMode)
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
ipv6Allowed: {
|
||||
get: true,
|
||||
set: [
|
||||
'ipv6Allowed',
|
||||
function (value, vif) {
|
||||
const lockingMode = isEmpty(value) && isEmpty(vif.ipv4_allowed)
|
||||
? 'network_default'
|
||||
: 'locked'
|
||||
|
||||
if (lockingMode !== vif.locking_mode) {
|
||||
return this._set('locking_mode', lockingMode)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
294
src/xapi/mixins/patching.js
Normal file
294
src/xapi/mixins/patching.js
Normal file
@@ -0,0 +1,294 @@
|
||||
import filter from 'lodash/filter'
|
||||
import includes from 'lodash/includes'
|
||||
import sortBy from 'lodash/sortBy'
|
||||
import unzip from 'julien-f-unzip'
|
||||
|
||||
import httpProxy from '../../http-proxy'
|
||||
import httpRequest from '../../http-request'
|
||||
import { debounce } from '../../decorators'
|
||||
import {
|
||||
createRawObject,
|
||||
ensureArray,
|
||||
forEach,
|
||||
mapToArray,
|
||||
parseXml
|
||||
} from '../../utils'
|
||||
|
||||
import {
|
||||
debug,
|
||||
put
|
||||
} from '../utils'
|
||||
|
||||
export default {
|
||||
// FIXME: should be static
|
||||
@debounce(24 * 60 * 60 * 1000)
|
||||
async _getXenUpdates () {
|
||||
const { readAll, statusCode } = await httpRequest(
|
||||
'http://updates.xensource.com/XenServer/updates.xml',
|
||||
{ agent: httpProxy }
|
||||
)
|
||||
|
||||
if (statusCode !== 200) {
|
||||
throw new Error('cannot fetch patches list from Citrix')
|
||||
}
|
||||
|
||||
const data = parseXml(await readAll()).patchdata
|
||||
|
||||
const patches = createRawObject()
|
||||
forEach(data.patches.patch, patch => {
|
||||
patches[patch.uuid] = {
|
||||
date: patch.timestamp,
|
||||
description: patch['name-description'],
|
||||
documentationUrl: patch.url,
|
||||
guidance: patch['after-apply-guidance'],
|
||||
name: patch['name-label'],
|
||||
url: patch['patch-url'],
|
||||
uuid: patch.uuid,
|
||||
conflicts: mapToArray(ensureArray(patch.conflictingpatches), patch => {
|
||||
return patch.conflictingpatch.uuid
|
||||
}),
|
||||
requirements: mapToArray(ensureArray(patch.requiredpatches), patch => {
|
||||
return patch.requiredpatch.uuid
|
||||
})
|
||||
// TODO: what does it mean, should we handle it?
|
||||
// version: patch.version,
|
||||
}
|
||||
if (patches[patch.uuid].conflicts[0] === undefined) {
|
||||
patches[patch.uuid].conflicts.length = 0
|
||||
}
|
||||
if (patches[patch.uuid].requirements[0] === undefined) {
|
||||
patches[patch.uuid].requirements.length = 0
|
||||
}
|
||||
})
|
||||
|
||||
const resolveVersionPatches = function (uuids) {
|
||||
const versionPatches = createRawObject()
|
||||
|
||||
forEach(ensureArray(uuids), ({uuid}) => {
|
||||
versionPatches[uuid] = patches[uuid]
|
||||
})
|
||||
|
||||
return versionPatches
|
||||
}
|
||||
|
||||
const versions = createRawObject()
|
||||
let latestVersion
|
||||
forEach(data.serverversions.version, version => {
|
||||
versions[version.value] = {
|
||||
date: version.timestamp,
|
||||
name: version.name,
|
||||
id: version.value,
|
||||
documentationUrl: version.url,
|
||||
patches: resolveVersionPatches(version.patch)
|
||||
}
|
||||
|
||||
if (version.latest) {
|
||||
latestVersion = versions[version.value]
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
patches,
|
||||
latestVersion,
|
||||
versions
|
||||
}
|
||||
},
|
||||
|
||||
// =================================================================
|
||||
|
||||
// Returns installed and not installed patches for a given host.
|
||||
async _getPoolPatchesForHost (host) {
|
||||
const versions = (await this._getXenUpdates()).versions
|
||||
|
||||
const hostVersions = host.software_version
|
||||
const version =
|
||||
versions[hostVersions.product_version] ||
|
||||
versions[hostVersions.product_version_text]
|
||||
|
||||
return version
|
||||
? version.patches
|
||||
: []
|
||||
},
|
||||
|
||||
_getInstalledPoolPatchesOnHost (host) {
|
||||
const installed = createRawObject()
|
||||
|
||||
forEach(host.$patches, hostPatch => {
|
||||
installed[hostPatch.$pool_patch.uuid] = true
|
||||
})
|
||||
|
||||
return installed
|
||||
},
|
||||
|
||||
async _listMissingPoolPatchesOnHost (host) {
|
||||
const all = await this._getPoolPatchesForHost(host)
|
||||
const installed = this._getInstalledPoolPatchesOnHost(host)
|
||||
|
||||
const installable = createRawObject()
|
||||
forEach(all, (patch, uuid) => {
|
||||
if (installed[uuid]) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const uuid of patch.conflicts) {
|
||||
if (uuid in installed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
installable[uuid] = patch
|
||||
})
|
||||
|
||||
return installable
|
||||
},
|
||||
|
||||
async listMissingPoolPatchesOnHost (hostId) {
|
||||
// Returns an array to not break compatibility.
|
||||
return mapToArray(
|
||||
await this._listMissingPoolPatchesOnHost(this.getObject(hostId))
|
||||
)
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
_isPoolPatchInstallableOnHost (patchUuid, host) {
|
||||
const installed = this._getInstalledPoolPatchesOnHost(host)
|
||||
|
||||
if (installed[patchUuid]) {
|
||||
return false
|
||||
}
|
||||
|
||||
let installable = true
|
||||
|
||||
forEach(installed, patch => {
|
||||
if (includes(patch.conflicts, patchUuid)) {
|
||||
installable = false
|
||||
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
return installable
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async uploadPoolPatch (stream, patchName = 'unknown') {
|
||||
const taskRef = await this._createTask('Patch upload', patchName)
|
||||
|
||||
const task = this._watchTask(taskRef)
|
||||
const [ patchRef ] = await Promise.all([
|
||||
task,
|
||||
put(stream, {
|
||||
hostname: this.pool.$master.address,
|
||||
path: '/pool_patch_upload',
|
||||
query: {
|
||||
session_id: this.sessionId,
|
||||
task_id: taskRef
|
||||
}
|
||||
}, task)
|
||||
])
|
||||
|
||||
return this._getOrWaitObject(patchRef)
|
||||
},
|
||||
|
||||
async _getOrUploadPoolPatch (uuid) {
|
||||
try {
|
||||
return this.getObjectByUuid(uuid)
|
||||
} catch (error) {}
|
||||
|
||||
debug('downloading patch %s', uuid)
|
||||
|
||||
const patchInfo = (await this._getXenUpdates()).patches[uuid]
|
||||
if (!patchInfo) {
|
||||
throw new Error('no such patch ' + uuid)
|
||||
}
|
||||
|
||||
let stream = await httpRequest(patchInfo.url, { agent: httpProxy })
|
||||
stream = await new Promise((resolve, reject) => {
|
||||
const PATCH_RE = /\.xsupdate$/
|
||||
stream.pipe(unzip.Parse()).on('entry', entry => {
|
||||
if (PATCH_RE.test(entry.path)) {
|
||||
entry.length = entry.size
|
||||
resolve(entry)
|
||||
} else {
|
||||
entry.autodrain()
|
||||
}
|
||||
}).on('error', reject)
|
||||
})
|
||||
|
||||
return this.uploadPoolPatch(stream, patchInfo.name)
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async _installPoolPatchOnHost (patchUuid, host) {
|
||||
debug('installing patch %s', patchUuid)
|
||||
|
||||
const patch = await this._getOrUploadPoolPatch(patchUuid)
|
||||
await this.call('pool_patch.apply', patch.$ref, host.$ref)
|
||||
},
|
||||
|
||||
async installPoolPatchOnHost (patchUuid, hostId) {
|
||||
return /* await */ this._installPoolPatchOnHost(
|
||||
patchUuid,
|
||||
this.getObject(hostId)
|
||||
)
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async installPoolPatchOnAllHosts (patchUuid) {
|
||||
const patch = await this._getOrUploadPoolPatch(patchUuid)
|
||||
|
||||
await this.call('pool_patch.pool_apply', patch.$ref)
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async _installPoolPatchOnHostAndRequirements (patch, host, patchesByUuid) {
|
||||
const { requirements } = patch
|
||||
if (requirements.length) {
|
||||
for (const requirementUuid of requirements) {
|
||||
if (this._isPoolPatchInstallableOnHost(requirementUuid, host)) {
|
||||
const requirement = patchesByUuid[requirementUuid]
|
||||
await this._installPoolPatchOnHostAndRequirements(requirement, host, patchesByUuid)
|
||||
|
||||
host = this.getObject(host.$id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this._installPoolPatchOnHost(patch.uuid, host)
|
||||
},
|
||||
|
||||
async installAllPoolPatchesOnHost (hostId) {
|
||||
let host = this.getObject(hostId)
|
||||
|
||||
const installableByUuid = await this._listMissingPoolPatchesOnHost(host)
|
||||
|
||||
// List of all installable patches sorted from the newest to the
|
||||
// oldest.
|
||||
const installable = sortBy(
|
||||
installableByUuid,
|
||||
patch => -Date.parse(patch.date)
|
||||
)
|
||||
|
||||
for (let i = 0, n = installable.length; i < n; ++i) {
|
||||
const patch = installable[i]
|
||||
|
||||
if (this._isPoolPatchInstallableOnHost(patch.uuid, host)) {
|
||||
await this._installPoolPatchOnHostAndRequirements(patch, host, installableByUuid)
|
||||
host = this.getObject(host.$id)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async installAllPoolPatchesOnAllHosts () {
|
||||
await this.installAllPoolPatchesOnHost(this.pool.master)
|
||||
await Promise.all(mapToArray(
|
||||
filter(this.objects.all, { $type: 'host' }),
|
||||
host => this.installAllPoolPatchesOnHost(host.$id)
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import deferrable from 'golike-defer'
|
||||
import find from 'lodash/find'
|
||||
import gte from 'lodash/gte'
|
||||
import lte from 'lodash/lte'
|
||||
@@ -18,12 +19,11 @@ import {
|
||||
|
||||
export default {
|
||||
// TODO: clean up on error.
|
||||
async createVm (templateId, {
|
||||
@deferrable.onFailure
|
||||
async createVm ($onFailure, templateId, {
|
||||
name_label, // deprecated
|
||||
nameLabel = name_label, // eslint-disable-line camelcase
|
||||
|
||||
bootAfterCreate = false,
|
||||
|
||||
clone = true,
|
||||
installRepository = undefined,
|
||||
vdis = undefined,
|
||||
@@ -34,7 +34,7 @@ export default {
|
||||
cloudConfig = undefined,
|
||||
|
||||
...props
|
||||
} = {}) {
|
||||
} = {}, checkLimits) {
|
||||
const installMethod = (() => {
|
||||
if (installRepository == null) {
|
||||
return 'none'
|
||||
@@ -50,23 +50,23 @@ export default {
|
||||
const template = this.getObject(templateId)
|
||||
|
||||
// Clones the template.
|
||||
let vm = await this._getOrWaitObject(
|
||||
await this[clone ? '_cloneVm' : '_copyVm'](template, nameLabel)
|
||||
)
|
||||
const vmRef = await this[clone ? '_cloneVm' : '_copyVm'](template, nameLabel)
|
||||
$onFailure(() => this.deleteVm(vmRef, true)::pCatch(noop))
|
||||
|
||||
// TODO: copy BIOS strings?
|
||||
|
||||
// Removes disks from the provision XML, we will create them by
|
||||
// ourselves.
|
||||
await this.call('VM.remove_from_other_config', vm.$ref, 'disks')::pCatch(noop)
|
||||
await this.call('VM.remove_from_other_config', vmRef, 'disks')::pCatch(noop)
|
||||
|
||||
// Creates the VDIs and executes the initial steps of the
|
||||
// installation.
|
||||
await this.call('VM.provision', vm.$ref)
|
||||
await this.call('VM.provision', vmRef)
|
||||
|
||||
let vm = await this._getOrWaitObject(vmRef)
|
||||
|
||||
// Set VMs params.
|
||||
// TODO: checkLimits
|
||||
this._editVm(vm, props)
|
||||
await this._editVm(vm, props, checkLimits)
|
||||
|
||||
// Sets boot parameters.
|
||||
{
|
||||
@@ -112,8 +112,12 @@ export default {
|
||||
})
|
||||
}
|
||||
|
||||
let nDisks = 0
|
||||
|
||||
// Modify existing (previous template) disks if necessary
|
||||
existingVdis && await Promise.all(mapToArray(existingVdis, async ({ size, $SR: srId, ...properties }, userdevice) => {
|
||||
++nDisks
|
||||
|
||||
const vbd = find(vm.$VBDs, { userdevice })
|
||||
if (!vbd) {
|
||||
return
|
||||
@@ -140,6 +144,8 @@ export default {
|
||||
if (vdis) {
|
||||
const devices = await this.call('VM.get_allowed_VBD_devices', vm.$ref)
|
||||
await Promise.all(mapToArray(vdis, (vdiDescription, i) => {
|
||||
++nDisks
|
||||
|
||||
return this._createVdi(
|
||||
vdiDescription.size, // FIXME: Should not be done in Xapi.
|
||||
{
|
||||
@@ -168,6 +174,8 @@ export default {
|
||||
vm,
|
||||
this.getObject(vif.network),
|
||||
{
|
||||
ipv4_allowed: vif.ipv4_allowed,
|
||||
ipv6_allowed: vif.ipv6_allowed,
|
||||
device: devices[index],
|
||||
mac: vif.mac,
|
||||
mtu: vif.mtu
|
||||
@@ -179,13 +187,16 @@ export default {
|
||||
|
||||
if (cloudConfig != null) {
|
||||
// Refresh the record.
|
||||
vm = this.getObject(vm.$id)
|
||||
vm = await this._waitObject(vm.$id, vm => vm.VBDs.length === nDisks)
|
||||
|
||||
// Find the SR of the first VDI.
|
||||
let srRef
|
||||
forEach(vm.$VBDs, vbd => {
|
||||
const vdi = vbd.$VDI
|
||||
if (vdi) {
|
||||
let vdi
|
||||
if (
|
||||
vbd.type === 'Disk' &&
|
||||
(vdi = vbd.$VDI)
|
||||
) {
|
||||
srRef = vdi.SR
|
||||
return false
|
||||
}
|
||||
@@ -197,10 +208,6 @@ export default {
|
||||
await this[method](vm.$id, srRef, cloudConfig)
|
||||
}
|
||||
|
||||
if (bootAfterCreate) {
|
||||
this._startVm(vm)::pCatch(noop)
|
||||
}
|
||||
|
||||
return this._waitObject(vm.$id)
|
||||
},
|
||||
|
||||
@@ -243,7 +250,6 @@ export default {
|
||||
},
|
||||
|
||||
cpuCap: {
|
||||
addToLimits: true,
|
||||
get: vm => vm.VCPUs_params.cap && +vm.VCPUs_params.cap,
|
||||
set (cap, vm) {
|
||||
return this._updateObjectMapProperty(vm, 'VCPUs_params', { cap })
|
||||
@@ -260,7 +266,6 @@ export default {
|
||||
},
|
||||
|
||||
cpuWeight: {
|
||||
addToLimits: true,
|
||||
get: vm => vm.VCPUs_params.weight && +vm.VCPUs_params.weight,
|
||||
set (weight, vm) {
|
||||
return this._updateObjectMapProperty(vm, 'VCPUs_params', { weight })
|
||||
@@ -285,6 +290,7 @@ export default {
|
||||
memory: 'memoryMax',
|
||||
memoryMax: {
|
||||
addToLimits: true,
|
||||
limitName: 'memory',
|
||||
constraints: {
|
||||
memoryMin: lte,
|
||||
memoryStaticMax: gte
|
||||
@@ -307,10 +313,20 @@ export default {
|
||||
|
||||
nameLabel: true,
|
||||
|
||||
PV_args: true
|
||||
PV_args: true,
|
||||
|
||||
tags: true
|
||||
}),
|
||||
|
||||
async editVm (id, props) {
|
||||
return /* await */ this._editVm(this.getObject(id), props)
|
||||
},
|
||||
|
||||
async revertVm (snapshotId, snapshotBefore = true) {
|
||||
const snapshot = this.getObject(snapshotId)
|
||||
if (snapshotBefore) {
|
||||
await this._snapshotVm(snapshot.$snapshot_of)
|
||||
}
|
||||
return this.call('VM.revert', snapshot.$ref)
|
||||
}
|
||||
}
|
||||
|
||||
53
src/xapi/other-config-template.js
Normal file
53
src/xapi/other-config-template.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { NULL_REF } from './utils'
|
||||
|
||||
const OTHER_CONFIG_TEMPLATE = {
|
||||
actions_after_crash: 'restart',
|
||||
actions_after_reboot: 'restart',
|
||||
actions_after_shutdown: 'destroy',
|
||||
affinity: null,
|
||||
blocked_operations: {},
|
||||
ha_always_run: false,
|
||||
HVM_boot_params: {
|
||||
order: 'cdn'
|
||||
},
|
||||
HVM_boot_policy: 'BIOS order',
|
||||
HVM_shadow_multiplier: 1,
|
||||
is_a_template: false,
|
||||
memory_dynamic_max: 4294967296,
|
||||
memory_dynamic_min: 4294967296,
|
||||
memory_static_max: 4294967296,
|
||||
memory_static_min: 134217728,
|
||||
order: 0,
|
||||
other_config: {
|
||||
vgpu_pci: '',
|
||||
base_template_name: 'Other install media',
|
||||
mac_seed: '5e88eb6a-d680-c47f-a94a-028886971ba4',
|
||||
'install-methods': 'cdrom'
|
||||
},
|
||||
PCI_bus: '',
|
||||
platform: {
|
||||
timeoffset: '0',
|
||||
nx: 'true',
|
||||
acpi: '1',
|
||||
apic: 'true',
|
||||
pae: 'true',
|
||||
hpet: 'true',
|
||||
viridian: 'true'
|
||||
},
|
||||
protection_policy: NULL_REF,
|
||||
PV_args: '',
|
||||
PV_bootloader: '',
|
||||
PV_bootloader_args: '',
|
||||
PV_kernel: '',
|
||||
PV_legacy_args: '',
|
||||
PV_ramdisk: '',
|
||||
recommendations: '<restrictions><restriction field="memory-static-max" max="137438953472" /><restriction field="vcpus-max" max="32" /><restriction property="number-of-vbds" max="255" /><restriction property="number-of-vifs" max="7" /><restriction field="has-vendor-device" value="false" /></restrictions>',
|
||||
shutdown_delay: 0,
|
||||
start_delay: 0,
|
||||
user_version: 1,
|
||||
VCPUs_at_startup: 1,
|
||||
VCPUs_max: 1,
|
||||
VCPUs_params: {},
|
||||
version: 0
|
||||
}
|
||||
export { OTHER_CONFIG_TEMPLATE as default }
|
||||
@@ -1,9 +1,12 @@
|
||||
// import isFinite from 'lodash/isFinite'
|
||||
import camelCase from 'lodash/camelCase'
|
||||
import createDebug from 'debug'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
import isPlainObject from 'lodash/isPlainObject'
|
||||
import pickBy from 'lodash/pickBy'
|
||||
import { utcFormat, utcParse } from 'd3-time-format'
|
||||
|
||||
import httpRequest from '../http-request'
|
||||
import {
|
||||
camelToSnakeCase,
|
||||
createRawObject,
|
||||
@@ -12,11 +15,11 @@ import {
|
||||
isBoolean,
|
||||
isFunction,
|
||||
isInteger,
|
||||
isObject,
|
||||
isString,
|
||||
map,
|
||||
mapToArray,
|
||||
noop
|
||||
noop,
|
||||
pFinally
|
||||
} from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
@@ -48,7 +51,10 @@ export const prepareXapiParam = param => {
|
||||
if (isBoolean(param)) {
|
||||
return asBoolean(param)
|
||||
}
|
||||
if (isObject(param)) {
|
||||
if (isArray(param)) {
|
||||
return map(param, prepareXapiParam)
|
||||
}
|
||||
if (isPlainObject(param)) {
|
||||
return map(filterUndefineds(param), prepareXapiParam)
|
||||
}
|
||||
|
||||
@@ -57,6 +63,10 @@ export const prepareXapiParam = param => {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const debug = createDebug('xo:xapi')
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const OPAQUE_REF_RE = /OpaqueRef:[0-9a-z-]+/
|
||||
export const extractOpaqueRef = str => {
|
||||
const matches = OPAQUE_REF_RE.exec(str)
|
||||
@@ -85,6 +95,7 @@ forEach([
|
||||
'VDI',
|
||||
'VGPU',
|
||||
'VGPU_type',
|
||||
'VIF',
|
||||
'VLAN',
|
||||
'VM',
|
||||
'VM_appliance',
|
||||
@@ -169,8 +180,9 @@ export const makeEditObject = specs => {
|
||||
if (isString(set)) {
|
||||
const index = set.indexOf('.')
|
||||
if (index === -1) {
|
||||
const prop = camelToSnakeCase(set)
|
||||
return function (value) {
|
||||
return this._set(set, value)
|
||||
return this._set(prop, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +225,9 @@ export const makeEditObject = specs => {
|
||||
if (spec.addToLimits === true) {
|
||||
spec.addToLimits = _DEFAULT_ADD_TO_LIMITS
|
||||
}
|
||||
if (!spec.limitName) {
|
||||
spec.limitName = name
|
||||
}
|
||||
|
||||
forEach(spec.constraints, (constraint, constraintName) => {
|
||||
if (!isFunction(constraint)) {
|
||||
@@ -291,7 +306,7 @@ export const makeEditObject = specs => {
|
||||
|
||||
let addToLimits
|
||||
if (limits && (addToLimits = spec.addToLimits)) {
|
||||
limits[name] = addToLimits(value, current)
|
||||
limits[spec.limitName] = addToLimits(value, current)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,14 +317,20 @@ export const makeEditObject = specs => {
|
||||
const cbs = []
|
||||
|
||||
forEach(constraints, (constraint, constraintName) => {
|
||||
// This constraint value is already defined: bypass the constraint.
|
||||
if (values[constraintName] != null) {
|
||||
return
|
||||
}
|
||||
// Before setting a property to a new value, if the constraint check fails (e.g. memoryMin > memoryMax):
|
||||
// - if the user wants to set the constraint (ie constraintNewValue is defined):
|
||||
// constraint <-- constraintNewValue THEN property <-- value (e.g. memoryMax <-- 2048 THEN memoryMin <-- 1024)
|
||||
// - if the user DOES NOT want to set the constraint (ie constraintNewValue is NOT defined):
|
||||
// constraint <-- value THEN property <-- value (e.g. memoryMax <-- 1024 THEN memoryMin <-- 1024)
|
||||
// FIXME: Some values combinations will lead to setting the same property twice, which is not perfect but works for now.
|
||||
const constraintCurrentValue = specs[constraintName].get(object)
|
||||
const constraintNewValue = values[constraintName]
|
||||
|
||||
if (!constraint(specs[constraintName].get(object), value)) {
|
||||
const cb = set(value, constraintName)
|
||||
cbs.push(cb)
|
||||
if (!constraint(constraintCurrentValue, value)) {
|
||||
const cb = set(constraintNewValue == null ? value : constraintNewValue, constraintName)
|
||||
if (cb) {
|
||||
cbs.push(cb)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -330,3 +351,46 @@ export const makeEditObject = specs => {
|
||||
return Promise.all(mapToArray(cbs, cb => cb())).then(noop)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const NULL_REF = 'OpaqueRef:NULL'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// HTTP put, use an ugly hack if the length is not known because XAPI
|
||||
// does not support chunk encoding.
|
||||
export const put = (stream, {
|
||||
headers: { ...headers } = {},
|
||||
...opts
|
||||
}, task) => {
|
||||
const makeRequest = () => httpRequest({
|
||||
...opts,
|
||||
body: stream,
|
||||
headers,
|
||||
method: 'put'
|
||||
})
|
||||
|
||||
// Xen API does not support chunk encoding.
|
||||
if (stream.length == null) {
|
||||
headers['transfer-encoding'] = null
|
||||
|
||||
const promise = makeRequest()
|
||||
|
||||
if (task) {
|
||||
// Some connections need the task to resolve (VDI import).
|
||||
task::pFinally(() => {
|
||||
promise.cancel()
|
||||
})
|
||||
} else {
|
||||
// Some tasks need the connection to close (VM import).
|
||||
promise.request.once('finish', () => {
|
||||
promise.cancel()
|
||||
})
|
||||
}
|
||||
|
||||
return promise.readAll()
|
||||
}
|
||||
|
||||
return makeRequest().readAll()
|
||||
}
|
||||
|
||||
0
src/xo-mixins/.index-modules
Normal file
0
src/xo-mixins/.index-modules
Normal file
@@ -19,11 +19,29 @@ export default class {
|
||||
constructor (xo) {
|
||||
this._xo = xo
|
||||
|
||||
this._acls = new Acls({
|
||||
const aclsDb = this._acls = new Acls({
|
||||
connection: xo._redis,
|
||||
prefix: 'xo:acl',
|
||||
indexes: ['subject', 'object']
|
||||
})
|
||||
|
||||
xo.on('start', () => {
|
||||
xo.addConfigManager('acls',
|
||||
() => aclsDb.get(),
|
||||
acls => aclsDb.update(acls)
|
||||
)
|
||||
})
|
||||
|
||||
xo.on('clean', async () => {
|
||||
const acls = await aclsDb.get()
|
||||
const toRemove = []
|
||||
forEach(acls, ({ subject, object, action, id }) => {
|
||||
if (!subject || !object || !action) {
|
||||
toRemove.push(id)
|
||||
}
|
||||
})
|
||||
await aclsDb.remove(toRemove)
|
||||
})
|
||||
}
|
||||
|
||||
async _getAclsForUser (userId) {
|
||||
@@ -39,10 +57,9 @@ export default class {
|
||||
push.apply(acls, entries)
|
||||
})(acls.push)
|
||||
|
||||
const collection = this._acls
|
||||
await Promise.all(mapToArray(
|
||||
subjects,
|
||||
subject => collection.get({subject}).then(pushAcls)
|
||||
subject => this.getAclsForSubject(subject).then(pushAcls)
|
||||
))
|
||||
|
||||
return acls
|
||||
@@ -67,6 +84,10 @@ export default class {
|
||||
return this._acls.get()
|
||||
}
|
||||
|
||||
async getAclsForSubject (subjectId) {
|
||||
return this._acls.get({ subject: subjectId })
|
||||
}
|
||||
|
||||
async getPermissionsForUser (userId) {
|
||||
const [
|
||||
acls,
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
import createDebug from 'debug'
|
||||
const debug = createDebug('xo:api')
|
||||
|
||||
import getKeys from 'lodash/keys'
|
||||
import kindOf from 'kindof'
|
||||
import moment from 'moment-timezone'
|
||||
import ms from 'ms'
|
||||
import schemaInspector from 'schema-inspector'
|
||||
|
||||
import * as methods from '../api'
|
||||
import {
|
||||
InvalidParameters,
|
||||
MethodNotFound,
|
||||
NoSuchObject,
|
||||
Unauthorized
|
||||
} from './api-errors'
|
||||
import {
|
||||
version as xoServerVersion
|
||||
} from '../package.json'
|
||||
MethodNotFound
|
||||
} from 'json-rpc-peer'
|
||||
import {
|
||||
createRawObject,
|
||||
forEach,
|
||||
isFunction,
|
||||
noop
|
||||
} from './utils'
|
||||
noop,
|
||||
serializeError
|
||||
} from '../utils'
|
||||
|
||||
import * as errors from 'xo-common/api-errors'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -32,39 +28,30 @@ const PERMISSIONS = {
|
||||
admin: 3
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// - error when adding a server to a pool with incompatible version
|
||||
// - error when halted VM migration failure is due to XS < 7
|
||||
const XAPI_ERROR_TO_XO_ERROR = {
|
||||
EHOSTUNREACH: errors.serverUnreachable,
|
||||
HOST_OFFLINE: ([ host ], getId) => errors.hostOffline({ host: getId(host) }),
|
||||
NO_HOSTS_AVAILABLE: errors.noHostsAvailable,
|
||||
NOT_SUPPORTED_DURING_UPGRADE: errors.notSupportedDuringUpgrade,
|
||||
OPERATION_BLOCKED: ([ ref, code ], getId) => errors.operationBlocked({ objectId: getId(ref), code }),
|
||||
PATCH_PRECHECK_FAILED_ISO_MOUNTED: ([ patch ]) => errors.patchPrecheck({ errorType: 'isoMounted', patch }),
|
||||
PIF_VLAN_EXISTS: ([ pif ], getId) => errors.objectAlreadyExists({ objectId: getId(pif), objectType: 'PIF' }),
|
||||
SESSION_AUTHENTICATION_FAILED: errors.authenticationFailed,
|
||||
VDI_IN_USE: ([ vdi, operation ], getId) => errors.vdiInUse({ vdi: getId(vdi), operation }),
|
||||
VM_BAD_POWER_STATE: ([ vm, expected, actual ], getId) => errors.vmBadPowerState({ vm: getId(vm), expected, actual }),
|
||||
VM_IS_TEMPLATE: errors.vmIsTemplate,
|
||||
VM_LACKS_FEATURE: ([ vm ], getId) => errors.vmLacksFeature({ vm: getId(vm) }),
|
||||
VM_LACKS_FEATURE_SHUTDOWN: ([ vm ], getId) => errors.vmLacksFeature({ vm: getId(vm), feature: 'shutdown' }),
|
||||
VM_MISSING_PV_DRIVERS: ([ vm ], getId) => errors.vmMissingPvDrivers({ vm: getId(vm) })
|
||||
}
|
||||
|
||||
const hasPermission = (user, permission) => (
|
||||
PERMISSIONS[user.permission] >= PERMISSIONS[permission]
|
||||
)
|
||||
|
||||
// FIXME: this function is specific to XO and should not be defined in
|
||||
// this file.
|
||||
function checkPermission (method) {
|
||||
/* jshint validthis: true */
|
||||
|
||||
const {permission} = method
|
||||
|
||||
// No requirement.
|
||||
if (permission === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const {user} = this
|
||||
if (!user) {
|
||||
throw new Unauthorized()
|
||||
}
|
||||
|
||||
// The only requirement is login.
|
||||
if (!permission) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasPermission(user, permission)) {
|
||||
throw new Unauthorized()
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
function checkParams (method, params) {
|
||||
const schema = method.params
|
||||
if (!schema) {
|
||||
@@ -77,11 +64,34 @@ function checkParams (method, params) {
|
||||
}, params)
|
||||
|
||||
if (!result.valid) {
|
||||
throw new InvalidParameters(result.error)
|
||||
throw errors.invalidParameters(result.error)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
function checkPermission (method) {
|
||||
/* jshint validthis: true */
|
||||
|
||||
const {permission} = method
|
||||
|
||||
// No requirement.
|
||||
if (permission === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const {user} = this
|
||||
if (!user) {
|
||||
throw errors.unauthorized()
|
||||
}
|
||||
|
||||
// The only requirement is login.
|
||||
if (!permission) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasPermission(user, permission)) {
|
||||
throw errors.unauthorized()
|
||||
}
|
||||
}
|
||||
|
||||
function resolveParams (method, params) {
|
||||
const resolve = method.resolve
|
||||
@@ -91,7 +101,7 @@ function resolveParams (method, params) {
|
||||
|
||||
const {user} = this
|
||||
if (!user) {
|
||||
throw new Unauthorized()
|
||||
throw errors.unauthorized()
|
||||
}
|
||||
|
||||
const userId = user.id
|
||||
@@ -127,95 +137,29 @@ function resolveParams (method, params) {
|
||||
return params
|
||||
}
|
||||
|
||||
throw new Unauthorized()
|
||||
throw errors.unauthorized()
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function getMethodsInfo () {
|
||||
const methods = {}
|
||||
|
||||
forEach(this.api._methods, (method, name) => {
|
||||
methods[name] = {
|
||||
description: method.description,
|
||||
params: method.params || {},
|
||||
permission: method.permission
|
||||
}
|
||||
})
|
||||
|
||||
return methods
|
||||
}
|
||||
getMethodsInfo.description = 'returns the signatures of all available API methods'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const getServerVersion = () => xoServerVersion
|
||||
getServerVersion.description = 'return the version of xo-server'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const getVersion = () => '0.1'
|
||||
getVersion.description = 'API version (unstable)'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
function listMethods () {
|
||||
return getKeys(this.api._methods)
|
||||
}
|
||||
listMethods.description = 'returns the name of all available API methods'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
function methodSignature ({method: name}) {
|
||||
const method = this.api.getMethod(name)
|
||||
|
||||
if (!method) {
|
||||
throw new NoSuchObject()
|
||||
}
|
||||
|
||||
// Return an array for compatibility with XML-RPC.
|
||||
return [
|
||||
// XML-RPC require the name of the method.
|
||||
{
|
||||
name,
|
||||
description: method.description,
|
||||
params: method.params || {},
|
||||
permission: method.permission
|
||||
}
|
||||
]
|
||||
}
|
||||
methodSignature.description = 'returns the signature of an API method'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const getServerTimezone = (tz => () => tz)(moment.tz.guess())
|
||||
getServerTimezone.description = 'return the timezone server'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Api {
|
||||
constructor ({
|
||||
context,
|
||||
verboseLogsOnErrors
|
||||
} = {}) {
|
||||
constructor (xo) {
|
||||
this._logger = null
|
||||
this._methods = createRawObject()
|
||||
this._verboseLogsOnErrors = verboseLogsOnErrors
|
||||
this.context = context
|
||||
this._xo = xo
|
||||
|
||||
this.addMethods({
|
||||
system: {
|
||||
getMethodsInfo,
|
||||
getServerVersion,
|
||||
getServerTimezone,
|
||||
getVersion,
|
||||
listMethods,
|
||||
methodSignature
|
||||
}
|
||||
this.addApiMethods(methods)
|
||||
xo.on('start', async () => {
|
||||
this._logger = await xo.getLogger('api')
|
||||
})
|
||||
}
|
||||
|
||||
addMethod (name, method) {
|
||||
get apiMethods () {
|
||||
return this._methods
|
||||
}
|
||||
|
||||
addApiMethod (name, method) {
|
||||
const methods = this._methods
|
||||
|
||||
if (name in methods) {
|
||||
@@ -224,21 +168,22 @@ export default class Api {
|
||||
|
||||
methods[name] = method
|
||||
|
||||
let unset = () => {
|
||||
let remove = () => {
|
||||
delete methods[name]
|
||||
unset = noop
|
||||
remove = noop
|
||||
}
|
||||
return () => unset()
|
||||
return () => remove()
|
||||
}
|
||||
|
||||
addMethods (methods) {
|
||||
addApiMethods (methods) {
|
||||
let base = ''
|
||||
const removes = []
|
||||
|
||||
const addMethod = (method, name) => {
|
||||
name = base + name
|
||||
|
||||
if (isFunction(method)) {
|
||||
this.addMethod(name, method)
|
||||
removes.push(this.addApiMethod(name, method))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -247,20 +192,35 @@ export default class Api {
|
||||
forEach(method, addMethod)
|
||||
base = oldBase
|
||||
}
|
||||
forEach(methods, addMethod)
|
||||
|
||||
try {
|
||||
forEach(methods, addMethod)
|
||||
} catch (error) {
|
||||
// Remove all added methods.
|
||||
forEach(removes, remove => remove())
|
||||
|
||||
// Forward the error
|
||||
throw error
|
||||
}
|
||||
|
||||
let remove = () => {
|
||||
forEach(removes, remove => remove())
|
||||
remove = noop
|
||||
}
|
||||
return remove
|
||||
}
|
||||
|
||||
async call (session, name, params) {
|
||||
async callApiMethod (session, name, params) {
|
||||
const startTime = Date.now()
|
||||
|
||||
const method = this.getMethod(name)
|
||||
const method = this._methods[name]
|
||||
if (!method) {
|
||||
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(this.context, {
|
||||
const context = Object.create(this._xo, {
|
||||
api: { // Used by system.*().
|
||||
value: this
|
||||
},
|
||||
@@ -269,10 +229,9 @@ export default class Api {
|
||||
}
|
||||
})
|
||||
|
||||
// FIXME: too coupled with XO.
|
||||
// Fetch and inject the current user.
|
||||
const userId = session.get('user_id', undefined)
|
||||
context.user = userId && await context.getUser(userId)
|
||||
context.user = userId && await this._xo.getUser(userId)
|
||||
const userName = context.user
|
||||
? context.user.email
|
||||
: '(unknown user)'
|
||||
@@ -293,7 +252,7 @@ export default class Api {
|
||||
params.id = params[namespace]
|
||||
}
|
||||
|
||||
checkParams(method, params)
|
||||
checkParams.call(context, method, params)
|
||||
|
||||
const resolvedParams = await resolveParams.call(context, method, params)
|
||||
|
||||
@@ -315,15 +274,19 @@ export default class Api {
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this._verboseLogsOnErrors) {
|
||||
debug(
|
||||
'%s | %s(%j) [%s] =!> %s',
|
||||
userName,
|
||||
name,
|
||||
params,
|
||||
ms(Date.now() - startTime),
|
||||
error
|
||||
)
|
||||
const data = {
|
||||
userId,
|
||||
method: name,
|
||||
params,
|
||||
duration: Date.now() - startTime,
|
||||
error: serializeError(error)
|
||||
}
|
||||
const message = `${userName} | ${name}(${JSON.stringify(params)}) [${ms(Date.now() - startTime)}] =!> ${error}`
|
||||
|
||||
this._logger.error(message, data)
|
||||
|
||||
if (this._xo._config.verboseLogsOnErrors) {
|
||||
debug(message)
|
||||
|
||||
const stack = error && error.stack
|
||||
if (stack) {
|
||||
@@ -339,11 +302,18 @@ export default class Api {
|
||||
)
|
||||
}
|
||||
|
||||
const xoError = XAPI_ERROR_TO_XO_ERROR[error.code]
|
||||
if (xoError) {
|
||||
throw xoError(error.params, ref => {
|
||||
try {
|
||||
return this._xo.getObject(ref).id
|
||||
} catch (e) {
|
||||
return ref
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
getMethod (name) {
|
||||
return this._methods[name]
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import Token, { Tokens } from '../models/token'
|
||||
import {
|
||||
NoSuchObject
|
||||
} from '../api-errors'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
import {
|
||||
createRawObject,
|
||||
forEach,
|
||||
generateToken,
|
||||
pCatch,
|
||||
noop
|
||||
@@ -11,13 +10,8 @@ import {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class NoSuchAuthenticationToken extends NoSuchObject {
|
||||
constructor (id) {
|
||||
super(id, 'authentication token')
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
const noSuchAuthenticationToken = id =>
|
||||
noSuchObject(id, 'authenticationToken')
|
||||
|
||||
export default class {
|
||||
constructor (xo) {
|
||||
@@ -30,7 +24,7 @@ export default class {
|
||||
this._providers = new Set()
|
||||
|
||||
// Creates persistent collections.
|
||||
this._tokens = new Tokens({
|
||||
const tokensDb = this._tokens = new Tokens({
|
||||
connection: xo._redis,
|
||||
prefix: 'xo:token',
|
||||
indexes: ['user_id']
|
||||
@@ -65,6 +59,25 @@ export default class {
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
xo.on('clean', async () => {
|
||||
const tokens = await tokensDb.get()
|
||||
const toRemove = []
|
||||
const now = Date.now()
|
||||
forEach(tokens, ({ expiration, id }) => {
|
||||
if (!expiration || expiration < now) {
|
||||
toRemove.push(id)
|
||||
}
|
||||
})
|
||||
await tokensDb.remove(toRemove)
|
||||
})
|
||||
|
||||
xo.on('start', () => {
|
||||
xo.addConfigManager('authTokens',
|
||||
() => tokensDb.get(),
|
||||
tokens => tokensDb.update(tokens)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
registerAuthenticationProvider (provider) {
|
||||
@@ -152,14 +165,14 @@ export default class {
|
||||
|
||||
async deleteAuthenticationToken (id) {
|
||||
if (!await this._tokens.remove(id)) {
|
||||
throw new NoSuchAuthenticationToken(id)
|
||||
throw noSuchAuthenticationToken(id)
|
||||
}
|
||||
}
|
||||
|
||||
async getAuthenticationToken (id) {
|
||||
let token = await this._tokens.first(id)
|
||||
if (!token) {
|
||||
throw new NoSuchAuthenticationToken(id)
|
||||
throw noSuchAuthenticationToken(id)
|
||||
}
|
||||
|
||||
token = token.properties
|
||||
@@ -169,7 +182,7 @@ export default class {
|
||||
)) {
|
||||
this._tokens.remove(id)::pCatch(noop)
|
||||
|
||||
throw new NoSuchAuthenticationToken(id)
|
||||
throw noSuchAuthenticationToken(id)
|
||||
}
|
||||
|
||||
return token
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import deferrable from 'golike-defer'
|
||||
import endsWith from 'lodash/endsWith'
|
||||
import escapeStringRegexp from 'escape-string-regexp'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
@@ -11,19 +12,18 @@ import {
|
||||
dirname
|
||||
} from 'path'
|
||||
import { satisfies as versionSatisfies } from 'semver'
|
||||
import { utcFormat } from 'd3-time-format'
|
||||
|
||||
import vhdMerge from '../vhd-merge'
|
||||
import vhdMerge, { chainVhd } from '../vhd-merge'
|
||||
import xapiObjectToXo from '../xapi-object-to-xo'
|
||||
import {
|
||||
deferrable
|
||||
} from '../decorators'
|
||||
import {
|
||||
forEach,
|
||||
mapToArray,
|
||||
noop,
|
||||
pCatch,
|
||||
pSettle,
|
||||
safeDateFormat
|
||||
safeDateFormat,
|
||||
safeDateParse
|
||||
} from '../utils'
|
||||
import {
|
||||
VDI_FORMAT_VHD
|
||||
@@ -34,6 +34,8 @@ import {
|
||||
const DELTA_BACKUP_EXT = '.json'
|
||||
const DELTA_BACKUP_EXT_LENGTH = DELTA_BACKUP_EXT.length
|
||||
|
||||
const shortDate = utcFormat('%Y-%m-%d')
|
||||
|
||||
// Test if a file is a vdi backup. (full or delta)
|
||||
const isVdiBackup = name => /^\d+T\d+Z_(?:full|delta)\.vhd$/.test(name)
|
||||
|
||||
@@ -41,6 +43,37 @@ const isVdiBackup = name => /^\d+T\d+Z_(?:full|delta)\.vhd$/.test(name)
|
||||
const isDeltaVdiBackup = name => /^\d+T\d+Z_delta\.vhd$/.test(name)
|
||||
const isFullVdiBackup = name => /^\d+T\d+Z_full\.vhd$/.test(name)
|
||||
|
||||
const parseVmBackupPath = name => {
|
||||
const base = basename(name)
|
||||
let baseMatches
|
||||
|
||||
baseMatches = /^([^_]+)_([^_]+)_(.+)\.xva$/.exec(base)
|
||||
if (baseMatches) {
|
||||
return {
|
||||
datetime: safeDateParse(baseMatches[1]),
|
||||
name: baseMatches[3],
|
||||
tag: baseMatches[2],
|
||||
type: 'xva'
|
||||
}
|
||||
}
|
||||
|
||||
let dirMatches
|
||||
if (
|
||||
(baseMatches = /^([^_]+)_(.+)\.json$/.exec(base)) &&
|
||||
(dirMatches = /^vm_delta_([^_]+)_(.+)$/.exec(basename(dirname(name))))
|
||||
) {
|
||||
return {
|
||||
datetime: safeDateParse(baseMatches[1]),
|
||||
name: baseMatches[2],
|
||||
uuid: dirMatches[2],
|
||||
tag: dirMatches[1],
|
||||
type: 'delta'
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('invalid VM backup filename')
|
||||
}
|
||||
|
||||
// Get the timestamp of a vdi backup. (full or delta)
|
||||
const getVdiTimestamp = name => {
|
||||
const arr = /^(\d+T\d+Z)_(?:full|delta)\.vhd$/.exec(name)
|
||||
@@ -50,21 +83,28 @@ const getVdiTimestamp = name => {
|
||||
const getDeltaBackupNameWithoutExt = name => name.slice(0, -DELTA_BACKUP_EXT_LENGTH)
|
||||
const isDeltaBackup = name => endsWith(name, DELTA_BACKUP_EXT)
|
||||
|
||||
// Checksums have been corrupted between 5.2.6 and 5.2.7.
|
||||
//
|
||||
// For a short period of time, bad checksums will be regenerated
|
||||
// instead of rejected.
|
||||
//
|
||||
// TODO: restore when enough time has passed (a week/a month).
|
||||
async function checkFileIntegrity (handler, name) {
|
||||
let stream
|
||||
|
||||
try {
|
||||
stream = await handler.createReadStream(name, { checksum: true })
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
stream.resume()
|
||||
await eventToPromise(stream, 'finish')
|
||||
await handler.refreshChecksum(name)
|
||||
// let stream
|
||||
//
|
||||
// try {
|
||||
// stream = await handler.createReadStream(name, { checksum: true })
|
||||
// } catch (error) {
|
||||
// if (error.code === 'ENOENT') {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// throw error
|
||||
// }
|
||||
//
|
||||
// stream.resume()
|
||||
// await eventToPromise(stream, 'finish')
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
@@ -107,6 +147,15 @@ export default class {
|
||||
const xapi = this._xo.getXapi(sr)
|
||||
|
||||
const vm = await xapi.importVm(stream, { srId: sr._xapiId })
|
||||
|
||||
const { datetime } = parseVmBackupPath(file)
|
||||
await Promise.all([
|
||||
xapi.addTag(vm.$id, 'restored from backup'),
|
||||
xapi.editVm(vm.$id, {
|
||||
name_label: `${vm.name_label} (${shortDate(datetime)})`
|
||||
})
|
||||
])
|
||||
|
||||
return xapiObjectToXo(vm).id
|
||||
}
|
||||
|
||||
@@ -291,6 +340,18 @@ export default class {
|
||||
return backups.slice(i)
|
||||
}
|
||||
|
||||
// fix the parent UUID and filename in delta files after download from xapi or backup compression
|
||||
async _chainDeltaVdiBackups ({handler, dir}) {
|
||||
const backups = await this._listVdiBackups(handler, dir)
|
||||
for (let i = 1; i < backups.length; i++) {
|
||||
const childPath = dir + '/' + backups[i]
|
||||
const modified = await chainVhd(handler, dir + '/' + backups[i - 1], handler, childPath)
|
||||
if (modified) {
|
||||
await handler.refreshChecksum(childPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _mergeDeltaVdiBackups ({handler, dir, depth}) {
|
||||
const backups = await this._listVdiBackups(handler, dir)
|
||||
let i = backups.length - depth
|
||||
@@ -553,7 +614,9 @@ export default class {
|
||||
mapToArray(vdiBackups, vdiBackup => {
|
||||
const backupName = vdiBackup.value()
|
||||
const backupDirectory = backupName.slice(0, backupName.lastIndexOf('/'))
|
||||
return this._mergeDeltaVdiBackups({ handler, dir: `${dir}/${backupDirectory}`, depth })
|
||||
const backupDir = `${dir}/${backupDirectory}`
|
||||
return this._mergeDeltaVdiBackups({ handler, dir: backupDir, depth })
|
||||
.then(() => { this._chainDeltaVdiBackups({ handler, dir: backupDir }) })
|
||||
})
|
||||
)
|
||||
|
||||
@@ -569,10 +632,13 @@ export default class {
|
||||
}
|
||||
|
||||
async importDeltaVmBackup ({sr, remoteId, filePath}) {
|
||||
filePath = `${filePath}${DELTA_BACKUP_EXT}`
|
||||
const { datetime } = parseVmBackupPath(filePath)
|
||||
|
||||
const handler = await this._xo.getRemoteHandler(remoteId)
|
||||
const xapi = this._xo.getXapi(sr)
|
||||
|
||||
const delta = JSON.parse(await handler.readFile(`${filePath}${DELTA_BACKUP_EXT}`))
|
||||
const delta = JSON.parse(await handler.readFile(filePath))
|
||||
let vm
|
||||
const { version } = delta
|
||||
|
||||
@@ -599,9 +665,12 @@ export default class {
|
||||
)
|
||||
)
|
||||
|
||||
delta.vm.name_label += ` (${shortDate(datetime)})`
|
||||
delta.vm.tags.push('restored from backup')
|
||||
|
||||
vm = await xapi.importDeltaVm(delta, {
|
||||
srId: sr._xapiId,
|
||||
disableStartAfterImport: false
|
||||
disableStartAfterImport: false,
|
||||
srId: sr._xapiId
|
||||
})
|
||||
} else {
|
||||
throw new Error(`Unsupported delta backup version: ${version}`)
|
||||
@@ -690,12 +759,12 @@ export default class {
|
||||
const sourceXapi = this._xo.getXapi(vm)
|
||||
vm = sourceXapi.getObject(vm._xapiId)
|
||||
|
||||
const vms = []
|
||||
const vms = {}
|
||||
forEach(sr.$VDIs, vdi => {
|
||||
const vbds = vdi.$VBDs
|
||||
const vm = vbds && vbds[0] && vbds[0].$VM
|
||||
if (vm && reg.test(vm.name_label)) {
|
||||
vms.push(vm)
|
||||
vms[vm.$id] = vm
|
||||
}
|
||||
})
|
||||
const olderCopies = sortBy(vms, 'name_label')
|
||||
@@ -706,11 +775,10 @@ export default class {
|
||||
})
|
||||
await targetXapi.addTag(drCopy.$id, 'Disaster Recovery')
|
||||
|
||||
const promises = []
|
||||
for (let surplus = olderCopies.length - (depth - 1); surplus > 0; surplus--) {
|
||||
const oldDRVm = olderCopies.shift()
|
||||
promises.push(targetXapi.deleteVm(oldDRVm.$id, true))
|
||||
}
|
||||
await Promise.all(promises)
|
||||
const n = 1 - depth
|
||||
await Promise.all(mapToArray(n ? olderCopies.slice(0, n) : olderCopies, vm =>
|
||||
// Do not consider a failure to delete an old copy as a fatal error.
|
||||
targetXapi.deleteVm(vm.$id, true)::pCatch(noop)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
33
src/xo-mixins/config-management.js
Normal file
33
src/xo-mixins/config-management.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { map, noop } from '../utils'
|
||||
|
||||
import { all as pAll } from 'promise-toolbox'
|
||||
|
||||
export default class ConfigManagement {
|
||||
constructor () {
|
||||
this._managers = { __proto__: null }
|
||||
}
|
||||
|
||||
addConfigManager (id, exporter, importer) {
|
||||
const managers = this._managers
|
||||
if (id in managers) {
|
||||
throw new Error(`${id} is already taken`)
|
||||
}
|
||||
|
||||
this._managers[id] = { exporter, importer }
|
||||
}
|
||||
|
||||
exportConfig () {
|
||||
return map(this._managers, ({ exporter }, key) => exporter())::pAll()
|
||||
}
|
||||
|
||||
importConfig (config) {
|
||||
const managers = this._managers
|
||||
|
||||
return map(config, (entry, key) => {
|
||||
const manager = managers[key]
|
||||
if (manager) {
|
||||
return manager.importer(entry)
|
||||
}
|
||||
})::pAll().then(noop)
|
||||
}
|
||||
}
|
||||
265
src/xo-mixins/ip-pools.js
Normal file
265
src/xo-mixins/ip-pools.js
Normal file
@@ -0,0 +1,265 @@
|
||||
import concat from 'lodash/concat'
|
||||
import diff from 'lodash/difference'
|
||||
import findIndex from 'lodash/findIndex'
|
||||
import flatten from 'lodash/flatten'
|
||||
import highland from 'highland'
|
||||
import includes from 'lodash/includes'
|
||||
import keys from 'lodash/keys'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import pick from 'lodash/pick'
|
||||
import remove from 'lodash/remove'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
import { fromCallback } from 'promise-toolbox'
|
||||
|
||||
import {
|
||||
forEach,
|
||||
generateUnsecureToken,
|
||||
isEmpty,
|
||||
lightSet,
|
||||
mapToArray,
|
||||
streamToArray,
|
||||
throwFn
|
||||
} from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const normalize = ({
|
||||
addresses,
|
||||
id = throwFn('id is a required field'),
|
||||
name = '',
|
||||
networks,
|
||||
resourceSets
|
||||
}) => ({
|
||||
addresses,
|
||||
id,
|
||||
name,
|
||||
networks,
|
||||
resourceSets
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Note: an address cannot be in two different pools sharing a
|
||||
// network.
|
||||
export default class IpPools {
|
||||
constructor (xo) {
|
||||
this._store = null
|
||||
this._xo = xo
|
||||
|
||||
xo.on('start', async () => {
|
||||
this._store = await xo.getStore('ipPools')
|
||||
|
||||
xo.addConfigManager('ipPools',
|
||||
() => this.getAllIpPools(),
|
||||
ipPools => Promise.all(mapToArray(ipPools, ipPool => this._save(ipPool)))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async createIpPool ({ addresses, name, networks }) {
|
||||
const id = await this._generateId()
|
||||
|
||||
await this._save({
|
||||
addresses,
|
||||
id,
|
||||
name,
|
||||
networks
|
||||
})
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
async deleteIpPool (id) {
|
||||
const store = this._store
|
||||
|
||||
if (await store.has(id)) {
|
||||
await Promise.all(mapToArray(await this._xo.getAllResourceSets(), async set => {
|
||||
await this._xo.removeLimitFromResourceSet(`ipPool:${id}`, set.id)
|
||||
return this._xo.removeIpPoolFromResourceSet(id, set.id)
|
||||
}))
|
||||
await this._removeIpAddressesFromVifs(
|
||||
mapValues((await this.getIpPool(id)).addresses, 'vifs')
|
||||
)
|
||||
|
||||
return store.del(id)
|
||||
}
|
||||
|
||||
throw noSuchObject(id, 'ipPool')
|
||||
}
|
||||
|
||||
async getAllIpPools (userId = undefined) {
|
||||
let filter
|
||||
if (userId != null) {
|
||||
const user = await this._xo.getUser(userId)
|
||||
if (user.permission !== 'admin') {
|
||||
const resourceSets = await this._xo.getAllResourceSets(userId)
|
||||
const ipPools = lightSet(flatten(mapToArray(resourceSets, 'ipPools')))
|
||||
filter = ({ id }) => ipPools.has(id)
|
||||
}
|
||||
}
|
||||
|
||||
return streamToArray(this._store.createValueStream(), {
|
||||
filter,
|
||||
mapper: normalize
|
||||
})
|
||||
}
|
||||
|
||||
getIpPool (id) {
|
||||
return this._store.get(id).then(normalize, error => {
|
||||
throw error.notFound ? noSuchObject(id, 'ipPool') : error
|
||||
})
|
||||
}
|
||||
|
||||
allocIpAddresses (vifId, addAddresses, removeAddresses) {
|
||||
const updatedIpPools = {}
|
||||
const limits = {}
|
||||
|
||||
const xoVif = this._xo.getObject(vifId)
|
||||
const xapi = this._xo.getXapi(xoVif)
|
||||
const vif = xapi.getObject(xoVif._xapiId)
|
||||
|
||||
const allocAndSave = (() => {
|
||||
const resourseSetId = xapi.xo.getData(vif.VM, 'resourceSet')
|
||||
|
||||
return () => {
|
||||
const saveIpPools = () => Promise.all(mapToArray(updatedIpPools, ipPool => this._save(ipPool)))
|
||||
return resourseSetId
|
||||
? this._xo.allocateLimitsInResourceSet(limits, resourseSetId).then(
|
||||
saveIpPools
|
||||
)
|
||||
: saveIpPools()
|
||||
}
|
||||
})()
|
||||
|
||||
return fromCallback(cb => {
|
||||
const network = vif.$network
|
||||
const networkId = network.$id
|
||||
|
||||
const isVif = id => id === vifId
|
||||
|
||||
highland(this._store.createValueStream()).each(ipPool => {
|
||||
const { addresses, networks } = updatedIpPools[ipPool.id] || ipPool
|
||||
if (!(addresses && networks && includes(networks, networkId))) {
|
||||
return false
|
||||
}
|
||||
|
||||
let allocations = 0
|
||||
let changed = false
|
||||
forEach(removeAddresses, address => {
|
||||
let vifs, i
|
||||
if (
|
||||
(vifs = addresses[address]) &&
|
||||
(vifs = vifs.vifs) &&
|
||||
(i = findIndex(vifs, isVif)) !== -1
|
||||
) {
|
||||
vifs.splice(i, 1)
|
||||
--allocations
|
||||
changed = true
|
||||
}
|
||||
})
|
||||
forEach(addAddresses, address => {
|
||||
const data = addresses[address]
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
const vifs = data.vifs || (data.vifs = [])
|
||||
if (!includes(vifs, vifId)) {
|
||||
vifs.push(vifId)
|
||||
++allocations
|
||||
changed = true
|
||||
}
|
||||
})
|
||||
|
||||
if (changed) {
|
||||
const { id } = ipPool
|
||||
updatedIpPools[id] = ipPool
|
||||
limits[`ipPool:${id}`] = (limits[`ipPool:${id}`] || 0) + allocations
|
||||
}
|
||||
}).toCallback(cb)
|
||||
}).then(allocAndSave)
|
||||
}
|
||||
|
||||
async _removeIpAddressesFromVifs (mapAddressVifs) {
|
||||
const mapVifAddresses = {}
|
||||
forEach(mapAddressVifs, (vifs, address) => {
|
||||
forEach(vifs, vifId => {
|
||||
if (mapVifAddresses[vifId]) {
|
||||
mapVifAddresses[vifId].push(address)
|
||||
} else {
|
||||
mapVifAddresses[vifId] = [ address ]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const { getXapi } = this._xo
|
||||
return Promise.all(mapToArray(mapVifAddresses, (addresses, vifId) => {
|
||||
const vif = this._xo.getObject(vifId)
|
||||
const { allowedIpv4Addresses, allowedIpv6Addresses } = vif
|
||||
remove(allowedIpv4Addresses, address => includes(addresses, address))
|
||||
remove(allowedIpv6Addresses, address => includes(addresses, address))
|
||||
this.allocIpAddresses(vifId, undefined, concat(allowedIpv4Addresses, allowedIpv6Addresses))
|
||||
|
||||
return getXapi(vif).editVif(vif._xapiId, {
|
||||
ipv4Allowed: allowedIpv4Addresses,
|
||||
ipv6Allowed: allowedIpv6Addresses
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
async updateIpPool (id, {
|
||||
addresses,
|
||||
name,
|
||||
networks,
|
||||
resourceSets
|
||||
}) {
|
||||
const ipPool = await this.getIpPool(id)
|
||||
const previousAddresses = { ...ipPool.addresses }
|
||||
|
||||
name != null && (ipPool.name = name)
|
||||
if (addresses) {
|
||||
const addresses_ = ipPool.addresses || {}
|
||||
forEach(addresses, (props, address) => {
|
||||
if (props === null) {
|
||||
delete addresses_[address]
|
||||
} else {
|
||||
addresses_[address] = props
|
||||
}
|
||||
})
|
||||
|
||||
// Remove the addresses that are no longer in the IP pool from the concerned VIFs
|
||||
const deletedAddresses = diff(keys(previousAddresses), keys(addresses_))
|
||||
await this._removeIpAddressesFromVifs(pick(previousAddresses, deletedAddresses))
|
||||
|
||||
if (isEmpty(addresses_)) {
|
||||
delete ipPool.addresses
|
||||
} else {
|
||||
ipPool.addresses = addresses_
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement patching like for addresses.
|
||||
if (networks) {
|
||||
ipPool.networks = networks
|
||||
}
|
||||
|
||||
// TODO: Implement patching like for addresses.
|
||||
if (resourceSets) {
|
||||
ipPool.resourceSets = resourceSets
|
||||
}
|
||||
|
||||
await this._save(ipPool)
|
||||
}
|
||||
|
||||
async _generateId () {
|
||||
let id
|
||||
do {
|
||||
id = generateUnsecureToken(8)
|
||||
} while (await this._store.has(id))
|
||||
return id
|
||||
}
|
||||
|
||||
_save (ipPool) {
|
||||
ipPool = normalize(ipPool)
|
||||
return this._store.put(ipPool.id, ipPool)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,29 @@
|
||||
import assign from 'lodash/assign'
|
||||
|
||||
import JobExecutor from '../job-executor'
|
||||
import { Jobs } from '../models/job'
|
||||
import {
|
||||
GenericError,
|
||||
NoSuchObject
|
||||
} from '../api-errors'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class NoSuchJob extends NoSuchObject {
|
||||
constructor (id) {
|
||||
super(id, 'job')
|
||||
}
|
||||
}
|
||||
import { mapToArray } from '../utils'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class {
|
||||
constructor (xo) {
|
||||
this._executor = new JobExecutor(xo)
|
||||
this._jobs = new Jobs({
|
||||
const jobsDb = this._jobs = new Jobs({
|
||||
connection: xo._redis,
|
||||
prefix: 'xo:job',
|
||||
indexes: ['user_id', 'key']
|
||||
})
|
||||
|
||||
xo.on('start', () => {
|
||||
xo.addConfigManager('jobs',
|
||||
() => jobsDb.get(),
|
||||
jobs => Promise.all(mapToArray(jobs, job =>
|
||||
jobsDb.save(job)
|
||||
))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async getAllJobs () {
|
||||
@@ -33,21 +33,21 @@ export default class {
|
||||
async getJob (id) {
|
||||
const job = await this._jobs.first(id)
|
||||
if (!job) {
|
||||
throw new NoSuchJob(id)
|
||||
throw noSuchObject(id, 'job')
|
||||
}
|
||||
|
||||
return job.properties
|
||||
}
|
||||
|
||||
async createJob (userId, job) {
|
||||
async createJob (job) {
|
||||
// TODO: use plain objects
|
||||
const job_ = await this._jobs.create(userId, job)
|
||||
const job_ = await this._jobs.create(job)
|
||||
return job_.properties
|
||||
}
|
||||
|
||||
async updateJob ({id, type, name, key, method, paramsVector}) {
|
||||
async updateJob ({id, userId, type, name, key, method, paramsVector}) {
|
||||
const oldJob = await this.getJob(id)
|
||||
assign(oldJob, {type, name, key, method, paramsVector})
|
||||
assign(oldJob, {userId, type, name, key, method, paramsVector})
|
||||
return /* await */ this._jobs.save(oldJob)
|
||||
}
|
||||
|
||||
@@ -56,24 +56,10 @@ export default class {
|
||||
}
|
||||
|
||||
async runJobSequence (idSequence) {
|
||||
const notFound = []
|
||||
for (const id of idSequence) {
|
||||
let job
|
||||
try {
|
||||
job = await this.getJob(id)
|
||||
} catch (error) {
|
||||
if (error instanceof NoSuchJob) {
|
||||
notFound.push(id)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
if (job) {
|
||||
await this._executor.exec(job)
|
||||
}
|
||||
}
|
||||
if (notFound.length > 0) {
|
||||
throw new GenericError(`The following jobs were not found: ${notFound.join()}`)
|
||||
const jobs = await Promise.all(mapToArray(idSequence, id => this.getJob(id)))
|
||||
|
||||
for (const job of jobs) {
|
||||
await this._executor.exec(job)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import createJsonSchemaValidator from 'is-my-json-valid'
|
||||
|
||||
import { PluginsMetadata } from '../models/plugin-metadata'
|
||||
import {
|
||||
InvalidParameters,
|
||||
NoSuchObject
|
||||
} from '../api-errors'
|
||||
invalidParameters,
|
||||
noSuchObject
|
||||
} from 'xo-common/api-errors'
|
||||
import {
|
||||
createRawObject,
|
||||
isFunction,
|
||||
@@ -13,14 +13,6 @@ import {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class NoSuchPlugin extends NoSuchObject {
|
||||
constructor (id) {
|
||||
super(id, 'plugin')
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class {
|
||||
constructor (xo) {
|
||||
this._plugins = createRawObject()
|
||||
@@ -29,12 +21,21 @@ export default class {
|
||||
connection: xo._redis,
|
||||
prefix: 'xo:plugin-metadata'
|
||||
})
|
||||
|
||||
xo.on('start', () => {
|
||||
xo.addConfigManager('plugins',
|
||||
() => this._pluginsMetadata.get(),
|
||||
plugins => Promise.all(mapToArray(plugins, plugin =>
|
||||
this._pluginsMetadata.save(plugin)
|
||||
))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
_getRawPlugin (id) {
|
||||
const plugin = this._plugins[id]
|
||||
if (!plugin) {
|
||||
throw new NoSuchPlugin(id)
|
||||
throw noSuchObject(id, 'plugin')
|
||||
}
|
||||
return plugin
|
||||
}
|
||||
@@ -51,16 +52,19 @@ export default class {
|
||||
instance,
|
||||
configurationSchema,
|
||||
configurationPresets,
|
||||
testSchema,
|
||||
version
|
||||
) {
|
||||
const id = name
|
||||
const plugin = this._plugins[id] = {
|
||||
configured: !configurationSchema,
|
||||
configurationPresets,
|
||||
configurationSchema,
|
||||
configured: !configurationSchema,
|
||||
id,
|
||||
instance,
|
||||
name,
|
||||
testable: isFunction(instance.test),
|
||||
testSchema,
|
||||
unloadable: isFunction(instance.unload),
|
||||
version
|
||||
}
|
||||
@@ -68,7 +72,6 @@ export default class {
|
||||
const metadata = await this._getPluginMetadata(id)
|
||||
let autoload = true
|
||||
let configuration
|
||||
|
||||
if (metadata) {
|
||||
({
|
||||
autoload,
|
||||
@@ -107,6 +110,8 @@ export default class {
|
||||
configurationSchema,
|
||||
loaded,
|
||||
name,
|
||||
testable,
|
||||
testSchema,
|
||||
unloadable,
|
||||
version
|
||||
} = this._getRawPlugin(id)
|
||||
@@ -124,7 +129,9 @@ export default class {
|
||||
version,
|
||||
configuration,
|
||||
configurationPresets,
|
||||
configurationSchema
|
||||
configurationSchema,
|
||||
testable,
|
||||
testSchema
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,12 +146,12 @@ export default class {
|
||||
const { configurationSchema } = plugin
|
||||
|
||||
if (!configurationSchema) {
|
||||
throw new InvalidParameters('plugin not configurable')
|
||||
throw invalidParameters('plugin not configurable')
|
||||
}
|
||||
|
||||
// See: https://github.com/mafintosh/is-my-json-valid/issues/116
|
||||
if (configuration == null) {
|
||||
throw new InvalidParameters([{
|
||||
throw invalidParameters([{
|
||||
field: 'data',
|
||||
message: 'is the wrong type'
|
||||
}])
|
||||
@@ -152,7 +159,7 @@ export default class {
|
||||
|
||||
const validate = createJsonSchemaValidator(configurationSchema)
|
||||
if (!validate(configuration)) {
|
||||
throw new InvalidParameters(validate.errors)
|
||||
throw invalidParameters(validate.errors)
|
||||
}
|
||||
|
||||
// Sets the plugin configuration.
|
||||
@@ -191,11 +198,11 @@ export default class {
|
||||
async loadPlugin (id) {
|
||||
const plugin = this._getRawPlugin(id)
|
||||
if (plugin.loaded) {
|
||||
throw new InvalidParameters('plugin already loaded')
|
||||
throw invalidParameters('plugin already loaded')
|
||||
}
|
||||
|
||||
if (!plugin.configured) {
|
||||
throw new InvalidParameters('plugin not configured')
|
||||
throw invalidParameters('plugin not configured')
|
||||
}
|
||||
|
||||
await plugin.instance.load()
|
||||
@@ -205,11 +212,11 @@ export default class {
|
||||
async unloadPlugin (id) {
|
||||
const plugin = this._getRawPlugin(id)
|
||||
if (!plugin.loaded) {
|
||||
throw new InvalidParameters('plugin already unloaded')
|
||||
throw invalidParameters('plugin already unloaded')
|
||||
}
|
||||
|
||||
if (plugin.unloadable === false) {
|
||||
throw new InvalidParameters('plugin cannot be unloaded')
|
||||
throw invalidParameters('plugin cannot be unloaded')
|
||||
}
|
||||
|
||||
await plugin.instance.unload()
|
||||
@@ -219,4 +226,31 @@ export default class {
|
||||
async purgePluginConfiguration (id) {
|
||||
await this._pluginsMetadata.merge(id, { configuration: undefined })
|
||||
}
|
||||
|
||||
async testPlugin (id, data) {
|
||||
const plugin = this._getRawPlugin(id)
|
||||
if (!plugin.testable) {
|
||||
throw invalidParameters('plugin not testable')
|
||||
}
|
||||
if (!plugin.loaded) {
|
||||
throw invalidParameters('plugin not loaded')
|
||||
}
|
||||
|
||||
const { testSchema } = plugin
|
||||
if (testSchema) {
|
||||
if (data == null) {
|
||||
throw invalidParameters([{
|
||||
field: 'data',
|
||||
message: 'is the wrong type'
|
||||
}])
|
||||
}
|
||||
|
||||
const validate = createJsonSchemaValidator(testSchema)
|
||||
if (!validate(data)) {
|
||||
throw invalidParameters(validate.errors)
|
||||
}
|
||||
}
|
||||
|
||||
await plugin.instance.test(data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
import RemoteHandlerLocal from '../remote-handlers/local'
|
||||
import RemoteHandlerNfs from '../remote-handlers/nfs'
|
||||
import RemoteHandlerSmb from '../remote-handlers/smb'
|
||||
import {
|
||||
forEach
|
||||
forEach,
|
||||
mapToArray
|
||||
} from '../utils'
|
||||
import {
|
||||
NoSuchObject
|
||||
} from '../api-errors'
|
||||
import {
|
||||
Remotes
|
||||
} from '../models/remote'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class NoSuchRemote extends NoSuchObject {
|
||||
constructor (id) {
|
||||
super(id, 'remote')
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class {
|
||||
constructor (xo) {
|
||||
this._remotes = new Remotes({
|
||||
@@ -30,6 +22,13 @@ export default class {
|
||||
})
|
||||
|
||||
xo.on('start', async () => {
|
||||
xo.addConfigManager('remotes',
|
||||
() => this._remotes.get(),
|
||||
remotes => Promise.all(mapToArray(remotes, remote =>
|
||||
this._remotes.save(remote)
|
||||
))
|
||||
)
|
||||
|
||||
await this.initRemotes()
|
||||
await this.syncAllRemotes()
|
||||
})
|
||||
@@ -66,7 +65,7 @@ export default class {
|
||||
async _getRemote (id) {
|
||||
const remote = await this._remotes.first(id)
|
||||
if (!remote) {
|
||||
throw new NoSuchRemote(id)
|
||||
throw noSuchObject(id, 'remote')
|
||||
}
|
||||
|
||||
return remote
|
||||
|
||||
@@ -2,11 +2,11 @@ import every from 'lodash/every'
|
||||
import keyBy from 'lodash/keyBy'
|
||||
import remove from 'lodash/remove'
|
||||
import some from 'lodash/some'
|
||||
|
||||
import {
|
||||
NoSuchObject,
|
||||
Unauthorized
|
||||
} from '../api-errors'
|
||||
noSuchObject,
|
||||
unauthorized
|
||||
} from 'xo-common/api-errors'
|
||||
|
||||
import {
|
||||
forEach,
|
||||
generateUnsecureToken,
|
||||
@@ -19,10 +19,12 @@ import {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class NoSuchResourceSet extends NoSuchObject {
|
||||
constructor (id) {
|
||||
super(id, 'resource set')
|
||||
}
|
||||
const VM_RESOURCES = {
|
||||
cpus: true,
|
||||
disk: true,
|
||||
disks: true,
|
||||
memory: true,
|
||||
vms: true
|
||||
}
|
||||
|
||||
const computeVmResourcesUsage = vm => {
|
||||
@@ -54,6 +56,7 @@ const computeVmResourcesUsage = vm => {
|
||||
|
||||
const normalize = set => ({
|
||||
id: set.id,
|
||||
ipPools: set.ipPools || [],
|
||||
limits: set.limits
|
||||
? map(set.limits, limit => isObject(limit)
|
||||
? limit
|
||||
@@ -76,6 +79,13 @@ export default class {
|
||||
|
||||
this._store = null
|
||||
xo.on('start', async () => {
|
||||
xo.addConfigManager('resourceSets',
|
||||
() => this.getAllResourceSets(),
|
||||
resourceSets => Promise.all(mapToArray(resourceSets, resourceSet =>
|
||||
this._save(resourceSet)
|
||||
))
|
||||
)
|
||||
|
||||
this._store = await xo.getStore('resourceSets')
|
||||
})
|
||||
}
|
||||
@@ -108,7 +118,7 @@ export default class {
|
||||
// The set does not contains ALL objects.
|
||||
!every(objectIds, lightSet(set.objects).has)
|
||||
)) {
|
||||
throw new Unauthorized()
|
||||
throw unauthorized()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,14 +150,15 @@ export default class {
|
||||
return store.del(id)
|
||||
}
|
||||
|
||||
throw new NoSuchResourceSet(id)
|
||||
throw noSuchObject(id, 'resourceSet')
|
||||
}
|
||||
|
||||
async updateResourceSet (id, {
|
||||
name = undefined,
|
||||
subjects = undefined,
|
||||
objects = undefined,
|
||||
limits = undefined
|
||||
limits = undefined,
|
||||
ipPools = undefined
|
||||
}) {
|
||||
const set = await this.getResourceSet(id)
|
||||
if (name) {
|
||||
@@ -178,6 +189,9 @@ export default class {
|
||||
}
|
||||
})
|
||||
}
|
||||
if (ipPools) {
|
||||
set.ipPools = ipPools
|
||||
}
|
||||
|
||||
await this._save(set)
|
||||
}
|
||||
@@ -203,7 +217,7 @@ export default class {
|
||||
getResourceSet (id) {
|
||||
return this._store.get(id).then(normalize, error => {
|
||||
if (error.notFound) {
|
||||
throw new NoSuchResourceSet(id)
|
||||
throw noSuchObject(id, 'resourceSet')
|
||||
}
|
||||
|
||||
throw error
|
||||
@@ -218,7 +232,19 @@ export default class {
|
||||
|
||||
async removeObjectFromResourceSet (objectId, setId) {
|
||||
const set = await this.getResourceSet(setId)
|
||||
remove(set.objects)
|
||||
remove(set.objects, id => id === objectId)
|
||||
await this._save(set)
|
||||
}
|
||||
|
||||
async addIpPoolToResourceSet (ipPoolId, setId) {
|
||||
const set = await this.getResourceSet(setId)
|
||||
set.ipPools.push(ipPoolId)
|
||||
await this._save(set)
|
||||
}
|
||||
|
||||
async removeIpPoolFromResourceSet (ipPoolId, setId) {
|
||||
const set = await this.getResourceSet(setId)
|
||||
remove(set.ipPools, id => id === ipPoolId)
|
||||
await this._save(set)
|
||||
}
|
||||
|
||||
@@ -230,7 +256,7 @@ export default class {
|
||||
|
||||
async removeSubjectToResourceSet (subjectId, setId) {
|
||||
const set = await this.getResourceSet(setId)
|
||||
remove(set.subjects, subjectId)
|
||||
remove(set.subjects, id => id === subjectId)
|
||||
await this._save(set)
|
||||
}
|
||||
|
||||
@@ -280,7 +306,9 @@ export default class {
|
||||
const sets = keyBy(await this.getAllResourceSets(), 'id')
|
||||
forEach(sets, ({ limits }) => {
|
||||
forEach(limits, (limit, id) => {
|
||||
limit.available = limit.total
|
||||
if (VM_RESOURCES[limit]) { // only reset VMs related limits
|
||||
limit.available = limit.total
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { BaseError } from 'make-error'
|
||||
import { NoSuchObject } from '../api-errors.js'
|
||||
import { Schedules } from '../models/schedule'
|
||||
import { noSuchObject } from 'xo-common/api-errors.js'
|
||||
|
||||
import { Schedules } from '../models/schedule'
|
||||
import {
|
||||
forEach,
|
||||
mapToArray,
|
||||
scheduleFn
|
||||
} from '../utils'
|
||||
|
||||
@@ -19,12 +20,6 @@ export class ScheduleOverride extends SchedulerError {
|
||||
}
|
||||
}
|
||||
|
||||
export class NoSuchSchedule extends NoSuchObject {
|
||||
constructor (scheduleOrId) {
|
||||
super(scheduleOrId, 'schedule')
|
||||
}
|
||||
}
|
||||
|
||||
export class ScheduleNotEnabled extends SchedulerError {
|
||||
constructor (scheduleOrId) {
|
||||
super('Schedule ' + _resolveId(scheduleOrId)) + ' is not enabled'
|
||||
@@ -42,14 +37,23 @@ export class ScheduleAlreadyEnabled extends SchedulerError {
|
||||
export default class {
|
||||
constructor (xo) {
|
||||
this.xo = xo
|
||||
this._redisSchedules = new Schedules({
|
||||
const schedules = this._redisSchedules = new Schedules({
|
||||
connection: xo._redis,
|
||||
prefix: 'xo:schedule',
|
||||
indexes: ['user_id', 'job']
|
||||
})
|
||||
this._scheduleTable = undefined
|
||||
|
||||
xo.on('start', () => this._loadSchedules())
|
||||
xo.on('start', () => {
|
||||
xo.addConfigManager('schedules',
|
||||
() => schedules.get(),
|
||||
schedules_ => Promise.all(mapToArray(schedules_, schedule =>
|
||||
schedules.save(schedule)
|
||||
))
|
||||
)
|
||||
|
||||
return this._loadSchedules()
|
||||
})
|
||||
xo.on('stop', () => this._disableAll())
|
||||
}
|
||||
|
||||
@@ -86,7 +90,7 @@ export default class {
|
||||
|
||||
_disable (scheduleOrId) {
|
||||
if (!this._exists(scheduleOrId)) {
|
||||
throw new NoSuchSchedule(scheduleOrId)
|
||||
throw noSuchObject(scheduleOrId, 'schedule')
|
||||
}
|
||||
if (!this._isEnabled(scheduleOrId)) {
|
||||
throw new ScheduleNotEnabled(scheduleOrId)
|
||||
@@ -125,7 +129,7 @@ export default class {
|
||||
const schedule = await this._redisSchedules.first(id)
|
||||
|
||||
if (!schedule) {
|
||||
throw new NoSuchSchedule(id)
|
||||
throw noSuchObject(id, 'schedule')
|
||||
}
|
||||
|
||||
return schedule
|
||||
@@ -166,7 +170,7 @@ export default class {
|
||||
const { properties } = schedule
|
||||
|
||||
if (!this._exists(properties)) {
|
||||
throw new NoSuchSchedule(properties)
|
||||
throw noSuchObject(properties, 'schedule')
|
||||
}
|
||||
|
||||
if (this._isEnabled(properties)) {
|
||||
@@ -182,7 +186,7 @@ export default class {
|
||||
try {
|
||||
this._disable(id)
|
||||
} catch (exc) {
|
||||
if (!exc instanceof SchedulerError) {
|
||||
if (!(exc instanceof SchedulerError)) {
|
||||
throw exc
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -54,7 +54,7 @@ const levelPromise = db => {
|
||||
dbP[name] = db::value
|
||||
} else {
|
||||
dbP[`${name}Sync`] = db::value
|
||||
dbP[name] = value::promisify(db)
|
||||
dbP[name] = promisify(value, db)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ import {
|
||||
needsRehash,
|
||||
verify
|
||||
} from 'hashy'
|
||||
|
||||
import {
|
||||
InvalidCredential,
|
||||
NoSuchObject
|
||||
} from '../api-errors'
|
||||
invalidCredentials,
|
||||
noSuchObject
|
||||
} from 'xo-common/api-errors'
|
||||
|
||||
import {
|
||||
Groups
|
||||
} from '../models/group'
|
||||
@@ -27,18 +27,6 @@ import {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class NoSuchGroup extends NoSuchObject {
|
||||
constructor (id) {
|
||||
super(id, 'group')
|
||||
}
|
||||
}
|
||||
|
||||
class NoSuchUser extends NoSuchObject {
|
||||
constructor (id) {
|
||||
super(id, 'user')
|
||||
}
|
||||
}
|
||||
|
||||
const addToArraySet = (set, value) => set && !includes(set, value)
|
||||
? set.concat(value)
|
||||
: [ value ]
|
||||
@@ -52,22 +40,40 @@ export default class {
|
||||
|
||||
const redis = xo._redis
|
||||
|
||||
this._groups = new Groups({
|
||||
const groupsDb = this._groups = new Groups({
|
||||
connection: redis,
|
||||
prefix: 'xo:group'
|
||||
})
|
||||
const users = this._users = new Users({
|
||||
const usersDb = this._users = new Users({
|
||||
connection: redis,
|
||||
prefix: 'xo:user',
|
||||
indexes: ['email']
|
||||
})
|
||||
|
||||
xo.on('start', async () => {
|
||||
if (!await users.exists()) {
|
||||
xo.addConfigManager('groups',
|
||||
() => groupsDb.get(),
|
||||
groups => Promise.all(mapToArray(groups, group => groupsDb.save(group)))
|
||||
)
|
||||
xo.addConfigManager('users',
|
||||
() => usersDb.get(),
|
||||
users => Promise.all(mapToArray(users, async user => {
|
||||
const userId = user.id
|
||||
const conflictUsers = await usersDb.get({ email: user.email })
|
||||
if (!isEmpty(conflictUsers)) {
|
||||
await Promise.all(mapToArray(conflictUsers, ({ id }) =>
|
||||
(id !== userId) && this.deleteUser(user.id)
|
||||
))
|
||||
}
|
||||
return usersDb.save(user)
|
||||
}))
|
||||
)
|
||||
|
||||
if (!await usersDb.exists()) {
|
||||
const email = 'admin@admin.net'
|
||||
const password = 'admin'
|
||||
|
||||
await this.createUser(email, {password, permission: 'admin'})
|
||||
await this.createUser({email, password, permission: 'admin'})
|
||||
console.log('[INFO] Default user created:', email, ' with password', password)
|
||||
}
|
||||
})
|
||||
@@ -75,13 +81,17 @@ export default class {
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async createUser (email, { password, ...properties }) {
|
||||
async createUser ({ name, password, ...properties }) {
|
||||
if (name) {
|
||||
properties.email = name
|
||||
}
|
||||
|
||||
if (password) {
|
||||
properties.pw_hash = await hash(password)
|
||||
}
|
||||
|
||||
// TODO: use plain objects
|
||||
const user = await this._users.create(email, properties)
|
||||
const user = await this._users.create(properties)
|
||||
|
||||
return user.properties
|
||||
}
|
||||
@@ -100,6 +110,13 @@ export default class {
|
||||
})
|
||||
::pCatch(noop) // Ignore any failures.
|
||||
|
||||
// Remove ACLs for this user.
|
||||
this._xo.getAclsForSubject(id).then(acls => {
|
||||
forEach(acls, acl => {
|
||||
this._xo.removeAcl(id, acl.object, acl.action)::pCatch(noop)
|
||||
})
|
||||
})
|
||||
|
||||
// Remove the user from all its groups.
|
||||
forEach(user.groups, groupId => {
|
||||
this.getGroup(groupId)
|
||||
@@ -152,7 +169,7 @@ export default class {
|
||||
async _getUser (id) {
|
||||
const user = await this._users.first(id)
|
||||
if (!user) {
|
||||
throw new NoSuchUser(id)
|
||||
throw noSuchObject(id, 'user')
|
||||
}
|
||||
|
||||
return user
|
||||
@@ -185,7 +202,7 @@ export default class {
|
||||
return null
|
||||
}
|
||||
|
||||
throw new NoSuchUser(username)
|
||||
throw noSuchObject(username, 'user')
|
||||
}
|
||||
|
||||
// Get or create a user associated with an auth provider.
|
||||
@@ -203,14 +220,15 @@ export default class {
|
||||
throw new Error(`registering ${name} user is forbidden`)
|
||||
}
|
||||
|
||||
return /* await */ this.createUser(name, {
|
||||
return /* await */ this.createUser({
|
||||
name,
|
||||
_provider: provider
|
||||
})
|
||||
}
|
||||
|
||||
async changeUserPassword (userId, oldPassword, newPassword) {
|
||||
if (!(await this.checkUserPassword(userId, oldPassword, false))) {
|
||||
throw new InvalidCredential()
|
||||
throw invalidCredentials()
|
||||
}
|
||||
|
||||
await this.updateUser(userId, { password: newPassword })
|
||||
@@ -247,6 +265,13 @@ export default class {
|
||||
|
||||
await this._groups.remove(id)
|
||||
|
||||
// Remove ACLs for this group.
|
||||
this._xo.getAclsForSubject(id).then(acls => {
|
||||
forEach(acls, acl => {
|
||||
this._xo.removeAcl(id, acl.object, acl.action)::pCatch(noop)
|
||||
})
|
||||
})
|
||||
|
||||
// Remove the group from all its users.
|
||||
forEach(group.users, userId => {
|
||||
this.getUser(userId)
|
||||
@@ -266,7 +291,7 @@ export default class {
|
||||
async getGroup (id) {
|
||||
const group = await this._groups.first(id)
|
||||
if (!group) {
|
||||
throw new NoSuchGroup(id)
|
||||
throw noSuchObject(id, 'group')
|
||||
}
|
||||
|
||||
return group.properties
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
import Xapi from '../xapi'
|
||||
import xapiObjectToXo from '../xapi-object-to-xo'
|
||||
import XapiStats from '../xapi-stats'
|
||||
import {
|
||||
GenericError,
|
||||
NoSuchObject
|
||||
} from '../api-errors'
|
||||
import {
|
||||
camelToSnakeCase,
|
||||
createRawObject,
|
||||
@@ -21,18 +19,10 @@ import {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class NoSuchXenServer extends NoSuchObject {
|
||||
constructor (id) {
|
||||
super(id, 'xen server')
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class {
|
||||
constructor (xo) {
|
||||
this._objectConflicts = createRawObject() // TODO: clean when a server is disconnected.
|
||||
this._servers = new Servers({
|
||||
const serversDb = this._servers = new Servers({
|
||||
connection: xo._redis,
|
||||
prefix: 'xo:server',
|
||||
indexes: ['host']
|
||||
@@ -43,8 +33,13 @@ export default class {
|
||||
this._xo = xo
|
||||
|
||||
xo.on('start', async () => {
|
||||
xo.addConfigManager('xenServers',
|
||||
() => serversDb.get(),
|
||||
servers => serversDb.update(servers)
|
||||
)
|
||||
|
||||
// Connects to existing servers.
|
||||
const servers = await this._servers.get()
|
||||
const servers = await serversDb.get()
|
||||
for (let server of servers) {
|
||||
if (server.enabled) {
|
||||
this.connectXenServer(server.id).catch(error => {
|
||||
@@ -79,7 +74,7 @@ export default class {
|
||||
this.disconnectXenServer(id)::pCatch(noop)
|
||||
|
||||
if (!await this._servers.remove(id)) {
|
||||
throw new NoSuchXenServer(id)
|
||||
throw noSuchObject(id, 'xenServer')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +105,7 @@ export default class {
|
||||
async _getXenServer (id) {
|
||||
const server = await this._servers.first(id)
|
||||
if (!server) {
|
||||
throw new NoSuchXenServer(id)
|
||||
throw noSuchObject(id, 'xenServer')
|
||||
}
|
||||
|
||||
return server
|
||||
@@ -283,23 +278,13 @@ export default class {
|
||||
|
||||
xapi.xo.install()
|
||||
|
||||
try {
|
||||
await xapi.connect()
|
||||
} catch (error) {
|
||||
if (error.code === 'SESSION_AUTHENTICATION_FAILED') {
|
||||
throw new GenericError('authentication failed')
|
||||
}
|
||||
if (error.code === 'EHOSTUNREACH') {
|
||||
throw new GenericError('host unreachable')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
await xapi.connect()
|
||||
}
|
||||
|
||||
async disconnectXenServer (id) {
|
||||
const xapi = this._xapis[id]
|
||||
if (!xapi) {
|
||||
throw new NoSuchXenServer(id)
|
||||
throw noSuchObject(id, 'xenServer')
|
||||
}
|
||||
|
||||
delete this._xapis[id]
|
||||
|
||||
34
src/xo.js
34
src/xo.js
@@ -3,6 +3,7 @@ import XoCollection from 'xo-collection'
|
||||
import XoUniqueIndex from 'xo-collection/unique-index'
|
||||
import {createClient as createRedisClient} from 'redis'
|
||||
import {EventEmitter} from 'events'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
import mixins from './xo-mixins'
|
||||
import Connection from './connection'
|
||||
@@ -20,9 +21,6 @@ import {
|
||||
mapToArray,
|
||||
noop
|
||||
} from './utils'
|
||||
import {
|
||||
NoSuchObject
|
||||
} from './api-errors'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -43,7 +41,29 @@ export default class Xo extends EventEmitter {
|
||||
this._httpRequestWatchers = createRawObject()
|
||||
|
||||
// Connects to Redis.
|
||||
this._redis = createRedisClient(config.redis && config.redis.uri)
|
||||
const redisConf = config.redis
|
||||
this._redis = createRedisClient(redisConf && {
|
||||
rename_commands: redisConf.renameCommands,
|
||||
url: redisConf.uri
|
||||
})
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async clean () {
|
||||
const handleCleanError = error => {
|
||||
console.error(
|
||||
'[WARN] clean error:',
|
||||
error && error.stack || error
|
||||
)
|
||||
}
|
||||
await Promise.all(mapToArray(
|
||||
this.listeners('clean'),
|
||||
|
||||
listener => new Promise(resolve => {
|
||||
resolve(listener.call(this))
|
||||
}).catch(handleCleanError)
|
||||
))
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
@@ -122,14 +142,14 @@ export default class Xo extends EventEmitter {
|
||||
|
||||
const obj = all[key] || byRef[key]
|
||||
if (!obj) {
|
||||
throw new NoSuchObject(key, type)
|
||||
throw noSuchObject(key, type)
|
||||
}
|
||||
|
||||
if (type != null && (
|
||||
isString(type) && type !== obj.type ||
|
||||
!includes(type, obj.type) // Array
|
||||
)) {
|
||||
throw new NoSuchObject(key, type)
|
||||
throw noSuchObject(key, type)
|
||||
}
|
||||
|
||||
return obj
|
||||
@@ -330,7 +350,7 @@ export default class Xo extends EventEmitter {
|
||||
|
||||
forEach(connections, connection => {
|
||||
// Notifies only authenticated clients.
|
||||
if (connection.has('user_id')) {
|
||||
if (connection.has('user_id') && connection.notify) {
|
||||
if (enteredMessage) {
|
||||
connection.notify('all', enteredMessage)
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var join = require('path').join
|
||||
var readdir = require('fs').readdirSync
|
||||
var stat = require('fs').statSync
|
||||
var writeFile = require('fs').writeFileSync
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function bind (fn, thisArg) {
|
||||
return function () {
|
||||
return fn.apply(thisArg, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
function camelCase (str) {
|
||||
return str.toLowerCase().replace(/[^a-z0-9]+([a-z0-9])/g, function (_, str) {
|
||||
return str.toUpperCase()
|
||||
})
|
||||
}
|
||||
|
||||
function removeSuffix (str, sfx) {
|
||||
var strLength = str.length
|
||||
var sfxLength = sfx.length
|
||||
|
||||
var pos = strLength - sfxLength
|
||||
if (pos < 0 || str.indexOf(sfx, pos) !== pos) {
|
||||
return false
|
||||
}
|
||||
|
||||
return str.slice(0, pos)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function handleEntry (entry, dir) {
|
||||
var stats = stat(join(dir, entry))
|
||||
var base
|
||||
if (stats.isDirectory()) {
|
||||
base = entry
|
||||
} else if (!(
|
||||
stats.isFile() && (
|
||||
(base = removeSuffix(entry, '.coffee')) ||
|
||||
(base = removeSuffix(entry, '.js'))
|
||||
)
|
||||
)) {
|
||||
return
|
||||
}
|
||||
|
||||
var identifier = camelCase(base)
|
||||
this(
|
||||
'import ' + identifier + " from './" + base + "'",
|
||||
'defaults.' + identifier + ' = ' + identifier,
|
||||
'export * as ' + identifier + " from './" + base + "'",
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
function generateIndex (dir) {
|
||||
var content = [
|
||||
'//',
|
||||
'// This file has been generated by /tools/generate-index',
|
||||
'//',
|
||||
'// It is automatically re-generated each time a build is started.',
|
||||
'//',
|
||||
'',
|
||||
'const defaults = {}',
|
||||
'export default defaults',
|
||||
''
|
||||
]
|
||||
var write = bind(content.push, content)
|
||||
|
||||
readdir(dir).map(function (entry) {
|
||||
if (entry === 'index.js') {
|
||||
return
|
||||
}
|
||||
|
||||
handleEntry.call(write, entry, dir)
|
||||
})
|
||||
|
||||
writeFile(dir + '/index.js', content.join('\n'))
|
||||
}
|
||||
|
||||
process.argv.slice(2).map(generateIndex)
|
||||
Reference in New Issue
Block a user