@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server",
|
||||
"version": "4.5.0",
|
||||
"version": "4.6.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -76,7 +76,6 @@
|
||||
"lodash.map": "^3.0.0",
|
||||
"lodash.pick": "^3.0.0",
|
||||
"lodash.result": "^3.0.0",
|
||||
"lodash.snakecase": "^3.0.1",
|
||||
"lodash.sortby": "^3.1.4",
|
||||
"lodash.startswith": "^3.0.1",
|
||||
"make-error": "^1",
|
||||
|
||||
@@ -91,14 +91,14 @@ const checkAuthorizationByTypes = {
|
||||
message: checkMemberAuthorization('$object'),
|
||||
|
||||
network (userId, network, permission) {
|
||||
return defaultCheckAuthorization(this, userId, network, permission).catch(() => {
|
||||
return checkAuthorization(this, userId, network.$pool, permission)
|
||||
return defaultCheckAuthorization.call(this, userId, network, permission).catch(() => {
|
||||
return checkAuthorization.call(this, userId, network.$pool, permission)
|
||||
})
|
||||
},
|
||||
|
||||
SR (userId, sr, permission) {
|
||||
return defaultCheckAuthorization(this, userId, sr, permission).catch(() => {
|
||||
return checkAuthorization(this, userId, sr.$pool, permission)
|
||||
return defaultCheckAuthorization.call(this, userId, sr, permission).catch(() => {
|
||||
return checkAuthorization.call(this, userId, sr.$pool, permission)
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -11,16 +11,17 @@ export * as group from './group'
|
||||
export * as host from './host'
|
||||
export * as job from './job'
|
||||
export * as message from './message'
|
||||
export * as remote from './remote'
|
||||
export * as pbd from './pbd'
|
||||
export * as pif from './pif'
|
||||
export * as pool from './pool'
|
||||
export * as remote from './remote'
|
||||
export * as role from './role'
|
||||
export * as schedule from './schedule'
|
||||
export * as scheduler from './scheduler'
|
||||
export * as server from './server'
|
||||
export * as session from './session'
|
||||
export * as sr from './sr'
|
||||
export * as tag from './tag'
|
||||
export * as task from './task'
|
||||
export * as test from './test'
|
||||
export * as token from './token'
|
||||
|
||||
31
src/api/tag.js
Normal file
31
src/api/tag.js
Normal file
@@ -0,0 +1,31 @@
|
||||
export async function add ({tag, object}) {
|
||||
await this.getXAPI(object).addTag(object.id, tag)
|
||||
}
|
||||
|
||||
add.description = 'add a new tag to an object'
|
||||
|
||||
add.resolve = {
|
||||
object: ['id', null, 'administrate']
|
||||
}
|
||||
|
||||
add.params = {
|
||||
tag: { type: 'string' },
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function remove ({tag, object}) {
|
||||
await this.getXAPI(object).removeTag(object.id, tag)
|
||||
}
|
||||
|
||||
remove.description = 'remove an existing tag from an object'
|
||||
|
||||
remove.resolve = {
|
||||
object: ['id', null, 'administrate']
|
||||
}
|
||||
|
||||
remove.params = {
|
||||
tag: { type: 'string' },
|
||||
id: { type: 'string' }
|
||||
}
|
||||
@@ -72,3 +72,17 @@ set.params = {
|
||||
password: { type: 'string', optional: true },
|
||||
permission: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
export async function changePassword ({oldPassword, newPassword}) {
|
||||
const id = this.session.get('user_id')
|
||||
await this.changePassword(id, oldPassword, newPassword)
|
||||
}
|
||||
|
||||
changePassword.description = 'change password after checking old password (user function)'
|
||||
|
||||
changePassword.permission = ''
|
||||
|
||||
changePassword.params = {
|
||||
oldPassword: {type: 'string'},
|
||||
newPassword: {type: 'string'}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ create = $coroutine ({
|
||||
name_description
|
||||
name_label
|
||||
template
|
||||
pv_args
|
||||
VDIs
|
||||
VIFs
|
||||
}) ->
|
||||
@@ -47,6 +48,7 @@ create = $coroutine ({
|
||||
installRepository: installation && installation.repository,
|
||||
nameDescription: name_description,
|
||||
nameLabel: name_label,
|
||||
pvArgs: pv_args,
|
||||
vdis: VDIs,
|
||||
vifs: VIFs
|
||||
})
|
||||
@@ -69,6 +71,9 @@ create.params = {
|
||||
name_label: { type: 'string' }
|
||||
name_description: { type: 'string', optional: true }
|
||||
|
||||
# PV Args
|
||||
pv_args: { type: 'string', optional: true }
|
||||
|
||||
# TODO: add the install repository!
|
||||
# VBD.insert/eject
|
||||
# Also for the console!
|
||||
@@ -170,14 +175,8 @@ exports.insertCd = insertCd
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
migrate = $coroutine ({vm, host}) ->
|
||||
unless $isVMRunning vm
|
||||
@throw 'INVALID_PARAMS', 'The VM can only be migrated when running'
|
||||
|
||||
xapi = @getXAPI vm
|
||||
|
||||
yield xapi.call 'VM.pool_migrate', vm.ref, host.ref, {'force': 'true'}
|
||||
|
||||
return true
|
||||
yield @getXAPI(vm).migrateVm(vm.id, @getXAPI(host), host.id)
|
||||
return
|
||||
|
||||
migrate.params = {
|
||||
# Identifier of the VM to migrate.
|
||||
@@ -197,62 +196,18 @@ exports.migrate = migrate
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
migratePool = $coroutine ({
|
||||
vm: VM,
|
||||
vm,
|
||||
host
|
||||
sr: SR
|
||||
sr
|
||||
network
|
||||
migrationNetwork
|
||||
}) ->
|
||||
# TODO: map multiple VDI and VIF
|
||||
|
||||
# Optional parameters
|
||||
# if no network given, try to use the management network
|
||||
unless network
|
||||
PIF = $findWhere (@getObjects host.$PIFs), management: true
|
||||
network = @getObject PIF.$network, 'network'
|
||||
|
||||
# if no migrationNetwork, use the network
|
||||
migrationNetwork ?= network
|
||||
|
||||
# if no sr is given, try to find the default Pool SR
|
||||
unless SR
|
||||
pool = @getObject host.poolRef, 'pool'
|
||||
target_sr_id = pool.default_SR
|
||||
SR = @getObject target_sr_id, 'SR'
|
||||
|
||||
unless $isVMRunning VM
|
||||
@throw 'INVALID_PARAMS', 'The VM can only be migrated when running'
|
||||
|
||||
vdiMap = {}
|
||||
for vbdId in VM.$VBDs
|
||||
VBD = @getObject vbdId, 'VBD'
|
||||
continue if VBD.is_cd_drive
|
||||
VDI = @getObject VBD.VDI, 'VDI'
|
||||
vdiMap[VDI.ref] = SR.ref
|
||||
|
||||
vifMap = {}
|
||||
for vifId in VM.VIFs
|
||||
VIF = @getObject vifId, 'VIF'
|
||||
vifMap[VIF.ref] = network.ref
|
||||
|
||||
token = yield (@getXAPI host).call(
|
||||
'host.migrate_receive'
|
||||
host.ref
|
||||
migrationNetwork.ref
|
||||
{} # Other parameters
|
||||
)
|
||||
|
||||
yield (@getXAPI VM).call(
|
||||
'VM.migrate_send'
|
||||
VM.ref
|
||||
token
|
||||
true # Live migration
|
||||
vdiMap
|
||||
vifMap
|
||||
{'force': 'true'} # Force migration even if CPUs are different
|
||||
)
|
||||
|
||||
return true
|
||||
yield @getXAPI(vm).migrateVm(vm.id, @getXAPI(host), host.id, {
|
||||
migrationNetworkId: migrationNetwork?.id
|
||||
networkId: network?.id,
|
||||
srId: sr?.id,
|
||||
})
|
||||
return
|
||||
|
||||
migratePool.params = {
|
||||
|
||||
@@ -354,6 +309,7 @@ set = $coroutine (params) ->
|
||||
for param, fields of {
|
||||
'name_label'
|
||||
'name_description'
|
||||
'PV_args'
|
||||
}
|
||||
continue unless param of params
|
||||
|
||||
@@ -383,6 +339,9 @@ set.params = {
|
||||
#
|
||||
# Note: static_min ≤ dynamic_min ≤ dynamic_max ≤ static_max
|
||||
memory: { type: 'integer', optional: true }
|
||||
|
||||
# Kernel arguments for PV VM.
|
||||
PV_args: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
@@ -582,7 +541,7 @@ stop = $coroutine ({vm, force}) ->
|
||||
try
|
||||
yield xapi.call 'VM.clean_shutdown', vm.ref
|
||||
catch error
|
||||
if error.code is 'VM_MISSING_PV_DRIVERS' or error.code 'VM_LACKS_FEATURE_SHUTDOWN'
|
||||
if error.code is 'VM_MISSING_PV_DRIVERS' or error.code is 'VM_LACKS_FEATURE_SHUTDOWN'
|
||||
# TODO: Improve reporting: this message is unclear.
|
||||
@throw 'INVALID_PARAMS'
|
||||
else
|
||||
@@ -661,6 +620,9 @@ exports.revert = revert
|
||||
handleExport = (req, res, { stream }) ->
|
||||
upstream = stream.response
|
||||
|
||||
# Remove the filename as it is already part of the URL.
|
||||
upstream.headers['content-disposition'] = 'attachment'
|
||||
|
||||
res.writeHead(
|
||||
upstream.statusCode,
|
||||
upstream.statusMessage ? '',
|
||||
@@ -677,7 +639,9 @@ export_ = $coroutine ({vm, compress, onlyMetadata}) ->
|
||||
})
|
||||
|
||||
return {
|
||||
$getFrom: yield @registerHttpRequest(handleExport, { stream })
|
||||
$getFrom: yield @registerHttpRequest(handleExport, { stream }, {
|
||||
suffix: encodeURI("/#{vm.name_label}.xva")
|
||||
})
|
||||
}
|
||||
|
||||
export_.params = {
|
||||
|
||||
17
src/index.js
17
src/index.js
@@ -18,6 +18,7 @@ import proxyRequest from 'proxy-http-request'
|
||||
import serveStatic from 'serve-static'
|
||||
import WebSocket from 'ws'
|
||||
import {compile as compileJade} from 'jade'
|
||||
import {posix as posixPath} from 'path'
|
||||
|
||||
import {
|
||||
AlreadyAuthenticated,
|
||||
@@ -143,6 +144,9 @@ async function setUpPassport (express, xo) {
|
||||
|
||||
const SIGNIN_STRATEGY_RE = /^\/signin\/([^/]+)(\/callback)?(:?\?.*)?$/
|
||||
express.use(async (req, res, next) => {
|
||||
// A relative path is needed to avoid breaking reverse proxies.
|
||||
const basePath = posixPath.relative(req.path, '/')
|
||||
|
||||
const matches = req.url.match(SIGNIN_STRATEGY_RE)
|
||||
if (matches) {
|
||||
return passport.authenticate(matches[1], async (err, user, info) => {
|
||||
@@ -152,7 +156,7 @@ async function setUpPassport (express, xo) {
|
||||
|
||||
if (!user) {
|
||||
req.flash('error', info ? info.message : 'Invalid credentials')
|
||||
return res.redirect('signin')
|
||||
return res.redirect(`${basePath}/signin`)
|
||||
}
|
||||
|
||||
// The cookie will be set in via the next request because some
|
||||
@@ -162,12 +166,7 @@ async function setUpPassport (express, xo) {
|
||||
(await xo.createAuthenticationToken({userId: user.id})).id
|
||||
)
|
||||
|
||||
// A relative path is needed to avoid breaking reverse proxies.
|
||||
res.redirect(
|
||||
matches[2]
|
||||
? '../../'
|
||||
: '../'
|
||||
)
|
||||
res.redirect(basePath)
|
||||
})(req, res, next)
|
||||
}
|
||||
|
||||
@@ -180,7 +179,7 @@ async function setUpPassport (express, xo) {
|
||||
} else if (/fontawesome|images|styles/.test(req.url)) {
|
||||
next()
|
||||
} else {
|
||||
res.redirect('signin')
|
||||
return res.redirect(`${basePath}/signin`)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -336,7 +335,7 @@ const apiHelpers = {
|
||||
// Handles both properties and wrapped models.
|
||||
const properties = user.properties || user
|
||||
|
||||
return pick(properties, 'id', 'email', 'groups', 'permission')
|
||||
return pick(properties, 'id', 'email', 'groups', 'permission', 'provider')
|
||||
},
|
||||
|
||||
getServerPublicProperties (server) {
|
||||
|
||||
@@ -87,7 +87,7 @@ class NfsMounter {
|
||||
if (this._matchesRealMount(mount)) {
|
||||
try {
|
||||
await this._umount(mount)
|
||||
} catch(_) {
|
||||
} catch (_) {
|
||||
// We have to go on...
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ class LocalHandler {
|
||||
try {
|
||||
await fs.ensureDirAsync(local.path)
|
||||
await fs.accessAsync(local.path, fs.R_OK | fs.W_OK)
|
||||
} catch(exc) {
|
||||
} catch (exc) {
|
||||
local.enabled = false
|
||||
local.error = exc.message
|
||||
}
|
||||
|
||||
@@ -11,6 +11,15 @@ import {randomBytes} from 'crypto'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export function camelToSnakeCase (string) {
|
||||
return string.replace(
|
||||
/([a-z])([A-Z])/g,
|
||||
(_, prevChar, currChar) => `${prevChar}_${currChar.toLowerCase()}`
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Ensure the value is an array, wrap it if necessary.
|
||||
export const ensureArray = (value) => {
|
||||
if (value === undefined) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import expect from 'must'
|
||||
// ===================================================================
|
||||
|
||||
import {
|
||||
camelToSnakeCase,
|
||||
ensureArray,
|
||||
extractProperty,
|
||||
formatXml,
|
||||
@@ -14,6 +15,22 @@ import {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('camelToSnakeCase()', function () {
|
||||
it('converts a string from camelCase to snake_case', function () {
|
||||
expect(camelToSnakeCase('fooBar')).to.equal('foo_bar')
|
||||
})
|
||||
|
||||
it('does not alter snake_case strings', function () {
|
||||
expect(camelToSnakeCase('foo_bar')).to.equal('foo_bar')
|
||||
})
|
||||
|
||||
it('does not alter upper case letters expect those from the camelCase', function () {
|
||||
expect(camelToSnakeCase('fooBar_BAZ')).to.equal('foo_bar_BAZ')
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('ensureArray()', function () {
|
||||
it('wrap the value in an array', function () {
|
||||
const value = 'foo'
|
||||
|
||||
@@ -55,6 +55,7 @@ export function pool (obj) {
|
||||
default_SR: link(obj, 'default_SR'),
|
||||
HA_enabled: Boolean(obj.ha_enabled),
|
||||
master: link(obj, 'master'),
|
||||
tags: obj.tags,
|
||||
name_description: obj.name_description,
|
||||
name_label: obj.name_label || obj.$master.name_label
|
||||
|
||||
@@ -111,6 +112,7 @@ export function host (obj) {
|
||||
patches: link(obj, 'patches'),
|
||||
powerOnMode: obj.power_on_mode,
|
||||
power_state: isRunning ? 'Running' : 'Halted',
|
||||
tags: obj.tags,
|
||||
version: obj.software_version.product_version,
|
||||
|
||||
// TODO: dedupe.
|
||||
@@ -221,10 +223,12 @@ export function vm (obj) {
|
||||
other: otherConfig,
|
||||
os_version: guestMetrics && guestMetrics.os_version || null,
|
||||
power_state: obj.power_state,
|
||||
PV_args: obj.PV_args,
|
||||
PV_drivers: Boolean(guestMetrics),
|
||||
PV_drivers_up_to_date: Boolean(guestMetrics && guestMetrics.PV_drivers_up_to_date),
|
||||
snapshot_time: toTimestamp(obj.snapshot_time),
|
||||
snapshots: link(obj, 'snapshots'),
|
||||
tags: obj.tags,
|
||||
VIFs: link(obj, 'VIFs'),
|
||||
|
||||
$container: (
|
||||
@@ -290,6 +294,7 @@ export function sr (obj) {
|
||||
physical_usage: +obj.physical_utilisation,
|
||||
size: +obj.physical_size,
|
||||
SR_type: obj.type,
|
||||
tags: obj.tags,
|
||||
usage: +obj.virtual_allocation,
|
||||
VDIs: link(obj, 'VDIs'),
|
||||
|
||||
@@ -357,6 +362,7 @@ export function vdi (obj) {
|
||||
size: +obj.virtual_size,
|
||||
snapshots: link(obj, 'snapshots'),
|
||||
snapshot_time: toTimestamp(obj.snapshot_time),
|
||||
tags: obj.tags,
|
||||
usage: +obj.physical_utilisation,
|
||||
|
||||
$snapshot_of: link(obj, 'snapshot_of'),
|
||||
@@ -405,6 +411,7 @@ export function network (obj) {
|
||||
MTU: +obj.MTU,
|
||||
name_description: obj.name_description,
|
||||
name_label: obj.name_label,
|
||||
tags: obj.tags,
|
||||
PIFs: link(obj, 'PIFs'),
|
||||
VIFs: link(obj, 'VIFs')
|
||||
}
|
||||
|
||||
110
src/xapi.js
110
src/xapi.js
@@ -8,7 +8,6 @@ import fs from 'fs-extra'
|
||||
import got from 'got'
|
||||
import includes from 'lodash.includes'
|
||||
import map from 'lodash.map'
|
||||
import snakeCase from 'lodash.snakecase'
|
||||
import sortBy from 'lodash.sortby'
|
||||
import unzip from 'julien-f-unzip'
|
||||
import {PassThrough} from 'stream'
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
|
||||
import {debounce} from './decorators'
|
||||
import {
|
||||
camelToSnakeCase,
|
||||
ensureArray,
|
||||
noop, parseXml,
|
||||
pFinally
|
||||
@@ -215,7 +215,7 @@ export default class Xapi extends XapiBase {
|
||||
// properties that failed to be set.
|
||||
await Promise.all(map(props, (value, name) => {
|
||||
if (value != null) {
|
||||
return this.call(`${namespace}.set_${snakeCase(name)}`, ref, value)
|
||||
return this.call(`${namespace}.set_${camelToSnakeCase(name)}`, ref, value)
|
||||
}
|
||||
}))
|
||||
}
|
||||
@@ -242,6 +242,28 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
// =================================================================
|
||||
|
||||
async addTag (id, tag) {
|
||||
const {
|
||||
$ref: ref,
|
||||
$type: type
|
||||
} = this.getObject(id)
|
||||
|
||||
const namespace = getNamespaceForType(type)
|
||||
await this.call(`${namespace}.add_tags`, ref, tag)
|
||||
}
|
||||
|
||||
async removeTag (id, tag) {
|
||||
const {
|
||||
$ref: ref,
|
||||
$type: type
|
||||
} = this.getObject(id)
|
||||
|
||||
const namespace = getNamespaceForType(type)
|
||||
await this.call(`${namespace}.remove_tags`, ref, tag)
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
// FIXME: should be static
|
||||
@debounce(24 * 60 * 60 * 1000)
|
||||
async _getXenUpdates () {
|
||||
@@ -448,6 +470,7 @@ export default class Xapi extends XapiBase {
|
||||
async createVm (templateId, {
|
||||
nameDescription = undefined,
|
||||
nameLabel = undefined,
|
||||
pvArgs = undefined,
|
||||
cpus = undefined,
|
||||
installRepository = undefined,
|
||||
vdis = [],
|
||||
@@ -486,6 +509,7 @@ export default class Xapi extends XapiBase {
|
||||
// Set VMs params.
|
||||
this._setObjectProperties(vm, {
|
||||
nameDescription,
|
||||
PV_args: pvArgs,
|
||||
VCPUs_at_startup: cpus
|
||||
})
|
||||
|
||||
@@ -638,6 +662,88 @@ export default class Xapi extends XapiBase {
|
||||
return stream
|
||||
}
|
||||
|
||||
async _migrateVMWithStorageMotion (vm, hostXapi, host, {
|
||||
migrationNetwork = find(host.$PIFs, pif => pif.management).$network, // TODO: handle not found
|
||||
sr = host.$pool.$default_SR, // TODO: handle not found
|
||||
vifsMap = {}
|
||||
}) {
|
||||
const vdis = {}
|
||||
for (const vbd of vm.$VBDs) {
|
||||
if (vbd.type !== 'CD') {
|
||||
vdis[vbd.$VDI.$ref] = sr.$ref
|
||||
}
|
||||
}
|
||||
|
||||
const token = await hostXapi.call(
|
||||
'host.migrate_receive',
|
||||
host.$ref,
|
||||
migrationNetwork.$ref,
|
||||
{}
|
||||
)
|
||||
|
||||
await this.call(
|
||||
'VM.migrate_send',
|
||||
vm.$ref,
|
||||
token,
|
||||
true, // Live migration.
|
||||
vdis,
|
||||
vifsMap,
|
||||
{
|
||||
force: 'true'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async migrateVm (vmId, hostXapi, hostId, {
|
||||
migrationNetworkId,
|
||||
networkId,
|
||||
srId
|
||||
} = {}) {
|
||||
const vm = this.getObject(vmId)
|
||||
if (!isVmRunning(vm)) {
|
||||
throw new Error('cannot migrate a non-running VM')
|
||||
}
|
||||
|
||||
const host = hostXapi.getObject(hostId)
|
||||
|
||||
const accrossPools = vm.$pool !== host.$pool
|
||||
const useStorageMotion = (
|
||||
accrossPools ||
|
||||
migrationNetworkId ||
|
||||
networkId ||
|
||||
srId
|
||||
)
|
||||
|
||||
if (useStorageMotion) {
|
||||
const vifsMap = {}
|
||||
if (accrossPools || networkId) {
|
||||
const {$ref: networkRef} = networkId
|
||||
? this.getObject(networkId)
|
||||
: find(host.$PIFs, pif => pif.management).$network
|
||||
for (const vif of vm.$VIFs) {
|
||||
vifsMap[vif.$ref] = networkRef
|
||||
}
|
||||
}
|
||||
|
||||
await this._migrateVMWithStorageMotion(vm, hostXapi, host, {
|
||||
migrationNetwork: migrationNetworkId && this.getObject(migrationNetworkId),
|
||||
sr: srId && this.getObject(srId),
|
||||
vifsMap
|
||||
})
|
||||
} else {
|
||||
try {
|
||||
await this.call('VM.pool_migrate', vm.$ref, host.$ref, { force: 'true' })
|
||||
} catch (error) {
|
||||
if (error.code !== 'VM_REQUIRES_SR') {
|
||||
throw error
|
||||
}
|
||||
|
||||
// Retry using motion storage.
|
||||
await this._migrateVMWithStorageMotion(vm, hostXapi, host, {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async snapshotVm (vmId) {
|
||||
return await this._getOrWaitObject(
|
||||
await this._snapshotVm(
|
||||
|
||||
31
src/xo.js
31
src/xo.js
@@ -28,7 +28,7 @@ import {autobind} from './decorators'
|
||||
import {generateToken} from './utils'
|
||||
import {Groups} from './models/group'
|
||||
import {Jobs} from './models/job'
|
||||
import {JsonRpcError, NoSuchObject} from './api-errors'
|
||||
import {InvalidCredential, JsonRpcError, NoSuchObject} from './api-errors'
|
||||
import {ModelAlreadyExists} from './collection'
|
||||
import {Remotes} from './models/remote'
|
||||
import {Schedules} from './models/schedule'
|
||||
@@ -282,7 +282,7 @@ export default class Xo extends EventEmitter {
|
||||
}
|
||||
|
||||
async deleteUser (id) {
|
||||
if (!await this._users.remove(id)) {
|
||||
if (!await this._users.remove(id)) { // eslint-disable-line space-before-keywords
|
||||
throw new NoSuchUser(id)
|
||||
}
|
||||
}
|
||||
@@ -334,6 +334,21 @@ export default class Xo extends EventEmitter {
|
||||
})
|
||||
}
|
||||
|
||||
async changePassword (id, oldPassword, newPassword) {
|
||||
const user = await this._getUser(id)
|
||||
|
||||
if (user.get('provider')) {
|
||||
throw new Error('Password change is only for locally created users')
|
||||
}
|
||||
|
||||
const auth = await user.checkPassword(oldPassword)
|
||||
if (!auth) {
|
||||
throw new InvalidCredential()
|
||||
}
|
||||
await user.setPassword(newPassword)
|
||||
await this._users.save(user.properties)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async createGroup ({name}) {
|
||||
@@ -345,7 +360,7 @@ export default class Xo extends EventEmitter {
|
||||
}
|
||||
|
||||
async deleteGroup (id) {
|
||||
if (!await this._groups.remove(id)) {
|
||||
if (!await this._groups.remove(id)) { // eslint-disable-line space-before-keywords
|
||||
throw new NoSuchGroup(id)
|
||||
}
|
||||
}
|
||||
@@ -698,7 +713,7 @@ export default class Xo extends EventEmitter {
|
||||
}
|
||||
|
||||
async deleteAuthenticationToken (id) {
|
||||
if (!await this._tokens.remove(id)) {
|
||||
if (!await this._tokens.remove(id)) { // eslint-disable-line space-before-keywords
|
||||
throw new NoSuchAuthenticationToken(id)
|
||||
}
|
||||
}
|
||||
@@ -726,7 +741,7 @@ export default class Xo extends EventEmitter {
|
||||
async unregisterXenServer (id) {
|
||||
this.disconnectXenServer(id).catch(() => {})
|
||||
|
||||
if (!await this._servers.remove(id)) {
|
||||
if (!await this._servers.remove(id)) { // eslint-disable-line space-before-keywords
|
||||
throw new NoSuchXenServer(id)
|
||||
}
|
||||
}
|
||||
@@ -969,12 +984,12 @@ export default class Xo extends EventEmitter {
|
||||
)
|
||||
}
|
||||
|
||||
async registerHttpRequest (fn, data) {
|
||||
async registerHttpRequest (fn, data, { suffix = '' } = {}) {
|
||||
const {_httpRequestWatchers: watchers} = this
|
||||
|
||||
const url = await (function generateUniqueUrl () {
|
||||
return generateToken().then(token => {
|
||||
const url = `/api/${token}`
|
||||
const url = `/api/${token}${suffix}`
|
||||
|
||||
return url in watchers
|
||||
? generateUniqueUrl()
|
||||
@@ -1067,7 +1082,7 @@ export default class Xo extends EventEmitter {
|
||||
|
||||
opts.createdAt = Date.now()
|
||||
|
||||
const url = `/${await generateToken()}`
|
||||
const url = `/${await generateToken()}` // eslint-disable-line space-before-keywords
|
||||
this._proxyRequests[url] = opts
|
||||
|
||||
return url
|
||||
|
||||
Reference in New Issue
Block a user