Compare commits

..

57 Commits

Author SHA1 Message Date
Julien Fontanet
522d6eed92 5.3.2 2016-10-27 18:49:32 +02:00
Julien Fontanet
9d1d6ea4c5 feat(xo): export/import config (#427)
See vatesfr/xo-web#786
2016-10-27 18:48:19 +02:00
Julien Fontanet
0afd506a41 5.3.1 2016-10-27 18:25:16 +02:00
Julien Fontanet
9dfb837e3f fix(Xapi#importDeltaVm): gracefully handle missing vif.$network$uuid (#433) 2016-10-27 16:46:45 +02:00
fufroma
4ab63b569f fix(RemoteHandlerNfs): move mount points in /run/xo-server/mounts
Fixes vatesfr/xo-web#1405
2016-10-27 15:56:33 +02:00
Julien Fontanet
8d390d256d fix(http-request): handle redirections (#432) 2016-10-27 15:34:54 +02:00
Julien Fontanet
4eec5e06fc fix(package): test on Node 6, not 7 (#431) 2016-10-27 12:24:40 +02:00
Julien Fontanet
e4063b1ba8 feat(sample.config.yaml): add warning about YAML 2016-10-24 22:52:11 +02:00
Greenkeeper
0c3227cf8e chore(package): update promise-toolbox to version 0.7.0 (#428)
https://greenkeeper.io/
2016-10-24 15:01:17 +02:00
Pierre Donias
7bed200bf5 feat(pif): editVlan (#426)
Fix vatesfr/xo-web#1092
2016-10-24 10:24:44 +02:00
Julien Fontanet
4f763e2109 5.3.0 2016-10-20 16:01:53 +02:00
Pierre Donias
75167fb65b feat(pif): expose IP config modes (#424)
See vatesfr/xo-web#1651
2016-10-20 12:44:35 +02:00
Julien Fontanet
675588f780 feat(delta backups): force checksums refresh
See vatesfr/xo-web#1672
2016-10-20 12:38:26 +02:00
Julien Fontanet
2d6f94edd8 fix(vhd-merge/chainVhd): correctly await _write()
Fixes vatesfr/xo-web#1672
2016-10-20 12:31:20 +02:00
Julien Fontanet
247c66ef4b feat(IP pools): can be used in resource sets (#413)
See vatesfr/xo-web#1565
2016-10-19 11:17:05 +02:00
Greenkeeper
1076fac40f Update gulp-sourcemaps to version 2.1.1 🚀 (#422)
https://greenkeeper.io/
2016-10-14 10:44:27 +02:00
Julien Fontanet
14a4a415a2 5.2.6 2016-10-13 18:51:16 +02:00
Julien Fontanet
524355b59c fix(vhd-merge/chainVhd): correctly compute header checksum (#419)
Fixes vatesfr/xo-web#1656
2016-10-13 18:49:58 +02:00
Greenkeeper
36fe49f3f5 Update promise-toolbox to version 0.6.0 🚀 (#416)
https://greenkeeper.io/
2016-10-12 09:19:19 +02:00
Greenkeeper
c0c0af9b14 chore(package): update execa to version 0.5.0 (#411)
https://greenkeeper.io/
2016-10-05 10:40:31 +02:00
Julien Fontanet
d1e472d482 chore(package): use babel-plugin-lodash 2016-10-04 16:05:01 +02:00
Julien Fontanet
c80e43ad0d fix(vm.create): don't require view perm on VM template 2016-10-04 16:03:06 +02:00
Julien Fontanet
fdd395e2b6 fix(vm.create): correctly check resourceSet objects
Related to vatesfr/xo-web#1620
2016-10-04 15:51:04 +02:00
Julien Fontanet
e094437168 fix(package): update xo-acl-resolver to version 0.2.2
See vatesfr/xo-web#1620
2016-10-04 15:24:01 +02:00
Pierre Donias
2ee0be7466 fix(xapi/utils/makeEditObject): constraints works with user props (#410) 2016-10-04 15:02:27 +02:00
Julien Fontanet
2784a7cc92 Create ISSUE_TEMPLATE.md 2016-10-03 16:24:24 +02:00
Julien Fontanet
b09f998d6c 5.2.5 2016-10-03 09:39:52 +02:00
Nicolas Raynaud
bdeb5895f6 fix(deltaBackups): update checksum after altering VHD files (#408)
Fixes vatesfr/xo-web#1606
2016-09-30 14:31:33 +02:00
Pierre Donias
3944b8aaee feat(network): create a bonded network (#407)
Fixes vatesfr/xo-web#876
2016-09-30 13:51:33 +02:00
Nicolas Raynaud
6e66cffb92 feat(deltaBackups): correctly chain VHDs (#406)
The goal is for a tool like vhdimount to be able to mount any file and use it as a disk to recover specific file in it.
2016-09-29 17:31:36 +02:00
Pierre Donias
57092ee788 feat(vif.set): support for network, MAC and currently_attached (#403)
Fixes vatesfr/xo-web#1446
2016-09-28 15:09:17 +02:00
Julien Fontanet
70e9e1c706 chore(package): update human-format to version 0.7.0 2016-09-28 09:58:54 +02:00
Greenkeeper
9662b8fbee chore(package): update babel-eslint to version 7.0.0 (#404)
https://greenkeeper.io/
2016-09-27 23:39:30 +02:00
Julien Fontanet
9f66421ae7 fix(bootstrap): C-c twice force stop the server 2016-09-27 10:44:24 +02:00
Greenkeeper
50584c2e50 chore(package): update http-server-plus to version 0.7.0 (#402)
https://greenkeeper.io/
2016-09-27 09:30:16 +02:00
Julien Fontanet
7be4e1901a chore(package): use index-modules 2016-09-26 15:41:41 +02:00
Julien Fontanet
b47146de45 fix(pbd/attached): should be a boolean 2016-09-22 13:20:49 +02:00
Julien Fontanet
97b229b2c7 fix(vm.set): works with VM templates
Fixes vatesfr/xo-web#1569
2016-09-22 10:39:20 +02:00
Julien Fontanet
6bb5bb9403 5.2.4 2016-09-21 10:20:46 +02:00
Julien Fontanet
8c4b8271d8 fix(pool.setDefaultSr): remove pool param
Fixes vatesfr/xo-web#1558
2016-09-20 11:45:36 +02:00
Julien Fontanet
69291c0574 chore(package): update xo-vmdk-to-vhd to version 0.0.12
Fixes vatesfr/xo-web#1551
2016-09-20 10:41:42 +02:00
Julien Fontanet
2dc073dcd6 fix(vm.resourceSet): handle xo namespace 2016-09-19 13:15:23 +02:00
Julien Fontanet
1894cb35d2 feat(vm): expose resourceSet prop 2016-09-19 12:10:09 +02:00
Julien Fontanet
cd37420b07 Merge pull request #398 from vatesfr/greenkeeper-standard-8.1.0
Update standard to version 8.1.0 🚀
2016-09-18 05:17:41 +02:00
Julien Fontanet
55cb6b39db fix(Xo#removeSchedule): correctly test instance of SchedulerError 2016-09-18 05:12:36 +02:00
greenkeeperio-bot
89d13b2285 chore(package): update standard to version 8.1.0
https://greenkeeper.io/
2016-09-17 20:51:59 +02:00
Julien Fontanet
1b64b0468a fix(group.delete): remove associated ACLs
Fixes vatesfr/xo-web#899
2016-09-16 16:04:41 +02:00
Julien Fontanet
085fb83294 fix(user.delete): remove associated ACLs
See vatesfr/xo-web#899
2016-09-16 16:04:41 +02:00
Julien Fontanet
edd606563f feat(vm.revert): can snapshot before (#395)
See vatesfr/xo-web#1445
2016-09-15 14:59:43 +02:00
Julien Fontanet
fb804e99f0 5.2.3 2016-09-14 18:02:32 +02:00
Pierre Donias
1707cbcb54 feat(signin): use XO 5 style (#394)
Fixes vatesfr/xo-web#1161
2016-09-14 17:56:05 +02:00
Julien Fontanet
6d6a630c31 5.2.2 2016-09-14 17:37:42 +02:00
Julien Fontanet
ff2990e8e5 chore(package): update @marsaud/smb2-promise to version 0.2.1
Fixes vatesfr/xo-web#1511
2016-09-14 17:32:52 +02:00
Nicolas Raynaud
d679aff0fb chore(package): remove node-smb2 dependency (#393) 2016-09-14 16:23:28 +02:00
Julien Fontanet
603a444905 fix(Xapi#importVm): remove VM's VDIs on failure 2016-09-14 14:11:20 +02:00
Julien Fontanet
a002958448 fix(DR): remove previous VDIs
Fixes vatesfr/xo-web#1510
2016-09-14 14:11:20 +02:00
Julien Fontanet
cb4bc37424 fix(DR): delete VMs in all cases
Previous copies were not deleted when there were as many as the depth.

Fixes vatesfr/xo-web#1509
2016-09-14 14:11:19 +02:00
53 changed files with 1026 additions and 400 deletions

View File

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

@@ -0,0 +1,3 @@
# ALL ISSUES SHOULD BE CREATED IN XO-WEB'S TRACKER!
https://github.com/vatesfr/xo-web/issues

View File

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

View File

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

View File

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

View File

@@ -7,8 +7,7 @@ startsWith = require 'lodash/startsWith'
{coroutine: $coroutine} = require 'bluebird'
{
extractProperty,
parseXml,
promisify
parseXml
} = require '../utils'
#=====================================================================

View File

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

View File

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

View File

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

View File

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

View File

@@ -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']
}
// -------------------------------------------------------------------

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,10 +14,6 @@ export class Groups extends Collection {
return Group
}
get idPrefix () {
return 'group:'
}
create (name) {
return this.add(new Group({
name,

View File

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

View File

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

View File

@@ -13,10 +13,6 @@ export class Remotes extends Collection {
return Remote
}
get idPrefix () {
return 'remote-'
}
create (name, url) {
return this.add(new Remote({
name,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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