chore: format all code (#2632)

This commit is contained in:
Julien Fontanet 2018-02-09 17:56:03 +01:00 committed by GitHub
parent 8bf35b2a63
commit 7cb720b11f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
170 changed files with 5802 additions and 4801 deletions

View File

@ -18,9 +18,10 @@ class Job {
} }
const scheduleNext = () => { const scheduleNext = () => {
const delay = schedule._nextDelay() const delay = schedule._nextDelay()
this._timeout = delay < MAX_DELAY this._timeout =
? setTimeout(wrapper, delay) delay < MAX_DELAY
: setTimeout(scheduleNext, MAX_DELAY) ? setTimeout(wrapper, delay)
: setTimeout(scheduleNext, MAX_DELAY)
} }
this._scheduleNext = scheduleNext this._scheduleNext = scheduleNext

View File

@ -9,7 +9,7 @@ const NEXT_MAPPING = {
minute: { hour: 1 }, minute: { hour: 1 },
} }
const getFirst = values => values !== undefined ? values[0] : 0 const getFirst = values => (values !== undefined ? values[0] : 0)
const setFirstAvailable = (date, unit, values) => { const setFirstAvailable = (date, unit, values) => {
if (values === undefined) { if (values === undefined) {

View File

@ -90,7 +90,7 @@ const createParser = ({ fields: [...fields], presets: { ...presets } }) => {
if (!match('/')) { if (!match('/')) {
return return
} }
[start, end] = field.range ;[start, end] = field.range
step = parseInteger() step = parseInteger()
} else { } else {
start = parseValue() start = parseValue()

View File

@ -28,9 +28,7 @@ describe('parse()', () => {
}) })
it('reports missing integer', () => { it('reports missing integer', () => {
expect(() => parse('*/a')).toThrow( expect(() => parse('*/a')).toThrow('minute: missing integer at character 2')
'minute: missing integer at character 2'
)
expect(() => parse('*')).toThrow('hour: missing integer at character 1') expect(() => parse('*')).toThrow('hour: missing integer at character 1')
}) })

View File

@ -325,7 +325,10 @@ class P {
value.push(result.value) value.push(result.value)
pos = result.pos pos = result.pos
} }
while (i < max && (result = this._parse(input, pos, end)) instanceof Success) { while (
i < max &&
(result = this._parse(input, pos, end)) instanceof Success
) {
++i ++i
value.push(result.value) value.push(result.value)
pos = result.pos pos = result.pos
@ -359,8 +362,9 @@ P.eof = new P(
const parser = P.grammar({ const parser = P.grammar({
default: r => default: r =>
P.seq(r.ws, r.term.repeat(), P.eof) P.seq(r.ws, r.term.repeat(), P.eof).map(
.map(([, terms]) => (terms.length === 0 ? new Null() : new And(terms))), ([, terms]) => (terms.length === 0 ? new Null() : new And(terms))
),
quotedString: new P((input, pos, end) => { quotedString: new P((input, pos, end) => {
if (input[pos] !== '"') { if (input[pos] !== '"') {
return new Failure(pos, '"') return new Failure(pos, '"')
@ -416,7 +420,7 @@ const parser = P.grammar({
? new StringNode(str) ? new StringNode(str)
: new NumberNode(asNum) : new NumberNode(asNum)
}) })
), )
).skip(r.ws), ).skip(r.ws),
ws: P.regex(/\s*/), ws: P.regex(/\s*/),
}).default }).default
@ -476,17 +480,19 @@ export const getPropertyClausesStrings = node => {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export const setPropertyClause = (node, name, child) => { export const setPropertyClause = (node, name, child) => {
const property = child && new Property( const property =
name, child &&
typeof child === 'string' ? new StringNode(child) : child new Property(
) name,
typeof child === 'string' ? new StringNode(child) : child
)
if (node === undefined) { if (node === undefined) {
return property return property
} }
const children = (node instanceof And ? node.children : [node]).filter(child => const children = (node instanceof And ? node.children : [node]).filter(
!(child instanceof Property && child.name === name) child => !(child instanceof Property && child.name === name)
) )
if (property !== undefined) { if (property !== undefined) {
children.push(property) children.push(property)

View File

@ -49,13 +49,15 @@ describe('Number', () => {
describe('setPropertyClause', () => { describe('setPropertyClause', () => {
it('creates a node if none passed', () => { it('creates a node if none passed', () => {
expect(setPropertyClause(undefined, 'foo', 'bar').toString()).toBe('foo:bar') expect(setPropertyClause(undefined, 'foo', 'bar').toString()).toBe(
'foo:bar'
)
}) })
it('adds a property clause if there was none', () => { it('adds a property clause if there was none', () => {
expect( expect(setPropertyClause(parse('baz'), 'foo', 'bar').toString()).toBe(
setPropertyClause(parse('baz'), 'foo', 'bar').toString() 'baz foo:bar'
).toBe('baz foo:bar') )
}) })
it('replaces the property clause if there was one', () => { it('replaces the property clause if there was one', () => {

View File

@ -26,13 +26,16 @@ type ObjectPattern = { [string]: Pattern }
type ArrayPattern = Array<Pattern> type ArrayPattern = Array<Pattern>
// value equals the pattern // value equals the pattern
type ValuePattern = bool | number | string type ValuePattern = boolean | number | string
const match = (pattern: Pattern, value: any) => { const match = (pattern: Pattern, value: any) => {
if (Array.isArray(pattern)) { if (Array.isArray(pattern)) {
return Array.isArray(value) && pattern.every((subpattern, i) => return (
// FIXME: subpatterns should match different subvalues Array.isArray(value) &&
value.some(subvalue => match(subpattern, subvalue)) pattern.every((subpattern, i) =>
// FIXME: subpatterns should match different subvalues
value.some(subvalue => match(subpattern, subvalue))
)
) )
} }
@ -41,7 +44,7 @@ const match = (pattern: Pattern, value: any) => {
const { length } = keys const { length } = keys
if (length === 1) { if (length === 1) {
const [ key ] = keys const [key] = keys
if (key === '__and') { if (key === '__and') {
const andPattern: AndPattern = (pattern: any) const andPattern: AndPattern = (pattern: any)
return andPattern.__and.every(subpattern => match(subpattern, value)) return andPattern.__and.every(subpattern => match(subpattern, value))
@ -74,4 +77,5 @@ const match = (pattern: Pattern, value: any) => {
return pattern === value return pattern === value
} }
export const createPredicate = (pattern: Pattern) => (value: any) => match(pattern, value) export const createPredicate = (pattern: Pattern) => (value: any) =>
match(pattern, value)

View File

@ -36,11 +36,13 @@ const fuFooter = fu.struct([
fu.char('creatorApplication', 4), // 28 fu.char('creatorApplication', 4), // 28
fu.uint32('creatorVersion'), // 32 fu.uint32('creatorVersion'), // 32
fu.uint32('creatorHostOs'), // 36 fu.uint32('creatorHostOs'), // 36
fu.struct('originalSize', [ // At the creation, current size of the hard disk. fu.struct('originalSize', [
// At the creation, current size of the hard disk.
fu.uint32('high'), // 40 fu.uint32('high'), // 40
fu.uint32('low'), // 44 fu.uint32('low'), // 44
]), ]),
fu.struct('currentSize', [ // Current size of the virtual disk. At the creation: currentSize = originalSize. fu.struct('currentSize', [
// Current size of the virtual disk. At the creation: currentSize = originalSize.
fu.uint32('high'), // 48 fu.uint32('high'), // 48
fu.uint32('low'), // 52 fu.uint32('low'), // 52
]), ]),
@ -60,11 +62,9 @@ const FOOTER_SIZE = fuFooter.size
const fuHeader = fu.struct([ const fuHeader = fu.struct([
fu.char('cookie', 8), fu.char('cookie', 8),
fu.struct('dataOffset', [ fu.struct('dataOffset', [fu.uint32('high'), fu.uint32('low')]),
fu.uint32('high'), fu.struct('tableOffset', [
fu.uint32('low'), // Absolute byte offset of the Block Allocation Table.
]),
fu.struct('tableOffset', [ // Absolute byte offset of the Block Allocation Table.
fu.uint32('high'), fu.uint32('high'),
fu.uint32('low'), fu.uint32('low'),
]), ]),
@ -76,16 +76,21 @@ const fuHeader = fu.struct([
fu.uint32('parentTimestamp'), fu.uint32('parentTimestamp'),
fu.byte('reserved1', 4), fu.byte('reserved1', 4),
fu.char16be('parentUnicodeName', 512), fu.char16be('parentUnicodeName', 512),
fu.struct('parentLocatorEntry', [ fu.struct(
fu.uint32('platformCode'), 'parentLocatorEntry',
fu.uint32('platformDataSpace'), [
fu.uint32('platformDataLength'), fu.uint32('platformCode'),
fu.uint32('reserved'), fu.uint32('platformDataSpace'),
fu.struct('platformDataOffset', [ // Absolute byte offset of the locator data. fu.uint32('platformDataLength'),
fu.uint32('high'), fu.uint32('reserved'),
fu.uint32('low'), fu.struct('platformDataOffset', [
]), // Absolute byte offset of the locator data.
], 8), fu.uint32('high'),
fu.uint32('low'),
]),
],
8
),
fu.byte('reserved2', 256), fu.byte('reserved2', 256),
]) ])
const HEADER_SIZE = fuHeader.size const HEADER_SIZE = fuHeader.size
@ -98,10 +103,10 @@ const SIZE_OF_32_BITS = Math.pow(2, 32)
const uint32ToUint64 = fu => fu.high * SIZE_OF_32_BITS + fu.low const uint32ToUint64 = fu => fu.high * SIZE_OF_32_BITS + fu.low
// Returns a 32 bits integer corresponding to a Vhd version. // Returns a 32 bits integer corresponding to a Vhd version.
const getVhdVersion = (major, minor) => (major << 16) | (minor & 0x0000FFFF) const getVhdVersion = (major, minor) => (major << 16) | (minor & 0x0000ffff)
// bytes[] bit manipulation // bytes[] bit manipulation
const testBit = (map, bit) => map[bit >> 3] & 1 << (bit & 7) const testBit = (map, bit) => map[bit >> 3] & (1 << (bit & 7))
const setBit = (map, bit) => { const setBit = (map, bit) => {
map[bit >> 3] |= 1 << (bit & 7) map[bit >> 3] |= 1 << (bit & 7)
} }
@ -109,98 +114,95 @@ const unsetBit = (map, bit) => {
map[bit >> 3] &= ~(1 << (bit & 7)) map[bit >> 3] &= ~(1 << (bit & 7))
} }
const addOffsets = (...offsets) => offsets.reduce( const addOffsets = (...offsets) =>
(a, b) => b == null offsets.reduce(
? a (a, b) =>
: typeof b === 'object' b == null
? { bytes: a.bytes + b.bytes, bits: a.bits + b.bits } ? a
: { bytes: a.bytes + b, bits: a.bits }, : typeof b === 'object'
{ bytes: 0, bits: 0 } ? { bytes: a.bytes + b.bytes, bits: a.bits + b.bits }
) : { bytes: a.bytes + b, bits: a.bits },
{ bytes: 0, bits: 0 }
)
const pack = (field, value, buf, offset) => { const pack = (field, value, buf, offset) => {
field.pack( field.pack(value, buf, addOffsets(field.offset, offset))
value,
buf,
addOffsets(field.offset, offset)
)
} }
const unpack = (field, buf, offset) => const unpack = (field, buf, offset) =>
field.unpack( field.unpack(buf, addOffsets(field.offset, offset))
buf,
addOffsets(field.offset, offset)
)
// =================================================================== // ===================================================================
const streamToNewBuffer = stream => new Promise((resolve, reject) => { const streamToNewBuffer = stream =>
const chunks = [] new Promise((resolve, reject) => {
let length = 0 const chunks = []
let length = 0
const onData = chunk => { const onData = chunk => {
chunks.push(chunk) chunks.push(chunk)
length += chunk.length length += chunk.length
} }
stream.on('data', onData) stream.on('data', onData)
const clean = () => { const clean = () => {
stream.removeListener('data', onData) stream.removeListener('data', onData)
stream.removeListener('end', onEnd) stream.removeListener('end', onEnd)
stream.removeListener('error', onError) stream.removeListener('error', onError)
} }
const onEnd = () => { const onEnd = () => {
resolve(Buffer.concat(chunks, length)) resolve(Buffer.concat(chunks, length))
clean() clean()
} }
stream.on('end', onEnd) stream.on('end', onEnd)
const onError = error => { const onError = error => {
reject(error) reject(error)
clean() clean()
} }
stream.on('error', onError) stream.on('error', onError)
}) })
const streamToExistingBuffer = ( const streamToExistingBuffer = (
stream, stream,
buffer, buffer,
offset = 0, offset = 0,
end = buffer.length end = buffer.length
) => new Promise((resolve, reject) => { ) =>
assert(offset >= 0) new Promise((resolve, reject) => {
assert(end > offset) assert(offset >= 0)
assert(end <= buffer.length) assert(end > offset)
assert(end <= buffer.length)
let i = offset let i = offset
const onData = chunk => { const onData = chunk => {
const prev = i const prev = i
i += chunk.length i += chunk.length
if (i > end) { if (i > end) {
return onError(new Error('too much data')) return onError(new Error('too much data'))
}
chunk.copy(buffer, prev)
} }
stream.on('data', onData)
chunk.copy(buffer, prev) const clean = () => {
} stream.removeListener('data', onData)
stream.on('data', onData) stream.removeListener('end', onEnd)
stream.removeListener('error', onError)
const clean = () => { }
stream.removeListener('data', onData) const onEnd = () => {
stream.removeListener('end', onEnd) resolve(i - offset)
stream.removeListener('error', onError) clean()
} }
const onEnd = () => { stream.on('end', onEnd)
resolve(i - offset) const onError = error => {
clean() reject(error)
} clean()
stream.on('end', onEnd) }
const onError = error => { stream.on('error', onError)
reject(error) })
clean()
}
stream.on('error', onError)
})
// =================================================================== // ===================================================================
@ -214,7 +216,11 @@ const computeChecksum = (struct, buf, offset = 0) => {
for (let i = offset, n = checksumOffset; i < n; ++i) { for (let i = offset, n = checksumOffset; i < n; ++i) {
sum += buf[i] sum += buf[i]
} }
for (let i = checksumOffset + checksumField.size, n = offset + struct.size; i < n; ++i) { for (
let i = checksumOffset + checksumField.size, n = offset + struct.size;
i < n;
++i
) {
sum += buf[i] sum += buf[i]
} }
@ -222,7 +228,8 @@ const computeChecksum = (struct, buf, offset = 0) => {
} }
const verifyChecksum = (struct, buf, offset) => const verifyChecksum = (struct, buf, offset) =>
unpack(struct.fields.checksum, buf, offset) === computeChecksum(struct, buf, offset) unpack(struct.fields.checksum, buf, offset) ===
computeChecksum(struct, buf, offset)
const getParentLocatorSize = parentLocatorEntry => { const getParentLocatorSize = parentLocatorEntry => {
const { platformDataSpace } = parentLocatorEntry const { platformDataSpace } = parentLocatorEntry
@ -231,15 +238,13 @@ const getParentLocatorSize = parentLocatorEntry => {
return platformDataSpace * SECTOR_SIZE return platformDataSpace * SECTOR_SIZE
} }
return (platformDataSpace % SECTOR_SIZE === 0) return platformDataSpace % SECTOR_SIZE === 0 ? platformDataSpace : 0
? platformDataSpace
: 0
} }
// =================================================================== // ===================================================================
// Euclidean division, returns the quotient and the remainder of a / b. // Euclidean division, returns the quotient and the remainder of a / b.
const div = (a, b) => [ Math.floor(a / b), a % b ] const div = (a, b) => [Math.floor(a / b), a % b]
export default class Vhd { export default class Vhd {
constructor (handler, path) { constructor (handler, path) {
@ -263,13 +268,22 @@ export default class Vhd {
assert(begin >= 0) assert(begin >= 0)
assert(length > 0) assert(length > 0)
return this._handler.createReadStream(this._path, { return this._handler
end: begin + length - 1, .createReadStream(this._path, {
start: begin, end: begin + length - 1,
}).then(buf start: begin,
? stream => streamToExistingBuffer(stream, buf, offset, (offset || 0) + length) })
: streamToNewBuffer .then(
) buf
? stream =>
streamToExistingBuffer(
stream,
buf,
offset,
(offset || 0) + length
)
: streamToNewBuffer
)
} }
// - if `buffer`: it is filled with 0 starting from `offset`, and // - if `buffer`: it is filled with 0 starting from `offset`, and
@ -296,7 +310,7 @@ export default class Vhd {
assert(block < this._header.maxTableEntries) assert(block < this._header.maxTableEntries)
const blockAddr = this._blockAllocationTable[block] const blockAddr = this._blockAllocationTable[block]
if (blockAddr !== 0xFFFFFFFF) { if (blockAddr !== 0xffffffff) {
return blockAddr * SECTOR_SIZE return blockAddr * SECTOR_SIZE
} }
} }
@ -325,7 +339,8 @@ export default class Vhd {
assert(sectorsPerBlock % 1 === 0) assert(sectorsPerBlock % 1 === 0)
// 1 bit per sector, rounded up to full sectors // 1 bit per sector, rounded up to full sectors
this._blockBitmapSize = Math.ceil(sectorsPerBlock / 8 / SECTOR_SIZE) * SECTOR_SIZE this._blockBitmapSize =
Math.ceil(sectorsPerBlock / 8 / SECTOR_SIZE) * SECTOR_SIZE
assert(this._blockBitmapSize === SECTOR_SIZE) assert(this._blockBitmapSize === SECTOR_SIZE)
this._footer = footer this._footer = footer
@ -368,10 +383,10 @@ export default class Vhd {
const blockBitmapSize = this._blockBitmapSize const blockBitmapSize = this._blockBitmapSize
const parent = this._parent const parent = this._parent
if (blockAddr && ( if (
!parent || blockAddr &&
testBit(await this._read(blockAddr, blockBitmapSize), sector) (!parent || testBit(await this._read(blockAddr, blockBitmapSize), sector))
)) { ) {
return this._read( return this._read(
blockAddr + blockBitmapSize + sector * SECTOR_SIZE + begin, blockAddr + blockBitmapSize + sector * SECTOR_SIZE + begin,
length, length,
@ -402,12 +417,17 @@ export default class Vhd {
} }
if (!parent) { if (!parent) {
return this._read(blockAddr + this._blockBitmapSize + begin, length, buf, offset) return this._read(
blockAddr + this._blockBitmapSize + begin,
length,
buf,
offset
)
} }
// FIXME: we should read as many sectors in a single pass as // FIXME: we should read as many sectors in a single pass as
// possible for maximum perf. // possible for maximum perf.
const [ sector, beginInSector ] = div(begin, SECTOR_SIZE) const [sector, beginInSector] = div(begin, SECTOR_SIZE)
return this._readBlockSector( return this._readBlockSector(
block, block,
sector, sector,
@ -428,7 +448,7 @@ export default class Vhd {
} }
const { blockSize } = this._header const { blockSize } = this._header
const [ block, beginInBlock ] = div(begin, blockSize) const [block, beginInBlock] = div(begin, blockSize)
return this._readBlock( return this._readBlock(
block, block,

View File

@ -29,13 +29,15 @@ exports.createOutputStream = path => {
exports.resolveRef = (xapi, type, refOrUuidOrNameLabel) => exports.resolveRef = (xapi, type, refOrUuidOrNameLabel) =>
isOpaqueRef(refOrUuidOrNameLabel) isOpaqueRef(refOrUuidOrNameLabel)
? refOrUuidOrNameLabel ? refOrUuidOrNameLabel
: xapi.call(`${type}.get_by_uuid`, refOrUuidOrNameLabel).catch( : xapi.call(`${type}.get_by_uuid`, refOrUuidOrNameLabel).catch(() =>
() => xapi.call(`${type}.get_by_name_label`, refOrUuidOrNameLabel).then( xapi
refs => { .call(`${type}.get_by_name_label`, refOrUuidOrNameLabel)
.then(refs => {
if (refs.length === 1) { if (refs.length === 1) {
return refs[0] return refs[0]
} }
throw new Error(`no single match for ${type} with name label ${refOrUuidOrNameLabel}`) throw new Error(
} `no single match for ${type} with name label ${refOrUuidOrNameLabel}`
) )
})
) )

View File

@ -56,7 +56,7 @@ const main = async args => {
let auth let auth
if (opts._.length > 1) { if (opts._.length > 1) {
const [ , user, password = await askPassword() ] = opts._ const [, user, password = await askPassword()] = opts._
auth = { user, password } auth = { user, password }
} }
@ -86,11 +86,11 @@ const main = async args => {
// Make the REPL waits for promise completion. // Make the REPL waits for promise completion.
repl.eval = (evaluate => (cmd, context, filename, cb) => { repl.eval = (evaluate => (cmd, context, filename, cb) => {
fromCallback(cb => { ;fromCallback(cb => {
evaluate.call(repl, cmd, context, filename, cb) evaluate.call(repl, cmd, context, filename, cb)
}).then(value => })
isArray(value) ? Promise.all(value) : value .then(value => (isArray(value) ? Promise.all(value) : value))
)::asCallback(cb) ::asCallback(cb)
})(repl.eval) })(repl.eval)
await eventToPromise(repl, 'exit') await eventToPromise(repl, 'exit')

View File

@ -55,7 +55,7 @@ const NETWORK_ERRORS = {
ETIMEDOUT: true, ETIMEDOUT: true,
} }
const isNetworkError = ({code}) => NETWORK_ERRORS[code] const isNetworkError = ({ code }) => NETWORK_ERRORS[code]
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@ -64,17 +64,17 @@ const XAPI_NETWORK_ERRORS = {
HOST_HAS_NO_MANAGEMENT_IP: true, HOST_HAS_NO_MANAGEMENT_IP: true,
} }
const isXapiNetworkError = ({code}) => XAPI_NETWORK_ERRORS[code] const isXapiNetworkError = ({ code }) => XAPI_NETWORK_ERRORS[code]
// ------------------------------------------------------------------- // -------------------------------------------------------------------
const areEventsLost = ({code}) => code === 'EVENTS_LOST' const areEventsLost = ({ code }) => code === 'EVENTS_LOST'
const isHostSlave = ({code}) => code === 'HOST_IS_SLAVE' const isHostSlave = ({ code }) => code === 'HOST_IS_SLAVE'
const isMethodUnknown = ({code}) => code === 'MESSAGE_METHOD_UNKNOWN' const isMethodUnknown = ({ code }) => code === 'MESSAGE_METHOD_UNKNOWN'
const isSessionInvalid = ({code}) => code === 'SESSION_INVALID' const isSessionInvalid = ({ code }) => code === 'SESSION_INVALID'
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@ -93,8 +93,9 @@ class XapiError extends BaseError {
export const wrapError = error => { export const wrapError = error => {
let code, params let code, params
if (isArray(error)) { // < XenServer 7.3 if (isArray(error)) {
[ code, ...params ] = error // < XenServer 7.3
;[code, ...params] = error
} else { } else {
code = error.message code = error.message
params = error.data params = error.data
@ -111,7 +112,7 @@ const parseUrl = url => {
throw new Error('invalid URL: ' + url) throw new Error('invalid URL: ' + url)
} }
const [ , protocol = 'https:', username, password, hostname, port ] = matches const [, protocol = 'https:', username, password, hostname, port] = matches
return { protocol, username, password, hostname, port } return { protocol, username, password, hostname, port }
} }
@ -128,17 +129,13 @@ const {
const OPAQUE_REF_PREFIX = 'OpaqueRef:' const OPAQUE_REF_PREFIX = 'OpaqueRef:'
export const isOpaqueRef = value => export const isOpaqueRef = value =>
typeof value === 'string' && typeof value === 'string' && startsWith(value, OPAQUE_REF_PREFIX)
startsWith(value, OPAQUE_REF_PREFIX)
// ------------------------------------------------------------------- // -------------------------------------------------------------------
const RE_READ_ONLY_METHOD = /^[^.]+\.get_/ const RE_READ_ONLY_METHOD = /^[^.]+\.get_/
const isReadOnlyCall = (method, args) => ( const isReadOnlyCall = (method, args) =>
args.length === 1 && args.length === 1 && isOpaqueRef(args[0]) && RE_READ_ONLY_METHOD.test(method)
isOpaqueRef(args[0]) &&
RE_READ_ONLY_METHOD.test(method)
)
// Prepare values before passing them to the XenAPI: // Prepare values before passing them to the XenAPI:
// //
@ -178,17 +175,17 @@ const EMPTY_ARRAY = freezeObject([])
const getTaskResult = (task, onSuccess, onFailure) => { const getTaskResult = (task, onSuccess, onFailure) => {
const { status } = task const { status } = task
if (status === 'cancelled') { if (status === 'cancelled') {
return [ onFailure(new Cancel('task canceled')) ] return [onFailure(new Cancel('task canceled'))]
} }
if (status === 'failure') { if (status === 'failure') {
return [ onFailure(wrapError(task.error_info)) ] return [onFailure(wrapError(task.error_info))]
} }
if (status === 'success') { if (status === 'success') {
// the result might be: // the result might be:
// - empty string // - empty string
// - an opaque reference // - an opaque reference
// - an XML-RPC value // - an XML-RPC value
return [ onSuccess(task.result) ] return [onSuccess(task.result)]
} }
} }
@ -209,7 +206,7 @@ export class Xapi extends EventEmitter {
this._pool = null this._pool = null
this._readOnly = Boolean(opts.readOnly) this._readOnly = Boolean(opts.readOnly)
this._sessionId = null this._sessionId = null
const url = this._url = parseUrl(opts.url) const url = (this._url = parseUrl(opts.url))
if (this._auth === undefined) { if (this._auth === undefined) {
const user = url.username const user = url.username
@ -224,9 +221,7 @@ export class Xapi extends EventEmitter {
} }
if (opts.watchEvents !== false) { if (opts.watchEvents !== false) {
this._debounce = opts.debounce == null this._debounce = opts.debounce == null ? 200 : opts.debounce
? 200
: opts.debounce
this._eventWatchers = createObject(null) this._eventWatchers = createObject(null)
@ -237,7 +232,7 @@ export class Xapi extends EventEmitter {
this._nTasks = 0 this._nTasks = 0
const objects = this._objects = new Collection() const objects = (this._objects = new Collection())
objects.getKey = getKey objects.getKey = getKey
this._objectsByRefs = createObject(null) this._objectsByRefs = createObject(null)
@ -286,13 +281,7 @@ export class Xapi extends EventEmitter {
get status () { get status () {
const id = this._sessionId const id = this._sessionId
return id return id ? (id === CONNECTING ? CONNECTING : CONNECTED) : DISCONNECTED
? (
id === CONNECTING
? CONNECTING
: CONNECTED
)
: DISCONNECTED
} }
get _humanId () { get _humanId () {
@ -305,36 +294,46 @@ export class Xapi extends EventEmitter {
barrier (ref) { barrier (ref) {
const eventWatchers = this._eventWatchers const eventWatchers = this._eventWatchers
if (eventWatchers === undefined) { if (eventWatchers === undefined) {
return Promise.reject(new Error('Xapi#barrier() requires events watching')) return Promise.reject(
new Error('Xapi#barrier() requires events watching')
)
} }
const key = `xo:barrier:${Math.random().toString(36).slice(2)}` const key = `xo:barrier:${Math.random()
.toString(36)
.slice(2)}`
const poolRef = this._pool.$ref const poolRef = this._pool.$ref
const { promise, resolve } = defer() const { promise, resolve } = defer()
eventWatchers[key] = resolve eventWatchers[key] = resolve
return this._sessionCall( return this._sessionCall('pool.add_to_other_config', [
'pool.add_to_other_config', poolRef,
[ poolRef, key, '' ] key,
).then(() => promise.then(() => { '',
this._sessionCall('pool.remove_from_other_config', [ poolRef, key ]).catch(noop) ]).then(() =>
promise.then(() => {
this._sessionCall('pool.remove_from_other_config', [
poolRef,
key,
]).catch(noop)
if (ref === undefined) { if (ref === undefined) {
return return
} }
// support legacy params (type, ref) // support legacy params (type, ref)
if (arguments.length === 2) { if (arguments.length === 2) {
ref = arguments[1] ref = arguments[1]
} }
return this.getObjectByRef(ref) return this.getObjectByRef(ref)
})) })
)
} }
connect () { connect () {
const {status} = this const { status } = this
if (status === CONNECTED) { if (status === CONNECTED) {
return Promise.reject(new Error('already connected')) return Promise.reject(new Error('already connected'))
@ -378,7 +377,7 @@ export class Xapi extends EventEmitter {
return Promise.reject(new Error('already disconnected')) return Promise.reject(new Error('already disconnected'))
} }
this._transportCall('session.logout', [ this._sessionId ]).catch(noop) this._transportCall('session.logout', [this._sessionId]).catch(noop)
this._sessionId = null this._sessionId = null
@ -434,13 +433,10 @@ export class Xapi extends EventEmitter {
// this lib), UUID (unique identifier that some objects have) or // this lib), UUID (unique identifier that some objects have) or
// opaque reference (internal to XAPI). // opaque reference (internal to XAPI).
getObject (idOrUuidOrRef, defaultValue) { getObject (idOrUuidOrRef, defaultValue) {
const object = typeof idOrUuidOrRef === 'string' const object =
? ( typeof idOrUuidOrRef === 'string'
// if there is an UUID, it is also the $id. ? this._objects.all[idOrUuidOrRef] || this._objectsByRefs[idOrUuidOrRef]
this._objects.all[idOrUuidOrRef] || : this._objects.all[idOrUuidOrRef.$id]
this._objectsByRefs[idOrUuidOrRef]
)
: this._objects.all[idOrUuidOrRef.$id]
if (object) return object if (object) return object
@ -479,158 +475,147 @@ export class Xapi extends EventEmitter {
} }
@cancelable @cancelable
getResource ($cancelToken, pathname, { getResource ($cancelToken, pathname, { host, query, task }) {
host, return this._autoTask(task, `Xapi#getResource ${pathname}`).then(
query, taskRef => {
task, query = { ...query, session_id: this.sessionId }
}) { let taskResult
return this._autoTask( if (taskRef !== undefined) {
task, query.task_id = taskRef
`Xapi#getResource ${pathname}` taskResult = this.watchTask(taskRef)
).then(taskRef => {
query = { ...query, session_id: this.sessionId }
let taskResult
if (taskRef !== undefined) {
query.task_id = taskRef
taskResult = this.watchTask(taskRef)
if (typeof $cancelToken.addHandler === 'function') { if (typeof $cancelToken.addHandler === 'function') {
$cancelToken.addHandler(() => taskResult) $cancelToken.addHandler(() => taskResult)
}
} }
}
let promise = httpRequest( let promise = httpRequest(
$cancelToken, $cancelToken,
this._url, this._url,
host && { host && {
hostname: this.getObject(host).address, hostname: this.getObject(host).address,
},
{
pathname,
query,
rejectUnauthorized: !this._allowUnauthorized,
}
)
if (taskResult !== undefined) {
promise = promise.then(response => {
response.task = taskResult
return response
})
}
return promise
})
}
@cancelable
putResource ($cancelToken, body, pathname, {
host,
query,
task,
} = {}) {
if (this._readOnly) {
return Promise.reject(new Error(new Error('cannot put resource in read only mode')))
}
return this._autoTask(
task,
`Xapi#putResource ${pathname}`
).then(taskRef => {
query = { ...query, session_id: this.sessionId }
let taskResult
if (taskRef !== undefined) {
query.task_id = taskRef
taskResult = this.watchTask(taskRef)
if (typeof $cancelToken.addHandler === 'function') {
$cancelToken.addHandler(() => taskResult)
}
}
const headers = {}
// Xen API does not support chunk encoding.
const isStream = typeof body.pipe === 'function'
const { length } = body
if (isStream && length === undefined) {
// add a fake huge content length (1 PiB)
headers['content-length'] = '1125899906842624'
}
const doRequest = override => httpRequest.put(
$cancelToken,
this._url,
host && {
hostname: this.getObject(host).address,
},
{
body,
headers,
pathname,
query,
rejectUnauthorized: !this._allowUnauthorized,
},
override
)
const promise = isStream
// dummy request to probe for a redirection before consuming body
? doRequest({
body: '',
// omit task_id because this request will fail on purpose
query: 'task_id' in query
? omit(query, 'task_id')
: query,
maxRedirects: 0,
}).then(
response => {
response.req.abort()
return doRequest()
}, },
error => { {
let response pathname,
if (error != null && (response = error.response) != null) { query,
response.req.abort() rejectUnauthorized: !this._allowUnauthorized,
const { headers: { location }, statusCode } = response
if (statusCode === 302 && location !== undefined) {
return doRequest(location)
}
}
throw error
} }
) )
// http-request-plus correctly handle redirects if body is not a stream
: doRequest()
return promise.then(response => {
const { req } = response
if (taskResult !== undefined) { if (taskResult !== undefined) {
taskResult = taskResult.catch(error => { promise = promise.then(response => {
error.url = response.url response.task = taskResult
throw error return response
}) })
} }
if (req.finished) { return promise
req.abort() }
return taskResult )
}
@cancelable
putResource ($cancelToken, body, pathname, { host, query, task } = {}) {
if (this._readOnly) {
return Promise.reject(
new Error(new Error('cannot put resource in read only mode'))
)
}
return this._autoTask(task, `Xapi#putResource ${pathname}`).then(
taskRef => {
query = { ...query, session_id: this.sessionId }
let taskResult
if (taskRef !== undefined) {
query.task_id = taskRef
taskResult = this.watchTask(taskRef)
if (typeof $cancelToken.addHandler === 'function') {
$cancelToken.addHandler(() => taskResult)
}
} }
return fromEvents(req, ['close', 'finish']).then(() => { const headers = {}
req.abort()
return taskResult // Xen API does not support chunk encoding.
const isStream = typeof body.pipe === 'function'
const { length } = body
if (isStream && length === undefined) {
// add a fake huge content length (1 PiB)
headers['content-length'] = '1125899906842624'
}
const doRequest = override =>
httpRequest.put(
$cancelToken,
this._url,
host && {
hostname: this.getObject(host).address,
},
{
body,
headers,
pathname,
query,
rejectUnauthorized: !this._allowUnauthorized,
},
override
)
// if a stream, sends a dummy request to probe for a
// redirection before consuming body
const promise = isStream
? doRequest({
body: '',
// omit task_id because this request will fail on purpose
query: 'task_id' in query ? omit(query, 'task_id') : query,
maxRedirects: 0,
}).then(
response => {
response.req.abort()
return doRequest()
},
error => {
let response
if (error != null && (response = error.response) != null) {
response.req.abort()
const { headers: { location }, statusCode } = response
if (statusCode === 302 && location !== undefined) {
return doRequest(location)
}
}
throw error
}
)
: doRequest()
return promise.then(response => {
const { req } = response
if (taskResult !== undefined) {
taskResult = taskResult.catch(error => {
error.url = response.url
throw error
})
}
if (req.finished) {
req.abort()
return taskResult
}
return fromEvents(req, ['close', 'finish']).then(() => {
req.abort()
return taskResult
})
}) })
}) }
}) )
} }
watchTask (ref) { watchTask (ref) {
@ -692,22 +677,24 @@ export class Xapi extends EventEmitter {
newArgs.push.apply(newArgs, args) newArgs.push.apply(newArgs, args)
} }
return this._transportCall(method, newArgs) return this._transportCall(method, newArgs)::pCatch(
::pCatch(isSessionInvalid, () => { isSessionInvalid,
() => {
// XAPI is sometimes reinitialized and sessions are lost. // XAPI is sometimes reinitialized and sessions are lost.
// Try to login again. // Try to login again.
debug('%s: the session has been reinitialized', this._humanId) debug('%s: the session has been reinitialized', this._humanId)
this._sessionId = null this._sessionId = null
return this.connect().then(() => this._sessionCall(method, args)) return this.connect().then(() => this._sessionCall(method, args))
}) }
)
} catch (error) { } catch (error) {
return Promise.reject(error) return Promise.reject(error)
} }
} }
_addObject (type, ref, object) { _addObject (type, ref, object) {
const {_objectsByRefs: objectsByRefs} = this const { _objectsByRefs: objectsByRefs } = this
const reservedKeys = { const reservedKeys = {
id: true, id: true,
@ -715,9 +702,8 @@ export class Xapi extends EventEmitter {
ref: true, ref: true,
type: true, type: true,
} }
const getKey = (key, obj) => reservedKeys[key] && obj === object const getKey = (key, obj) =>
? `$$${key}` reservedKeys[key] && obj === object ? `$$${key}` : `$${key}`
: `$${key}`
// Creates resolved properties. // Creates resolved properties.
forEach(object, function resolveObject (value, key, object) { forEach(object, function resolveObject (value, key, object) {
@ -736,7 +722,7 @@ export class Xapi extends EventEmitter {
} else if (isOpaqueRef(value[0])) { } else if (isOpaqueRef(value[0])) {
// This is an array of refs. // This is an array of refs.
defineProperty(object, getKey(key, object), { defineProperty(object, getKey(key, object), {
get: () => freezeObject(map(value, (ref) => objectsByRefs[ref])), get: () => freezeObject(map(value, ref => objectsByRefs[ref])),
}) })
freezeObject(value) freezeObject(value)
@ -836,38 +822,40 @@ export class Xapi extends EventEmitter {
} }
_watchEvents () { _watchEvents () {
const loop = () => this.status === CONNECTED && this._sessionCall('event.from', [ const loop = () =>
['*'], this.status === CONNECTED &&
this._fromToken, this._sessionCall('event.from', [
60 + 0.1, // Force float. ['*'],
]).then(onSuccess, onFailure) this._fromToken,
60 + 0.1, // Force float.
]).then(onSuccess, onFailure)
const onSuccess = ({ events, token, valid_ref_counts: { task } }) => { const onSuccess = ({ events, token, valid_ref_counts: { task } }) => {
this._fromToken = token this._fromToken = token
this._processEvents(events) this._processEvents(events)
if (task !== this._nTasks) { if (task !== this._nTasks) {
this._sessionCall('task.get_all_records').then(tasks => { this._sessionCall('task.get_all_records')
const toRemove = new Set() .then(tasks => {
forEach(this.objects.all, object => { const toRemove = new Set()
if (object.$type === 'task') { forEach(this.objects.all, object => {
toRemove.add(object.$ref) if (object.$type === 'task') {
} toRemove.add(object.$ref)
}
})
forEach(tasks, (task, ref) => {
toRemove.delete(ref)
this._addObject('task', ref, task)
})
toRemove.forEach(ref => {
this._removeObject('task', ref)
})
}) })
forEach(tasks, (task, ref) => { .catch(noop)
toRemove.delete(ref)
this._addObject('task', ref, task)
})
toRemove.forEach(ref => {
this._removeObject('task', ref)
})
}).catch(noop)
} }
const debounce = this._debounce const debounce = this._debounce
return debounce != null return debounce != null ? pDelay(debounce).then(loop) : loop()
? pDelay(debounce).then(loop)
: loop()
} }
const onFailure = error => { const onFailure = error => {
if (areEventsLost(error)) { if (areEventsLost(error)) {
@ -906,41 +894,43 @@ export class Xapi extends EventEmitter {
::/\.get_all_records$/.test ::/\.get_all_records$/.test
) )
return Promise.all(map( return Promise.all(
getAllRecordsMethods, map(getAllRecordsMethods, method =>
method => this._sessionCall(method).then( this._sessionCall(method).then(
objects => { objects => {
const type = method.slice(0, method.indexOf('.')).toLowerCase() const type = method.slice(0, method.indexOf('.')).toLowerCase()
forEach(objects, (object, ref) => { forEach(objects, (object, ref) => {
this._addObject(type, ref, object) this._addObject(type, ref, object)
}) })
}, },
error => { error => {
if (error.code !== 'MESSAGE_REMOVED') { if (error.code !== 'MESSAGE_REMOVED') {
throw error throw error
}
} }
} )
) )
)) )
}) })
} }
const watchEvents = () => this._sessionCall('event.register', [ ['*'] ]).then(loop) const watchEvents = () =>
this._sessionCall('event.register', [['*']]).then(loop)
const loop = () => this.status === CONNECTED && this._sessionCall('event.next').then(onSuccess, onFailure) const loop = () =>
this.status === CONNECTED &&
this._sessionCall('event.next').then(onSuccess, onFailure)
const onSuccess = events => { const onSuccess = events => {
this._processEvents(events) this._processEvents(events)
const debounce = this._debounce const debounce = this._debounce
return debounce == null return debounce == null ? loop() : pDelay(debounce).then(loop)
? loop()
: pDelay(debounce).then(loop)
} }
const onFailure = error => { const onFailure = error => {
if (areEventsLost(error)) { if (areEventsLost(error)) {
return this._sessionCall('event.unregister', [ ['*'] ]).then(watchEvents) return this._sessionCall('event.unregister', [['*']]).then(watchEvents)
} }
throw error throw error
@ -950,85 +940,106 @@ export class Xapi extends EventEmitter {
} }
} }
Xapi.prototype._transportCall = reduce([ Xapi.prototype._transportCall = reduce(
function (method, args) { [
return this._call(method, args).catch(error => { function (method, args) {
if (!(error instanceof Error)) { return this._call(method, args).catch(error => {
error = wrapError(error) if (!(error instanceof Error)) {
} error = wrapError(error)
error.method = method
throw error
})
},
call => function () {
let iterator // lazily created
const loop = () => call.apply(this, arguments)
::pCatch(isNetworkError, isXapiNetworkError, error => {
if (iterator === undefined) {
iterator = fibonacci().clamp(undefined, 60).take(10).toMs()
} }
const cursor = iterator.next() error.method = method
if (!cursor.done) {
// TODO: ability to cancel the connection
// TODO: ability to force immediate reconnection
const delay = cursor.value
debug('%s: network error %s, next try in %s ms', this._humanId, error.code, delay)
return pDelay(delay).then(loop)
}
debug('%s: network error %s, aborting', this._humanId, error.code)
// mark as disconnected
this.disconnect()::pCatch(noop)
throw error throw error
}) })
return loop() },
}, call =>
call => function loop () { function () {
return call.apply(this, arguments) let iterator // lazily created
::pCatch(isHostSlave, ({params: [master]}) => { const loop = () =>
debug('%s: host is slave, attempting to connect at %s', this._humanId, master) call
.apply(this, arguments)
::pCatch(isNetworkError, isXapiNetworkError, error => {
if (iterator === undefined) {
iterator = fibonacci()
.clamp(undefined, 60)
.take(10)
.toMs()
}
const newUrl = { const cursor = iterator.next()
...this._url, if (!cursor.done) {
hostname: master, // TODO: ability to cancel the connection
} // TODO: ability to force immediate reconnection
this.emit('redirect', newUrl)
this._url = newUrl
return loop.apply(this, arguments) const delay = cursor.value
}) debug(
}, '%s: network error %s, next try in %s ms',
call => function (method) { this._humanId,
const startTime = Date.now() error.code,
return call.apply(this, arguments).then( delay
result => { )
debug( return pDelay(delay).then(loop)
'%s: %s(...) [%s] ==> %s', }
this._humanId,
method, debug('%s: network error %s, aborting', this._humanId, error.code)
ms(Date.now() - startTime),
kindOf(result) // mark as disconnected
) this.disconnect()::pCatch(noop)
return result
throw error
})
return loop()
}, },
error => { call =>
debug( function loop () {
'%s: %s(...) [%s] =!> %s', return call
this._humanId, .apply(this, arguments)
method, ::pCatch(isHostSlave, ({ params: [master] }) => {
ms(Date.now() - startTime), debug(
error '%s: host is slave, attempting to connect at %s',
this._humanId,
master
)
const newUrl = {
...this._url,
hostname: master,
}
this.emit('redirect', newUrl)
this._url = newUrl
return loop.apply(this, arguments)
})
},
call =>
function (method) {
const startTime = Date.now()
return call.apply(this, arguments).then(
result => {
debug(
'%s: %s(...) [%s] ==> %s',
this._humanId,
method,
ms(Date.now() - startTime),
kindOf(result)
)
return result
},
error => {
debug(
'%s: %s(...) [%s] =!> %s',
this._humanId,
method,
ms(Date.now() - startTime),
error
)
throw error
}
) )
throw error },
} ],
) (call, decorator) => decorator(call)
}, )
], (call, decorator) => decorator(call))
// =================================================================== // ===================================================================

View File

@ -5,7 +5,7 @@ import { delay as pDelay } from 'promise-toolbox'
import { createClient } from './' import { createClient } from './'
const xapi = (() => { const xapi = (() => {
const [ , , url, user, password ] = process.argv const [, , url, user, password] = process.argv
return createClient({ return createClient({
auth: { user, password }, auth: { user, password },
@ -14,16 +14,19 @@ const xapi = (() => {
}) })
})() })()
xapi.connect() xapi
.connect()
// Get the pool record's ref. // Get the pool record's ref.
.then(() => xapi.call('pool.get_all')) .then(() => xapi.call('pool.get_all'))
// Injects lots of events. // Injects lots of events.
.then(([ poolRef ]) => { .then(([poolRef]) => {
const loop = () => xapi.call('event.inject', 'pool', poolRef) const loop = () =>
::pDelay(10) // A small delay is required to avoid overloading the Xen API. xapi
.then(loop) .call('event.inject', 'pool', poolRef)
::pDelay(10) // A small delay is required to avoid overloading the Xen API.
.then(loop)
return loop() return loop()
}) })

View File

@ -14,7 +14,7 @@ setInterval(() => {
) )
}, 1e2) }, 1e2)
const [ , , url, user, password ] = process.argv const [, , url, user, password] = process.argv
createClient({ createClient({
auth: { user, password }, auth: { user, password },
readOnly: true, readOnly: true,

View File

@ -3,7 +3,7 @@ import xmlRpc from './xml-rpc'
import xmlRpcJson from './xml-rpc-json' import xmlRpcJson from './xml-rpc-json'
import { UnsupportedTransport } from './_utils' import { UnsupportedTransport } from './_utils'
const factories = [ jsonRpc, xmlRpcJson, xmlRpc ] const factories = [jsonRpc, xmlRpcJson, xmlRpc]
const { length } = factories const { length } = factories
export default opts => { export default opts => {
@ -14,18 +14,18 @@ export default opts => {
const current = factories[i++](opts) const current = factories[i++](opts)
if (i < length) { if (i < length) {
const currentI = i const currentI = i
call = (method, args) => current(method, args).catch( call = (method, args) =>
error => { current(method, args).catch(error => {
if (error instanceof UnsupportedTransport) { if (error instanceof UnsupportedTransport) {
if (currentI === i) { // not changed yet if (currentI === i) {
// not changed yet
create() create()
} }
return call(method, args) return call(method, args)
} }
throw error throw error
} })
)
} else { } else {
call = current call = current
} }

View File

@ -4,35 +4,40 @@ import { format, parse } from 'json-rpc-protocol'
import { UnsupportedTransport } from './_utils' import { UnsupportedTransport } from './_utils'
export default ({ allowUnauthorized, url }) => { export default ({ allowUnauthorized, url }) => {
return (method, args) => httpRequestPlus.post(url, { return (method, args) =>
rejectUnauthorized: !allowUnauthorized, httpRequestPlus
body: format.request(0, method, args), .post(url, {
headers: { rejectUnauthorized: !allowUnauthorized,
'Accept': 'application/json', body: format.request(0, method, args),
'Content-Type': 'application/json', headers: {
}, Accept: 'application/json',
path: '/jsonrpc', 'Content-Type': 'application/json',
}).readAll('utf8').then( },
text => { path: '/jsonrpc',
let response })
try { .readAll('utf8')
response = parse(text) .then(
} catch (error) { text => {
throw new UnsupportedTransport() let response
} try {
response = parse(text)
} catch (error) {
throw new UnsupportedTransport()
}
if (response.type === 'response') { if (response.type === 'response') {
return response.result return response.result
} }
throw response.error throw response.error
}, },
error => { error => {
if (error.response !== undefined) { // HTTP error if (error.response !== undefined) {
throw new UnsupportedTransport() // HTTP error
} throw new UnsupportedTransport()
}
throw error throw error
} }
) )
} }

View File

@ -20,10 +20,7 @@ const SPECIAL_CHARS = {
'\r': '\\r', '\r': '\\r',
'\t': '\\t', '\t': '\\t',
} }
const SPECIAL_CHARS_RE = new RegExp( const SPECIAL_CHARS_RE = new RegExp(Object.keys(SPECIAL_CHARS).join('|'), 'g')
Object.keys(SPECIAL_CHARS).join('|'),
'g'
)
const parseResult = result => { const parseResult = result => {
const status = result.Status const status = result.Status
@ -78,11 +75,7 @@ export default ({
allowUnauthorized, allowUnauthorized,
url: { hostname, path, port, protocol }, url: { hostname, path, port, protocol },
}) => { }) => {
const client = ( const client = (protocol === 'https:' ? createSecureClient : createClient)({
protocol === 'https:'
? createSecureClient
: createClient
)({
host: hostname, host: hostname,
path: '/json', path: '/json',
port, port,
@ -90,8 +83,5 @@ export default ({
}) })
const call = promisify(client.methodCall, client) const call = promisify(client.methodCall, client)
return (method, args) => call(method, args).then( return (method, args) => call(method, args).then(parseResult, logError)
parseResult,
logError
)
} }

View File

@ -34,19 +34,12 @@ export default ({
allowUnauthorized, allowUnauthorized,
url: { hostname, path, port, protocol }, url: { hostname, path, port, protocol },
}) => { }) => {
const client = ( const client = (protocol === 'https:' ? createSecureClient : createClient)({
protocol === 'https:'
? createSecureClient
: createClient
)({
host: hostname, host: hostname,
port, port,
rejectUnauthorized: !allowUnauthorized, rejectUnauthorized: !allowUnauthorized,
}) })
const call = promisify(client.methodCall, client) const call = promisify(client.methodCall, client)
return (method, args) => call(method, args).then( return (method, args) => call(method, args).then(parseResult, logError)
parseResult,
logError
)
} }

View File

@ -8,7 +8,8 @@ let getObject
const authorized = () => true // eslint-disable-line no-unused-vars const authorized = () => true // eslint-disable-line no-unused-vars
const forbiddden = () => false // eslint-disable-line no-unused-vars const forbiddden = () => false // eslint-disable-line no-unused-vars
const and = (...checkers) => (object, permission) => { // eslint-disable-line no-unused-vars // eslint-disable-next-line no-unused-vars
const and = (...checkers) => (object, permission) => {
for (const checker of checkers) { for (const checker of checkers) {
if (!checker(object, permission)) { if (!checker(object, permission)) {
return false return false
@ -17,7 +18,8 @@ const and = (...checkers) => (object, permission) => { // eslint-disable-line no
return true return true
} }
const or = (...checkers) => (object, permission) => { // eslint-disable-line no-unused-vars // eslint-disable-next-line no-unused-vars
const or = (...checkers) => (object, permission) => {
for (const checker of checkers) { for (const checker of checkers) {
if (checker(object, permission)) { if (checker(object, permission)) {
return true return true
@ -28,7 +30,7 @@ const or = (...checkers) => (object, permission) => { // eslint-disable-line no-
// ------------------------------------------------------------------- // -------------------------------------------------------------------
const checkMember = (memberName) => (object, permission) => { const checkMember = memberName => (object, permission) => {
const member = object[memberName] const member = object[memberName]
return member !== object.id && checkAuthorization(member, permission) return member !== object.id && checkAuthorization(member, permission)
} }
@ -36,10 +38,7 @@ const checkMember = (memberName) => (object, permission) => {
const checkSelf = ({ id }, permission) => { const checkSelf = ({ id }, permission) => {
const permissionsForObject = permissionsByObject[id] const permissionsForObject = permissionsByObject[id]
return ( return permissionsForObject && permissionsForObject[permission]
permissionsForObject &&
permissionsForObject[permission]
)
} }
// =================================================================== // ===================================================================
@ -102,12 +101,7 @@ function checkAuthorization (objectId, permission) {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export default ( export default (permissionsByObject_, getObject_, permissions, permission) => {
permissionsByObject_,
getObject_,
permissions,
permission
) => {
// Assign global variables. // Assign global variables.
permissionsByObject = permissionsByObject_ permissionsByObject = permissionsByObject_
getObject = getObject_ getObject = getObject_

View File

@ -5,7 +5,7 @@ const __TEST__ = NODE_ENV === 'test'
module.exports = { module.exports = {
comments: !__PROD__, comments: !__PROD__,
compact: __PROD__, compact: __PROD__,
ignore: __TEST__ ? undefined : [ /\.spec\.js$/ ], ignore: __TEST__ ? undefined : [/\.spec\.js$/],
plugins: ['lodash'], plugins: ['lodash'],
presets: [ presets: [
[ [
@ -14,9 +14,7 @@ module.exports = {
debug: !__TEST__, debug: !__TEST__,
loose: true, loose: true,
shippedProposals: true, shippedProposals: true,
targets: __PROD__ targets: __PROD__ ? { node: '6' } : { node: 'current' },
? { node: '6' }
: { node: 'current' },
useBuiltIns: 'usage', useBuiltIns: 'usage',
}, },
], ],

View File

@ -19,11 +19,13 @@ const configFile = configPath + '/config.json'
// =================================================================== // ===================================================================
const load = exports.load = function () { const load = (exports.load = function () {
return readFile(configFile).then(JSON.parse).catch(function () { return readFile(configFile)
return {} .then(JSON.parse)
}) .catch(function () {
} return {}
})
})
exports.get = function (path) { exports.get = function (path) {
return load().then(function (config) { return load().then(function (config) {
@ -31,11 +33,11 @@ exports.get = function (path) {
}) })
} }
const save = exports.save = function (config) { const save = (exports.save = function (config) {
return mkdirp(configPath).then(function () { return mkdirp(configPath).then(function () {
return writeFile(configFile, JSON.stringify(config)) return writeFile(configFile, JSON.stringify(config))
}) })
} })
exports.set = function (data) { exports.set = function (data) {
return load().then(function (config) { return load().then(function (config) {

View File

@ -108,14 +108,16 @@ const humanFormatOpts = {
function printProgress (progress) { function printProgress (progress) {
if (progress.length) { if (progress.length) {
console.warn('%s% of %s @ %s/s - ETA %s', console.warn(
'%s% of %s @ %s/s - ETA %s',
Math.round(progress.percentage), Math.round(progress.percentage),
humanFormat(progress.length, humanFormatOpts), humanFormat(progress.length, humanFormatOpts),
humanFormat(progress.speed, humanFormatOpts), humanFormat(progress.speed, humanFormatOpts),
prettyMs(progress.eta * 1e3) prettyMs(progress.eta * 1e3)
) )
} else { } else {
console.warn('%s @ %s/s', console.warn(
'%s @ %s/s',
humanFormat(progress.transferred, humanFormatOpts), humanFormat(progress.transferred, humanFormatOpts),
humanFormat(progress.speed, humanFormatOpts) humanFormat(progress.speed, humanFormatOpts)
) )
@ -130,8 +132,10 @@ function wrap (val) {
// =================================================================== // ===================================================================
const help = wrap((function (pkg) { const help = wrap(
return require('strip-indent')(` (function (pkg) {
return require('strip-indent')(
`
Usage: Usage:
$name --register [--expiresIn duration] <XO-Server URL> <username> [<password>] $name --register [--expiresIn duration] <XO-Server URL> <username> [<password>]
@ -162,18 +166,20 @@ const help = wrap((function (pkg) {
Executes a command on the current XO instance. Executes a command on the current XO instance.
$name v$version $name v$version
`).replace(/<([^>]+)>|\$(\w+)/g, function (_, arg, key) { `
if (arg) { ).replace(/<([^>]+)>|\$(\w+)/g, function (_, arg, key) {
return '<' + chalk.yellow(arg) + '>' if (arg) {
} return '<' + chalk.yellow(arg) + '>'
}
if (key === 'name') { if (key === 'name') {
return chalk.bold(pkg[key]) return chalk.bold(pkg[key])
} }
return pkg[key] return pkg[key]
}) })
})(require('../package'))) })(require('../package'))
)
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@ -230,10 +236,7 @@ async function register (args) {
exports.register = register exports.register = register
function unregister () { function unregister () {
return config.unset([ return config.unset(['server', 'token'])
'server',
'token',
])
} }
exports.unregister = unregister exports.unregister = unregister
@ -284,11 +287,7 @@ async function listCommands (args) {
str.push( str.push(
name, name,
'=<', '=<',
type == null type == null ? 'unknown type' : isArray(type) ? type.join('|') : type,
? 'unknown type'
: isArray(type)
? type.join('|')
: type,
'>' '>'
) )
@ -347,10 +346,7 @@ async function call (args) {
const result = await xo.call(method, params) const result = await xo.call(method, params)
let keys, key, url let keys, key, url
if ( if (isObject(result) && (keys = getKeys(result)).length === 1) {
isObject(result) &&
(keys = getKeys(result)).length === 1
) {
key = keys[0] key = keys[0]
if (key === '$getFrom') { if (key === '$getFrom') {
@ -359,16 +355,19 @@ async function call (args) {
const progress = progressStream({ time: 1e3 }, printProgress) const progress = progressStream({ time: 1e3 }, printProgress)
return eventToPromise(nicePipe([ return eventToPromise(
got.stream(url).on('response', function (response) { nicePipe([
const length = response.headers['content-length'] got.stream(url).on('response', function (response) {
if (length !== undefined) { const length = response.headers['content-length']
progress.length(length) if (length !== undefined) {
} progress.length(length)
}), }
progress, }),
output, progress,
]), 'finish') output,
]),
'finish'
)
} }
if (key === '$sendTo') { if (key === '$sendTo') {
@ -379,10 +378,13 @@ async function call (args) {
const input = nicePipe([ const input = nicePipe([
createReadStream(file), createReadStream(file),
progressStream({ progressStream(
length: length, {
time: 1e3, length: length,
}, printProgress), time: 1e3,
},
printProgress
),
]) ])
const response = await got.post(url, { const response = await got.post(url, {

View File

@ -1,17 +1,14 @@
import kindOf from 'kindof' import kindOf from 'kindof'
import {BaseError} from 'make-error' import { BaseError } from 'make-error'
import {EventEmitter} from 'events' import { EventEmitter } from 'events'
import {forEach} from 'lodash' import { forEach } from 'lodash'
import isEmpty from './is-empty' import isEmpty from './is-empty'
import isObject from './is-object' import isObject from './is-object'
// =================================================================== // ===================================================================
const { const { create: createObject, prototype: { hasOwnProperty } } = Object
create: createObject,
prototype: { hasOwnProperty },
} = Object
export const ACTION_ADD = 'add' export const ACTION_ADD = 'add'
export const ACTION_UPDATE = 'update' export const ACTION_UPDATE = 'update'
@ -189,7 +186,7 @@ export default class Collection extends EventEmitter {
// ----------------------------------------------------------------- // -----------------------------------------------------------------
createIndex (name, index) { createIndex (name, index) {
const {_indexes: indexes} = this const { _indexes: indexes } = this
if (hasOwnProperty.call(indexes, name)) { if (hasOwnProperty.call(indexes, name)) {
throw new DuplicateIndex(name) throw new DuplicateIndex(name)
} }
@ -201,7 +198,7 @@ export default class Collection extends EventEmitter {
} }
deleteIndex (name) { deleteIndex (name) {
const {_indexes: indexes} = this const { _indexes: indexes } = this
if (!hasOwnProperty.call(indexes, name)) { if (!hasOwnProperty.call(indexes, name)) {
throw new NoSuchIndex(name) throw new NoSuchIndex(name)
} }
@ -218,7 +215,7 @@ export default class Collection extends EventEmitter {
// ----------------------------------------------------------------- // -----------------------------------------------------------------
* [Symbol.iterator] () { * [Symbol.iterator] () {
const {_items: items} = this const { _items: items } = this
for (const key in items) { for (const key in items) {
yield [key, items[key]] yield [key, items[key]]
@ -226,7 +223,7 @@ export default class Collection extends EventEmitter {
} }
* keys () { * keys () {
const {_items: items} = this const { _items: items } = this
for (const key in items) { for (const key in items) {
yield key yield key
@ -234,7 +231,7 @@ export default class Collection extends EventEmitter {
} }
* values () { * values () {
const {_items: items} = this const { _items: items } = this
for (const key in items) { for (const key in items) {
yield items[key] yield items[key]
@ -259,7 +256,7 @@ export default class Collection extends EventEmitter {
return return
} }
const {_buffer: buffer} = this const { _buffer: buffer } = this
// Due to deduplication there could be nothing in the buffer. // Due to deduplication there could be nothing in the buffer.
if (isEmpty(buffer)) { if (isEmpty(buffer)) {
@ -354,7 +351,8 @@ export default class Collection extends EventEmitter {
} else { } else {
this._buffer[key] = ACTION_REMOVE this._buffer[key] = ACTION_REMOVE
} }
} else { // update } else {
// update
if (!this._buffer[key]) { if (!this._buffer[key]) {
this._buffer[key] = ACTION_UPDATE this._buffer[key] = ACTION_UPDATE
} }

View File

@ -3,15 +3,15 @@
import eventToPromise from 'event-to-promise' import eventToPromise from 'event-to-promise'
import { forEach } from 'lodash' import { forEach } from 'lodash'
import Collection, {DuplicateItem, NoSuchItem} from './collection' import Collection, { DuplicateItem, NoSuchItem } from './collection'
// =================================================================== // ===================================================================
function waitTicks (n = 2) { function waitTicks (n = 2) {
const {nextTick} = process const { nextTick } = process
return new Promise(function (resolve) { return new Promise(function (resolve) {
(function waitNextTick () { ;(function waitNextTick () {
// The first tick is handled by Promise#then() // The first tick is handled by Promise#then()
if (--n) { if (--n) {
nextTick(waitNextTick) nextTick(waitNextTick)
@ -34,16 +34,16 @@ describe('Collection', function () {
it('is iterable', function () { it('is iterable', function () {
const iterator = col[Symbol.iterator]() const iterator = col[Symbol.iterator]()
expect(iterator.next()).toEqual({done: false, value: ['bar', 0]}) expect(iterator.next()).toEqual({ done: false, value: ['bar', 0] })
expect(iterator.next()).toEqual({done: true, value: undefined}) expect(iterator.next()).toEqual({ done: true, value: undefined })
}) })
describe('#keys()', function () { describe('#keys()', function () {
it('returns an iterator over the keys', function () { it('returns an iterator over the keys', function () {
const iterator = col.keys() const iterator = col.keys()
expect(iterator.next()).toEqual({done: false, value: 'bar'}) expect(iterator.next()).toEqual({ done: false, value: 'bar' })
expect(iterator.next()).toEqual({done: true, value: undefined}) expect(iterator.next()).toEqual({ done: true, value: undefined })
}) })
}) })
@ -51,8 +51,8 @@ describe('Collection', function () {
it('returns an iterator over the values', function () { it('returns an iterator over the values', function () {
const iterator = col.values() const iterator = col.values()
expect(iterator.next()).toEqual({done: false, value: 0}) expect(iterator.next()).toEqual({ done: false, value: 0 })
expect(iterator.next()).toEqual({done: true, value: undefined}) expect(iterator.next()).toEqual({ done: true, value: undefined })
}) })
}) })
@ -70,7 +70,7 @@ describe('Collection', function () {
// Async event. // Async event.
return eventToPromise(col, 'add').then(function (added) { return eventToPromise(col, 'add').then(function (added) {
expect(Object.keys(added)).toEqual([ 'foo' ]) expect(Object.keys(added)).toEqual(['foo'])
expect(added.foo).toBe(true) expect(added.foo).toBe(true)
}) })
}) })
@ -216,7 +216,7 @@ describe('Collection', function () {
}) })
it('accepts an object with an id property', function () { it('accepts an object with an id property', function () {
col.unset({id: 'bar'}) col.unset({ id: 'bar' })
expect(col.has('bar')).toBe(false) expect(col.has('bar')).toBe(false)
@ -235,7 +235,7 @@ describe('Collection', function () {
return waitTicks().then(() => { return waitTicks().then(() => {
col.touch(foo) col.touch(foo)
return eventToPromise(col, 'update', (items) => { return eventToPromise(col, 'update', items => {
expect(Object.keys(items)).toEqual(['foo']) expect(Object.keys(items)).toEqual(['foo'])
expect(items.foo).toBe(foo) expect(items.foo).toBe(foo)
}) })
@ -249,7 +249,7 @@ describe('Collection', function () {
expect(col.size).toBe(0) expect(col.size).toBe(0)
return eventToPromise(col, 'remove').then((items) => { return eventToPromise(col, 'remove').then(items => {
expect(Object.keys(items)).toEqual(['bar']) expect(Object.keys(items)).toEqual(['bar'])
expect(items.bar).toBeUndefined() expect(items.bar).toBeUndefined()
}) })
@ -257,84 +257,69 @@ describe('Collection', function () {
}) })
describe('deduplicates events', function () { describe('deduplicates events', function () {
forEach({ forEach(
'add & update → add': [ {
[ 'add & update → add': [
['add', 'foo', 0], [['add', 'foo', 0], ['update', 'foo', 1]],
['update', 'foo', 1], {
], add: {
{ foo: 1,
add: { },
foo: 1,
}, },
},
],
'add & remove → ∅': [
[
['add', 'foo', 0],
['remove', 'foo'],
], ],
{},
],
'update & update → update': [ 'add & remove → ∅': [[['add', 'foo', 0], ['remove', 'foo']], {}],
[
['update', 'bar', 1], 'update & update → update': [
['update', 'bar', 2], [['update', 'bar', 1], ['update', 'bar', 2]],
], {
{ update: {
update: { bar: 2,
bar: 2, },
}, },
},
],
'update & remove → remove': [
[
['update', 'bar', 1],
['remove', 'bar'],
], ],
{
remove: {
bar: undefined,
},
},
],
'remove & add → update': [ 'update & remove → remove': [
[ [['update', 'bar', 1], ['remove', 'bar']],
['remove', 'bar'], {
['add', 'bar', 0], remove: {
bar: undefined,
},
},
], ],
{
update: { 'remove & add → update': [
bar: 0, [['remove', 'bar'], ['add', 'bar', 0]],
{
update: {
bar: 0,
},
}, },
}, ],
], },
}, ([operations, results], label) => { ([operations, results], label) => {
it(label, function () { it(label, function () {
forEach(operations, ([method, ...args]) => { forEach(operations, ([method, ...args]) => {
col[method](...args) col[method](...args)
}) })
const spies = Object.create(null) const spies = Object.create(null)
forEach(['add', 'update', 'remove'], event => { forEach(['add', 'update', 'remove'], event => {
col.on(event, (spies[event] = jest.fn())) col.on(event, (spies[event] = jest.fn()))
}) })
return waitTicks().then(() => { return waitTicks().then(() => {
forEach(spies, (spy, event) => { forEach(spies, (spy, event) => {
const items = results[event] const items = results[event]
if (items) { if (items) {
expect(spy.mock.calls).toEqual([ [ items ] ]) expect(spy.mock.calls).toEqual([[items]])
} else { } else {
expect(spy).not.toHaveBeenCalled() expect(spy).not.toHaveBeenCalled()
} }
})
}) })
}) })
}) }
}) )
}) })
}) })

View File

@ -3,11 +3,7 @@ import { bind, iteratee } from 'lodash'
import clearObject from './clear-object' import clearObject from './clear-object'
import isEmpty from './is-empty' import isEmpty from './is-empty'
import NotImplemented from './not-implemented' import NotImplemented from './not-implemented'
import { import { ACTION_ADD, ACTION_UPDATE, ACTION_REMOVE } from './collection'
ACTION_ADD,
ACTION_UPDATE,
ACTION_REMOVE,
} from './collection'
// =================================================================== // ===================================================================
@ -34,7 +30,7 @@ export default class Index {
// Remove empty items lists. // Remove empty items lists.
sweep () { sweep () {
const {_itemsByHash: itemsByHash} = this const { _itemsByHash: itemsByHash } = this
for (const hash in itemsByHash) { for (const hash in itemsByHash) {
if (isEmpty(itemsByHash[hash])) { if (isEmpty(itemsByHash[hash])) {
delete itemsByHash[hash] delete itemsByHash[hash]
@ -86,14 +82,11 @@ export default class Index {
const hash = computeHash(value, key) const hash = computeHash(value, key)
if (hash != null) { if (hash != null) {
( ;(itemsByHash[hash] ||
itemsByHash[hash] ||
// FIXME: We do not use objects without prototype for now // FIXME: We do not use objects without prototype for now
// because it breaks Angular in xo-web, change it back when // because it breaks Angular in xo-web, change it back when
// this is fixed. // this is fixed.
(itemsByHash[hash] = {}) (itemsByHash[hash] = {}))[key] = value
)[key] = value
keysToHash[key] = hash keysToHash[key] = hash
} }
@ -118,12 +111,9 @@ export default class Index {
// Inserts item into the new hash's list if any. // Inserts item into the new hash's list if any.
if (hash != null) { if (hash != null) {
( ;(itemsByHash[hash] ||
itemsByHash[hash] ||
// FIXME: idem: change back to Object.create(null) // FIXME: idem: change back to Object.create(null)
(itemsByHash[hash] = {}) (itemsByHash[hash] = {}))[key] = value
)[key] = value
keysToHash[key] = hash keysToHash[key] = hash
} else { } else {
@ -133,10 +123,7 @@ export default class Index {
} }
_onRemove (items) { _onRemove (items) {
const { const { _itemsByHash: itemsByHash, _keysToHash: keysToHash } = this
_itemsByHash: itemsByHash,
_keysToHash: keysToHash,
} = this
for (const key in items) { for (const key in items) {
const prev = keysToHash[key] const prev = keysToHash[key]

View File

@ -9,10 +9,10 @@ import Index from './index'
// =================================================================== // ===================================================================
const waitTicks = (n = 2) => { const waitTicks = (n = 2) => {
const {nextTick} = process const { nextTick } = process
return new Promise(resolve => { return new Promise(resolve => {
(function waitNextTick () { ;(function waitNextTick () {
// The first tick is handled by Promise#then() // The first tick is handled by Promise#then()
if (--n) { if (--n) {
nextTick(waitNextTick) nextTick(waitNextTick)

View File

@ -1,3 +1,3 @@
export default function isObject (value) { export default function isObject (value) {
return (value !== null) && (typeof value === 'object') return value !== null && typeof value === 'object'
} }

View File

@ -1,4 +1,4 @@
import {BaseError} from 'make-error' import { BaseError } from 'make-error'
export default class NotImplemented extends BaseError { export default class NotImplemented extends BaseError {
constructor (message) { constructor (message) {

View File

@ -2,11 +2,7 @@ import { bind, iteratee } from 'lodash'
import clearObject from './clear-object' import clearObject from './clear-object'
import NotImplemented from './not-implemented' import NotImplemented from './not-implemented'
import { import { ACTION_ADD, ACTION_UPDATE, ACTION_REMOVE } from './collection'
ACTION_ADD,
ACTION_UPDATE,
ACTION_REMOVE,
} from './collection'
// =================================================================== // ===================================================================
@ -108,10 +104,7 @@ export default class UniqueIndex {
} }
_onRemove (items) { _onRemove (items) {
const { const { _itemByHash: itemByHash, _keysToHash: keysToHash } = this
_itemByHash: itemByHash,
_keysToHash: keysToHash,
} = this
for (const key in items) { for (const key in items) {
const prev = keysToHash[key] const prev = keysToHash[key]

View File

@ -9,10 +9,10 @@ import Index from './unique-index'
// =================================================================== // ===================================================================
const waitTicks = (n = 2) => { const waitTicks = (n = 2) => {
const {nextTick} = process const { nextTick } = process
return new Promise(resolve => { return new Promise(resolve => {
(function waitNextTick () { ;(function waitNextTick () {
// The first tick is handled by Promise#then() // The first tick is handled by Promise#then()
if (--n) { if (--n) {
nextTick(waitNextTick) nextTick(waitNextTick)

View File

@ -7,7 +7,7 @@ import View from './view'
// Create the collection. // Create the collection.
const users = new Collection() const users = new Collection()
users.getKey = (user) => user.name users.getKey = user => user.name
// Inserts some data. // Inserts some data.
users.add({ users.add({

View File

@ -54,7 +54,7 @@ export default class View extends Collection {
} }
_onAdd (items) { _onAdd (items) {
const {_predicate: predicate} = this const { _predicate: predicate } = this
forEach(items, (value, key) => { forEach(items, (value, key) => {
if (predicate(value, key, this)) { if (predicate(value, key, this)) {
@ -67,7 +67,7 @@ export default class View extends Collection {
} }
_onUpdate (items) { _onUpdate (items) {
const {_predicate: predicate} = this const { _predicate: predicate } = this
forEach(items, (value, key) => { forEach(items, (value, key) => {
if (predicate(value, key, this)) { if (predicate(value, key, this)) {

View File

@ -10,36 +10,53 @@ const xo = new Xo({
url: 'localhost:9000', url: 'localhost:9000',
}) })
xo.open().then(function () { xo
return xo.call('acl.get', {}).then(function (result) { .open()
console.log('success:', result) .then(function () {
}).catch(function (error) { return xo
console.log('failure:', error) .call('acl.get', {})
.then(function (result) {
console.log('success:', result)
})
.catch(function (error) {
console.log('failure:', error)
})
}) })
}).then(function () { .then(function () {
return xo.signIn({ return xo
email: 'admin@admin.net', .signIn({
password: 'admin', email: 'admin@admin.net',
}).then(function () { password: 'admin',
console.log('connected as ', xo.user) })
}).catch(function (error) { .then(function () {
console.log('failure:', error) console.log('connected as ', xo.user)
})
.catch(function (error) {
console.log('failure:', error)
})
}) })
}).then(function () { .then(function () {
return xo.signIn({ return xo
email: 'tom', .signIn({
password: 'tom', email: 'tom',
}).then(function () { password: 'tom',
console.log('connected as', xo.user) })
.then(function () {
console.log('connected as', xo.user)
return xo.call('acl.get', {}).then(function (result) { return xo
console.log('success:', result) .call('acl.get', {})
}).catch(function (error) { .then(function (result) {
console.log('failure:', error) console.log('success:', result)
}) })
}).catch(function (error) { .catch(function (error) {
console.log('failure', error) console.log('failure:', error)
})
})
.catch(function (error) {
console.log('failure', error)
})
})
.then(function () {
return xo.close()
}) })
}).then(function () {
return xo.close()
})

View File

@ -1,7 +1,4 @@
import JsonRpcWebSocketClient, { import JsonRpcWebSocketClient, { OPEN, CLOSED } from 'jsonrpc-websocket-client'
OPEN,
CLOSED,
} from 'jsonrpc-websocket-client'
import { BaseError } from 'make-error' import { BaseError } from 'make-error'
import { startsWith } from 'lodash' import { startsWith } from 'lodash'
@ -20,7 +17,7 @@ export default class Xo extends JsonRpcWebSocketClient {
const url = opts != null ? opts.url : '.' const url = opts != null ? opts.url : '.'
super(`${url === '/' ? '' : url}/api/`) super(`${url === '/' ? '' : url}/api/`)
this._credentials = (opts != null ? opts.credentials : null) this._credentials = opts != null ? opts.credentials : null
this._user = null this._user = null
this.on(OPEN, () => { this.on(OPEN, () => {
@ -45,12 +42,13 @@ export default class Xo extends JsonRpcWebSocketClient {
} }
const promise = super.call(method, args) const promise = super.call(method, args)
promise.retry = (predicate) => promise.catch((error) => { promise.retry = predicate =>
i = (i || 0) + 1 promise.catch(error => {
if (predicate(error, i)) { i = (i || 0) + 1
return this.call(method, args, i) if (predicate(error, i)) {
} return this.call(method, args, i)
}) }
})
return promise return promise
} }

View File

@ -3,10 +3,13 @@ import map from 'lodash/map'
import trim from 'lodash/trim' import trim from 'lodash/trim'
import trimStart from 'lodash/trimStart' import trimStart from 'lodash/trimStart'
const sanitizePath = (...paths) => filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/') const sanitizePath = (...paths) =>
filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join(
'/'
)
export const parse = string => { export const parse = string => {
const object = { } const object = {}
const [type, rest] = string.split('://') const [type, rest] = string.split('://')
if (type === 'file') { if (type === 'file') {
@ -36,7 +39,7 @@ export const parse = string => {
return object return object
} }
export const format = ({type, host, path, username, password, domain}) => { export const format = ({ type, host, path, username, password, domain }) => {
type === 'local' && (type = 'file') type === 'local' && (type = 'file')
let string = `${type}://` let string = `${type}://`
if (type === 'nfs') { if (type === 'nfs') {

View File

@ -1,4 +1,4 @@
import {Strategy} from 'passport-github' import { Strategy } from 'passport-github'
// =================================================================== // ===================================================================
@ -27,18 +27,23 @@ class AuthGitHubXoPlugin {
} }
load () { load () {
const {_xo: xo} = this const { _xo: xo } = this
xo.registerPassportStrategy(new Strategy(this._conf, async (accessToken, refreshToken, profile, done) => { xo.registerPassportStrategy(
try { new Strategy(
done(null, await xo.registerUser('github', profile.username)) this._conf,
} catch (error) { async (accessToken, refreshToken, profile, done) => {
done(error.message) try {
} done(null, await xo.registerUser('github', profile.username))
})) } catch (error) {
done(error.message)
}
}
)
)
} }
} }
// =================================================================== // ===================================================================
export default ({xo}) => new AuthGitHubXoPlugin(xo) export default ({ xo }) => new AuthGitHubXoPlugin(xo)

View File

@ -7,7 +7,8 @@ export const configurationSchema = {
properties: { properties: {
callbackURL: { callbackURL: {
type: 'string', type: 'string',
description: 'Must be exactly the same as specified on the Google developer console.', description:
'Must be exactly the same as specified on the Google developer console.',
}, },
clientID: { clientID: {
type: 'string', type: 'string',
@ -18,8 +19,8 @@ export const configurationSchema = {
scope: { scope: {
default: 'https://www.googleapis.com/auth/plus.login', default: 'https://www.googleapis.com/auth/plus.login',
description: 'Note that changing this value will break existing users.', description: 'Note that changing this value will break existing users.',
enum: [ 'https://www.googleapis.com/auth/plus.login', 'email' ], enum: ['https://www.googleapis.com/auth/plus.login', 'email'],
enumNames: [ 'Google+ name', 'Simple email address' ], enumNames: ['Google+ name', 'Simple email address'],
}, },
}, },
required: ['callbackURL', 'clientID', 'clientSecret'], required: ['callbackURL', 'clientID', 'clientSecret'],
@ -41,18 +42,23 @@ class AuthGoogleXoPlugin {
const conf = this._conf const conf = this._conf
const xo = this._xo const xo = this._xo
xo.registerPassportStrategy(new Strategy(conf, async (accessToken, refreshToken, profile, done) => { xo.registerPassportStrategy(
try { new Strategy(conf, async (accessToken, refreshToken, profile, done) => {
done(null, await xo.registerUser( try {
'google', done(
conf.scope === 'email' null,
? profile.emails[0].value await xo.registerUser(
: profile.displayName 'google',
)) conf.scope === 'email'
} catch (error) { ? profile.emails[0].value
done(error.message) : profile.displayName
} )
})) )
} catch (error) {
done(error.message)
}
})
)
} }
} }

View File

@ -10,15 +10,16 @@ import { readFile } from 'fs'
// =================================================================== // ===================================================================
const VAR_RE = /\{\{([^}]+)\}\}/g const VAR_RE = /\{\{([^}]+)\}\}/g
const evalFilter = (filter, vars) => filter.replace(VAR_RE, (_, name) => { const evalFilter = (filter, vars) =>
const value = vars[name] filter.replace(VAR_RE, (_, name) => {
const value = vars[name]
if (value === undefined) { if (value === undefined) {
throw new Error('invalid variable: ' + name) throw new Error('invalid variable: ' + name)
} }
return escape(value) return escape(value)
}) })
export const configurationSchema = { export const configurationSchema = {
type: 'object', type: 'object',
@ -39,7 +40,8 @@ If not specified, it will use a default set of well-known CAs.
}, },
}, },
checkCertificate: { checkCertificate: {
description: 'Enforce the validity of the server\'s certificates. You can disable it when connecting to servers that use a self-signed certificate.', description:
"Enforce the validity of the server's certificates. You can disable it when connecting to servers that use a self-signed certificate.",
type: 'boolean', type: 'boolean',
default: true, default: true,
}, },
@ -58,14 +60,16 @@ For Microsoft Active Directory, it can also be \`<user>@<domain>\`.
type: 'string', type: 'string',
}, },
password: { password: {
description: 'Password of the user permitted of search the LDAP directory.', description:
'Password of the user permitted of search the LDAP directory.',
type: 'string', type: 'string',
}, },
}, },
required: ['dn', 'password'], required: ['dn', 'password'],
}, },
base: { base: {
description: 'The base is the part of the description tree where the users are looked for.', description:
'The base is the part of the description tree where the users are looked for.',
type: 'string', type: 'string',
}, },
filter: { filter: {
@ -116,25 +120,21 @@ class AuthLdap {
} }
async configure (conf) { async configure (conf) {
const clientOpts = this._clientOpts = { const clientOpts = (this._clientOpts = {
url: conf.uri, url: conf.uri,
maxConnections: 5, maxConnections: 5,
tlsOptions: {}, tlsOptions: {},
} })
{ {
const { const { bind, checkCertificate = true, certificateAuthorities } = conf
bind,
checkCertificate = true,
certificateAuthorities,
} = conf
if (bind) { if (bind) {
clientOpts.bindDN = bind.dn clientOpts.bindDN = bind.dn
clientOpts.bindCredentials = bind.password clientOpts.bindCredentials = bind.password
} }
const {tlsOptions} = clientOpts const { tlsOptions } = clientOpts
tlsOptions.rejectUnauthorized = checkCertificate tlsOptions.rejectUnauthorized = checkCertificate
if (certificateAuthorities) { if (certificateAuthorities) {
@ -192,7 +192,7 @@ class AuthLdap {
// Bind if necessary. // Bind if necessary.
{ {
const {_credentials: credentials} = this const { _credentials: credentials } = this
if (credentials) { if (credentials) {
logger(`attempting to bind with as ${credentials.dn}...`) logger(`attempting to bind with as ${credentials.dn}...`)
await bind(credentials.dn, credentials.password) await bind(credentials.dn, credentials.password)
@ -216,7 +216,7 @@ class AuthLdap {
entries.push(entry.json) entries.push(entry.json)
}) })
const {status} = await eventToPromise(response, 'end') const { status } = await eventToPromise(response, 'end')
if (status) { if (status) {
throw new Error('unexpected search response status: ' + status) throw new Error('unexpected search response status: ' + status)
} }
@ -229,7 +229,11 @@ class AuthLdap {
try { try {
logger(`attempting to bind as ${entry.objectName}`) logger(`attempting to bind as ${entry.objectName}`)
await bind(entry.objectName, password) await bind(entry.objectName, password)
logger(`successfully bound as ${entry.objectName} => ${username} authenticated`) logger(
`successfully bound as ${
entry.objectName
} => ${username} authenticated`
)
return { username } return { username }
} catch (error) { } catch (error) {
logger(`failed to bind as ${entry.objectName}: ${error.message}`) logger(`failed to bind as ${entry.objectName}: ${error.message}`)
@ -246,4 +250,4 @@ class AuthLdap {
// =================================================================== // ===================================================================
export default ({xo}) => new AuthLdap(xo) export default ({ xo }) => new AuthLdap(xo)

View File

@ -8,48 +8,61 @@ const EMPTY_OBJECT = Object.freeze({ __proto__: null })
const _extractValue = ({ value }) => value const _extractValue = ({ value }) => value
export const confirm = (message, { export const confirm = (
default: defaultValue = null,
} = EMPTY_OBJECT) => prompt({
default: defaultValue,
message, message,
name: 'value', { default: defaultValue = null } = EMPTY_OBJECT
type: 'confirm', ) =>
}).then(_extractValue) prompt({
default: defaultValue,
message,
name: 'value',
type: 'confirm',
}).then(_extractValue)
export const input = (message, { export const input = (
default: defaultValue = null,
filter = undefined,
validate = undefined,
} = EMPTY_OBJECT) => prompt({
default: defaultValue,
message, message,
name: 'value', {
type: 'input', default: defaultValue = null,
validate, filter = undefined,
}).then(_extractValue) validate = undefined,
} = EMPTY_OBJECT
) =>
prompt({
default: defaultValue,
message,
name: 'value',
type: 'input',
validate,
}).then(_extractValue)
export const list = (message, choices, { export const list = (
default: defaultValue = null, message,
} = EMPTY_OBJECT) => prompt({
default: defaultValue,
choices, choices,
message, { default: defaultValue = null } = EMPTY_OBJECT
name: 'value', ) =>
type: 'list', prompt({
}).then(_extractValue) default: defaultValue,
choices,
message,
name: 'value',
type: 'list',
}).then(_extractValue)
export const password = (message, { export const password = (
default: defaultValue = null,
filter = undefined,
validate = undefined,
} = EMPTY_OBJECT) => prompt({
default: defaultValue,
message, message,
name: 'value', {
type: 'password', default: defaultValue = null,
validate, filter = undefined,
}).then(_extractValue) validate = undefined,
} = EMPTY_OBJECT
) =>
prompt({
default: defaultValue,
message,
name: 'value',
type: 'password',
validate,
}).then(_extractValue)
// =================================================================== // ===================================================================
@ -69,25 +82,25 @@ const promptByType = {
items[i] = await promptGeneric( items[i] = await promptGeneric(
itemSchema, itemSchema,
defaultValue[i], defaultValue[i],
path path ? `${path} [${i}]` : `[${i}]`
? `${path} [${i}]`
: `[${i}]`
) )
++i ++i
} }
let n = schema.minItems || 0 let n = schema.minItems || 0
while (i < n) { // eslint-disable-line no-unmodified-loop-condition // eslint-disable-next-line no-unmodified-loop-condition
while (i < n) {
await promptItem() await promptItem()
} }
n = schema.maxItems || Infinity n = schema.maxItems || Infinity
while ( while (
i < n && // eslint-disable-line no-unmodified-loop-condition // eslint-disable-next-line no-unmodified-loop-condition
await confirm('additional item?', { i < n &&
(await confirm('additional item?', {
default: false, default: false,
}) }))
) { ) {
await promptItem() await promptItem()
} }
@ -95,33 +108,38 @@ const promptByType = {
return items return items
}, },
boolean: (schema, defaultValue, path) => confirm(path, { boolean: (schema, defaultValue, path) =>
default: defaultValue != null ? defaultValue : schema.default, confirm(path, {
}), default: defaultValue != null ? defaultValue : schema.default,
}),
enum: (schema, defaultValue, path) => list(path, schema.enum, { enum: (schema, defaultValue, path) =>
defaultValue: defaultValue || schema.defaultValue, list(path, schema.enum, {
}), defaultValue: defaultValue || schema.defaultValue,
}),
integer: (schema, defaultValue, path) => input(path, { integer: (schema, defaultValue, path) =>
default: defaultValue || schema.default, input(path, {
filter: input => +input, default: defaultValue || schema.default,
validate: input => isInteger(+input), filter: input => +input,
}), validate: input => isInteger(+input),
}),
number: (schema, defaultValue, path) => input(path, { number: (schema, defaultValue, path) =>
default: defaultValue || schema.default, input(path, {
filter: input => +input, default: defaultValue || schema.default,
validate: input => isFinite(+input), filter: input => +input,
}), validate: input => isFinite(+input),
}),
object: async (schema, defaultValue, path) => { object: async (schema, defaultValue, path) => {
const value = {} const value = {}
const required = {} const required = {}
schema.required && forEach(schema.required, name => { schema.required &&
required[name] = true forEach(schema.required, name => {
}) required[name] = true
})
const promptProperty = async (schema, name) => { const promptProperty = async (schema, name) => {
const subpath = path const subpath = path
@ -130,9 +148,9 @@ const promptByType = {
if ( if (
required[name] || required[name] ||
await confirm(`fill optional ${subpath}?`, { (await confirm(`fill optional ${subpath}?`, {
default: Boolean(defaultValue && name in defaultValue), default: Boolean(defaultValue && name in defaultValue),
}) }))
) { ) {
value[name] = await promptGeneric( value[name] = await promptGeneric(
schema, schema,
@ -147,15 +165,14 @@ const promptByType = {
return value return value
}, },
string: (schema, defaultValue, path) => input(path, { string: (schema, defaultValue, path) =>
default: defaultValue || schema.default, input(path, {
}), default: defaultValue || schema.default,
}),
} }
export default function promptGeneric (schema, defaultValue, path) { export default function promptGeneric (schema, defaultValue, path) {
const type = schema.enum const type = schema.enum ? 'enum' : schema.type
? 'enum'
: schema.type
const prompt = promptByType[type.toLowerCase()] const prompt = promptByType[type.toLowerCase()]
if (!prompt) { if (!prompt) {

View File

@ -5,13 +5,8 @@ import { bind } from 'lodash'
import { fromCallback } from 'promise-toolbox' import { fromCallback } from 'promise-toolbox'
import { readFile, writeFile } from 'fs' import { readFile, writeFile } from 'fs'
import promptSchema, { import promptSchema, { input, password } from './prompt-schema'
input, import createPlugin, { configurationSchema } from './'
password,
} from './prompt-schema'
import createPlugin, {
configurationSchema,
} from './'
// =================================================================== // ===================================================================
@ -27,7 +22,9 @@ execPromise(async args => {
() => ({}) () => ({})
) )
) )
await fromCallback(cb => writeFile(CACHE_FILE, JSON.stringify(config, null, 2), cb)).then( await fromCallback(cb =>
writeFile(CACHE_FILE, JSON.stringify(config, null, 2), cb)
).then(
() => { () => {
console.log('configuration saved in %s', CACHE_FILE) console.log('configuration saved in %s', CACHE_FILE)
}, },
@ -40,10 +37,13 @@ execPromise(async args => {
const plugin = createPlugin({}) const plugin = createPlugin({})
await plugin.configure(config) await plugin.configure(config)
await plugin._authenticate({ await plugin._authenticate(
username: await input('Username', { {
validate: input => !!input.length, username: await input('Username', {
}), validate: input => !!input.length,
password: await password('Password'), }),
}, bind(console.log, console)) password: await password('Password'),
},
bind(console.log, console)
)
}) })

View File

@ -1,4 +1,4 @@
import {Strategy} from 'passport-saml' import { Strategy } from 'passport-saml'
// =================================================================== // ===================================================================
@ -38,19 +38,21 @@ class AuthSamlXoPlugin {
load () { load () {
const xo = this._xo const xo = this._xo
xo.registerPassportStrategy(new Strategy(this._conf, async (profile, done) => { xo.registerPassportStrategy(
const name = profile[this._usernameField] new Strategy(this._conf, async (profile, done) => {
if (!name) { const name = profile[this._usernameField]
done('no name found for this user') if (!name) {
return done('no name found for this user')
} return
}
try { try {
done(null, await xo.registerUser('saml', name)) done(null, await xo.registerUser('saml', name))
} catch (error) { } catch (error) {
done(error.message) done(error.message)
} }
})) })
)
} }
} }

View File

@ -37,15 +37,17 @@ const ICON_FAILURE = '🚨'
const ICON_SUCCESS = '✔' const ICON_SUCCESS = '✔'
const DATE_FORMAT = 'dddd, MMMM Do YYYY, h:mm:ss a' const DATE_FORMAT = 'dddd, MMMM Do YYYY, h:mm:ss a'
const createDateFormater = timezone => timezone !== undefined const createDateFormater = timezone =>
? timestamp => moment(timestamp).tz(timezone).format(DATE_FORMAT) timezone !== undefined
: timestamp => moment(timestamp).format(DATE_FORMAT) ? timestamp =>
moment(timestamp)
.tz(timezone)
.format(DATE_FORMAT)
: timestamp => moment(timestamp).format(DATE_FORMAT)
const formatDuration = milliseconds => const formatDuration = milliseconds => moment.duration(milliseconds).humanize()
moment.duration(milliseconds).humanize()
const formatMethod = method => const formatMethod = method => startCase(method.slice(method.indexOf('.') + 1))
startCase(method.slice(method.indexOf('.') + 1))
const formatSize = bytes => const formatSize = bytes =>
humanFormat(bytes, { humanFormat(bytes, {
@ -83,7 +85,9 @@ class BackupReportsXoPlugin {
} }
_wrapper (status) { _wrapper (status) {
return new Promise(resolve => resolve(this._listener(status))).catch(logError) return new Promise(resolve => resolve(this._listener(status))).catch(
logError
)
} }
_listener (status) { _listener (status) {
@ -114,8 +118,7 @@ class BackupReportsXoPlugin {
} }
const reportOnFailure = const reportOnFailure =
reportWhen === 'fail' || // xo-web < 5 reportWhen === 'fail' || reportWhen === 'failure' // xo-web < 5 // xo-web >= 5
reportWhen === 'failure' // xo-web >= 5
let globalMergeSize = 0 let globalMergeSize = 0
let globalTransferSize = 0 let globalTransferSize = 0
@ -152,11 +155,7 @@ class BackupReportsXoPlugin {
const { message } = error const { message } = error
failedBackupsText.push( failedBackupsText.push(...text, `- **Error**: ${message}`, '')
...text,
`- **Error**: ${message}`,
''
)
nagiosText.push( nagiosText.push(
`[ ${vm !== undefined ? vm.name_label : 'undefined'} : ${message} ]` `[ ${vm !== undefined ? vm.name_label : 'undefined'} : ${message} ]`
@ -169,22 +168,25 @@ class BackupReportsXoPlugin {
globalTransferSize += transferSize globalTransferSize += transferSize
text.push( text.push(
`- **Transfer size**: ${formatSize(transferSize)}`, `- **Transfer size**: ${formatSize(transferSize)}`,
`- **Transfer speed**: ${formatSpeed(transferSize, returnedValue.transferDuration)}` `- **Transfer speed**: ${formatSpeed(
transferSize,
returnedValue.transferDuration
)}`
) )
} }
if (mergeSize !== undefined) { if (mergeSize !== undefined) {
globalMergeSize += mergeSize globalMergeSize += mergeSize
text.push( text.push(
`- **Merge size**: ${formatSize(mergeSize)}`, `- **Merge size**: ${formatSize(mergeSize)}`,
`- **Merge speed**: ${formatSpeed(mergeSize, returnedValue.mergeDuration)}` `- **Merge speed**: ${formatSpeed(
mergeSize,
returnedValue.mergeDuration
)}`
) )
} }
} }
successfulBackupText.push( successfulBackupText.push(...text, '')
...text,
''
)
} }
}) })
@ -208,14 +210,10 @@ class BackupReportsXoPlugin {
`- **Successes**: ${nSuccesses} / ${nCalls}`, `- **Successes**: ${nSuccesses} / ${nCalls}`,
] ]
if (globalTransferSize !== 0) { if (globalTransferSize !== 0) {
markdown.push( markdown.push(`- **Transfer size**: ${formatSize(globalTransferSize)}`)
`- **Transfer size**: ${formatSize(globalTransferSize)}`
)
} }
if (globalMergeSize !== 0) { if (globalMergeSize !== 0) {
markdown.push( markdown.push(`- **Merge size**: ${formatSize(globalMergeSize)}`)
`- **Merge size**: ${formatSize(globalMergeSize)}`
)
} }
markdown.push('') markdown.push('')
@ -239,38 +237,40 @@ class BackupReportsXoPlugin {
) )
} }
markdown.push( markdown.push('---', '', `*${pkg.name} v${pkg.version}*`)
'---',
'',
`*${pkg.name} v${pkg.version}*`
)
markdown = markdown.join('\n') markdown = markdown.join('\n')
const xo = this._xo const xo = this._xo
return Promise.all([ return Promise.all([
xo.sendEmail !== undefined && xo.sendEmail({ xo.sendEmail !== undefined &&
to: this._mailsReceivers, xo.sendEmail({
subject: `[Xen Orchestra] ${ to: this._mailsReceivers,
globalSuccess ? 'Success' : 'Failure' subject: `[Xen Orchestra] ${
} Backup report for ${tag} ${ globalSuccess ? 'Success' : 'Failure'
globalSuccess ? ICON_SUCCESS : ICON_FAILURE } Backup report for ${tag} ${
}`, globalSuccess ? ICON_SUCCESS : ICON_FAILURE
markdown, }`,
}), markdown,
xo.sendToXmppClient !== undefined && xo.sendToXmppClient({ }),
to: this._xmppReceivers, xo.sendToXmppClient !== undefined &&
message: markdown, xo.sendToXmppClient({
}), to: this._xmppReceivers,
xo.sendSlackMessage !== undefined && xo.sendSlackMessage({ message: markdown,
message: markdown, }),
}), xo.sendSlackMessage !== undefined &&
xo.sendPassiveCheck !== undefined && xo.sendPassiveCheck({ xo.sendSlackMessage({
status: globalSuccess ? 0 : 2, message: markdown,
message: globalSuccess }),
? `[Xen Orchestra] [Success] Backup report for ${tag}` xo.sendPassiveCheck !== undefined &&
: `[Xen Orchestra] [Failure] Backup report for ${tag} - VMs : ${nagiosText.join(' ')}`, xo.sendPassiveCheck({
}), status: globalSuccess ? 0 : 2,
message: globalSuccess
? `[Xen Orchestra] [Success] Backup report for ${tag}`
: `[Xen Orchestra] [Failure] Backup report for ${tag} - VMs : ${nagiosText.join(
' '
)}`,
}),
]) ])
} }
} }

View File

@ -27,7 +27,8 @@ class XoServerCloud {
getResourceCatalog.description = 'Get the list of all available resources' getResourceCatalog.description = 'Get the list of all available resources'
getResourceCatalog.permission = 'admin' getResourceCatalog.permission = 'admin'
const registerResource = ({ namespace }) => this._registerResource(namespace) const registerResource = ({ namespace }) =>
this._registerResource(namespace)
registerResource.description = 'Register a resource via cloud plugin' registerResource.description = 'Register a resource via cloud plugin'
registerResource.params = { registerResource.params = {
namespace: { namespace: {
@ -42,21 +43,22 @@ class XoServerCloud {
registerResource, registerResource,
}, },
}) })
this._unsetRequestResource = this._xo.defineProperty('requestResource', this._requestResource, this) this._unsetRequestResource = this._xo.defineProperty(
'requestResource',
this._requestResource,
this
)
const updater = this._updater = new Client(`${UPDATER_URL}:${WS_PORT}`) const updater = (this._updater = new Client(`${UPDATER_URL}:${WS_PORT}`))
const connect = () => updater.open(createBackoff()).catch( const connect = () =>
error => { updater.open(createBackoff()).catch(error => {
console.error('xo-server-cloud: fail to connect to updater', error) console.error('xo-server-cloud: fail to connect to updater', error)
return connect() return connect()
}
)
updater
.on('closed', connect)
.on('scheduledAttempt', ({ delay }) => {
console.warn('xo-server-cloud: next attempt in %s ms', delay)
}) })
updater.on('closed', connect).on('scheduledAttempt', ({ delay }) => {
console.warn('xo-server-cloud: next attempt in %s ms', delay)
})
connect() connect()
} }
@ -138,13 +140,15 @@ class XoServerCloud {
throw new Error('cannot get download token') throw new Error('cannot get download token')
} }
const req = request.get(`${UPDATER_URL}:${HTTP_PORT}/`) const req = request
.get(`${UPDATER_URL}:${HTTP_PORT}/`)
.set('Authorization', `Bearer ${downloadToken}`) .set('Authorization', `Bearer ${downloadToken}`)
// Impossible to pipe the response directly: https://github.com/visionmedia/superagent/issues/1187 // Impossible to pipe the response directly: https://github.com/visionmedia/superagent/issues/1187
const pt = new PassThrough() const pt = new PassThrough()
req.pipe(pt) req.pipe(pt)
pt.length = (await eventToPromise(req, 'response')).headers['content-length'] const { headers } = await eventToPromise(req, 'response')
pt.length = headers['content-length']
return pt return pt
} }

View File

@ -7,8 +7,9 @@ import { debug } from './utils'
export default class DensityPlan extends Plan { export default class DensityPlan extends Plan {
_checkRessourcesThresholds (objects, averages) { _checkRessourcesThresholds (objects, averages) {
return filter(objects, object => return filter(
averages[object.id].memoryFree > this._thresholds.memoryFree.low objects,
object => averages[object.id].memoryFree > this._thresholds.memoryFree.low
) )
} }
@ -19,27 +20,17 @@ export default class DensityPlan extends Plan {
return return
} }
const { const { hosts, toOptimize } = results
hosts,
toOptimize,
} = results
let { let { averages: hostsAverages } = results
averages: hostsAverages,
} = results
const pools = await this._getPlanPools() const pools = await this._getPlanPools()
let optimizationsCount = 0 let optimizationsCount = 0
for (const hostToOptimize of toOptimize) { for (const hostToOptimize of toOptimize) {
const { const { id: hostId, $poolId: poolId } = hostToOptimize
id: hostId,
$poolId: poolId,
} = hostToOptimize
const { const { master: masterId } = pools[poolId]
master: masterId,
} = pools[poolId]
// Avoid master optimization. // Avoid master optimization.
if (masterId === hostId) { if (masterId === hostId) {
@ -58,10 +49,7 @@ export default class DensityPlan extends Plan {
const otherHosts = [] const otherHosts = []
for (const dest of hosts) { for (const dest of hosts) {
const { const { id: destId, $poolId: destPoolId } = dest
id: destId,
$poolId: destPoolId,
} = dest
// Destination host != Host to optimize! // Destination host != Host to optimize!
if (destId === hostId) { if (destId === hostId) {
@ -83,12 +71,7 @@ export default class DensityPlan extends Plan {
const simulResults = await this._simulate({ const simulResults = await this._simulate({
host: hostToOptimize, host: hostToOptimize,
destinations: [ destinations: [[poolMaster], poolHosts, masters, otherHosts],
[ poolMaster ],
poolHosts,
masters,
otherHosts,
],
hostsAverages: clone(hostsAverages), hostsAverages: clone(hostsAverages),
}) })
@ -115,15 +98,15 @@ export default class DensityPlan extends Plan {
for (const vm of vms) { for (const vm of vms) {
if (!vm.xenTools) { if (!vm.xenTools) {
debug(`VM (${vm.id}) of Host (${hostId}) does not support pool migration.`) debug(
`VM (${vm.id}) of Host (${hostId}) does not support pool migration.`
)
return return
} }
} }
// Sort vms by amount of memory. (+ -> -) // Sort vms by amount of memory. (+ -> -)
vms.sort((a, b) => vms.sort((a, b) => vmsAverages[b.id].memory - vmsAverages[a.id].memory)
vmsAverages[b.id].memory - vmsAverages[a.id].memory
)
const simulResults = { const simulResults = {
hostsAverages, hostsAverages,
@ -162,15 +145,11 @@ export default class DensityPlan extends Plan {
// Test if a VM migration on a destination (of a destinations set) is possible. // Test if a VM migration on a destination (of a destinations set) is possible.
_testMigration ({ vm, destinations, hostsAverages, vmsAverages }) { _testMigration ({ vm, destinations, hostsAverages, vmsAverages }) {
const { const { _thresholds: { critical: criticalThreshold } } = this
_thresholds: {
critical: criticalThreshold,
},
} = this
// Sort the destinations by available memory. (- -> +) // Sort the destinations by available memory. (- -> +)
destinations.sort((a, b) => destinations.sort(
hostsAverages[a.id].memoryFree - hostsAverages[b.id].memoryFree (a, b) => hostsAverages[a.id].memoryFree - hostsAverages[b.id].memoryFree
) )
for (const destination of destinations) { for (const destination of destinations) {
@ -204,13 +183,18 @@ export default class DensityPlan extends Plan {
await Promise.all( await Promise.all(
mapToArray(moves, move => { mapToArray(moves, move => {
const { const { vm, destination } = move
vm,
destination,
} = move
const xapiDest = this.xo.getXapi(destination) const xapiDest = this.xo.getXapi(destination)
debug(`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${vm.$container}).`) debug(
return xapiDest.migrateVm(vm._xapiId, this.xo.getXapi(destination), destination._xapiId) `Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${
vm.$container
}).`
)
return xapiDest.migrateVm(
vm._xapiId,
this.xo.getXapi(destination),
destination._xapiId
)
}) })
) )

View File

@ -9,10 +9,7 @@ import {
DEFAULT_CRITICAL_THRESHOLD_CPU, DEFAULT_CRITICAL_THRESHOLD_CPU,
DEFAULT_CRITICAL_THRESHOLD_MEMORY_FREE, DEFAULT_CRITICAL_THRESHOLD_MEMORY_FREE,
} from './plan' } from './plan'
import { import { EXECUTION_DELAY, debug } from './utils'
EXECUTION_DELAY,
debug,
} from './utils'
// =================================================================== // ===================================================================
@ -41,7 +38,7 @@ export const configurationSchema = {
}, },
mode: { mode: {
enum: [ 'Performance mode', 'Density mode' ], enum: ['Performance mode', 'Density mode'],
title: 'Mode', title: 'Mode',
}, },
@ -85,7 +82,7 @@ export const configurationSchema = {
}, },
}, },
required: [ 'name', 'mode', 'pools' ], required: ['name', 'mode', 'pools'],
}, },
minItems: 1, minItems: 1,
@ -115,7 +112,10 @@ const makeJob = (cronPattern, fn) => {
try { try {
await fn() await fn()
} catch (error) { } catch (error) {
console.error('[WARN] scheduled function:', (error && error.stack) || error) console.error(
'[WARN] scheduled function:',
(error && error.stack) || error
)
} finally { } finally {
job.running = false job.running = false
job.emitter.emit('finish') job.emitter.emit('finish')
@ -133,7 +133,10 @@ const makeJob = (cronPattern, fn) => {
class LoadBalancerPlugin { class LoadBalancerPlugin {
constructor (xo) { constructor (xo) {
this.xo = xo this.xo = xo
this._job = makeJob(`*/${EXECUTION_DELAY} * * * *`, this._executePlans.bind(this)) this._job = makeJob(
`*/${EXECUTION_DELAY} * * * *`,
this._executePlans.bind(this)
)
} }
async configure ({ plans }) { async configure ({ plans }) {
@ -154,7 +157,10 @@ class LoadBalancerPlugin {
if (plans) { if (plans) {
for (const plan of plans) { for (const plan of plans) {
this._addPlan(plan.mode === 'Performance mode' ? PERFORMANCE_MODE : DENSITY_MODE, plan) this._addPlan(
plan.mode === 'Performance mode' ? PERFORMANCE_MODE : DENSITY_MODE,
plan
)
} }
} }
@ -180,18 +186,17 @@ class LoadBalancerPlugin {
} }
this._poolIds = this._poolIds.concat(pools) this._poolIds = this._poolIds.concat(pools)
this._plans.push(mode === PERFORMANCE_MODE this._plans.push(
? new PerformancePlan(this.xo, name, pools, options) mode === PERFORMANCE_MODE
: new DensityPlan(this.xo, name, pools, options) ? new PerformancePlan(this.xo, name, pools, options)
: new DensityPlan(this.xo, name, pools, options)
) )
} }
_executePlans () { _executePlans () {
debug('Execute plans!') debug('Execute plans!')
return Promise.all( return Promise.all(mapToArray(this._plans, plan => plan.execute()))
mapToArray(this._plans, plan => plan.execute())
)
} }
} }

View File

@ -35,7 +35,10 @@ export default class PerformancePlan extends Plan {
try { try {
await Promise.all( await Promise.all(
mapToArray( mapToArray(
filter(this._getHosts({ powerState: 'Halted' }), host => host.powerOnMode !== ''), filter(
this._getHosts({ powerState: 'Halted' }),
host => host.powerOnMode !== ''
),
host => { host => {
const { id } = host const { id } = host
return this.xo.getXapi(id).powerOnHost(id) return this.xo.getXapi(id).powerOnHost(id)
@ -52,17 +55,14 @@ export default class PerformancePlan extends Plan {
return return
} }
const { const { averages, toOptimize } = results
averages,
toOptimize,
} = results
let { hosts } = results let { hosts } = results
toOptimize.sort((a, b) => { toOptimize.sort((a, b) => {
a = averages[a.id] a = averages[a.id]
b = averages[b.id] b = averages[b.id]
return (b.cpu - a.cpu) || (a.memoryFree - b.memoryFree) return b.cpu - a.cpu || a.memoryFree - b.memoryFree
}) })
for (const exceededHost of toOptimize) { for (const exceededHost of toOptimize) {
@ -85,9 +85,7 @@ export default class PerformancePlan extends Plan {
const vmsAverages = await this._getVmsAverages(vms, exceededHost) const vmsAverages = await this._getVmsAverages(vms, exceededHost)
// Sort vms by cpu usage. (lower to higher) // Sort vms by cpu usage. (lower to higher)
vms.sort((a, b) => vms.sort((a, b) => vmsAverages[b.id].cpu - vmsAverages[a.id].cpu)
vmsAverages[b.id].cpu - vmsAverages[a.id].cpu
)
const exceededAverages = hostsAverages[exceededHost.id] const exceededAverages = hostsAverages[exceededHost.id]
const promises = [] const promises = []
@ -95,11 +93,15 @@ export default class PerformancePlan extends Plan {
const xapiSrc = this.xo.getXapi(exceededHost) const xapiSrc = this.xo.getXapi(exceededHost)
let optimizationsCount = 0 let optimizationsCount = 0
const searchFunction = (a, b) => hostsAverages[b.id].cpu - hostsAverages[a.id].cpu const searchFunction = (a, b) =>
hostsAverages[b.id].cpu - hostsAverages[a.id].cpu
for (const vm of vms) { for (const vm of vms) {
// Search host with lower cpu usage in the same pool first. In other pool if necessary. // Search host with lower cpu usage in the same pool first. In other pool if necessary.
let destination = searchBestObject(find(hosts, host => host.$poolId === vm.$poolId), searchFunction) let destination = searchBestObject(
find(hosts, host => host.$poolId === vm.$poolId),
searchFunction
)
if (!destination) { if (!destination) {
destination = searchBestObject(hosts, searchFunction) destination = searchBestObject(hosts, searchFunction)
@ -110,7 +112,8 @@ export default class PerformancePlan extends Plan {
// Unable to move the vm. // Unable to move the vm.
if ( if (
exceededAverages.cpu - vmAverages.cpu < destinationAverages.cpu + vmAverages.cpu || exceededAverages.cpu - vmAverages.cpu <
destinationAverages.cpu + vmAverages.cpu ||
destinationAverages.memoryFree > vmAverages.memory destinationAverages.memoryFree > vmAverages.memory
) { ) {
continue continue
@ -122,15 +125,27 @@ export default class PerformancePlan extends Plan {
exceededAverages.memoryFree += vmAverages.memory exceededAverages.memoryFree += vmAverages.memory
destinationAverages.memoryFree -= vmAverages.memory destinationAverages.memoryFree -= vmAverages.memory
debug(`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${exceededHost.id}).`) debug(
`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${
exceededHost.id
}).`
)
optimizationsCount++ optimizationsCount++
promises.push( promises.push(
xapiSrc.migrateVm(vm._xapiId, this.xo.getXapi(destination), destination._xapiId) xapiSrc.migrateVm(
vm._xapiId,
this.xo.getXapi(destination),
destination._xapiId
)
) )
} }
await Promise.all(promises) await Promise.all(promises)
debug(`Performance mode: ${optimizationsCount} optimizations for Host (${exceededHost.id}).`) debug(
`Performance mode: ${optimizationsCount} optimizations for Host (${
exceededHost.id
}).`
)
} }
} }

View File

@ -1,9 +1,6 @@
import { filter, includes, map as mapToArray } from 'lodash' import { filter, includes, map as mapToArray } from 'lodash'
import { import { EXECUTION_DELAY, debug } from './utils'
EXECUTION_DELAY,
debug,
} from './utils'
const MINUTES_OF_HISTORICAL_DATA = 30 const MINUTES_OF_HISTORICAL_DATA = 30
@ -20,7 +17,7 @@ const LOW_THRESHOLD_FACTOR = 0.25
const HIGH_THRESHOLD_MEMORY_FREE_FACTOR = 1.25 const HIGH_THRESHOLD_MEMORY_FREE_FACTOR = 1.25
const LOW_THRESHOLD_MEMORY_FREE_FACTOR = 20.0 const LOW_THRESHOLD_MEMORY_FREE_FACTOR = 20.0
const numberOrDefault = (value, def) => (value >= 0) ? value : def const numberOrDefault = (value, def) => (value >= 0 ? value : def)
// =================================================================== // ===================================================================
// Averages. // Averages.
@ -69,10 +66,12 @@ function computeRessourcesAverageWithWeight (averages1, averages2, ratio) {
const averages = {} const averages = {}
for (const id in averages1) { for (const id in averages1) {
const objectAverages = averages[id] = {} const objectAverages = (averages[id] = {})
for (const averageName in averages1[id]) { for (const averageName in averages1[id]) {
objectAverages[averageName] = averages1[id][averageName] * ratio + averages2[id][averageName] * (1 - ratio) objectAverages[averageName] =
averages1[id][averageName] * ratio +
averages2[id][averageName] * (1 - ratio)
} }
} }
@ -89,20 +88,24 @@ function setRealCpuAverageOfVms (vms, vmsAverages, nCpus) {
// =================================================================== // ===================================================================
export default class Plan { export default class Plan {
constructor (xo, name, poolIds, { constructor (xo, name, poolIds, { excludedHosts, thresholds } = {}) {
excludedHosts,
thresholds,
} = {}) {
this.xo = xo this.xo = xo
this._name = name this._name = name
this._poolIds = poolIds this._poolIds = poolIds
this._excludedHosts = excludedHosts this._excludedHosts = excludedHosts
this._thresholds = { this._thresholds = {
cpu: { cpu: {
critical: numberOrDefault(thresholds && thresholds.cpu, DEFAULT_CRITICAL_THRESHOLD_CPU), critical: numberOrDefault(
thresholds && thresholds.cpu,
DEFAULT_CRITICAL_THRESHOLD_CPU
),
}, },
memoryFree: { memoryFree: {
critical: numberOrDefault(thresholds && thresholds.memoryFree, DEFAULT_CRITICAL_THRESHOLD_MEMORY_FREE) * 1024, critical:
numberOrDefault(
thresholds && thresholds.memoryFree,
DEFAULT_CRITICAL_THRESHOLD_MEMORY_FREE
) * 1024,
}, },
} }
@ -143,8 +146,16 @@ export default class Plan {
} }
// Check in the last 30 min interval with ratio. // Check in the last 30 min interval with ratio.
const avgBefore = computeRessourcesAverage(hosts, hostsStats, MINUTES_OF_HISTORICAL_DATA) const avgBefore = computeRessourcesAverage(
const avgWithRatio = computeRessourcesAverageWithWeight(avgNow, avgBefore, 0.75) hosts,
hostsStats,
MINUTES_OF_HISTORICAL_DATA
)
const avgWithRatio = computeRessourcesAverageWithWeight(
avgNow,
avgBefore,
0.75
)
toOptimize = this._checkRessourcesThresholds(toOptimize, avgWithRatio) toOptimize = this._checkRessourcesThresholds(toOptimize, avgWithRatio)
@ -185,19 +196,23 @@ export default class Plan {
// Compute hosts for each pool. They can change over time. // Compute hosts for each pool. They can change over time.
_getHosts ({ powerState = 'Running' } = {}) { _getHosts ({ powerState = 'Running' } = {}) {
return filter(this.xo.getObjects(), object => ( return filter(
object.type === 'host' && this.xo.getObjects(),
includes(this._poolIds, object.$poolId) && object =>
object.power_state === powerState && object.type === 'host' &&
!includes(this._excludedHosts, object.id) includes(this._poolIds, object.$poolId) &&
)) object.power_state === powerState &&
!includes(this._excludedHosts, object.id)
)
} }
async _getVms (hostId) { async _getVms (hostId) {
return filter(this.xo.getObjects(), object => return filter(
object.type === 'VM' && this.xo.getObjects(),
object.power_state === 'Running' && object =>
object.$container === hostId object.type === 'VM' &&
object.power_state === 'Running' &&
object.$container === hostId
) )
} }
@ -208,15 +223,17 @@ export default class Plan {
async _getHostsStats (hosts, granularity) { async _getHostsStats (hosts, granularity) {
const hostsStats = {} const hostsStats = {}
await Promise.all(mapToArray(hosts, host => await Promise.all(
this.xo.getXapiHostStats(host, granularity).then(hostStats => { mapToArray(hosts, host =>
hostsStats[host.id] = { this.xo.getXapiHostStats(host, granularity).then(hostStats => {
nPoints: hostStats.stats.cpus[0].length, hostsStats[host.id] = {
stats: hostStats.stats, nPoints: hostStats.stats.cpus[0].length,
averages: {}, stats: hostStats.stats,
} averages: {},
}) }
)) })
)
)
return hostsStats return hostsStats
} }
@ -224,15 +241,17 @@ export default class Plan {
async _getVmsStats (vms, granularity) { async _getVmsStats (vms, granularity) {
const vmsStats = {} const vmsStats = {}
await Promise.all(mapToArray(vms, vm => await Promise.all(
this.xo.getXapiVmStats(vm, granularity).then(vmStats => { mapToArray(vms, vm =>
vmsStats[vm.id] = { this.xo.getXapiVmStats(vm, granularity).then(vmStats => {
nPoints: vmStats.stats.cpus[0].length, vmsStats[vm.id] = {
stats: vmStats.stats, nPoints: vmStats.stats.cpus[0].length,
averages: {}, stats: vmStats.stats,
} averages: {},
}) }
)) })
)
)
return vmsStats return vmsStats
} }

View File

@ -27,9 +27,7 @@ module.exports = {
debug: !__TEST__, debug: !__TEST__,
loose: true, loose: true,
shippedProposals: true, shippedProposals: true,
targets: __PROD__ targets: __PROD__ ? { node: nodeCompat } : { node: 'current' },
? { node: nodeCompat }
: { node: 'current' },
useBuiltIns: '@babel/polyfill' in (pkg.dependencies || {}) && 'usage', useBuiltIns: '@babel/polyfill' in (pkg.dependencies || {}) && 'usage',
}, },
], ],

View File

@ -91,9 +91,7 @@ const HOST_FUNCTIONS = {
unit: '% used', unit: '% used',
comparator: '>', comparator: '>',
createParser: (legend, threshold) => { createParser: (legend, threshold) => {
const memoryKBytesLegend = legend.find( const memoryKBytesLegend = legend.find(l => l.name === 'memory_total_kib')
l => l.name === 'memory_total_kib'
)
const memoryKBytesFreeLegend = legend.find( const memoryKBytesFreeLegend = legend.find(
l => l.name === 'memory_free_kib' l => l.name === 'memory_free_kib'
) )

View File

@ -37,7 +37,6 @@ exports.default = function (opts) {
// For simplicity's sake, this plugin returns a plain object, but // For simplicity's sake, this plugin returns a plain object, but
// usually it returns a new instance of an existing class. // usually it returns a new instance of an existing class.
return { return {
// This (optional) method is called each time the plugin is // This (optional) method is called each time the plugin is
// (re-)configured. // (re-)configured.
// //

View File

@ -16,7 +16,10 @@ const removeUndefined = obj => {
const markdownCompiler = nodemailerMarkdown() const markdownCompiler = nodemailerMarkdown()
const logAndRethrow = error => { const logAndRethrow = error => {
console.error('[WARN] plugin transport-email:', (error && error.stack) || error) console.error(
'[WARN] plugin transport-email:',
(error && error.stack) || error
)
throw error throw error
} }
@ -59,7 +62,7 @@ export const configurationSchema = {
}, },
secure: { secure: {
default: false, default: false,
enum: [ false, 'force', 'disabled', true ], enum: [false, 'force', 'disabled', true],
enumNames: [ enumNames: [
'auto (uses STARTTLS if available)', 'auto (uses STARTTLS if available)',
'force (requires STARTTLS or fail)', 'force (requires STARTTLS or fail)',
@ -70,7 +73,8 @@ export const configurationSchema = {
}, },
ignoreUnauthorized: { ignoreUnauthorized: {
type: 'boolean', type: 'boolean',
description: 'ignore certificates error (e.g. self-signed certificate)', description:
'ignore certificates error (e.g. self-signed certificate)',
}, },
// FIXME: xo-web does not support edition of too nested // FIXME: xo-web does not support edition of too nested
@ -138,18 +142,11 @@ class TransportEmailPlugin {
configure ({ configure ({
from, from,
transport: { transport: { ignoreUnauthorized, password, secure, user, ...transportConf },
ignoreUnauthorized,
password,
secure,
user,
...transportConf
},
}) { }) {
if (ignoreUnauthorized != null) { if (ignoreUnauthorized != null) {
( ;(
transportConf.tls || transportConf.tls || (transportConf.tls = {})
(transportConf.tls = {})
).rejectUnauthorized = !ignoreUnauthorized ).rejectUnauthorized = !ignoreUnauthorized
} }
@ -159,11 +156,14 @@ class TransportEmailPlugin {
switch (secure) { switch (secure) {
case true: case true:
transportConf.secure = true; break transportConf.secure = true
break
case 'disabled': case 'disabled':
transportConf.ignoreTLS = true; break transportConf.ignoreTLS = true
break
case 'required': case 'required':
transportConf.requireTLS = true; break transportConf.requireTLS = true
break
} }
const transport = createTransport(transportConf, { from }) const transport = createTransport(transportConf, { from })
@ -180,7 +180,7 @@ class TransportEmailPlugin {
this._unset() this._unset()
} }
test ({to}) { test ({ to }) {
return this._sendEmail({ return this._sendEmail({
to, to,
subject: '[Xen Orchestra] Test of transport-email plugin', subject: '[Xen Orchestra] Test of transport-email plugin',
@ -188,29 +188,27 @@ class TransportEmailPlugin {
The transport-email plugin for Xen Orchestra server seems to be working fine, nicely done :) The transport-email plugin for Xen Orchestra server seems to be working fine, nicely done :)
`, `,
attachments: [ { attachments: [
filename: 'example.txt', {
content: 'Attachments are working too, great!\n', filename: 'example.txt',
} ], content: 'Attachments are working too, great!\n',
},
],
}) })
} }
_sendEmail ({ _sendEmail ({ from, to, cc, bcc, subject, markdown, attachments }) {
from, return this._send(
to, cc, bcc, removeUndefined({
subject, from,
markdown, to,
attachments, cc,
}) { bcc,
return this._send(removeUndefined({ subject,
from, markdown,
to, attachments,
cc, })
bcc, ).catch(logAndRethrow)
subject,
markdown,
attachments,
})).catch(logAndRethrow)
} }
} }

View File

@ -35,18 +35,12 @@ export const configurationSchema = {
// =================================================================== // ===================================================================
const bind = (fn, thisArg) => function __bound__ () { const bind = (fn, thisArg) =>
return fn.apply(thisArg, arguments) function __bound__ () {
} return fn.apply(thisArg, arguments)
}
function nscaPacketBuilder ({ function nscaPacketBuilder ({ host, iv, message, service, status, timestamp }) {
host,
iv,
message,
service,
status,
timestamp,
}) {
// Building NSCA packet // Building NSCA packet
const SIZE = 720 const SIZE = 720
const packet = Buffer.alloc(SIZE) const packet = Buffer.alloc(SIZE)
@ -112,15 +106,13 @@ class XoServerNagios {
test () { test () {
return this._sendPassiveCheck({ return this._sendPassiveCheck({
message: 'The server-nagios plugin for Xen Orchestra server seems to be working fine, nicely done :)', message:
'The server-nagios plugin for Xen Orchestra server seems to be working fine, nicely done :)',
status: OK, status: OK,
}) })
} }
_sendPassiveCheck ({ _sendPassiveCheck ({ message, status }) {
message,
status,
}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (/\r|\n/.test(message)) { if (/\r|\n/.test(message)) {
throw new Error('the message must not contain a line break') throw new Error('the message must not contain a line break')
@ -145,13 +137,7 @@ class XoServerNagios {
// 1) Using xor between the NSCA packet and the initialization vector // 1) Using xor between the NSCA packet and the initialization vector
// 2) Using xor between the result of the first operation and the encryption key // 2) Using xor between the result of the first operation and the encryption key
const xorPacketBuffer = xor( const xorPacketBuffer = xor(xor(packet, iv), this._key)
xor(
packet,
iv
),
this._key
)
client.write(xorPacketBuffer, res => { client.write(xorPacketBuffer, res => {
client.destroy() client.destroy()

View File

@ -4,7 +4,10 @@ import { promisify } from 'promise-toolbox'
// =================================================================== // ===================================================================
const logAndRethrow = error => { const logAndRethrow = error => {
console.error('[WARN] plugin transport-slack:', (error != null && error.stack) || error) console.error(
'[WARN] plugin transport-slack:',
(error != null && error.stack) || error
)
throw error throw error
} }
@ -48,10 +51,7 @@ class XoServerTransportSlack {
this._send = null this._send = null
} }
configure ({ configure ({ webhookUri, ...conf }) {
webhookUri,
...conf
}) {
const slack = new Slack() const slack = new Slack()
slack.setWebhook(webhookUri) slack.setWebhook(webhookUri)
this._conf = conf this._conf = conf
@ -74,9 +74,7 @@ The transport-slack plugin for Xen Orchestra server seems to be working fine, ni
}) })
} }
_sendSlack ({ _sendSlack ({ message }) {
message,
}) {
// TODO: handle errors // TODO: handle errors
return this._send({ ...this._conf, text: message }).catch(logAndRethrow) return this._send({ ...this._conf, text: message }).catch(logAndRethrow)
} }

View File

@ -68,13 +68,15 @@ class TransportXmppPlugin {
this._unset = this._client = null this._unset = this._client = null
} }
_sendToXmppClient ({to, message}) { _sendToXmppClient ({ to, message }) {
for (const receiver of to) { for (const receiver of to) {
this._client.send( this._client.send(
new XmppClient.Stanza('message', { new XmppClient.Stanza('message', {
to: receiver, to: receiver,
type: 'chat', type: 'chat',
}).c('body').t(message) })
.c('body')
.t(message)
) )
} }
} }

View File

@ -14,13 +14,8 @@ import {
values, values,
zipObject, zipObject,
} from 'lodash' } from 'lodash'
import { import { promisify } from 'promise-toolbox'
promisify, import { readFile, writeFile } from 'fs'
} from 'promise-toolbox'
import {
readFile,
writeFile,
} from 'fs'
// =================================================================== // ===================================================================
@ -41,9 +36,9 @@ const mibPower = Math.pow(2, 20)
const kibPower = Math.pow(2, 10) const kibPower = Math.pow(2, 10)
let template = null let template = null
pReadFile(`${__dirname}/../report.html.tpl`, 'utf8') pReadFile(`${__dirname}/../report.html.tpl`, 'utf8').then(tpl => {
.then(tpl => { template = Handlebars.compile(
template = Handlebars.compile(minify(tpl, { minify(tpl, {
collapseBooleanAttributes: true, collapseBooleanAttributes: true,
collapseWhitespace: true, collapseWhitespace: true,
minifyCSS: true, minifyCSS: true,
@ -51,14 +46,14 @@ pReadFile(`${__dirname}/../report.html.tpl`, 'utf8')
removeComments: true, removeComments: true,
removeOptionalTags: true, removeOptionalTags: true,
removeRedundantAttributes: true, removeRedundantAttributes: true,
})) })
}) )
})
let imgXo = null let imgXo = null
pReadFile(`${__dirname}/../images/xo.png`, 'base64') pReadFile(`${__dirname}/../images/xo.png`, 'base64').then(data => {
.then(data => { imgXo = `data:image/png;base64,${data}`
imgXo = `data:image/png;base64,${data}` })
})
// =================================================================== // ===================================================================
@ -75,26 +70,36 @@ export const configurationSchema = {
periodicity: { periodicity: {
type: 'string', type: 'string',
enum: ['monthly', 'weekly'], enum: ['monthly', 'weekly'],
description: 'If you choose weekly you will receive the report every sunday and if you choose monthly you will receive it every first day of the month.', description:
'If you choose weekly you will receive the report every sunday and if you choose monthly you will receive it every first day of the month.',
}, },
}, },
additionalProperties: false, additionalProperties: false,
required: [ 'emails', 'periodicity' ], required: ['emails', 'periodicity'],
} }
// =================================================================== // ===================================================================
Handlebars.registerHelper('compare', function (lvalue, operator, rvalue, options) { Handlebars.registerHelper('compare', function (
lvalue,
operator,
rvalue,
options
) {
if (arguments.length < 3) { if (arguments.length < 3) {
throw new Error('Handlerbars Helper "compare" needs 2 parameters') throw new Error('Handlerbars Helper "compare" needs 2 parameters')
} }
if (!compareOperators[operator]) { if (!compareOperators[operator]) {
throw new Error(`Handlerbars Helper "compare" doesn't know the operator ${operator}`) throw new Error(
`Handlerbars Helper "compare" doesn't know the operator ${operator}`
)
} }
return compareOperators[operator](lvalue, rvalue) ? options.fn(this) : options.inverse(this) return compareOperators[operator](lvalue, rvalue)
? options.fn(this)
: options.inverse(this)
}) })
Handlebars.registerHelper('math', function (lvalue, operator, rvalue, options) { Handlebars.registerHelper('math', function (lvalue, operator, rvalue, options) {
@ -103,7 +108,9 @@ Handlebars.registerHelper('math', function (lvalue, operator, rvalue, options) {
} }
if (!mathOperators[operator]) { if (!mathOperators[operator]) {
throw new Error(`Handlerbars Helper "math" doesn't know the operator ${operator}`) throw new Error(
`Handlerbars Helper "math" doesn't know the operator ${operator}`
)
} }
return mathOperators[operator](+lvalue, +rvalue) return mathOperators[operator](+lvalue, +rvalue)
@ -135,24 +142,24 @@ const computeDoubleMean = val => computeMean(val.map(computeMean))
function computeMeans (objects, options) { function computeMeans (objects, options) {
return zipObject( return zipObject(
options, options,
map( map(options, opt => round(computeMean(map(objects, opt)), 2))
options,
opt => round(computeMean(map(objects, opt)), 2)
)
) )
} }
function getTop (objects, options) { function getTop (objects, options) {
return zipObject( return zipObject(
options, options,
map( map(options, opt =>
options, map(
opt => map( orderBy(
orderBy(objects, object => { objects,
const value = object[opt] object => {
const value = object[opt]
return isNaN(value) ? -Infinity : value return isNaN(value) ? -Infinity : value
}, 'desc').slice(0, 3), },
'desc'
).slice(0, 3),
obj => ({ obj => ({
uuid: obj.uuid, uuid: obj.uuid,
name: obj.name, name: obj.name,
@ -168,7 +175,10 @@ function conputePercentage (curr, prev, options) {
options, options,
map( map(
options, options,
opt => prev[opt] === 0 ? 'NONE' : `${round((curr[opt] - prev[opt]) * 100 / prev[opt], 2)}` opt =>
prev[opt] === 0
? 'NONE'
: `${round((curr[opt] - prev[opt]) * 100 / prev[opt], 2)}`
) )
) )
} }
@ -182,48 +192,42 @@ function getDiff (oldElements, newElements) {
// =================================================================== // ===================================================================
function getVmsStats ({ function getVmsStats ({ runningVms, xo }) {
runningVms, return Promise.all(
xo, map(runningVms, async vm => {
}) { const vmStats = await xo.getXapiVmStats(vm, 'days')
return Promise.all(map(runningVms, async vm => { return {
const vmStats = await xo.getXapiVmStats(vm, 'days') uuid: vm.uuid,
return { name: vm.name_label,
uuid: vm.uuid, cpu: computeDoubleMean(vmStats.stats.cpus),
name: vm.name_label, ram: computeMean(vmStats.stats.memoryUsed) / gibPower,
cpu: computeDoubleMean(vmStats.stats.cpus), diskRead: computeDoubleMean(values(vmStats.stats.xvds.r)) / mibPower,
ram: computeMean(vmStats.stats.memoryUsed) / gibPower, diskWrite: computeDoubleMean(values(vmStats.stats.xvds.w)) / mibPower,
diskRead: computeDoubleMean(values(vmStats.stats.xvds.r)) / mibPower, netReception: computeDoubleMean(vmStats.stats.vifs.rx) / kibPower,
diskWrite: computeDoubleMean(values(vmStats.stats.xvds.w)) / mibPower, netTransmission: computeDoubleMean(vmStats.stats.vifs.tx) / kibPower,
netReception: computeDoubleMean(vmStats.stats.vifs.rx) / kibPower, }
netTransmission: computeDoubleMean(vmStats.stats.vifs.tx) / kibPower, })
} )
}))
} }
function getHostsStats ({ function getHostsStats ({ runningHosts, xo }) {
runningHosts, return Promise.all(
xo, map(runningHosts, async host => {
}) { const hostStats = await xo.getXapiHostStats(host, 'days')
return Promise.all(map(runningHosts, async host => { return {
const hostStats = await xo.getXapiHostStats(host, 'days') uuid: host.uuid,
return { name: host.name_label,
uuid: host.uuid, cpu: computeDoubleMean(hostStats.stats.cpus),
name: host.name_label, ram: computeMean(hostStats.stats.memoryUsed) / gibPower,
cpu: computeDoubleMean(hostStats.stats.cpus), load: computeMean(hostStats.stats.load),
ram: computeMean(hostStats.stats.memoryUsed) / gibPower, netReception: computeDoubleMean(hostStats.stats.pifs.rx) / kibPower,
load: computeMean(hostStats.stats.load), netTransmission: computeDoubleMean(hostStats.stats.pifs.tx) / kibPower,
netReception: computeDoubleMean(hostStats.stats.pifs.rx) / kibPower, }
netTransmission: computeDoubleMean(hostStats.stats.pifs.tx) / kibPower, })
} )
}))
} }
function computeGlobalVmsStats ({ function computeGlobalVmsStats ({ haltedVms, vmsStats, xo }) {
haltedVms,
vmsStats,
xo,
}) {
const allVms = concat( const allVms = concat(
map(vmsStats, vm => ({ map(vmsStats, vm => ({
uuid: vm.uuid, uuid: vm.uuid,
@ -235,17 +239,23 @@ function computeGlobalVmsStats ({
})) }))
) )
return assign(computeMeans(vmsStats, ['cpu', 'ram', 'diskRead', 'diskWrite', 'netReception', 'netTransmission']), { return assign(
number: allVms.length, computeMeans(vmsStats, [
allVms, 'cpu',
}) 'ram',
'diskRead',
'diskWrite',
'netReception',
'netTransmission',
]),
{
number: allVms.length,
allVms,
}
)
} }
function computeGlobalHostsStats ({ function computeGlobalHostsStats ({ haltedHosts, hostsStats, xo }) {
haltedHosts,
hostsStats,
xo,
}) {
const allHosts = concat( const allHosts = concat(
map(hostsStats, host => ({ map(hostsStats, host => ({
uuid: host.uuid, uuid: host.uuid,
@ -257,52 +267,65 @@ function computeGlobalHostsStats ({
})) }))
) )
return assign(computeMeans(hostsStats, ['cpu', 'ram', 'load', 'netReception', 'netTransmission']), { return assign(
number: allHosts.length, computeMeans(hostsStats, [
allHosts, 'cpu',
}) 'ram',
} 'load',
'netReception',
function getTopVms ({ 'netTransmission',
vmsStats, ]),
xo, {
}) { number: allHosts.length,
return getTop(vmsStats, ['cpu', 'ram', 'diskRead', 'diskWrite', 'netReception', 'netTransmission']) allHosts,
}
function getTopHosts ({
hostsStats,
xo,
}) {
return getTop(hostsStats, ['cpu', 'ram', 'load', 'netReception', 'netTransmission'])
}
function getMostAllocatedSpaces ({
disks,
xo,
}) {
return map(
orderBy(disks, ['size'], ['desc']).slice(0, 3), disk => ({
uuid: disk.uuid,
name: disk.name_label,
size: round(disk.size / gibPower, 2),
}))
}
async function getHostsMissingPatches ({
runningHosts,
xo,
}) {
const hostsMissingPatches = await Promise.all(map(runningHosts, async host => {
const hostsPatches = await xo.getXapi(host).listMissingPoolPatchesOnHost(host._xapiId)
if (hostsPatches.length > 0) {
return {
uuid: host.uuid,
name: host.name_label,
patches: map(hostsPatches, 'name'),
}
} }
)
}
function getTopVms ({ vmsStats, xo }) {
return getTop(vmsStats, [
'cpu',
'ram',
'diskRead',
'diskWrite',
'netReception',
'netTransmission',
])
}
function getTopHosts ({ hostsStats, xo }) {
return getTop(hostsStats, [
'cpu',
'ram',
'load',
'netReception',
'netTransmission',
])
}
function getMostAllocatedSpaces ({ disks, xo }) {
return map(orderBy(disks, ['size'], ['desc']).slice(0, 3), disk => ({
uuid: disk.uuid,
name: disk.name_label,
size: round(disk.size / gibPower, 2),
})) }))
}
async function getHostsMissingPatches ({ runningHosts, xo }) {
const hostsMissingPatches = await Promise.all(
map(runningHosts, async host => {
const hostsPatches = await xo
.getXapi(host)
.listMissingPoolPatchesOnHost(host._xapiId)
if (hostsPatches.length > 0) {
return {
uuid: host.uuid,
name: host.name_label,
patches: map(hostsPatches, 'name'),
}
}
})
)
return filter(hostsMissingPatches, host => host !== undefined) return filter(hostsMissingPatches, host => host !== undefined)
} }
@ -310,17 +333,11 @@ function getAllUsersEmail (users) {
return map(users, 'email') return map(users, 'email')
} }
async function storeStats ({ async function storeStats ({ data, storedStatsPath }) {
data,
storedStatsPath,
}) {
await pWriteFile(storedStatsPath, JSON.stringify(data)) await pWriteFile(storedStatsPath, JSON.stringify(data))
} }
async function computeEvolution ({ async function computeEvolution ({ storedStatsPath, ...newStats }) {
storedStatsPath,
...newStats
}) {
try { try {
const oldStats = JSON.parse(await pReadFile(storedStatsPath, 'utf8')) const oldStats = JSON.parse(await pReadFile(storedStatsPath, 'utf8'))
const newStatsVms = newStats.vms const newStatsVms = newStats.vms
@ -332,16 +349,35 @@ async function computeEvolution ({
const vmsEvolution = { const vmsEvolution = {
number: newStatsVms.number - oldStatsVms.number, number: newStatsVms.number - oldStatsVms.number,
...conputePercentage(newStatsVms, oldStatsVms, ['cpu', 'ram', 'diskRead', 'diskWrite', 'netReception', 'netTransmission']), ...conputePercentage(newStatsVms, oldStatsVms, [
'cpu',
'ram',
'diskRead',
'diskWrite',
'netReception',
'netTransmission',
]),
} }
const hostsEvolution = { const hostsEvolution = {
number: newStatsHosts.number - oldStatsHosts.number, number: newStatsHosts.number - oldStatsHosts.number,
...conputePercentage(newStatsHosts, oldStatsHosts, ['cpu', 'ram', 'load', 'netReception', 'netTransmission']), ...conputePercentage(newStatsHosts, oldStatsHosts, [
'cpu',
'ram',
'load',
'netReception',
'netTransmission',
]),
} }
const vmsRessourcesEvolution = getDiff(oldStatsVms.allVms, newStatsVms.allVms) const vmsRessourcesEvolution = getDiff(
const hostsRessourcesEvolution = getDiff(oldStatsHosts.allHosts, newStatsHosts.allHosts) oldStatsVms.allVms,
newStatsVms.allVms
)
const hostsRessourcesEvolution = getDiff(
oldStatsHosts.allHosts,
newStatsHosts.allHosts
)
const usersEvolution = getDiff(oldStats.users, newStats.users) const usersEvolution = getDiff(oldStats.users, newStats.users)
@ -358,29 +394,41 @@ async function computeEvolution ({
} }
} }
async function dataBuilder ({ async function dataBuilder ({ xo, storedStatsPath }) {
xo,
storedStatsPath,
}) {
const xoObjects = values(xo.getObjects()) const xoObjects = values(xo.getObjects())
const runningVms = filter(xoObjects, {type: 'VM', power_state: 'Running'}) const runningVms = filter(xoObjects, { type: 'VM', power_state: 'Running' })
const haltedVms = filter(xoObjects, {type: 'VM', power_state: 'Halted'}) const haltedVms = filter(xoObjects, { type: 'VM', power_state: 'Halted' })
const runningHosts = filter(xoObjects, {type: 'host', power_state: 'Running'}) const runningHosts = filter(xoObjects, {
const haltedHosts = filter(xoObjects, {type: 'host', power_state: 'Halted'}) type: 'host',
const disks = filter(xoObjects, {type: 'SR'}) power_state: 'Running',
const [users, vmsStats, hostsStats, topAllocation, hostsMissingPatches] = await Promise.all([ })
const haltedHosts = filter(xoObjects, { type: 'host', power_state: 'Halted' })
const disks = filter(xoObjects, { type: 'SR' })
const [
users,
vmsStats,
hostsStats,
topAllocation,
hostsMissingPatches,
] = await Promise.all([
xo.getAllUsers(), xo.getAllUsers(),
getVmsStats({xo, runningVms}), getVmsStats({ xo, runningVms }),
getHostsStats({xo, runningHosts}), getHostsStats({ xo, runningHosts }),
getMostAllocatedSpaces({xo, disks}), getMostAllocatedSpaces({ xo, disks }),
getHostsMissingPatches({xo, runningHosts}), getHostsMissingPatches({ xo, runningHosts }),
]) ])
const [globalVmsStats, globalHostsStats, topVms, topHosts, usersEmail] = await Promise.all([ const [
computeGlobalVmsStats({xo, vmsStats, haltedVms}), globalVmsStats,
computeGlobalHostsStats({xo, hostsStats, haltedHosts}), globalHostsStats,
getTopVms({xo, vmsStats}), topVms,
getTopHosts({xo, hostsStats}), topHosts,
usersEmail,
] = await Promise.all([
computeGlobalVmsStats({ xo, vmsStats, haltedVms }),
computeGlobalHostsStats({ xo, hostsStats, haltedHosts }),
getTopVms({ xo, vmsStats }),
getTopHosts({ xo, hostsStats }),
getAllUsersEmail(users), getAllUsersEmail(users),
]) ])
const evolution = await computeEvolution({ const evolution = await computeEvolution({
@ -419,7 +467,7 @@ async function dataBuilder ({
// =================================================================== // ===================================================================
class UsageReportPlugin { class UsageReportPlugin {
constructor ({xo, getDataDir}) { constructor ({ xo, getDataDir }) {
this._xo = xo this._xo = xo
this._dir = getDataDir this._dir = getDataDir
// Defined in configure(). // Defined in configure().
@ -430,7 +478,8 @@ class UsageReportPlugin {
this._conf = configuration this._conf = configuration
this._job = new CronJob({ this._job = new CronJob({
cronTime: configuration.periodicity === 'monthly' ? '00 06 1 * *' : '00 06 * * 0', cronTime:
configuration.periodicity === 'monthly' ? '00 06 1 * *' : '00 06 * * 0',
onTick: () => this._sendReport(), onTick: () => this._sendReport(),
start: false, start: false,
}) })
@ -467,10 +516,12 @@ class UsageReportPlugin {
Please, find the attached report. Please, find the attached report.
best regards.`, best regards.`,
attachments: [{ attachments: [
filename: `xoReport_${currDate}.html`, {
content: template(data), filename: `xoReport_${currDate}.html`,
}], content: template(data),
},
],
}), }),
storeStats({ storeStats({
data, data,

View File

@ -8,25 +8,21 @@ try {
const filtered = frames.filter(function (frame) { const filtered = frames.filter(function (frame) {
const name = frame && frame.getFileName() const name = frame && frame.getFileName()
return ( return (// has a filename
// has a filename
name && name &&
// contains a separator (no internal modules) // contains a separator (no internal modules)
name.indexOf(sep) !== -1 && name.indexOf(sep) !== -1 &&
// does not start with `internal` // does not start with `internal`
name.lastIndexOf('internal', 0) !== -1 name.lastIndexOf('internal', 0) !== -1)
)
}) })
// depd (used amongst other by express requires at least 3 frames // depd (used amongst other by express requires at least 3 frames
// in the stack. // in the stack.
return filtered.length > 2 return filtered.length > 2 ? filtered : frames
? filtered
: frames
}) })
} catch (_) {} } catch (_) {}
// Source maps. // Source maps.
try { require('julien-f-source-map-support/register') } catch (_) {} try {
require('julien-f-source-map-support/register')
} catch (_) {}

View File

@ -14,11 +14,12 @@ export async function getCurrentPermissions () {
getCurrentPermissions.permission = '' getCurrentPermissions.permission = ''
getCurrentPermissions.description = 'get (explicit) permissions by object for the current user' getCurrentPermissions.description =
'get (explicit) permissions by object for the current user'
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function add ({subject, object, action}) { export async function add ({ subject, object, action }) {
await this.addAcl(subject, object, action) await this.addAcl(subject, object, action)
} }
@ -34,7 +35,7 @@ add.description = 'add a new ACL entry'
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function remove ({subject, object, action}) { export async function remove ({ subject, object, action }) {
await this.removeAcl(subject, object, action) await this.removeAcl(subject, object, action)
} }

View File

@ -42,46 +42,57 @@ scanFiles.params = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
function handleFetchFiles (req, res, { remote, disk, partition, paths, format: archiveFormat }) { function handleFetchFiles (
this.fetchFilesInDiskBackup(remote, disk, partition, paths).then(files => { req,
res.setHeader('content-disposition', 'attachment') res,
res.setHeader('content-type', 'application/octet-stream') { remote, disk, partition, paths, format: archiveFormat }
) {
this.fetchFilesInDiskBackup(remote, disk, partition, paths)
.then(files => {
res.setHeader('content-disposition', 'attachment')
res.setHeader('content-type', 'application/octet-stream')
const nFiles = paths.length const nFiles = paths.length
// Send lone file directly // Send lone file directly
if (nFiles === 1) { if (nFiles === 1) {
files[0].pipe(res) files[0].pipe(res)
return return
} }
const archive = archiver(archiveFormat) const archive = archiver(archiveFormat)
archive.on('error', error => { archive.on('error', error => {
console.error(error)
res.end(format.error(0, error))
})
forEach(files, file => {
archive.append(file, { name: basename(file.path) })
})
archive.finalize()
archive.pipe(res)
})
.catch(error => {
console.error(error) console.error(error)
res.writeHead(500)
res.end(format.error(0, error)) res.end(format.error(0, error))
}) })
forEach(files, file => {
archive.append(file, { name: basename(file.path) })
})
archive.finalize()
archive.pipe(res)
}).catch(error => {
console.error(error)
res.writeHead(500)
res.end(format.error(0, error))
})
} }
export async function fetchFiles ({ format = 'zip', ...params }) { export async function fetchFiles ({ format = 'zip', ...params }) {
const fileName = params.paths.length > 1 const fileName =
? `restore_${new Date().toJSON()}.${format}` params.paths.length > 1
: basename(params.paths[0]) ? `restore_${new Date().toJSON()}.${format}`
: basename(params.paths[0])
return this.registerHttpRequest(handleFetchFiles, { ...params, format }, { return this.registerHttpRequest(
suffix: encodeURI(`/${fileName}`), handleFetchFiles,
}).then(url => ({ $getFrom: url })) { ...params, format },
{
suffix: encodeURI(`/${fileName}`),
}
).then(url => ({ $getFrom: url }))
} }
fetchFiles.permission = 'admin' fetchFiles.permission = 'admin'

View File

@ -8,9 +8,11 @@ export async function create ({ name, size, sr, vm, bootable, position, mode })
let resourceSet let resourceSet
if (attach && (resourceSet = vm.resourceSet) != null) { if (attach && (resourceSet = vm.resourceSet) != null) {
await this.checkResourceSetConstraints(resourceSet, this.user.id, [ sr.id ]) await this.checkResourceSetConstraints(resourceSet, this.user.id, [sr.id])
await this.allocateLimitsInResourceSet({ disk: size }, resourceSet) await this.allocateLimitsInResourceSet({ disk: size }, resourceSet)
} else if (!(await this.hasPermissions(this.user.id, [ [ sr.id, 'administrate' ] ]))) { } else if (
!await this.hasPermissions(this.user.id, [[sr.id, 'administrate']])
) {
throw unauthorized() throw unauthorized()
} }

View File

@ -1,4 +1,4 @@
export async function register ({vm}) { export async function register ({ vm }) {
await this.getXapi(vm).registerDockerContainer(vm._xapiId) await this.getXapi(vm).registerDockerContainer(vm._xapiId)
} }
register.description = 'Register the VM for Docker management' register.description = 'Register the VM for Docker management'
@ -13,7 +13,7 @@ register.resolve = {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
export async function deregister ({vm}) { export async function deregister ({ vm }) {
await this.getXapi(vm).unregisterDockerContainer(vm._xapiId) await this.getXapi(vm).unregisterDockerContainer(vm._xapiId)
} }
deregister.description = 'Deregister the VM for Docker management' deregister.description = 'Deregister the VM for Docker management'
@ -28,23 +28,23 @@ deregister.resolve = {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
export async function start ({vm, container}) { export async function start ({ vm, container }) {
await this.getXapi(vm).startDockerContainer(vm._xapiId, container) await this.getXapi(vm).startDockerContainer(vm._xapiId, container)
} }
export async function stop ({vm, container}) { export async function stop ({ vm, container }) {
await this.getXapi(vm).stopDockerContainer(vm._xapiId, container) await this.getXapi(vm).stopDockerContainer(vm._xapiId, container)
} }
export async function restart ({vm, container}) { export async function restart ({ vm, container }) {
await this.getXapi(vm).restartDockerContainer(vm._xapiId, container) await this.getXapi(vm).restartDockerContainer(vm._xapiId, container)
} }
export async function pause ({vm, container}) { export async function pause ({ vm, container }) {
await this.getXapi(vm).pauseDockerContainer(vm._xapiId, container) await this.getXapi(vm).pauseDockerContainer(vm._xapiId, container)
} }
export async function unpause ({vm, container}) { export async function unpause ({ vm, container }) {
await this.getXapi(vm).unpauseDockerContainer(vm._xapiId, container) await this.getXapi(vm).unpauseDockerContainer(vm._xapiId, container)
} }

View File

@ -1,27 +1,27 @@
export async function create ({name}) { export async function create ({ name }) {
return (await this.createGroup({name})).id return (await this.createGroup({ name })).id
} }
create.description = 'creates a new group' create.description = 'creates a new group'
create.permission = 'admin' create.permission = 'admin'
create.params = { create.params = {
name: {type: 'string'}, name: { type: 'string' },
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Deletes an existing group. // Deletes an existing group.
async function delete_ ({id}) { async function delete_ ({ id }) {
await this.deleteGroup(id) await this.deleteGroup(id)
} }
// delete is not a valid identifier. // delete is not a valid identifier.
export {delete_ as delete} export { delete_ as delete }
delete_.description = 'deletes an existing group' delete_.description = 'deletes an existing group'
delete_.permission = 'admin' delete_.permission = 'admin'
delete_.params = { delete_.params = {
id: {type: 'string'}, id: { type: 'string' },
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@ -36,35 +36,35 @@ getAll.permission = 'admin'
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// sets group.users with an array of user ids // sets group.users with an array of user ids
export async function setUsers ({id, userIds}) { export async function setUsers ({ id, userIds }) {
await this.setGroupUsers(id, userIds) await this.setGroupUsers(id, userIds)
} }
setUsers.description = 'sets the users belonging to a group' setUsers.description = 'sets the users belonging to a group'
setUsers.permission = 'admin' setUsers.permission = 'admin'
setUsers.params = { setUsers.params = {
id: {type: 'string'}, id: { type: 'string' },
userIds: {}, userIds: {},
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// adds the user id to group.users // adds the user id to group.users
export async function addUser ({id, userId}) { export async function addUser ({ id, userId }) {
await this.addUserToGroup(userId, id) await this.addUserToGroup(userId, id)
} }
addUser.description = 'adds a user to a group' addUser.description = 'adds a user to a group'
addUser.permission = 'admin' addUser.permission = 'admin'
addUser.params = { addUser.params = {
id: {type: 'string'}, id: { type: 'string' },
userId: {type: 'string'}, userId: { type: 'string' },
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// remove the user id from group.users // remove the user id from group.users
export async function removeUser ({id, userId}) { export async function removeUser ({ id, userId }) {
await this.removeUserFromGroup(userId, id) await this.removeUserFromGroup(userId, id)
} }
@ -73,14 +73,14 @@ export async function removeUser ({id, userId}) {
removeUser.description = 'removes a user from a group' removeUser.description = 'removes a user from a group'
removeUser.permission = 'admin' removeUser.permission = 'admin'
removeUser.params = { removeUser.params = {
id: {type: 'string'}, id: { type: 'string' },
userId: {type: 'string'}, userId: { type: 'string' },
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function set ({id, name}) { export async function set ({ id, name }) {
await this.updateGroup(id, {name}) await this.updateGroup(id, { name })
} }
set.description = 'changes the properties of an existing group' set.description = 'changes the properties of an existing group'

View File

@ -1,5 +1,4 @@
import { format } from 'json-rpc-peer'
import {format} from 'json-rpc-peer'
// =================================================================== // ===================================================================
@ -58,7 +57,7 @@ restart.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export function restartAgent ({host}) { export function restartAgent ({ host }) {
return this.getXapi(host).restartHostAgent(host._xapiId) return this.getXapi(host).restartHostAgent(host._xapiId)
} }
@ -77,7 +76,7 @@ export { restartAgent as restart_agent } // eslint-disable-line camelcase
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export function start ({host}) { export function start ({ host }) {
return this.getXapi(host).powerOnHost(host._xapiId) return this.getXapi(host).powerOnHost(host._xapiId)
} }
@ -93,7 +92,7 @@ start.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export function stop ({host}) { export function stop ({ host }) {
return this.getXapi(host).shutdownHost(host._xapiId) return this.getXapi(host).shutdownHost(host._xapiId)
} }
@ -109,7 +108,7 @@ stop.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export function detach ({host}) { export function detach ({ host }) {
return this.getXapi(host).ejectHostFromPool(host._xapiId) return this.getXapi(host).ejectHostFromPool(host._xapiId)
} }
@ -125,7 +124,7 @@ detach.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export function enable ({host}) { export function enable ({ host }) {
return this.getXapi(host).enableHost(host._xapiId) return this.getXapi(host).enableHost(host._xapiId)
} }
@ -141,7 +140,7 @@ enable.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export function disable ({host}) { export function disable ({ host }) {
return this.getXapi(host).disableHost(host._xapiId) return this.getXapi(host).disableHost(host._xapiId)
} }
@ -157,7 +156,7 @@ disable.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export function forget ({host}) { export function forget ({ host }) {
return this.getXapi(host).forgetHost(host._xapiId) return this.getXapi(host).forgetHost(host._xapiId)
} }
@ -176,11 +175,12 @@ forget.resolve = {
// Returns an array of missing new patches in the host // Returns an array of missing new patches in the host
// Returns an empty array if up-to-date // Returns an empty array if up-to-date
// Throws an error if the host is not running the latest XS version // Throws an error if the host is not running the latest XS version
export function listMissingPatches ({host}) { export function listMissingPatches ({ host }) {
return this.getXapi(host).listMissingPoolPatchesOnHost(host._xapiId) return this.getXapi(host).listMissingPoolPatchesOnHost(host._xapiId)
} }
listMissingPatches.description = 'return an array of missing new patches in the host' listMissingPatches.description =
'return an array of missing new patches in the host'
listMissingPatches.params = { listMissingPatches.params = {
host: { type: 'string' }, host: { type: 'string' },
@ -192,7 +192,7 @@ listMissingPatches.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export function installPatch ({host, patch: patchUuid}) { export function installPatch ({ host, patch: patchUuid }) {
return this.getXapi(host).installPoolPatchOnHost(patchUuid, host._xapiId) return this.getXapi(host).installPoolPatchOnHost(patchUuid, host._xapiId)
} }
@ -209,7 +209,7 @@ installPatch.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export function installAllPatches ({host}) { export function installAllPatches ({ host }) {
return this.getXapi(host).installAllPoolPatchesOnHost(host._xapiId) return this.getXapi(host).installAllPoolPatchesOnHost(host._xapiId)
} }
@ -225,7 +225,7 @@ installAllPatches.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export function emergencyShutdownHost ({host}) { export function emergencyShutdownHost ({ host }) {
return this.getXapi(host).emergencyShutdownHost(host._xapiId) return this.getXapi(host).emergencyShutdownHost(host._xapiId)
} }
@ -241,7 +241,7 @@ emergencyShutdownHost.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export function stats ({host, granularity}) { export function stats ({ host, granularity }) {
return this.getXapiHostStats(host, granularity) return this.getXapiHostStats(host, granularity)
} }
@ -278,9 +278,11 @@ async function handleInstallSupplementalPack (req, res, { hostId }) {
} }
} }
export async function installSupplementalPack ({host}) { export async function installSupplementalPack ({ host }) {
return { return {
$sendTo: (await this.registerHttpRequest(handleInstallSupplementalPack, { hostId: host.id })), $sendTo: await this.registerHttpRequest(handleInstallSupplementalPack, {
hostId: host.id,
}),
} }
} }

View File

@ -26,9 +26,8 @@ export function getAll (params) {
throw unauthorized() throw unauthorized()
} }
return this.getAllIpPools(user.permission === 'admin' return this.getAllIpPools(
? params && params.userId user.permission === 'admin' ? params && params.userId : user.id
: user.id
) )
} }

View File

@ -14,10 +14,10 @@ export async function get (id) {
get.permission = 'admin' get.permission = 'admin'
get.description = 'Gets an existing job' get.description = 'Gets an existing job'
get.params = { get.params = {
id: {type: 'string'}, id: { type: 'string' },
} }
export async function create ({job}) { export async function create ({ job }) {
if (!job.userId) { if (!job.userId) {
job.userId = this.session.get('user_id') job.userId = this.session.get('user_id')
} }
@ -31,16 +31,16 @@ create.params = {
job: { job: {
type: 'object', type: 'object',
properties: { properties: {
userId: {type: 'string', optional: true}, userId: { type: 'string', optional: true },
name: {type: 'string', optional: true}, name: { type: 'string', optional: true },
timeout: {type: 'number', optional: true}, timeout: { type: 'number', optional: true },
type: {type: 'string'}, type: { type: 'string' },
key: {type: 'string'}, key: { type: 'string' },
method: {type: 'string'}, method: { type: 'string' },
paramsVector: { paramsVector: {
type: 'object', type: 'object',
properties: { properties: {
type: {type: 'string'}, type: { type: 'string' },
items: { items: {
type: 'array', type: 'array',
items: { items: {
@ -54,7 +54,7 @@ create.params = {
}, },
} }
export async function set ({job}) { export async function set ({ job }) {
await this.updateJob(job) await this.updateJob(job)
} }
@ -64,16 +64,16 @@ set.params = {
job: { job: {
type: 'object', type: 'object',
properties: { properties: {
id: {type: 'string'}, id: { type: 'string' },
name: {type: 'string', optional: true}, name: { type: 'string', optional: true },
timeout: {type: ['number', 'null'], optional: true}, timeout: { type: ['number', 'null'], optional: true },
type: {type: 'string', optional: true}, type: { type: 'string', optional: true },
key: {type: 'string', optional: true}, key: { type: 'string', optional: true },
method: {type: 'string', optional: true}, method: { type: 'string', optional: true },
paramsVector: { paramsVector: {
type: 'object', type: 'object',
properties: { properties: {
type: {type: 'string'}, type: { type: 'string' },
items: { items: {
type: 'array', type: 'array',
items: { items: {
@ -87,24 +87,24 @@ set.params = {
}, },
} }
async function delete_ ({id}) { async function delete_ ({ id }) {
await this.removeJob(id) await this.removeJob(id)
} }
delete_.permission = 'admin' delete_.permission = 'admin'
delete_.description = 'Deletes an existing job' delete_.description = 'Deletes an existing job'
delete_.params = { delete_.params = {
id: {type: 'string'}, id: { type: 'string' },
} }
export {delete_ as delete} export { delete_ as delete }
export async function runSequence ({idSequence}) { export async function runSequence ({ idSequence }) {
await this.runJobSequence(idSequence) await this.runJobSequence(idSequence)
} }
runSequence.permission = 'admin' runSequence.permission = 'admin'
runSequence.description = 'Runs jobs sequentially, in the provided order' runSequence.description = 'Runs jobs sequentially, in the provided order'
runSequence.params = { runSequence.params = {
idSequence: {type: 'array', items: {type: 'string'}}, idSequence: { type: 'array', items: { type: 'string' } },
} }

View File

@ -1,11 +1,12 @@
export async function get ({namespace}) { export async function get ({ namespace }) {
const logger = await this.getLogger(namespace) const logger = await this.getLogger(namespace)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const logs = {} const logs = {}
logger.createReadStream() logger
.on('data', (data) => { .createReadStream()
.on('data', data => {
logs[data.key] = data.value logs[data.key] = data.value
}) })
.on('end', () => { .on('end', () => {
@ -23,16 +24,16 @@ get.permission = 'admin'
// ------------------------------------------------------------------- // -------------------------------------------------------------------
async function delete_ ({namespace, id}) { async function delete_ ({ namespace, id }) {
const logger = await this.getLogger(namespace) const logger = await this.getLogger(namespace)
logger.del(id) logger.del(id)
} }
delete_.description = 'deletes one or several logs from a namespace' delete_.description = 'deletes one or several logs from a namespace'
delete_.params = { delete_.params = {
id: { type: [ 'array', 'string' ] }, id: { type: ['array', 'string'] },
namespace: { type: 'string' }, namespace: { type: 'string' },
} }
delete_.permission = 'admin' delete_.permission = 'admin'
export {delete_ as delete} export { delete_ as delete }

View File

@ -1,7 +1,7 @@
async function delete_ ({ message }) { async function delete_ ({ message }) {
await this.getXapi(message).call('message.destroy', message._xapiRef) await this.getXapi(message).call('message.destroy', message._xapiRef)
} }
export {delete_ as delete} export { delete_ as delete }
delete_.params = { delete_.params = {
id: { type: 'string' }, id: { type: 'string' },

View File

@ -4,7 +4,14 @@ export function getBondModes () {
return ['balance-slb', 'active-backup', 'lacp'] return ['balance-slb', 'active-backup', 'lacp']
} }
export async function create ({ pool, name, description, pif, mtu = 1500, vlan = 0 }) { export async function create ({
pool,
name,
description,
pif,
mtu = 1500,
vlan = 0,
}) {
return this.getXapi(pool).createNetwork({ return this.getXapi(pool).createNetwork({
name, name,
description, description,
@ -30,13 +37,19 @@ create.permission = 'admin'
// ================================================================= // =================================================================
export async function createBonded ({ pool, name, description, pifs, mtu = 1500, mac, bondMode }) { export async function createBonded ({
pool,
name,
description,
pifs,
mtu = 1500,
mac,
bondMode,
}) {
return this.getXapi(pool).createBondedNetwork({ return this.getXapi(pool).createBondedNetwork({
name, name,
description, description,
pifIds: mapToArray(pifs, pif => pifIds: mapToArray(pifs, pif => this.getObject(pif, 'PIF')._xapiId),
this.getObject(pif, 'PIF')._xapiId
),
mtu: +mtu, mtu: +mtu,
mac, mac,
bondMode, bondMode,
@ -56,14 +69,18 @@ createBonded.params = {
mtu: { type: ['integer', 'string'], optional: true }, mtu: { type: ['integer', 'string'], optional: true },
mac: { type: 'string', optional: true }, mac: { type: 'string', optional: true },
// RegExp since schema-inspector does not provide a param check based on an enumeration // RegExp since schema-inspector does not provide a param check based on an enumeration
bondMode: { type: 'string', pattern: new RegExp(`^(${getBondModes().join('|')})$`) }, bondMode: {
type: 'string',
pattern: new RegExp(`^(${getBondModes().join('|')})$`),
},
} }
createBonded.resolve = { createBonded.resolve = {
pool: ['pool', 'pool', 'administrate'], pool: ['pool', 'pool', 'administrate'],
} }
createBonded.permission = 'admin' createBonded.permission = 'admin'
createBonded.description = 'Create a bonded network. bondMode can be balance-slb, active-backup or lacp' createBonded.description =
'Create a bonded network. bondMode can be balance-slb, active-backup or lacp'
// =================================================================== // ===================================================================
@ -109,7 +126,7 @@ set.resolve = {
export async function delete_ ({ network }) { export async function delete_ ({ network }) {
return this.getXapi(network).deleteNetwork(network._xapiId) return this.getXapi(network).deleteNetwork(network._xapiId)
} }
export {delete_ as delete} export { delete_ as delete }
delete_.params = { delete_.params = {
id: { type: 'string' }, id: { type: 'string' },

View File

@ -3,11 +3,11 @@
// =================================================================== // ===================================================================
// Delete // Delete
async function delete_ ({PBD}) { async function delete_ ({ PBD }) {
// TODO: check if PBD is attached before // TODO: check if PBD is attached before
await this.getXapi(PBD).call('PBD.destroy', PBD._xapiRef) await this.getXapi(PBD).call('PBD.destroy', PBD._xapiRef)
} }
export {delete_ as delete} export { delete_ as delete }
delete_.params = { delete_.params = {
id: { type: 'string' }, id: { type: 'string' },
@ -35,7 +35,7 @@ disconnect.resolve = {
// =================================================================== // ===================================================================
// Connect // Connect
export async function connect ({PBD}) { export async function connect ({ PBD }) {
// TODO: check if PBD is attached before // TODO: check if PBD is attached before
await this.getXapi(PBD).call('PBD.plug', PBD._xapiRef) await this.getXapi(PBD).call('PBD.plug', PBD._xapiRef)
} }

View File

@ -13,11 +13,11 @@ export function getIpv6ConfigurationModes () {
// =================================================================== // ===================================================================
// Delete // Delete
async function delete_ ({pif}) { async function delete_ ({ pif }) {
// TODO: check if PIF is attached before // TODO: check if PIF is attached before
await this.getXapi(pif).call('PIF.destroy', pif._xapiRef) await this.getXapi(pif).call('PIF.destroy', pif._xapiRef)
} }
export {delete_ as delete} export { delete_ as delete }
delete_.params = { delete_.params = {
id: { type: 'string' }, id: { type: 'string' },
@ -30,7 +30,7 @@ delete_.resolve = {
// =================================================================== // ===================================================================
// Disconnect // Disconnect
export async function disconnect ({pif}) { export async function disconnect ({ pif }) {
// TODO: check if PIF is attached before // TODO: check if PIF is attached before
await this.getXapi(pif).call('PIF.unplug', pif._xapiRef) await this.getXapi(pif).call('PIF.unplug', pif._xapiRef)
} }
@ -45,7 +45,7 @@ disconnect.resolve = {
// =================================================================== // ===================================================================
// Connect // Connect
export async function connect ({pif}) { export async function connect ({ pif }) {
// TODO: check if PIF is attached before // TODO: check if PIF is attached before
await this.getXapi(pif).call('PIF.plug', pif._xapiRef) await this.getXapi(pif).call('PIF.plug', pif._xapiRef)
} }
@ -60,8 +60,23 @@ connect.resolve = {
// =================================================================== // ===================================================================
// Reconfigure IP // Reconfigure IP
export async function reconfigureIp ({ pif, mode = 'DHCP', ip = '', netmask = '', gateway = '', dns = '' }) { export async function reconfigureIp ({
await this.getXapi(pif).call('PIF.reconfigure_ip', pif._xapiRef, mode, ip, netmask, gateway, dns) pif,
mode = 'DHCP',
ip = '',
netmask = '',
gateway = '',
dns = '',
}) {
await this.getXapi(pif).call(
'PIF.reconfigure_ip',
pif._xapiRef,
mode,
ip,
netmask,
gateway,
dns
)
} }
reconfigureIp.params = { reconfigureIp.params = {

View File

@ -38,7 +38,7 @@ set.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function setDefaultSr ({ sr }) { export async function setDefaultSr ({ sr }) {
await this.hasPermissions(this.user.id, [ [ sr.$pool, 'administrate' ] ]) await this.hasPermissions(this.user.id, [[sr.$pool, 'administrate']])
await this.getXapi(sr).setDefaultSr(sr._xapiId) await this.getXapi(sr).setDefaultSr(sr._xapiId)
} }
@ -58,7 +58,7 @@ setDefaultSr.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function setPoolMaster ({ host }) { export async function setPoolMaster ({ host }) {
await this.hasPermissions(this.user.id, [ [ host.$pool, 'administrate' ] ]) await this.hasPermissions(this.user.id, [[host.$pool, 'administrate']])
await this.getXapi(host).setPoolMaster(host._xapiId) await this.getXapi(host).setPoolMaster(host._xapiId)
} }
@ -75,7 +75,7 @@ setPoolMaster.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function installPatch ({pool, patch: patchUuid}) { export async function installPatch ({ pool, patch: patchUuid }) {
await this.getXapi(pool).installPoolPatchOnAllHosts(patchUuid) await this.getXapi(pool).installPoolPatchOnAllHosts(patchUuid)
} }
@ -107,11 +107,12 @@ installAllPatches.resolve = {
pool: ['pool', 'pool', 'administrate'], pool: ['pool', 'pool', 'administrate'],
} }
installAllPatches.description = 'Install automatically all patches for every hosts of a pool' installAllPatches.description =
'Install automatically all patches for every hosts of a pool'
// ------------------------------------------------------------------- // -------------------------------------------------------------------
async function handlePatchUpload (req, res, {pool}) { async function handlePatchUpload (req, res, { pool }) {
const contentLength = req.headers['content-length'] const contentLength = req.headers['content-length']
if (!contentLength) { if (!contentLength) {
res.writeHead(411) res.writeHead(411)
@ -122,9 +123,9 @@ async function handlePatchUpload (req, res, {pool}) {
await this.getXapi(pool).uploadPoolPatch(req, contentLength) await this.getXapi(pool).uploadPoolPatch(req, contentLength)
} }
export async function uploadPatch ({pool}) { export async function uploadPatch ({ pool }) {
return { return {
$sendTo: await this.registerHttpRequest(handlePatchUpload, {pool}), $sendTo: await this.registerHttpRequest(handlePatchUpload, { pool }),
} }
} }
@ -139,7 +140,7 @@ uploadPatch.resolve = {
// Compatibility // Compatibility
// //
// TODO: remove when no longer used in xo-web // TODO: remove when no longer used in xo-web
export {uploadPatch as patch} export { uploadPatch as patch }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@ -177,11 +178,8 @@ mergeInto.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function getLicenseState ({pool}) { export async function getLicenseState ({ pool }) {
return this.getXapi(pool).call( return this.getXapi(pool).call('pool.get_license_state', pool._xapiId.$ref)
'pool.get_license_state',
pool._xapiId.$ref
)
} }
getLicenseState.params = { getLicenseState.params = {
@ -215,11 +213,14 @@ async function handleInstallSupplementalPack (req, res, { poolId }) {
export async function installSupplementalPack ({ pool }) { export async function installSupplementalPack ({ pool }) {
return { return {
$sendTo: await this.registerHttpRequest(handleInstallSupplementalPack, { poolId: pool.id }), $sendTo: await this.registerHttpRequest(handleInstallSupplementalPack, {
poolId: pool.id,
}),
} }
} }
installSupplementalPack.description = 'installs supplemental pack from ISO file on all hosts' installSupplementalPack.description =
'installs supplemental pack from ISO file on all hosts'
installSupplementalPack.params = { installSupplementalPack.params = {
pool: { type: 'string' }, pool: { type: 'string' },

View File

@ -5,68 +5,68 @@ export async function getAll () {
getAll.permission = 'admin' getAll.permission = 'admin'
getAll.description = 'Gets all existing fs remote points' getAll.description = 'Gets all existing fs remote points'
export async function get ({id}) { export async function get ({ id }) {
return this.getRemote(id) return this.getRemote(id)
} }
get.permission = 'admin' get.permission = 'admin'
get.description = 'Gets an existing fs remote point' get.description = 'Gets an existing fs remote point'
get.params = { get.params = {
id: {type: 'string'}, id: { type: 'string' },
} }
export async function test ({id}) { export async function test ({ id }) {
return this.testRemote(id) return this.testRemote(id)
} }
test.permission = 'admin' test.permission = 'admin'
test.description = 'Performs a read/write matching test on a remote point' test.description = 'Performs a read/write matching test on a remote point'
test.params = { test.params = {
id: {type: 'string'}, id: { type: 'string' },
} }
export async function list ({id}) { export async function list ({ id }) {
return this.listRemoteBackups(id) return this.listRemoteBackups(id)
} }
list.permission = 'admin' list.permission = 'admin'
list.description = 'Lists the files found in a remote point' list.description = 'Lists the files found in a remote point'
list.params = { list.params = {
id: {type: 'string'}, id: { type: 'string' },
} }
export async function create ({name, url}) { export async function create ({ name, url }) {
return this.createRemote({name, url}) return this.createRemote({ name, url })
} }
create.permission = 'admin' create.permission = 'admin'
create.description = 'Creates a new fs remote point' create.description = 'Creates a new fs remote point'
create.params = { create.params = {
name: {type: 'string'}, name: { type: 'string' },
url: {type: 'string'}, url: { type: 'string' },
} }
export async function set ({id, name, url, enabled}) { export async function set ({ id, name, url, enabled }) {
await this.updateRemote(id, {name, url, enabled}) await this.updateRemote(id, { name, url, enabled })
} }
set.permission = 'admin' set.permission = 'admin'
set.description = 'Modifies an existing fs remote point' set.description = 'Modifies an existing fs remote point'
set.params = { set.params = {
id: {type: 'string'}, id: { type: 'string' },
name: {type: 'string', optional: true}, name: { type: 'string', optional: true },
url: {type: 'string', optional: true}, url: { type: 'string', optional: true },
enabled: {type: 'boolean', optional: true}, enabled: { type: 'boolean', optional: true },
} }
async function delete_ ({id}) { async function delete_ ({ id }) {
await this.removeRemote(id) await this.removeRemote(id)
} }
delete_.permission = 'admin' delete_.permission = 'admin'
delete_.description = 'Deletes an existing fs remote point' delete_.description = 'Deletes an existing fs remote point'
delete_.params = { delete_.params = {
id: {type: 'string'}, id: { type: 'string' },
} }
export {delete_ as delete} export { delete_ as delete }

View File

@ -1,6 +1,4 @@
import { import { unauthorized } from 'xo-common/api-errors'
unauthorized,
} from 'xo-common/api-errors'
// =================================================================== // ===================================================================
@ -237,4 +235,5 @@ export function recomputeAllLimits () {
} }
recomputeAllLimits.permission = 'admin' recomputeAllLimits.permission = 'admin'
recomputeAllLimits.description = 'Recompute manually the current resource set usage' recomputeAllLimits.description =
'Recompute manually the current resource set usage'

View File

@ -14,20 +14,26 @@ export async function get (id) {
get.permission = 'admin' get.permission = 'admin'
get.description = 'Gets an existing schedule' get.description = 'Gets an existing schedule'
get.params = { get.params = {
id: {type: 'string'}, id: { type: 'string' },
} }
export async function create ({ jobId, cron, enabled, name, timezone }) { export async function create ({ jobId, cron, enabled, name, timezone }) {
return /* await */ this.createSchedule(this.session.get('user_id'), { job: jobId, cron, enabled, name, timezone }) return /* await */ this.createSchedule(this.session.get('user_id'), {
job: jobId,
cron,
enabled,
name,
timezone,
})
} }
create.permission = 'admin' create.permission = 'admin'
create.description = 'Creates a new schedule' create.description = 'Creates a new schedule'
create.params = { create.params = {
jobId: {type: 'string'}, jobId: { type: 'string' },
cron: {type: 'string'}, cron: { type: 'string' },
enabled: {type: 'boolean', optional: true}, enabled: { type: 'boolean', optional: true },
name: {type: 'string', optional: true}, name: { type: 'string', optional: true },
} }
export async function set ({ id, jobId, cron, enabled, name, timezone }) { export async function set ({ id, jobId, cron, enabled, name, timezone }) {
@ -37,21 +43,21 @@ export async function set ({ id, jobId, cron, enabled, name, timezone }) {
set.permission = 'admin' set.permission = 'admin'
set.description = 'Modifies an existing schedule' set.description = 'Modifies an existing schedule'
set.params = { set.params = {
id: {type: 'string'}, id: { type: 'string' },
jobId: {type: 'string', optional: true}, jobId: { type: 'string', optional: true },
cron: {type: 'string', optional: true}, cron: { type: 'string', optional: true },
enabled: {type: 'boolean', optional: true}, enabled: { type: 'boolean', optional: true },
name: {type: 'string', optional: true}, name: { type: 'string', optional: true },
} }
async function delete_ ({id}) { async function delete_ ({ id }) {
await this.removeSchedule(id) await this.removeSchedule(id)
} }
delete_.permission = 'admin' delete_.permission = 'admin'
delete_.description = 'Deletes an existing schedule' delete_.description = 'Deletes an existing schedule'
delete_.params = { delete_.params = {
id: {type: 'string'}, id: { type: 'string' },
} }
export {delete_ as delete} export { delete_ as delete }

View File

@ -1,16 +1,16 @@
export async function enable ({id}) { export async function enable ({ id }) {
const schedule = await this.getSchedule(id) const schedule = await this.getSchedule(id)
schedule.enabled = true schedule.enabled = true
await this.updateSchedule(id, schedule) await this.updateSchedule(id, schedule)
} }
enable.permission = 'admin' enable.permission = 'admin'
enable.description = 'Enables a schedule to run it\'s job as scheduled' enable.description = "Enables a schedule to run it's job as scheduled"
enable.params = { enable.params = {
id: {type: 'string'}, id: { type: 'string' },
} }
export async function disable ({id}) { export async function disable ({ id }) {
const schedule = await this.getSchedule(id) const schedule = await this.getSchedule(id)
schedule.enabled = false schedule.enabled = false
await this.updateSchedule(id, schedule) await this.updateSchedule(id, schedule)
@ -19,7 +19,7 @@ export async function disable ({id}) {
disable.permission = 'admin' disable.permission = 'admin'
disable.description = 'Disables a schedule' disable.description = 'Disables a schedule'
disable.params = { disable.params = {
id: {type: 'string'}, id: { type: 'string' },
} }
export function getScheduleTable () { export function getScheduleTable () {

View File

@ -1,10 +1,10 @@
import { ignoreErrors } from 'promise-toolbox' import { ignoreErrors } from 'promise-toolbox'
export async function add ({autoConnect = true, ...props}) { export async function add ({ autoConnect = true, ...props }) {
const server = await this.registerXenServer(props) const server = await this.registerXenServer(props)
if (autoConnect) { if (autoConnect) {
this.connectXenServer(server.id)::ignoreErrors() ;this.connectXenServer(server.id)::ignoreErrors()
} }
return server.id return server.id
@ -40,7 +40,7 @@ add.params = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function remove ({id}) { export async function remove ({ id }) {
await this.unregisterXenServer(id) await this.unregisterXenServer(id)
} }
@ -68,7 +68,7 @@ getAll.permission = 'admin'
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function set ({id, ...props}) { export async function set ({ id, ...props }) {
await this.updateXenServer(id, props) await this.updateXenServer(id, props)
} }
@ -104,8 +104,8 @@ set.params = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function connect ({id}) { export async function connect ({ id }) {
this.updateXenServer(id, {enabled: true})::ignoreErrors() ;this.updateXenServer(id, { enabled: true })::ignoreErrors()
await this.connectXenServer(id) await this.connectXenServer(id)
} }
@ -121,8 +121,8 @@ connect.params = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function disconnect ({id}) { export async function disconnect ({ id }) {
this.updateXenServer(id, {enabled: false})::ignoreErrors() ;this.updateXenServer(id, { enabled: false })::ignoreErrors()
await this.disconnectXenServer(id) await this.disconnectXenServer(id)
} }

View File

@ -1,7 +1,7 @@
import {deprecate} from 'util' import { deprecate } from 'util'
import { getUserPublicProperties } from '../utils' import { getUserPublicProperties } from '../utils'
import {invalidCredentials} from 'xo-common/api-errors' import { invalidCredentials } from 'xo-common/api-errors'
// =================================================================== // ===================================================================
@ -19,7 +19,10 @@ signIn.description = 'sign in'
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export const signInWithPassword = deprecate(signIn, 'use session.signIn() instead') export const signInWithPassword = deprecate(
signIn,
'use session.signIn() instead'
)
signInWithPassword.params = { signInWithPassword.params = {
email: { type: 'string' }, email: { type: 'string' },

View File

@ -1,12 +1,7 @@
import { some } from 'lodash' import { some } from 'lodash'
import { asInteger } from '../xapi/utils' import { asInteger } from '../xapi/utils'
import { import { asyncMap, ensureArray, forEach, parseXml } from '../utils'
asyncMap,
ensureArray,
forEach,
parseXml,
} from '../utils'
// =================================================================== // ===================================================================
@ -50,10 +45,11 @@ scan.resolve = {
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
const srIsBackingHa = (sr) => sr.$pool.ha_enabled && some(sr.$pool.$ha_statefiles, f => f.$SR === sr) const srIsBackingHa = sr =>
sr.$pool.ha_enabled && some(sr.$pool.$ha_statefiles, f => f.$SR === sr)
// TODO: find a way to call this "delete" and not destroy // TODO: find a way to call this "delete" and not destroy
export async function destroy ({sr}) { export async function destroy ({ sr }) {
const xapi = this.getXapi(sr) const xapi = this.getXapi(sr)
if (sr.SR_type !== 'xosan') { if (sr.SR_type !== 'xosan') {
await xapi.destroySr(sr._xapiId) await xapi.destroySr(sr._xapiId)
@ -61,7 +57,9 @@ export async function destroy ({sr}) {
} }
const xapiSr = xapi.getObject(sr) const xapiSr = xapi.getObject(sr)
if (srIsBackingHa(xapiSr)) { if (srIsBackingHa(xapiSr)) {
throw new Error('You tried to remove a SR the High Availability is relying on. Please disable HA first.') throw new Error(
'You tried to remove a SR the High Availability is relying on. Please disable HA first.'
)
} }
const config = xapi.xo.getData(sr, 'xosan_config') const config = xapi.xo.getData(sr, 'xosan_config')
// we simply forget because the hosted disks are being destroyed with the VMs // we simply forget because the hosted disks are being destroyed with the VMs
@ -239,12 +237,7 @@ createNfs.resolve = {
// This functions creates an HBA SR // This functions creates an HBA SR
export async function createHba ({ export async function createHba ({ host, nameLabel, nameDescription, scsiId }) {
host,
nameLabel,
nameDescription,
scsiId,
}) {
const xapi = this.getXapi(host) const xapi = this.getXapi(host)
const deviceConfig = { const deviceConfig = {
@ -284,12 +277,7 @@ createHba.resolve = {
// This functions creates a local LVM SR // This functions creates a local LVM SR
export async function createLvm ({ export async function createLvm ({ host, nameLabel, nameDescription, device }) {
host,
nameLabel,
nameDescription,
device,
}) {
const xapi = this.getXapi(host) const xapi = this.getXapi(host)
const deviceConfig = { const deviceConfig = {
@ -328,10 +316,7 @@ createLvm.resolve = {
// This function helps to detect all NFS shares (exports) on a NFS server // This function helps to detect all NFS shares (exports) on a NFS server
// Return a table of exports with their paths and ACLs // Return a table of exports with their paths and ACLs
export async function probeNfs ({ export async function probeNfs ({ host, server }) {
host,
server,
}) {
const xapi = this.getXapi(host) const xapi = this.getXapi(host)
const deviceConfig = { const deviceConfig = {
@ -341,13 +326,7 @@ export async function probeNfs ({
let xml let xml
try { try {
await xapi.call( await xapi.call('SR.probe', host._xapiRef, deviceConfig, 'nfs', {})
'SR.probe',
host._xapiRef,
deviceConfig,
'nfs',
{}
)
throw new Error('the call above should have thrown an error') throw new Error('the call above should have thrown an error')
} catch (error) { } catch (error) {
@ -381,20 +360,13 @@ probeNfs.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// This function helps to detect all HBA devices on the host // This function helps to detect all HBA devices on the host
export async function probeHba ({ export async function probeHba ({ host }) {
host,
}) {
const xapi = this.getXapi(host) const xapi = this.getXapi(host)
let xml let xml
try { try {
await xapi.call( await xapi.call('SR.probe', host._xapiRef, 'type', {})
'SR.probe',
host._xapiRef,
'type',
{}
)
throw new Error('the call above should have thrown an error') throw new Error('the call above should have thrown an error')
} catch (error) { } catch (error) {
@ -527,13 +499,7 @@ export async function probeIscsiIqns ({
let xml let xml
try { try {
await xapi.call( await xapi.call('SR.probe', host._xapiRef, deviceConfig, 'lvmoiscsi', {})
'SR.probe',
host._xapiRef,
deviceConfig,
'lvmoiscsi',
{}
)
throw new Error('the call above should have thrown an error') throw new Error('the call above should have thrown an error')
} catch (error) { } catch (error) {
@ -605,13 +571,7 @@ export async function probeIscsiLuns ({
let xml let xml
try { try {
await xapi.call( await xapi.call('SR.probe', host._xapiRef, deviceConfig, 'lvmoiscsi', {})
'SR.probe',
host._xapiRef,
deviceConfig,
'lvmoiscsi',
{}
)
throw new Error('the call above should have thrown an error') throw new Error('the call above should have thrown an error')
} catch (error) { } catch (error) {
@ -681,7 +641,9 @@ export async function probeIscsiExists ({
deviceConfig.port = asInteger(port) deviceConfig.port = asInteger(port)
} }
const xml = parseXml(await xapi.call('SR.probe', host._xapiRef, deviceConfig, 'lvmoiscsi', {})) const xml = parseXml(
await xapi.call('SR.probe', host._xapiRef, deviceConfig, 'lvmoiscsi', {})
)
const srs = [] const srs = []
forEach(ensureArray(xml['SRlist'].SR), sr => { forEach(ensureArray(xml['SRlist'].SR), sr => {
@ -710,11 +672,7 @@ probeIscsiExists.resolve = {
// This function helps to detect if this NFS SR already exists in XAPI // This function helps to detect if this NFS SR already exists in XAPI
// It returns a table of SR UUID, empty if no existing connections // It returns a table of SR UUID, empty if no existing connections
export async function probeNfsExists ({ export async function probeNfsExists ({ host, server, serverPath }) {
host,
server,
serverPath,
}) {
const xapi = this.getXapi(host) const xapi = this.getXapi(host)
const deviceConfig = { const deviceConfig = {
@ -722,7 +680,9 @@ export async function probeNfsExists ({
serverpath: serverPath, serverpath: serverPath,
} }
const xml = parseXml(await xapi.call('SR.probe', host._xapiRef, deviceConfig, 'nfs', {})) const xml = parseXml(
await xapi.call('SR.probe', host._xapiRef, deviceConfig, 'nfs', {})
)
const srs = [] const srs = []

View File

@ -20,7 +20,8 @@ export function getMethodsInfo () {
return methods return methods
} }
getMethodsInfo.description = 'returns the signatures of all available API methods' getMethodsInfo.description =
'returns the signatures of all available API methods'
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@ -46,7 +47,7 @@ listMethods.description = 'returns the name of all available API methods'
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export function methodSignature ({method: name}) { export function methodSignature ({ method: name }) {
const method = this.apiMethods[name] const method = this.apiMethods[name]
if (!method) { if (!method) {

View File

@ -1,4 +1,4 @@
export async function add ({tag, object}) { export async function add ({ tag, object }) {
await this.getXapi(object).addTag(object._xapiId, tag) await this.getXapi(object).addTag(object._xapiId, tag)
} }
@ -15,7 +15,7 @@ add.params = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function remove ({tag, object}) { export async function remove ({ tag, object }) {
await this.getXapi(object).removeTag(object._xapiId, tag) await this.getXapi(object).removeTag(object._xapiId, tag)
} }

View File

@ -1,4 +1,4 @@
export async function cancel ({task}) { export async function cancel ({ task }) {
await this.getXapi(task).call('task.cancel', task._xapiRef) await this.getXapi(task).call('task.cancel', task._xapiRef)
} }
@ -12,7 +12,7 @@ cancel.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function destroy ({task}) { export async function destroy ({ task }) {
await this.getXapi(task).call('task.destroy', task._xapiRef) await this.getXapi(task).call('task.destroy', task._xapiRef)
} }

View File

@ -13,9 +13,7 @@ getPermissionsForUser.params = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export function hasPermission ({ userId, objectId, permission }) { export function hasPermission ({ userId, objectId, permission }) {
return this.hasPermissions(userId, [ return this.hasPermissions(userId, [[objectId, permission]])
[ objectId, permission ],
])
} }
hasPermission.permission = 'admin' hasPermission.permission = 'admin'
@ -34,7 +32,7 @@ hasPermission.params = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export function wait ({duration, returnValue}) { export function wait ({ duration, returnValue }) {
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(() => { setTimeout(() => {
resolve(returnValue) resolve(returnValue)
@ -81,6 +79,6 @@ copyVm.params = {
} }
copyVm.resolve = { copyVm.resolve = {
vm: [ 'vm', 'VM' ], vm: ['vm', 'VM'],
sr: [ 'sr', 'SR' ], sr: ['sr', 'SR'],
} }

View File

@ -12,7 +12,7 @@ create.description = 'create a new authentication token'
create.params = { create.params = {
expiresIn: { expiresIn: {
optional: true, optional: true,
type: [ 'number', 'string' ], type: ['number', 'string'],
}, },
} }
@ -21,11 +21,11 @@ create.permission = '' // sign in
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// TODO: an user should be able to delete its own tokens. // TODO: an user should be able to delete its own tokens.
async function delete_ ({token: id}) { async function delete_ ({ token: id }) {
await this.deleteAuthenticationToken(id) await this.deleteAuthenticationToken(id)
} }
export {delete_ as delete} export { delete_ as delete }
delete_.description = 'delete an existing authentication token' delete_.description = 'delete an existing authentication token'

View File

@ -1,10 +1,10 @@
import {invalidParameters} from 'xo-common/api-errors' import { invalidParameters } from 'xo-common/api-errors'
import { getUserPublicProperties, mapToArray } from '../utils' import { getUserPublicProperties, mapToArray } from '../utils'
// =================================================================== // ===================================================================
export async function create ({email, password, permission}) { 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' create.description = 'creates a new user'
@ -20,7 +20,7 @@ create.params = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Deletes an existing user. // Deletes an existing user.
async function delete_ ({id}) { async function delete_ ({ id }) {
if (id === this.session.get('user_id')) { if (id === this.session.get('user_id')) {
throw invalidParameters('a user cannot delete itself') throw invalidParameters('a user cannot delete itself')
} }
@ -29,7 +29,7 @@ async function delete_ ({id}) {
} }
// delete is not a valid identifier. // delete is not a valid identifier.
export {delete_ as delete} export { delete_ as delete }
delete_.description = 'deletes an existing user' delete_.description = 'deletes an existing user'
@ -57,17 +57,19 @@ getAll.permission = 'admin'
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function set ({id, email, password, permission, preferences}) { export async function set ({ id, email, password, permission, preferences }) {
const isAdmin = this.user && this.user.permission === 'admin' const isAdmin = this.user && this.user.permission === 'admin'
if (isAdmin) { if (isAdmin) {
if (permission && id === this.session.get('user_id')) { if (permission && id === this.session.get('user_id')) {
throw invalidParameters('a user cannot change its own permission') throw invalidParameters('a user cannot change its own permission')
} }
} else if (email || password || permission) { } else if (email || password || permission) {
throw invalidParameters('this properties can only changed by an administrator') throw invalidParameters(
'this properties can only changed by an administrator'
)
} }
await this.updateUser(id, {email, password, permission, preferences}) await this.updateUser(id, { email, password, permission, preferences })
} }
set.description = 'changes the properties of an existing user' set.description = 'changes the properties of an existing user'
@ -84,16 +86,17 @@ set.params = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function changePassword ({oldPassword, newPassword}) { export async function changePassword ({ oldPassword, newPassword }) {
const id = this.session.get('user_id') const id = this.session.get('user_id')
await this.changeUserPassword(id, oldPassword, newPassword) await this.changeUserPassword(id, oldPassword, newPassword)
} }
changePassword.description = 'change password after checking old password (user function)' changePassword.description =
'change password after checking old password (user function)'
changePassword.permission = '' changePassword.permission = ''
changePassword.params = { changePassword.params = {
oldPassword: {type: 'string'}, oldPassword: { type: 'string' },
newPassword: {type: 'string'}, newPassword: { type: 'string' },
} }

View File

@ -1,6 +1,6 @@
// FIXME: too low level, should be removed. // FIXME: too low level, should be removed.
async function delete_ ({vbd}) { async function delete_ ({ vbd }) {
await this.getXapi(vbd).deleteVbd(vbd) await this.getXapi(vbd).deleteVbd(vbd)
} }
@ -16,7 +16,7 @@ export { delete_ as delete }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function disconnect ({vbd}) { export async function disconnect ({ vbd }) {
const xapi = this.getXapi(vbd) const xapi = this.getXapi(vbd)
await xapi.disconnectVbd(vbd._xapiRef) await xapi.disconnectVbd(vbd._xapiRef)
} }
@ -31,7 +31,7 @@ disconnect.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function connect ({vbd}) { export async function connect ({ vbd }) {
const xapi = this.getXapi(vbd) const xapi = this.getXapi(vbd)
await xapi.connectVbd(vbd._xapiRef) await xapi.connectVbd(vbd._xapiRef)
} }
@ -46,7 +46,7 @@ connect.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function set ({position, vbd}) { export async function set ({ position, vbd }) {
if (position !== undefined) { if (position !== undefined) {
const xapi = this.getXapi(vbd) const xapi = this.getXapi(vbd)
await xapi.call('VBD.set_userdevice', vbd._xapiRef, String(position)) await xapi.call('VBD.set_userdevice', vbd._xapiRef, String(position))
@ -66,7 +66,7 @@ set.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function setBootable ({vbd, bootable}) { export async function setBootable ({ vbd, bootable }) {
const xapi = this.getXapi(vbd) const xapi = this.getXapi(vbd)
await xapi.call('VBD.set_bootable', vbd._xapiRef, bootable) await xapi.call('VBD.set_bootable', vbd._xapiRef, bootable)

View File

@ -7,10 +7,11 @@ import { parseSize } from '../utils'
// ==================================================================== // ====================================================================
export async function delete_ ({vdi}) { export async function delete_ ({ vdi }) {
const resourceSet = reduce( const resourceSet = reduce(
vdi.$VBDs, vdi.$VBDs,
(resourceSet, vbd) => resourceSet || this.getObject(this.getObject(vbd, 'VBD').VM).resourceSet, (resourceSet, vbd) =>
resourceSet || this.getObject(this.getObject(vbd, 'VBD').VM).resourceSet,
undefined undefined
) )
@ -35,7 +36,7 @@ export { delete_ as delete }
// FIXME: human readable strings should be handled. // FIXME: human readable strings should be handled.
export async function set (params) { export async function set (params) {
const {vdi} = params const { vdi } = params
const xapi = this.getXapi(vdi) const xapi = this.getXapi(vdi)
const ref = vdi._xapiRef const ref = vdi._xapiRef
@ -52,18 +53,26 @@ export async function set (params) {
const vbds = vdi.$VBDs const vbds = vdi.$VBDs
if ( if (
(vbds.length === 1) && vbds.length === 1 &&
((resourceSetId = xapi.xo.getData(this.getObject(vbds[0], 'VBD').VM, 'resourceSet')) !== undefined) (resourceSetId = xapi.xo.getData(
this.getObject(vbds[0], 'VBD').VM,
'resourceSet'
)) !== undefined
) { ) {
if (this.user.permission !== 'admin') { if (this.user.permission !== 'admin') {
await this.checkResourceSetConstraints(resourceSetId, this.user.id) await this.checkResourceSetConstraints(resourceSetId, this.user.id)
} }
await this.allocateLimitsInResourceSet({ disk: size - vdi.size }, resourceSetId) await this.allocateLimitsInResourceSet(
} else if (!( { disk: size - vdi.size },
(this.user.permission === 'admin') || resourceSetId
(await this.hasPermissions(this.user.id, [ [ vdi.$SR, 'operate' ] ])) )
)) { } else if (
!(
this.user.permission === 'admin' ||
(await this.hasPermissions(this.user.id, [[vdi.$SR, 'operate']]))
)
) {
throw unauthorized() throw unauthorized()
} }
@ -72,14 +81,16 @@ export async function set (params) {
// Other fields. // Other fields.
const object = { const object = {
'name_label': 'name_label', name_label: 'name_label',
'name_description': 'name_description', name_description: 'name_description',
} }
for (const param in object) { for (const param in object) {
const fields = object[param] const fields = object[param]
if (!(param in params)) { continue } if (!(param in params)) {
continue
}
for (const field of (isArray(fields) ? fields : [fields])) { for (const field of isArray(fields) ? fields : [fields]) {
await xapi.call(`VDI.set_${field}`, ref, `${params[param]}`) await xapi.call(`VDI.set_${field}`, ref, `${params[param]}`)
} }
} }
@ -103,7 +114,7 @@ set.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function migrate ({vdi, sr}) { export async function migrate ({ vdi, sr }) {
const xapi = this.getXapi(vdi) const xapi = this.getXapi(vdi)
await xapi.moveVdi(vdi._xapiRef, sr._xapiRef) await xapi.moveVdi(vdi._xapiRef, sr._xapiRef)

View File

@ -5,8 +5,8 @@ import { diffItems } from '../utils'
// =================================================================== // ===================================================================
// TODO: move into vm and rename to removeInterface // TODO: move into vm and rename to removeInterface
async function delete_ ({vif}) { async function delete_ ({ vif }) {
this.allocIpAddresses( ;this.allocIpAddresses(
vif.id, vif.id,
null, null,
vif.allowedIpv4Addresses.concat(vif.allowedIpv6Addresses) vif.allowedIpv4Addresses.concat(vif.allowedIpv6Addresses)
@ -14,7 +14,7 @@ async function delete_ ({vif}) {
await this.getXapi(vif).deleteVif(vif._xapiId) await this.getXapi(vif).deleteVif(vif._xapiId)
} }
export {delete_ as delete} export { delete_ as delete }
delete_.params = { delete_.params = {
id: { type: 'string' }, id: { type: 'string' },
@ -27,7 +27,7 @@ delete_.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// TODO: move into vm and rename to disconnectInterface // TODO: move into vm and rename to disconnectInterface
export async function disconnect ({vif}) { export async function disconnect ({ vif }) {
// TODO: check if VIF is attached before // TODO: check if VIF is attached before
await this.getXapi(vif).disconnectVif(vif._xapiId) await this.getXapi(vif).disconnectVif(vif._xapiId)
} }
@ -42,7 +42,7 @@ disconnect.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// TODO: move into vm and rename to connectInterface // TODO: move into vm and rename to connectInterface
export async function connect ({vif}) { export async function connect ({ vif }) {
// TODO: check if VIF is attached before // TODO: check if VIF is attached before
await this.getXapi(vif).connectVif(vif._xapiId) await this.getXapi(vif).connectVif(vif._xapiId)
} }
@ -65,7 +65,9 @@ export async function set ({
allowedIpv6Addresses, allowedIpv6Addresses,
attached, attached,
}) { }) {
const oldIpAddresses = vif.allowedIpv4Addresses.concat(vif.allowedIpv6Addresses) const oldIpAddresses = vif.allowedIpv4Addresses.concat(
vif.allowedIpv6Addresses
)
const newIpAddresses = [] const newIpAddresses = []
{ {
const { push } = newIpAddresses const { push } = newIpAddresses
@ -96,15 +98,11 @@ export async function set ({
return return
} }
const [ addAddresses, removeAddresses ] = diffItems( const [addAddresses, removeAddresses] = diffItems(
newIpAddresses, newIpAddresses,
oldIpAddresses oldIpAddresses
) )
await this.allocIpAddresses( await this.allocIpAddresses(vif.id, addAddresses, removeAddresses)
vif.id,
addAddresses,
removeAddresses
)
return this.getXapi(vif).editVif(vif._xapiId, { return this.getXapi(vif).editVif(vif._xapiId, {
ipv4Allowed: allowedIpv4Addresses, ipv4Allowed: allowedIpv4Addresses,

View File

@ -25,14 +25,13 @@ function checkPermissionOnSrs (vm, permission = 'operate') {
return permissions.push([this.getObject(vdiId, 'VDI').$SR, permission]) return permissions.push([this.getObject(vdiId, 'VDI').$SR, permission])
}) })
return this.hasPermissions( return this.hasPermissions(this.session.get('user_id'), permissions).then(
this.session.get('user_id'), success => {
permissions if (!success) {
).then(success => { throw unauthorized()
if (!success) { }
throw unauthorized()
} }
}) )
} }
// =================================================================== // ===================================================================
@ -359,7 +358,7 @@ async function delete_ ({
// Update resource sets // Update resource sets
const resourceSet = xapi.xo.getData(vm._xapiId, 'resourceSet') const resourceSet = xapi.xo.getData(vm._xapiId, 'resourceSet')
if (resourceSet != null) { if (resourceSet != null) {
this.setVmResourceSet(vm._xapiId, null)::ignoreErrors() ;this.setVmResourceSet(vm._xapiId, null)::ignoreErrors()
} }
return xapi.deleteVm(vm._xapiId, deleteDisks, force) return xapi.deleteVm(vm._xapiId, deleteDisks, force)
@ -1239,8 +1238,10 @@ export async function createInterface ({
}) { }) {
const { resourceSet } = vm const { resourceSet } = vm
if (resourceSet != null) { if (resourceSet != null) {
await this.checkResourceSetConstraints(resourceSet, this.user.id, [ network.id ]) await this.checkResourceSetConstraints(resourceSet, this.user.id, [
} else if (!(await this.hasPermissions(this.user.id, [ [ network.id, 'view' ] ]))) { network.id,
])
} else if (!await this.hasPermissions(this.user.id, [[network.id, 'view']])) {
throw unauthorized() throw unauthorized()
} }

View File

@ -12,15 +12,17 @@ clean.permission = 'admin'
export async function exportConfig () { export async function exportConfig () {
return { return {
$getFrom: await this.registerHttpRequest((req, res) => { $getFrom: await this.registerHttpRequest(
res.writeHead(200, 'OK', { (req, res) => {
'content-disposition': 'attachment', res.writeHead(200, 'OK', {
}) 'content-disposition': 'attachment',
})
return this.exportConfig() return this.exportConfig()
}, },
undefined, undefined,
{ suffix: '/config.json' }), { suffix: '/config.json' }
),
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,7 @@
import Model from './model' import Model from './model'
import {BaseError} from 'make-error' import { BaseError } from 'make-error'
import {EventEmitter} from 'events' import { EventEmitter } from 'events'
import { import { isArray, isObject, map } from './utils'
isArray,
isObject,
map,
} from './utils'
// =================================================================== // ===================================================================
@ -39,34 +35,34 @@ export default class Collection extends EventEmitter {
models = [models] models = [models]
} }
const {Model} = this const { Model } = this
map(models, model => { map(
if (!(model instanceof Model)) { models,
model = new Model(model) model => {
} if (!(model instanceof Model)) {
model = new Model(model)
}
const error = model.validate() const error = model.validate()
if (error) { if (error) {
// TODO: Better system inspired by Backbone.js // TODO: Better system inspired by Backbone.js
throw error throw error
} }
return model.properties return model.properties
}, models) },
models
)
models = await this._add(models, opts) models = await this._add(models, opts)
this.emit('add', models) this.emit('add', models)
return array return array ? models : new this.Model(models[0])
? models
: new this.Model(models[0])
} }
async first (properties) { async first (properties) {
if (!isObject(properties)) { if (!isObject(properties)) {
properties = (properties !== undefined) properties = properties !== undefined ? { id: properties } : {}
? { id: properties }
: {}
} }
const model = await this._first(properties) const model = await this._first(properties)
@ -75,9 +71,7 @@ export default class Collection extends EventEmitter {
async get (properties) { async get (properties) {
if (!isObject(properties)) { if (!isObject(properties)) {
properties = (properties !== undefined) properties = properties !== undefined ? { id: properties } : {}
? { id: properties }
: {}
} }
return /* await */ this._get(properties) return /* await */ this._get(properties)
@ -100,37 +94,39 @@ export default class Collection extends EventEmitter {
models = [models] models = [models]
} }
const {Model} = this const { Model } = this
map(models, model => { map(
if (!(model instanceof Model)) { models,
// TODO: Problems, we may be mixing in some default model => {
// properties which will overwrite existing ones. if (!(model instanceof Model)) {
model = new Model(model) // TODO: Problems, we may be mixing in some default
} // properties which will overwrite existing ones.
model = new Model(model)
}
const id = model.get('id') const id = model.get('id')
// Missing models should be added not updated. // Missing models should be added not updated.
if (id === undefined) { if (id === undefined) {
// FIXME: should not throw an exception but return a rejected promise. // FIXME: should not throw an exception but return a rejected promise.
throw new Error('a model without an id cannot be updated') throw new Error('a model without an id cannot be updated')
} }
const error = model.validate() const error = model.validate()
if (error !== undefined) { if (error !== undefined) {
// TODO: Better system inspired by Backbone.js. // TODO: Better system inspired by Backbone.js.
throw error throw error
} }
return model.properties return model.properties
}, models) },
models
)
models = await this._update(models) models = await this._update(models)
this.emit('update', models) this.emit('update', models)
return array return array ? models : new this.Model(models[0])
? models
: new this.Model(models[0])
} }
// Methods to override in implementations. // Methods to override in implementations.
@ -165,8 +161,6 @@ export default class Collection extends EventEmitter {
async _first (properties) { async _first (properties) {
const models = await this.get(properties) const models = await this.get(properties)
return models.length return models.length ? models[0] : null
? models[0]
: null
} }
} }

View File

@ -1,5 +1,12 @@
import { createClient as createRedisClient } from 'redis' import { createClient as createRedisClient } from 'redis'
import { difference, filter, forEach, isEmpty, keys as getKeys, map } from 'lodash' import {
difference,
filter,
forEach,
isEmpty,
keys as getKeys,
map,
} from 'lodash'
import { ignoreErrors, promisifyAll } from 'promise-toolbox' import { ignoreErrors, promisifyAll } from 'promise-toolbox'
import { v4 as generateUuid } from 'uuid' import { v4 as generateUuid } from 'uuid'
@ -28,33 +35,33 @@ import { asyncMap } from '../utils'
const VERSION = '20170905' const VERSION = '20170905'
export default class Redis extends Collection { export default class Redis extends Collection {
constructor ({ constructor ({ connection, indexes = [], prefix, uri }) {
connection,
indexes = [],
prefix,
uri,
}) {
super() super()
this.indexes = indexes this.indexes = indexes
this.prefix = prefix this.prefix = prefix
const redis = this.redis = promisifyAll(connection || createRedisClient(uri)) const redis = (this.redis = promisifyAll(
connection || createRedisClient(uri)
))
const key = `${prefix}:version` const key = `${prefix}:version`
redis.get(key).then(version => { redis
if (version === VERSION) { .get(key)
return .then(version => {
} if (version === VERSION) {
return
}
let p = redis.set(`${prefix}:version`, VERSION) let p = redis.set(`${prefix}:version`, VERSION)
switch (version) { switch (version) {
case undefined: case undefined:
// - clean indexes // - clean indexes
// - indexes are now case insensitive // - indexes are now case insensitive
p = p.then(() => this.rebuildIndexes()) p = p.then(() => this.rebuildIndexes())
} }
return p return p
})::ignoreErrors() })
::ignoreErrors()
} }
rebuildIndexes () { rebuildIndexes () {
@ -66,113 +73,120 @@ export default class Redis extends Collection {
const idsIndex = `${prefix}_ids` const idsIndex = `${prefix}_ids`
return asyncMap(indexes, index => return asyncMap(indexes, index =>
redis.keys(`${prefix}_${index}:*`).then(keys => redis
keys.length !== 0 && redis.del(keys) .keys(`${prefix}_${index}:*`)
.then(keys => keys.length !== 0 && redis.del(keys))
).then(() =>
asyncMap(redis.smembers(idsIndex), id =>
redis.hgetall(`${prefix}:${id}`).then(
values =>
values == null
? redis.srem(idsIndex, id) // entry no longer exists
: asyncMap(indexes, index => {
const value = values[index]
if (value !== undefined) {
return redis.sadd(
`${prefix}_${index}:${String(value).toLowerCase()}`,
id
)
}
})
)
) )
).then(() => asyncMap(redis.smembers(idsIndex), id => )
redis.hgetall(`${prefix}:${id}`).then(values =>
values == null
? redis.srem(idsIndex, id) // entry no longer exists
: asyncMap(indexes, index => {
const value = values[index]
if (value !== undefined) {
return redis.sadd(
`${prefix}_${index}:${String(value).toLowerCase()}`,
id
)
}
})
)
))
} }
_extract (ids) { _extract (ids) {
const prefix = this.prefix + ':' const prefix = this.prefix + ':'
const {redis} = this const { redis } = this
const models = [] const models = []
return Promise.all(map(ids, id => { return Promise.all(
return redis.hgetall(prefix + id).then(model => { map(ids, id => {
// If empty, consider it a no match. return redis.hgetall(prefix + id).then(model => {
if (isEmpty(model)) { // If empty, consider it a no match.
return if (isEmpty(model)) {
} return
}
// Mix the identifier in. // Mix the identifier in.
model.id = id model.id = id
models.push(model) models.push(model)
})
}) })
})).then(() => models) ).then(() => models)
} }
_add (models, {replace = false} = {}) { _add (models, { replace = false } = {}) {
// TODO: remove “replace” which is a temporary measure, implement // TODO: remove “replace” which is a temporary measure, implement
// “set()” instead. // “set()” instead.
const {indexes, prefix, redis} = this const { indexes, prefix, redis } = this
return Promise.all(map(models, async model => { return Promise.all(
// Generate a new identifier if necessary. map(models, async model => {
if (model.id === undefined) { // Generate a new identifier if necessary.
model.id = generateUuid() if (model.id === undefined) {
} model.id = generateUuid()
const { id } = model }
const { id } = model
const newEntry = await redis.sadd(prefix + '_ids', id) const newEntry = await redis.sadd(prefix + '_ids', id)
if (!newEntry) { if (!newEntry) {
if (!replace) { if (!replace) {
throw new ModelAlreadyExists(id) throw new ModelAlreadyExists(id)
}
// remove the previous values from indexes
if (indexes.length !== 0) {
const previous = await redis.hgetall(`${prefix}:${id}`)
await asyncMap(indexes, index => {
const value = previous[index]
if (value !== undefined) {
return redis.srem(
`${prefix}_${index}:${String(value).toLowerCase()}`,
id
)
}
})
}
} }
// remove the previous values from indexes const params = []
if (indexes.length !== 0) { forEach(model, (value, name) => {
const previous = await redis.hgetall(`${prefix}:${id}`) // No need to store the identifier (already in the key).
await asyncMap(indexes, index => { if (name === 'id') {
const value = previous[index] return
if (value !== undefined) { }
return redis.srem(`${prefix}_${index}:${String(value).toLowerCase()}`, id)
}
})
}
}
const params = [] params.push(name, value)
forEach(model, (value, name) => { })
// No need to store the identifier (already in the key).
if (name === 'id') {
return
}
params.push(name, value) const key = `${prefix}:${id}`
const promises = [redis.del(key), redis.hmset(key, ...params)]
// Update indexes.
forEach(indexes, index => {
const value = model[index]
if (value === undefined) {
return
}
const key = prefix + '_' + index + ':' + String(value).toLowerCase()
promises.push(redis.sadd(key, id))
})
await Promise.all(promises)
return model
}) })
)
const key = `${prefix}:${id}`
const promises = [
redis.del(key),
redis.hmset(key, ...params),
]
// Update indexes.
forEach(indexes, (index) => {
const value = model[index]
if (value === undefined) {
return
}
const key = prefix + '_' + index + ':' + String(value).toLowerCase()
promises.push(redis.sadd(key, id))
})
await Promise.all(promises)
return model
}))
} }
_get (properties) { _get (properties) {
const {prefix, redis} = this const { prefix, redis } = this
if (isEmpty(properties)) { if (isEmpty(properties)) {
return redis.smembers(prefix + '_ids').then(ids => this._extract(ids)) return redis.smembers(prefix + '_ids').then(ids => this._extract(ids))
@ -183,13 +197,11 @@ export default class Redis extends Collection {
if (id !== undefined) { if (id !== undefined) {
delete properties.id delete properties.id
return this._extract([id]).then(models => { return this._extract([id]).then(models => {
return (models.length && !isEmpty(properties)) return models.length && !isEmpty(properties) ? filter(models) : models
? filter(models)
: models
}) })
} }
const {indexes} = this const { indexes } = this
// Check for non indexed fields. // Check for non indexed fields.
const unfit = difference(getKeys(properties), indexes) const unfit = difference(getKeys(properties), indexes)
@ -197,7 +209,10 @@ export default class Redis extends Collection {
throw new Error('fields not indexed: ' + unfit.join()) throw new Error('fields not indexed: ' + unfit.join())
} }
const keys = map(properties, (value, index) => `${prefix}_${index}:${String(value).toLowerCase()}`) const keys = map(
properties,
(value, index) => `${prefix}_${index}:${String(value).toLowerCase()}`
)
return redis.sinter(...keys).then(ids => this._extract(ids)) return redis.sinter(...keys).then(ids => this._extract(ids))
} }
@ -213,16 +228,24 @@ export default class Redis extends Collection {
// update other indexes // update other indexes
if (indexes.length !== 0) { if (indexes.length !== 0) {
promise = Promise.all([ promise, asyncMap(ids, id => promise = Promise.all([
redis.hgetall(`${prefix}:${id}`).then(values => promise,
values != null && asyncMap(indexes, index => { asyncMap(ids, id =>
const value = values[index] redis.hgetall(`${prefix}:${id}`).then(
if (value !== undefined) { values =>
return redis.srem(`${prefix}_${index}:${String(value).toLowerCase()}`, id) values != null &&
} asyncMap(indexes, index => {
}) const value = values[index]
) if (value !== undefined) {
) ]) return redis.srem(
`${prefix}_${index}:${String(value).toLowerCase()}`,
id
)
}
})
)
),
])
} }
return promise.then(() => return promise.then(() =>

View File

@ -1,6 +1,6 @@
import {EventEmitter} from 'events' import { EventEmitter } from 'events'
import {createRawObject, noop} from './utils' import { createRawObject, noop } from './utils'
// =================================================================== // ===================================================================
@ -21,7 +21,7 @@ export default class Connection extends EventEmitter {
// Gets the value for this key. // Gets the value for this key.
get (key, defaultValue) { get (key, defaultValue) {
const {_data: data} = this const { _data: data } = this
if (key in data) { if (key in data) {
return data[key] return data[key]

View File

@ -1,16 +1,10 @@
import { getBoundPropertyDescriptor } from 'bind-property-descriptor' import { getBoundPropertyDescriptor } from 'bind-property-descriptor'
import { import { isArray, isFunction } from './utils'
isArray,
isFunction,
} from './utils'
// =================================================================== // ===================================================================
const { const { defineProperties, getOwnPropertyDescriptor } = Object
defineProperties,
getOwnPropertyDescriptor,
} = Object
// =================================================================== // ===================================================================
@ -27,10 +21,12 @@ export const debounce = duration => (target, name, descriptor) => {
const s = Symbol(`debounced ${name} data`) const s = Symbol(`debounced ${name} data`)
function debounced () { function debounced () {
const data = this[s] || (this[s] = { const data =
lastCall: 0, this[s] ||
wrapper: null, (this[s] = {
}) lastCall: 0,
wrapper: null,
})
const now = Date.now() const now = Date.now()
if (now > data.lastCall + duration) { if (now > data.lastCall + duration) {
@ -39,12 +35,16 @@ export const debounce = duration => (target, name, descriptor) => {
const result = fn.apply(this, arguments) const result = fn.apply(this, arguments)
data.wrapper = () => result data.wrapper = () => result
} catch (error) { } catch (error) {
data.wrapper = () => { throw error } data.wrapper = () => {
throw error
}
} }
} }
return data.wrapper() return data.wrapper()
} }
debounced.reset = obj => { delete obj[s] } debounced.reset = obj => {
delete obj[s]
}
descriptor.value = debounced descriptor.value = debounced
return descriptor return descriptor
@ -52,21 +52,12 @@ export const debounce = duration => (target, name, descriptor) => {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
const _ownKeys = ( const _ownKeys =
(typeof Reflect !== 'undefined' && Reflect.ownKeys) || (typeof Reflect !== 'undefined' && Reflect.ownKeys) ||
(({ (({ getOwnPropertyNames: names, getOwnPropertySymbols: symbols }) =>
getOwnPropertyNames: names, symbols ? obj => names(obj).concat(symbols(obj)) : names)(Object)
getOwnPropertySymbols: symbols,
}) => symbols
? obj => names(obj).concat(symbols(obj))
: names
)(Object)
)
const _isIgnoredProperty = name => ( const _isIgnoredProperty = name => name[0] === '_' || name === 'constructor'
name[0] === '_' ||
name === 'constructor'
)
const _IGNORED_STATIC_PROPERTIES = { const _IGNORED_STATIC_PROPERTIES = {
__proto__: null, __proto__: null,
@ -81,7 +72,7 @@ const _isIgnoredStaticProperty = name => _IGNORED_STATIC_PROPERTIES[name]
export const mixin = MixIns => Class => { export const mixin = MixIns => Class => {
if (!isArray(MixIns)) { if (!isArray(MixIns)) {
MixIns = [ MixIns ] MixIns = [MixIns]
} }
const { name } = Class const { name } = Class
@ -103,9 +94,10 @@ export const mixin = MixIns => Class => {
throw new Error(`${name}#${prop} is already defined`) throw new Error(`${name}#${prop} is already defined`)
} }
( ;(descriptors[prop] = getOwnPropertyDescriptor(
descriptors[prop] = getOwnPropertyDescriptor(MixIn, prop) MixIn,
).enumerable = false // Object methods are enumerable but class methods are not. prop
)).enumerable = false // Object methods are enumerable but class methods are not.
} }
} }
defineProperties(prototype, descriptors) defineProperties(prototype, descriptors)
@ -143,16 +135,15 @@ export const mixin = MixIns => Class => {
const descriptors = { __proto__: null } const descriptors = { __proto__: null }
for (const prop of _ownKeys(Class)) { for (const prop of _ownKeys(Class)) {
let descriptor let descriptor
if (!( if (
// Special properties are not defined... !(
_isIgnoredStaticProperty(prop) && _isIgnoredStaticProperty(prop) &&
// if they already exist...
// if they already exist... (descriptor = getOwnPropertyDescriptor(Decorator, prop)) &&
(descriptor = getOwnPropertyDescriptor(Decorator, prop)) && // and are not configurable.
!descriptor.configurable
// and are not configurable. )
!descriptor.configurable ) {
)) {
descriptors[prop] = getOwnPropertyDescriptor(Class, prop) descriptors[prop] = getOwnPropertyDescriptor(Class, prop)
} }
} }

View File

@ -1,6 +1,6 @@
/* eslint-env jest */ /* eslint-env jest */
import {debounce} from './decorators' import { debounce } from './decorators'
// =================================================================== // ===================================================================

View File

@ -27,28 +27,31 @@ export function init () {
const buf = Buffer.alloc(TEN_MIB) const buf = Buffer.alloc(TEN_MIB)
// https://github.com/natevw/fatfs/blob/master/structs.js // https://github.com/natevw/fatfs/blob/master/structs.js
fat16.pack({ fat16.pack(
jmpBoot: Buffer.from('eb3c90', 'hex'), {
OEMName: 'mkfs.fat', jmpBoot: Buffer.from('eb3c90', 'hex'),
BytsPerSec: SECTOR_SIZE, OEMName: 'mkfs.fat',
SecPerClus: 4, BytsPerSec: SECTOR_SIZE,
ResvdSecCnt: 1, SecPerClus: 4,
NumFATs: 2, ResvdSecCnt: 1,
RootEntCnt: 512, NumFATs: 2,
TotSec16: 20480, RootEntCnt: 512,
Media: 248, TotSec16: 20480,
FATSz16: 20, Media: 248,
SecPerTrk: 32, FATSz16: 20,
NumHeads: 64, SecPerTrk: 32,
HiddSec: 0, NumHeads: 64,
TotSec32: 0, HiddSec: 0,
DrvNum: 128, TotSec32: 0,
Reserved1: 0, DrvNum: 128,
BootSig: 41, Reserved1: 0,
VolID: 895111106, BootSig: 41,
VolLab: 'NO NAME ', VolID: 895111106,
FilSysType: 'FAT16 ', VolLab: 'NO NAME ',
}, buf) FilSysType: 'FAT16 ',
},
buf
)
// End of sector. // End of sector.
buf[0x1fe] = 0x55 buf[0x1fe] = 0x55

View File

@ -17,11 +17,7 @@ import { join as joinPath } from 'path'
import JsonRpcPeer from 'json-rpc-peer' import JsonRpcPeer from 'json-rpc-peer'
import { invalidCredentials } from 'xo-common/api-errors' import { invalidCredentials } from 'xo-common/api-errors'
import { import { ensureDir, readdir, readFile } from 'fs-extra'
ensureDir,
readdir,
readFile,
} from 'fs-extra'
import WebServer from 'http-server-plus' import WebServer from 'http-server-plus'
import Xo from './xo' import Xo from './xo'
@ -52,10 +48,7 @@ const warn = (...args) => {
// =================================================================== // ===================================================================
const DEPRECATED_ENTRIES = [ const DEPRECATED_ENTRIES = ['users', 'servers']
'users',
'servers',
]
async function loadConfiguration () { async function loadConfiguration () {
const config = await appConf.load('xo-server', { const config = await appConf.load('xo-server', {
@ -85,13 +78,15 @@ function createExpressApp () {
// Registers the cookie-parser and express-session middlewares, // Registers the cookie-parser and express-session middlewares,
// necessary for connect-flash. // necessary for connect-flash.
app.use(cookieParser()) app.use(cookieParser())
app.use(expressSession({ app.use(
resave: false, expressSession({
saveUninitialized: false, resave: false,
saveUninitialized: false,
// TODO: should be in the config file. // TODO: should be in the config file.
secret: 'CLWguhRZAZIXZcbrMzHCYmefxgweItKnS', secret: 'CLWguhRZAZIXZcbrMzHCYmefxgweItKnS',
})) })
)
// Registers the connect-flash middleware, necessary for Passport to // Registers the connect-flash middleware, necessary for Passport to
// display error messages. // display error messages.
@ -112,7 +107,7 @@ async function setUpPassport (express, xo) {
xo.registerPassportStrategy = strategy => { xo.registerPassportStrategy = strategy => {
passport.use(strategy) passport.use(strategy)
const {name} = strategy const { name } = strategy
if (name !== 'local') { if (name !== 'local') {
strategies[name] = strategy.label || name strategies[name] = strategy.label || name
} }
@ -123,10 +118,12 @@ async function setUpPassport (express, xo) {
await readFile(joinPath(__dirname, '..', 'signin.pug')) await readFile(joinPath(__dirname, '..', 'signin.pug'))
) )
express.get('/signin', (req, res, next) => { express.get('/signin', (req, res, next) => {
res.send(signInPage({ res.send(
error: req.flash('error')[0], signInPage({
strategies, error: req.flash('error')[0],
})) strategies,
})
)
}) })
express.get('/signout', (req, res) => { express.get('/signout', (req, res) => {
@ -154,7 +151,7 @@ async function setUpPassport (express, xo) {
// browsers do not save cookies on redirect. // browsers do not save cookies on redirect.
req.flash( req.flash(
'token', 'token',
(await xo.createAuthenticationToken({userId: user.id})).id (await xo.createAuthenticationToken({ userId: user.id })).id
) )
// The session is only persistent for internal provider and if 'Remember me' box is checked // The session is only persistent for internal provider and if 'Remember me' box is checked
@ -183,7 +180,9 @@ async function setUpPassport (express, xo) {
next() next()
} else if (req.cookies.token) { } else if (req.cookies.token) {
next() next()
} else if (/favicon|fontawesome|images|styles|\.(?:css|jpg|png)$/.test(url)) { } else if (
/favicon|fontawesome|images|styles|\.(?:css|jpg|png)$/.test(url)
) {
next() next()
} else { } else {
req.flash('return-url', url) req.flash('return-url', url)
@ -192,16 +191,16 @@ async function setUpPassport (express, xo) {
}) })
// Install the local strategy. // Install the local strategy.
xo.registerPassportStrategy(new LocalStrategy( xo.registerPassportStrategy(
async (username, password, done) => { new LocalStrategy(async (username, password, done) => {
try { try {
const user = await xo.authenticateUser({username, password}) const user = await xo.authenticateUser({ username, password })
done(null, user) done(null, user)
} catch (error) { } catch (error) {
done(null, false, { message: error.message }) done(null, false, { message: error.message })
} }
} })
)) )
} }
// =================================================================== // ===================================================================
@ -274,40 +273,44 @@ async function registerPluginsInPath (path) {
throw error throw error
}) })
await Promise.all(mapToArray(files, name => { await Promise.all(
if (startsWith(name, PLUGIN_PREFIX)) { mapToArray(files, name => {
return registerPluginWrapper.call( if (startsWith(name, PLUGIN_PREFIX)) {
this, return registerPluginWrapper.call(
`${path}/${name}`, this,
name.slice(PLUGIN_PREFIX_LENGTH) `${path}/${name}`,
) name.slice(PLUGIN_PREFIX_LENGTH)
} )
})) }
})
)
} }
async function registerPlugins (xo) { async function registerPlugins (xo) {
await Promise.all(mapToArray([ await Promise.all(
`${__dirname}/../node_modules/`, mapToArray(
'/usr/local/lib/node_modules/', [`${__dirname}/../node_modules/`, '/usr/local/lib/node_modules/'],
], xo::registerPluginsInPath)) xo::registerPluginsInPath
)
)
} }
// =================================================================== // ===================================================================
async function makeWebServerListen (webServer, { async function makeWebServerListen (
certificate, webServer,
{
certificate,
// The properties was called `certificate` before. // The properties was called `certificate` before.
cert = certificate, cert = certificate,
key, key,
...opts ...opts
}) { }
) {
if (cert && key) { if (cert && key) {
[opts.cert, opts.key] = await Promise.all([ ;[opts.cert, opts.key] = await Promise.all([readFile(cert), readFile(key)])
readFile(cert),
readFile(key),
])
} }
try { try {
const niceAddress = await webServer.listen(opts) const niceAddress = await webServer.listen(opts)
@ -316,7 +319,7 @@ async function makeWebServerListen (webServer, {
if (error.niceAddress) { if (error.niceAddress) {
warn(`Web server could not listen on ${error.niceAddress}`) warn(`Web server could not listen on ${error.niceAddress}`)
const {code} = error const { code } = error
if (code === 'EACCES') { if (code === 'EACCES') {
warn(' Access denied.') warn(' Access denied.')
warn(' Ports < 1024 are often reserved to privileges users.') warn(' Ports < 1024 are often reserved to privileges users.')
@ -332,9 +335,11 @@ async function makeWebServerListen (webServer, {
async function createWebServer ({ listen, listenOptions }) { async function createWebServer ({ listen, listenOptions }) {
const webServer = new WebServer() const webServer = new WebServer()
await Promise.all(mapToArray(listen, await Promise.all(
opts => makeWebServerListen(webServer, { ...listenOptions, ...opts }) mapToArray(listen, opts =>
)) makeWebServerListen(webServer, { ...listenOptions, ...opts })
)
)
return webServer return webServer
} }
@ -348,7 +353,7 @@ const setUpProxies = (express, opts, xo) => {
const proxy = createProxyServer({ const proxy = createProxyServer({
ignorePath: true, ignorePath: true,
}).on('error', (error) => console.error(error)) }).on('error', error => console.error(error))
// TODO: sort proxies by descending prefix length. // TODO: sort proxies by descending prefix length.
@ -464,7 +469,9 @@ const setUpApi = (webServer, xo, verboseLogsOnErrors) => {
} }
webServer.on('upgrade', (req, socket, head) => { webServer.on('upgrade', (req, socket, head) => {
if (req.url === '/api/') { if (req.url === '/api/') {
webSocketServer.handleUpgrade(req, socket, head, ws => onConnection(ws, req)) webSocketServer.handleUpgrade(req, socket, head, ws =>
onConnection(ws, req)
)
} }
}) })
} }
@ -492,7 +499,7 @@ const setUpConsoleProxy = (webServer, xo) => {
const { token } = parseCookies(req.headers.cookie) const { token } = parseCookies(req.headers.cookie)
const user = await xo.authenticateUser({ token }) const user = await xo.authenticateUser({ token })
if (!await xo.hasPermissions(user.id, [ [ id, 'operate' ] ])) { if (!await xo.hasPermissions(user.id, [[id, 'operate']])) {
throw invalidCredentials() throw invalidCredentials()
} }
@ -518,10 +525,7 @@ const setUpConsoleProxy = (webServer, xo) => {
// =================================================================== // ===================================================================
const USAGE = (({ const USAGE = (({ name, version }) => `Usage: ${name} [--safe-mode]
name,
version,
}) => `Usage: ${name} [--safe-mode]
${name} v${version}`)(require('../package.json')) ${name} v${version}`)(require('../package.json'))
@ -545,7 +549,7 @@ export default async function main (args) {
// Now the web server is listening, drop privileges. // Now the web server is listening, drop privileges.
try { try {
const {user, group} = config const { user, group } = config
if (group) { if (group) {
process.setgid(group) process.setgid(group)
debug('Group changed to', group) debug('Group changed to', group)
@ -576,10 +580,7 @@ export default async function main (args) {
if (config.http.redirectToHttps) { if (config.http.redirectToHttps) {
let port let port
forEach(config.http.listen, listen => { forEach(config.http.listen, listen => {
if ( if (listen.port && (listen.cert || listen.certificate)) {
listen.port &&
(listen.cert || listen.certificate)
) {
port = listen.port port = listen.port
return false return false
} }
@ -629,7 +630,7 @@ export default async function main (args) {
// //
// TODO: implements a timeout? (or maybe it is the services launcher // TODO: implements a timeout? (or maybe it is the services launcher
// responsibility?) // responsibility?)
forEach([ 'SIGINT', 'SIGTERM' ], signal => { forEach(['SIGINT', 'SIGTERM'], signal => {
let alreadyCalled = false let alreadyCalled = false
process.on(signal, () => { process.on(signal, () => {

View File

@ -2,20 +2,10 @@ import Bluebird from 'bluebird'
import { BaseError } from 'make-error' import { BaseError } from 'make-error'
import { createPredicate } from 'value-matcher' import { createPredicate } from 'value-matcher'
import { timeout } from 'promise-toolbox' import { timeout } from 'promise-toolbox'
import { import { assign, filter, find, isEmpty, map, mapValues } from 'lodash'
assign,
filter,
find,
isEmpty,
map,
mapValues,
} from 'lodash'
import { crossProduct } from './math' import { crossProduct } from './math'
import { import { serializeError, thunkToArray } from './utils'
serializeError,
thunkToArray,
} from './utils'
export class JobExecutorError extends BaseError {} export class JobExecutorError extends BaseError {}
export class UnsupportedJobType extends JobExecutorError { export class UnsupportedJobType extends JobExecutorError {
@ -36,9 +26,9 @@ const paramsVectorActionsMap = {
return mapValues(mapping, key => value[key]) return mapValues(mapping, key => value[key])
}, },
crossProduct ({ items }) { crossProduct ({ items }) {
return thunkToArray(crossProduct( return thunkToArray(
map(items, value => resolveParamsVector.call(this, value)) crossProduct(map(items, value => resolveParamsVector.call(this, value)))
)) )
}, },
fetchObjects ({ pattern }) { fetchObjects ({ pattern }) {
const objects = filter(this.xo.getObjects(), createPredicate(pattern)) const objects = filter(this.xo.getObjects(), createPredicate(pattern))
@ -74,9 +64,11 @@ export default class JobExecutor {
this.xo = xo this.xo = xo
// The logger is not available until Xo has started. // The logger is not available until Xo has started.
xo.on('start', () => xo.getLogger('jobs').then(logger => { xo.on('start', () =>
this._logger = logger xo.getLogger('jobs').then(logger => {
})) this._logger = logger
})
)
} }
async exec (job) { async exec (job) {
@ -130,51 +122,68 @@ export default class JobExecutor {
timezone: schedule !== undefined ? schedule.timezone : undefined, timezone: schedule !== undefined ? schedule.timezone : undefined,
} }
await Bluebird.map(paramsFlatVector, params => { await Bluebird.map(
const runCallId = this._logger.notice(`Starting ${job.method} call. (${job.id})`, { paramsFlatVector,
event: 'jobCall.start', params => {
runJobId, const runCallId = this._logger.notice(
method: job.method, `Starting ${job.method} call. (${job.id})`,
params, {
}) event: 'jobCall.start',
const call = execStatus.calls[runCallId] = {
method: job.method,
params,
start: Date.now(),
}
let promise = this.xo.callApiMethod(connection, job.method, assign({}, params))
if (job.timeout) {
promise = promise::timeout(job.timeout)
}
return promise.then(
value => {
this._logger.notice(`Call ${job.method} (${runCallId}) is a success. (${job.id})`, {
event: 'jobCall.end',
runJobId, runJobId,
runCallId, method: job.method,
returnedValue: value, params,
}) }
)
call.returnedValue = value const call = (execStatus.calls[runCallId] = {
call.end = Date.now() method: job.method,
}, params,
reason => { start: Date.now(),
this._logger.notice(`Call ${job.method} (${runCallId}) has failed. (${job.id})`, { })
event: 'jobCall.end', let promise = this.xo.callApiMethod(
runJobId, connection,
runCallId, job.method,
error: serializeError(reason), assign({}, params)
}) )
if (job.timeout) {
call.error = reason promise = promise::timeout(job.timeout)
call.end = Date.now()
} }
)
}, { return promise.then(
concurrency: 2, value => {
}) this._logger.notice(
`Call ${job.method} (${runCallId}) is a success. (${job.id})`,
{
event: 'jobCall.end',
runJobId,
runCallId,
returnedValue: value,
}
)
call.returnedValue = value
call.end = Date.now()
},
reason => {
this._logger.notice(
`Call ${job.method} (${runCallId}) has failed. (${job.id})`,
{
event: 'jobCall.end',
runJobId,
runCallId,
error: serializeError(reason),
}
)
call.error = reason
call.end = Date.now()
}
)
},
{
concurrency: 2,
}
)
connection.close() connection.close()
execStatus.end = Date.now() execStatus.end = Date.now()

View File

@ -4,97 +4,113 @@ import { forEach } from 'lodash'
import { resolveParamsVector } from './job-executor' import { resolveParamsVector } from './job-executor'
describe('resolveParamsVector', function () { describe('resolveParamsVector', function () {
forEach({ forEach(
'cross product with three sets': [ {
// Expected result. 'cross product with three sets': [
[ { id: 3, value: 'foo', remote: 'local' }, // Expected result.
{ id: 7, value: 'foo', remote: 'local' }, [
{ id: 10, value: 'foo', remote: 'local' }, { id: 3, value: 'foo', remote: 'local' },
{ id: 3, value: 'bar', remote: 'local' }, { id: 7, value: 'foo', remote: 'local' },
{ id: 7, value: 'bar', remote: 'local' }, { id: 10, value: 'foo', remote: 'local' },
{ id: 10, value: 'bar', remote: 'local' } ], { id: 3, value: 'bar', remote: 'local' },
// Entry. { id: 7, value: 'bar', remote: 'local' },
{ { id: 10, value: 'bar', remote: 'local' },
type: 'crossProduct', ],
items: [{ // Entry.
type: 'set', {
values: [ { id: 3 }, { id: 7 }, { id: 10 } ], type: 'crossProduct',
}, { items: [
type: 'set', {
values: [ { value: 'foo' }, { value: 'bar' } ], type: 'set',
}, { values: [{ id: 3 }, { id: 7 }, { id: 10 }],
type: 'set', },
values: [ { remote: 'local' } ], {
}], type: 'set',
}, values: [{ value: 'foo' }, { value: 'bar' }],
], },
'cross product with `set` and `map`': [ {
// Expected result. type: 'set',
[ values: [{ remote: 'local' }],
{ remote: 'local', id: 'vm:2' }, },
{ remote: 'smb', id: 'vm:2' }, ],
},
], ],
'cross product with `set` and `map`': [
// Expected result.
[{ remote: 'local', id: 'vm:2' }, { remote: 'smb', id: 'vm:2' }],
// Entry. // Entry.
{ {
type: 'crossProduct', type: 'crossProduct',
items: [{ items: [
type: 'set', {
values: [ { remote: 'local' }, { remote: 'smb' } ], type: 'set',
}, { values: [{ remote: 'local' }, { remote: 'smb' }],
type: 'map', },
collection: { {
type: 'fetchObjects', type: 'map',
pattern: { collection: {
$pool: { __or: [ 'pool:1', 'pool:8', 'pool:12' ] }, type: 'fetchObjects',
power_state: 'Running', pattern: {
tags: [ 'foo' ], $pool: { __or: ['pool:1', 'pool:8', 'pool:12'] },
type: 'VM', power_state: 'Running',
tags: ['foo'],
type: 'VM',
},
},
iteratee: {
type: 'extractProperties',
mapping: { id: 'id' },
},
},
],
},
// Context.
{
xo: {
getObjects: function () {
return [
{
id: 'vm:1',
$pool: 'pool:1',
tags: [],
type: 'VM',
power_state: 'Halted',
},
{
id: 'vm:2',
$pool: 'pool:1',
tags: ['foo'],
type: 'VM',
power_state: 'Running',
},
{
id: 'host:1',
type: 'host',
power_state: 'Running',
},
{
id: 'vm:3',
$pool: 'pool:8',
tags: ['foo'],
type: 'VM',
power_state: 'Halted',
},
]
}, },
}, },
iteratee: {
type: 'extractProperties',
mapping: { id: 'id' },
},
}],
},
// Context.
{
xo: {
getObjects: function () {
return [{
id: 'vm:1',
$pool: 'pool:1',
tags: [],
type: 'VM',
power_state: 'Halted',
}, {
id: 'vm:2',
$pool: 'pool:1',
tags: [ 'foo' ],
type: 'VM',
power_state: 'Running',
}, {
id: 'host:1',
type: 'host',
power_state: 'Running',
}, {
id: 'vm:3',
$pool: 'pool:8',
tags: [ 'foo' ],
type: 'VM',
power_state: 'Halted',
}]
},
}, },
}, ],
], },
}, ([ expectedResult, entry, context ], name) => { ([expectedResult, entry, context], name) => {
describe(`with ${name}`, () => { describe(`with ${name}`, () => {
it('Resolves params vector', () => { it('Resolves params vector', () => {
expect(resolveParamsVector.call(context, entry)).toEqual(expectedResult) expect(resolveParamsVector.call(context, entry)).toEqual(
expectedResult
)
})
}) })
}) }
}) )
}) })

View File

@ -8,26 +8,26 @@ import sublevel from 'level-sublevel'
import util from 'util' import util from 'util'
import { repair as repairDb } from 'level' import { repair as repairDb } from 'level'
import {forEach} from './utils' import { forEach } from './utils'
import globMatcher from './glob-matcher' import globMatcher from './glob-matcher'
// =================================================================== // ===================================================================
async function printLogs (db, args) { async function printLogs (db, args) {
let stream = highland(db.createReadStream({reverse: true})) let stream = highland(db.createReadStream({ reverse: true }))
if (args.since) { if (args.since) {
stream = stream.filter(({value}) => (value.time >= args.since)) stream = stream.filter(({ value }) => value.time >= args.since)
} }
if (args.until) { if (args.until) {
stream = stream.filter(({value}) => (value.time <= args.until)) stream = stream.filter(({ value }) => value.time <= args.until)
} }
const fields = Object.keys(args.matchers) const fields = Object.keys(args.matchers)
if (fields.length > 0) { if (fields.length > 0) {
stream = stream.filter(({value}) => { stream = stream.filter(({ value }) => {
for (const field of fields) { for (const field of fields) {
const fieldValue = get(value, field) const fieldValue = get(value, field)
if (fieldValue === undefined || !args.matchers[field](fieldValue)) { if (fieldValue === undefined || !args.matchers[field](fieldValue)) {
@ -42,10 +42,9 @@ async function printLogs (db, args) {
stream = stream.take(args.limit) stream = stream.take(args.limit)
if (args.json) { if (args.json) {
stream = highland(stream.pipe(ndjson.serialize())) stream = highland(stream.pipe(ndjson.serialize())).each(value => {
.each(value => { process.stdout.write(value)
process.stdout.write(value) })
})
} else { } else {
stream = stream.each(value => { stream = stream.each(value => {
console.log(util.inspect(value, { depth: null })) console.log(util.inspect(value, { depth: null }))
@ -126,7 +125,7 @@ function getArgs () {
patterns[pattern] patterns[pattern]
? patterns[field].push(pattern) ? patterns[field].push(pattern)
: patterns[field] = [ pattern ] : (patterns[field] = [pattern])
} else if (!patterns[value]) { } else if (!patterns[value]) {
patterns[value] = null patterns[value] = null
} }
@ -137,7 +136,7 @@ function getArgs () {
for (const field in patterns) { for (const field in patterns) {
const values = patterns[field] const values = patterns[field]
args.matchers[field] = (values === null) ? trueFunction : globMatcher(values) args.matchers[field] = values === null ? trueFunction : globMatcher(values)
} }
// Warning: minimist makes one array of values if the same option is used many times. // Warning: minimist makes one array of values if the same option is used many times.
@ -147,7 +146,6 @@ function getArgs () {
throw new Error(`error: too many values for ${arg} argument`) throw new Error(`error: too many values for ${arg} argument`)
} }
}) })
;['since', 'until'].forEach(arg => { ;['since', 'until'].forEach(arg => {
if (args[arg] !== undefined) { if (args[arg] !== undefined) {
args[arg] = Date.parse(args[arg]) args[arg] = Date.parse(args[arg])
@ -158,7 +156,7 @@ function getArgs () {
} }
}) })
if (isNaN(args.limit = +args.limit)) { if (isNaN((args.limit = +args.limit))) {
throw new Error('error: limit is not a valid number') throw new Error('error: limit is not a valid number')
} }
@ -193,10 +191,9 @@ export default async function main () {
return return
} }
const db = sublevel(levelup( const db = sublevel(
`${config.datadir}/leveldb`, levelup(`${config.datadir}/leveldb`, { valueEncoding: 'json' })
{ valueEncoding: 'json' } ).sublevel('logs')
)).sublevel('logs')
return printLogs(db, args) return printLogs(db, args)
} }

View File

@ -9,25 +9,29 @@ const parse = createParser({
keyTransform: key => key.slice(5).toLowerCase(), keyTransform: key => key.slice(5).toLowerCase(),
}) })
const makeFunction = command => (fields, ...args) => const makeFunction = command => (fields, ...args) =>
execa.stdout(command, [ execa
'--noheading', .stdout(command, [
'--nosuffix', '--noheading',
'--nameprefixes', '--nosuffix',
'--unbuffered', '--nameprefixes',
'--units', '--unbuffered',
'b', '--units',
'-o', 'b',
String(fields), '-o',
...args, String(fields),
]).then(stdout => map( ...args,
splitLines(stdout), ])
isArray(fields) .then(stdout =>
? parse map(
: line => { splitLines(stdout),
const data = parse(line) isArray(fields)
return data[fields] ? parse
} : line => {
)) const data = parse(line)
return data[fields]
}
)
)
export const lvs = makeFunction('lvs') export const lvs = makeFunction('lvs')
export const pvs = makeFunction('pvs') export const pvs = makeFunction('pvs')

View File

@ -11,7 +11,7 @@ const _combine = (vectors, n, cb) => {
const m = vector.length const m = vector.length
if (n === 1) { if (n === 1) {
for (let i = 0; i < m; ++i) { for (let i = 0; i < m; ++i) {
cb([ vector[i] ]) // eslint-disable-line standard/no-callback-literal cb([vector[i]]) // eslint-disable-line standard/no-callback-literal
} }
return return
} }
@ -19,7 +19,7 @@ const _combine = (vectors, n, cb) => {
for (let i = 0; i < m; ++i) { for (let i = 0; i < m; ++i) {
const value = vector[i] const value = vector[i]
_combine(vectors, nLast, (vector) => { _combine(vectors, nLast, vector => {
vector.push(value) vector.push(value)
cb(vector) cb(vector)
}) })
@ -41,8 +41,7 @@ export const mergeObjects = objects => assign({}, ...objects)
// //
// Ex: crossProduct([ [ { a: 2 }, { b: 3 } ], [ { c: 5 }, { d: 7 } ] ] ) // Ex: crossProduct([ [ { a: 2 }, { b: 3 } ], [ { c: 5 }, { d: 7 } ] ] )
// => [ { a: 2, c: 5 }, { b: 3, c: 5 }, { a: 2, d: 7 }, { b: 3, d: 7 } ] // => [ { a: 2, c: 5 }, { b: 3, c: 5 }, { a: 2, d: 7 }, { b: 3, d: 7 } ]
export const crossProduct = (vectors, mergeFn = mergeObjects) => cb => ( export const crossProduct = (vectors, mergeFn = mergeObjects) => cb =>
combine(vectors)(vector => { combine(vectors)(vector => {
cb(mergeFn(vector)) cb(mergeFn(vector))
}) })
)

Some files were not shown because too many files have changed in this diff Show More