Compare commits
57 Commits
xo-server/
...
xo-server/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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
|
||||
32
package.json
32
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server",
|
||||
"version": "5.2.1",
|
||||
"version": "5.3.2",
|
||||
"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,7 +51,7 @@
|
||||
"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",
|
||||
@@ -62,9 +62,10 @@
|
||||
"helmet": "^2.0.0",
|
||||
"highland": "^2.5.1",
|
||||
"http-proxy": "^1.13.2",
|
||||
"http-server-plus": "^0.6.4",
|
||||
"human-format": "^0.6.0",
|
||||
"http-server-plus": "^0.7.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.12.0",
|
||||
"json5": "^0.5.0",
|
||||
@@ -86,7 +87,7 @@
|
||||
"partial-stream": "0.0.0",
|
||||
"passport": "^0.3.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"promise-toolbox": "^0.5.0",
|
||||
"promise-toolbox": "^0.7.0",
|
||||
"proxy-agent": "^2.0.0",
|
||||
"pug": "^2.0.0-alpha6",
|
||||
"redis": "^2.0.1",
|
||||
@@ -94,20 +95,21 @@
|
||||
"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": "^2.0.3",
|
||||
"ws": "^1.1.1",
|
||||
"xen-api": "^0.9.4",
|
||||
"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-vmdk-to-vhd": "0.0.5"
|
||||
"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",
|
||||
@@ -119,20 +121,21 @@
|
||||
"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.0.0",
|
||||
"leche": "^2.1.1",
|
||||
"mocha": "^3.0.2",
|
||||
"must": "^0.13.1",
|
||||
"nyc": "^8.1.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",
|
||||
@@ -146,6 +149,7 @@
|
||||
},
|
||||
"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.
|
||||
|
||||
#=====================================================================
|
||||
|
||||
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.m-b-2(style = 'display: flex;')
|
||||
img(src = 'assets/logo.png' style = 'margin: auto;')
|
||||
h2.text-xs-center.m-b-2 Xen Orchestra
|
||||
form(action = 'signin/local' method = 'post')
|
||||
fieldset
|
||||
if error
|
||||
p.text-danger #{error}
|
||||
.input-group.m-b-1
|
||||
span.input-group-addon
|
||||
i.xo-icon-user.fa-fw
|
||||
input.form-control(
|
||||
name = 'username'
|
||||
type = 'text'
|
||||
placeholder = 'Username'
|
||||
required
|
||||
)
|
||||
.input-group.m-b-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}
|
||||
|
||||
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'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Unauthorized } from '../api-errors'
|
||||
|
||||
export function create (props) {
|
||||
return this.createIpPool(props)
|
||||
}
|
||||
@@ -15,11 +17,18 @@ delete_.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function getAll () {
|
||||
return this.getAllIpPools()
|
||||
}
|
||||
export function getAll (params) {
|
||||
const { user } = this
|
||||
|
||||
getAll.permission = 'admin'
|
||||
if (!user) {
|
||||
throw new Unauthorized()
|
||||
}
|
||||
|
||||
return this.getAllIpPools(user.permission === 'admin'
|
||||
? params && params.userId
|
||||
: user.id
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ get.params = {
|
||||
}
|
||||
|
||||
export async function create ({job}) {
|
||||
return (await this.createJob(this.session.get('user_id'), job)).id
|
||||
job.userId = this.session.get('user_id')
|
||||
|
||||
return (await this.createJob(job)).id
|
||||
}
|
||||
|
||||
create.permission = 'admin'
|
||||
|
||||
@@ -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,39 @@ 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 ({
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
|
||||
@@ -35,21 +35,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']
|
||||
}
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@ 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'
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import forEach from 'lodash/forEach'
|
||||
|
||||
import {
|
||||
diffItems,
|
||||
noop,
|
||||
@@ -10,12 +8,12 @@ import {
|
||||
|
||||
// TODO: move into vm and rename to removeInterface
|
||||
async function delete_ ({vif}) {
|
||||
const { id } = vif
|
||||
const dealloc = address => {
|
||||
this.deallocIpAddress(address, id)::pCatch(noop)
|
||||
}
|
||||
forEach(vif.allowedIpv4Addresses, dealloc)
|
||||
forEach(vif.allowedIpv6Addresses, dealloc)
|
||||
this.allocIpAddresses(
|
||||
vif.id,
|
||||
vif.$network,
|
||||
null,
|
||||
vif.allowedIpv4Addresses.concat(vif.allowedIpv6Addresses)
|
||||
)::pCatch(noop)
|
||||
|
||||
await this.getXapi(vif).deleteVif(vif._xapiId)
|
||||
}
|
||||
@@ -34,7 +32,7 @@ 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 = {
|
||||
@@ -49,7 +47,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 = {
|
||||
@@ -62,18 +60,54 @@ connect.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function set ({ vif, allowedIpv4Addresses, allowedIpv6Addresses }) {
|
||||
const { id } = vif
|
||||
const handle = ([ newAddresses, oldAddresses ]) => {
|
||||
forEach(newAddresses, address => {
|
||||
this.allocIpAddress(address, id)::pCatch(noop)
|
||||
})
|
||||
forEach(oldAddresses, address => {
|
||||
this.deallocIpAddress(address, id)::pCatch(noop)
|
||||
})
|
||||
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)
|
||||
}
|
||||
handle(diffItems(allowedIpv4Addresses, vif.allowedIpv4Addresses))
|
||||
handle(diffItems(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)
|
||||
|
||||
// create new VIF with new parameters
|
||||
await xapi.createVif(vm.$id, network.$id, {
|
||||
mac,
|
||||
currently_attached: attached,
|
||||
ipv4Allowed: allowedIpv4Addresses,
|
||||
ipv6Allowed: allowedIpv6Addresses
|
||||
})
|
||||
|
||||
await this.allocIpAddresses(vif.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,
|
||||
@@ -82,6 +116,9 @@ export function set ({ vif, allowedIpv4Addresses, allowedIpv6Addresses }) {
|
||||
}
|
||||
|
||||
set.params = {
|
||||
id: { type: 'string' },
|
||||
network: { type: 'string', optional: true },
|
||||
mac: { type: 'string', optional: true },
|
||||
allowedIpv4Addresses: {
|
||||
type: 'array',
|
||||
items: {
|
||||
@@ -95,9 +132,11 @@ set.params = {
|
||||
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'
|
||||
@@ -61,16 +62,9 @@ extract = (obj, prop) ->
|
||||
|
||||
# TODO: Implement ACLs
|
||||
create = $coroutine (params) ->
|
||||
checkLimits = limits = null
|
||||
|
||||
{ user } = this
|
||||
resourceSet = extract(params, 'resourceSet')
|
||||
if resourceSet
|
||||
yield this.checkResourceSetConstraints(resourceSet, user.id, objectIds)
|
||||
checkLimits = $coroutine (limits2) =>
|
||||
yield this.allocateLimitsInResourceSet(limits, resourceSet)
|
||||
yield this.allocateLimitsInResourceSet(limits2, resourceSet)
|
||||
else unless user.permission is 'admin'
|
||||
if not resourceSet and user.permission isnt 'admin'
|
||||
throw new Unauthorized()
|
||||
|
||||
template = extract(params, 'template')
|
||||
@@ -141,12 +135,22 @@ 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
|
||||
|
||||
checkLimits = null
|
||||
|
||||
if resourceSet
|
||||
yield this.checkResourceSetConstraints(resourceSet, user.id, objectIds)
|
||||
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)
|
||||
|
||||
@@ -156,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
|
||||
@@ -215,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' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,7 +287,7 @@ create.params = {
|
||||
}
|
||||
|
||||
create.resolve = {
|
||||
template: ['template', 'VM-template', 'administrate'],
|
||||
template: ['template', 'VM-template', ''],
|
||||
}
|
||||
|
||||
exports.create = create
|
||||
@@ -465,7 +495,7 @@ set.params = {
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
VM: ['id', ['VM', 'VM-snapshot'], 'administrate']
|
||||
VM: ['id', ['VM', 'VM-snapshot', 'VM-template'], 'administrate']
|
||||
}
|
||||
|
||||
exports.set = set
|
||||
@@ -878,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 = {
|
||||
@@ -1057,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: ['integer', 'string'], optional: true }
|
||||
mtu: { 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 = {
|
||||
|
||||
@@ -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,
|
||||
@@ -41,7 +42,7 @@ export default class Redis extends Collection {
|
||||
|
||||
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)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
24
src/index.js
24
src/index.js
@@ -179,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)
|
||||
@@ -608,16 +608,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')
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -226,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))
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -272,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,
|
||||
@@ -383,7 +394,7 @@ const TRANSFORMS = {
|
||||
return {
|
||||
type: 'PBD',
|
||||
|
||||
attached: obj.currently_attached,
|
||||
attached: Boolean(obj.currently_attached),
|
||||
host: link(obj, 'host'),
|
||||
SR: link(obj, 'SR')
|
||||
}
|
||||
@@ -396,6 +407,7 @@ const TRANSFORMS = {
|
||||
type: 'PIF',
|
||||
|
||||
attached: Boolean(obj.currently_attached),
|
||||
isBondMaster: !isEmpty(obj.bond_master_of),
|
||||
device: obj.device,
|
||||
dns: obj.DNS,
|
||||
disallowUnplug: Boolean(obj.disallow_unplug),
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
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 tarStream from 'tar-stream'
|
||||
import uniq from 'lodash/uniq'
|
||||
import vmdkToVhd from 'xo-vmdk-to-vhd'
|
||||
import { defer } from 'promise-toolbox'
|
||||
import {
|
||||
@@ -53,6 +55,7 @@ import {
|
||||
getNamespaceForType,
|
||||
isVmHvm,
|
||||
isVmRunning,
|
||||
NULL_REF,
|
||||
optional,
|
||||
prepareXapiParam,
|
||||
put
|
||||
@@ -73,6 +76,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))
|
||||
@@ -624,7 +630,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),
|
||||
@@ -761,7 +767,8 @@ export default class Xapi extends XapiBase {
|
||||
session_id: this.sessionId,
|
||||
task_id: taskRef,
|
||||
use_compression: compress ? 'true' : 'false'
|
||||
}
|
||||
},
|
||||
rejectUnauthorized: false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -905,7 +912,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, {
|
||||
@@ -995,7 +1002,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
|
||||
|
||||
@@ -1149,7 +1156,7 @@ export default class Xapi extends XapiBase {
|
||||
VCPUs_max: nCpus
|
||||
})
|
||||
)
|
||||
$onFailure(() => this._deleteVm(vm))
|
||||
$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...'),
|
||||
@@ -1799,15 +1806,14 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
async _createVif (vm, network, {
|
||||
mac = '',
|
||||
mtu = network.MTU,
|
||||
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 = ''
|
||||
@@ -1824,7 +1830,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,
|
||||
@@ -1832,18 +1838,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(
|
||||
@@ -1854,10 +1855,6 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
async deleteVif (vifId) {
|
||||
await this._deleteVif(this.getObject(vifId))
|
||||
}
|
||||
|
||||
async createNetwork ({
|
||||
name,
|
||||
description = 'Created with Xen Orchestra',
|
||||
@@ -1878,10 +1875,56 @@ 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 || obj.bond_master_of) &&
|
||||
obj.$pool === pif.$pool &&
|
||||
obj.device === pif.device
|
||||
))
|
||||
|
||||
if (!physPif) {
|
||||
throw new Error('PIF not found')
|
||||
}
|
||||
|
||||
await this.call('VLAN.destroy', pif.VLAN_master_of)
|
||||
const pifs = await this.call('pool.create_VLAN_from_PIF', physPif.$ref, pif.network, asInteger(vlan))
|
||||
if (!pif.currently_attached) {
|
||||
forEach(pifs, pifRef => 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)
|
||||
@@ -1960,7 +2003,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
@@ -3,6 +3,28 @@ 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: {
|
||||
get: true,
|
||||
|
||||
@@ -26,8 +26,6 @@ export default {
|
||||
name_label, // deprecated
|
||||
nameLabel = name_label, // eslint-disable-line camelcase
|
||||
|
||||
bootAfterCreate = false,
|
||||
|
||||
clone = true,
|
||||
installRepository = undefined,
|
||||
vdis = undefined,
|
||||
@@ -178,6 +176,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
|
||||
@@ -210,10 +210,6 @@ export default {
|
||||
await this[method](vm.$id, srRef, cloudConfig)
|
||||
}
|
||||
|
||||
if (bootAfterCreate) {
|
||||
this._startVm(vm)::pCatch(noop)
|
||||
}
|
||||
|
||||
return this._waitObject(vm.$id)
|
||||
},
|
||||
|
||||
@@ -326,5 +322,13 @@ export default {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { NULL_REF } from './utils'
|
||||
|
||||
const OTHER_CONFIG_TEMPLATE = {
|
||||
actions_after_crash: 'restart',
|
||||
actions_after_reboot: 'restart',
|
||||
@@ -32,7 +34,7 @@ const OTHER_CONFIG_TEMPLATE = {
|
||||
hpet: 'true',
|
||||
viridian: 'true'
|
||||
},
|
||||
protection_policy: 'OpaqueRef:NULL',
|
||||
protection_policy: NULL_REF,
|
||||
PV_args: '',
|
||||
PV_bootloader: '',
|
||||
PV_bootloader_args: '',
|
||||
|
||||
@@ -317,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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -348,6 +354,10 @@ export const makeEditObject = specs => {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
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, {
|
||||
|
||||
0
src/xo-mixins/.index-modules
Normal file
0
src/xo-mixins/.index-modules
Normal file
@@ -24,6 +24,15 @@ export default class {
|
||||
prefix: 'xo:acl',
|
||||
indexes: ['subject', 'object']
|
||||
})
|
||||
|
||||
xo.on('start', () => {
|
||||
xo.addConfigManager('acls',
|
||||
() => this.getAllAcls(),
|
||||
acls => Promise.all(mapToArray(acls, acl =>
|
||||
this.addAcl(acl.subjectId, acl.objectId, acl.action)
|
||||
))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async _getAclsForUser (userId) {
|
||||
@@ -39,10 +48,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 +75,10 @@ export default class {
|
||||
return this._acls.get()
|
||||
}
|
||||
|
||||
async getAclsForSubject (subjectId) {
|
||||
return this._acls.get({ subject: subjectId })
|
||||
}
|
||||
|
||||
async getPermissionsForUser (userId) {
|
||||
const [
|
||||
acls,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
} from '../api-errors'
|
||||
import {
|
||||
createRawObject,
|
||||
forEach,
|
||||
generateToken,
|
||||
pCatch,
|
||||
noop
|
||||
@@ -30,7 +31,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 +66,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) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from 'path'
|
||||
import { satisfies as versionSatisfies } from 'semver'
|
||||
|
||||
import vhdMerge from '../vhd-merge'
|
||||
import vhdMerge, { chainVhd } from '../vhd-merge'
|
||||
import xapiObjectToXo from '../xapi-object-to-xo'
|
||||
import {
|
||||
deferrable
|
||||
@@ -50,21 +50,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')
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
@@ -291,6 +298,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 +572,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 }) })
|
||||
})
|
||||
)
|
||||
|
||||
@@ -706,9 +727,10 @@ export default class {
|
||||
})
|
||||
await targetXapi.addTag(drCopy.$id, 'Disaster Recovery')
|
||||
|
||||
await Promise.all(mapToArray(olderCopies.slice(0, 1 - depth), vm =>
|
||||
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)::pCatch(noop)
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
import highland from 'highland'
|
||||
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 { fromCallback } from 'promise-toolbox'
|
||||
|
||||
import { NoSuchObject } from '../api-errors'
|
||||
@@ -8,6 +15,8 @@ import {
|
||||
forEach,
|
||||
generateUnsecureToken,
|
||||
isEmpty,
|
||||
lightSet,
|
||||
mapToArray,
|
||||
streamToArray,
|
||||
throwFn
|
||||
} from '../utils'
|
||||
@@ -24,16 +33,20 @@ const normalize = ({
|
||||
addresses,
|
||||
id = throwFn('id is a required field'),
|
||||
name = '',
|
||||
networks
|
||||
networks,
|
||||
resourceSets
|
||||
}) => ({
|
||||
addresses,
|
||||
id,
|
||||
name,
|
||||
networks
|
||||
networks,
|
||||
resourceSets
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Note: an address cannot be in two different pools sharing a
|
||||
// network.
|
||||
export default class IpPools {
|
||||
constructor (xo) {
|
||||
this._store = null
|
||||
@@ -41,6 +54,11 @@ export default class IpPools {
|
||||
|
||||
xo.on('start', async () => {
|
||||
this._store = await xo.getStore('ipPools')
|
||||
|
||||
xo.addConfigManager('ipPools',
|
||||
() => this.getAllIpPools(),
|
||||
ipPools => Promise.all(mapToArray(ipPools, ipPool => this._save(ipPool)))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,14 +79,33 @@ export default class IpPools {
|
||||
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 new NoSuchIpPool(id)
|
||||
}
|
||||
|
||||
getAllIpPools () {
|
||||
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
|
||||
})
|
||||
}
|
||||
@@ -79,37 +116,110 @@ export default class IpPools {
|
||||
})
|
||||
}
|
||||
|
||||
allocIpAddress (address, vifId) {
|
||||
// FIXME: does not work correctly if the address is in multiple
|
||||
// pools.
|
||||
return this._getForAddress(address).then(ipPool => {
|
||||
const data = ipPool.addresses[address]
|
||||
const vifs = data.vifs || (data.vifs = [])
|
||||
if (!includes(vifs, vifId)) {
|
||||
vifs.push(vifId)
|
||||
return this._save(ipPool)
|
||||
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)
|
||||
}
|
||||
|
||||
deallocIpAddress (address, vifId) {
|
||||
return this._getForAddress(address).then(ipPool => {
|
||||
const data = ipPool.addresses[address]
|
||||
const vifs = data.vifs || (data.vifs = [])
|
||||
const i = findIndex(vifs, id => id === vifId)
|
||||
if (i !== -1) {
|
||||
vifs.splice(i, 1)
|
||||
return this._save(ipPool)
|
||||
}
|
||||
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
|
||||
networks,
|
||||
resourceSets
|
||||
}) {
|
||||
const ipPool = await this.getIpPool(id)
|
||||
const previousAddresses = { ...ipPool.addresses }
|
||||
|
||||
name != null && (ipPool.name = name)
|
||||
if (addresses) {
|
||||
@@ -121,6 +231,11 @@ export default class IpPools {
|
||||
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 {
|
||||
@@ -133,6 +248,11 @@ export default class IpPools {
|
||||
ipPool.networks = networks
|
||||
}
|
||||
|
||||
// TODO: Implement patching like for addresses.
|
||||
if (resourceSets) {
|
||||
ipPool.resourceSets = resourceSets
|
||||
}
|
||||
|
||||
await this._save(ipPool)
|
||||
}
|
||||
|
||||
@@ -144,15 +264,6 @@ export default class IpPools {
|
||||
return id
|
||||
}
|
||||
|
||||
_getForAddress (address) {
|
||||
return fromCallback(cb => {
|
||||
highland(this._store.createValueStream()).find(ipPool => {
|
||||
const { addresses } = ipPool
|
||||
return addresses && addresses[address]
|
||||
}).pull(cb)
|
||||
})
|
||||
}
|
||||
|
||||
_save (ipPool) {
|
||||
ipPool = normalize(ipPool)
|
||||
return this._store.put(ipPool.id, ipPool)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import assign from 'lodash/assign'
|
||||
|
||||
import JobExecutor from '../job-executor'
|
||||
import { Jobs } from '../models/job'
|
||||
import { mapToArray } from '../utils'
|
||||
import {
|
||||
GenericError,
|
||||
NoSuchObject
|
||||
@@ -19,11 +21,20 @@ class NoSuchJob extends NoSuchObject {
|
||||
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 () {
|
||||
@@ -39,9 +50,9 @@ export default class {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,15 @@ 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) {
|
||||
|
||||
@@ -2,7 +2,8 @@ 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
|
||||
@@ -30,6 +31,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()
|
||||
})
|
||||
|
||||
@@ -25,6 +25,14 @@ class NoSuchResourceSet extends NoSuchObject {
|
||||
}
|
||||
}
|
||||
|
||||
const VM_RESOURCES = {
|
||||
cpus: true,
|
||||
disk: true,
|
||||
disks: true,
|
||||
memory: true,
|
||||
vms: true
|
||||
}
|
||||
|
||||
const computeVmResourcesUsage = vm => {
|
||||
const processed = {}
|
||||
let disks = 0
|
||||
@@ -54,6 +62,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 +85,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')
|
||||
})
|
||||
}
|
||||
@@ -147,7 +163,8 @@ export default class {
|
||||
name = undefined,
|
||||
subjects = undefined,
|
||||
objects = undefined,
|
||||
limits = undefined
|
||||
limits = undefined,
|
||||
ipPools = undefined
|
||||
}) {
|
||||
const set = await this.getResourceSet(id)
|
||||
if (name) {
|
||||
@@ -178,6 +195,9 @@ export default class {
|
||||
}
|
||||
})
|
||||
}
|
||||
if (ipPools) {
|
||||
set.ipPools = ipPools
|
||||
}
|
||||
|
||||
await this._save(set)
|
||||
}
|
||||
@@ -218,7 +238,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 +262,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 +312,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
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Schedules } from '../models/schedule'
|
||||
|
||||
import {
|
||||
forEach,
|
||||
mapToArray,
|
||||
scheduleFn
|
||||
} from '../utils'
|
||||
|
||||
@@ -42,14 +43,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())
|
||||
}
|
||||
|
||||
@@ -182,7 +192,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)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -52,22 +52,39 @@ 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 conflictUsers = await usersDb.get({ email: user.email })
|
||||
if (!isEmpty(conflictUsers)) {
|
||||
await Promise.all(mapToArray(conflictUsers, user =>
|
||||
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 +92,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 +121,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)
|
||||
@@ -203,7 +231,8 @@ export default class {
|
||||
throw new Error(`registering ${name} user is forbidden`)
|
||||
}
|
||||
|
||||
return /* await */ this.createUser(name, {
|
||||
return /* await */ this.createUser({
|
||||
name,
|
||||
_provider: provider
|
||||
})
|
||||
}
|
||||
@@ -247,6 +276,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)
|
||||
|
||||
@@ -32,7 +32,7 @@ class NoSuchXenServer extends NoSuchObject {
|
||||
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 +43,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 => {
|
||||
|
||||
18
src/xo.js
18
src/xo.js
@@ -48,6 +48,24 @@ export default class Xo extends EventEmitter {
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
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)
|
||||
))
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async start () {
|
||||
this.start = noop
|
||||
|
||||
|
||||
@@ -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