Compare commits

...

29 Commits

Author SHA1 Message Date
Julien Fontanet
b0ac14363d 5.1.0 2016-07-26 16:52:49 +02:00
Julien Fontanet
5d346aba37 fix(vm.create): cloudConfig handling 2016-07-26 14:26:24 +02:00
Julien Fontanet
124cb15ebe fix(resource sets): fix VM resources computation
Fixes vatesfr/xo-web#1276
2016-07-25 17:08:09 +02:00
Julien Fontanet
a244ab898d fix(vm.create): correctly store the resource set 2016-07-25 17:08:08 +02:00
Julien Fontanet
3c551590eb fix(vm.set): correctly save memory in limits 2016-07-25 17:08:07 +02:00
ABHAMON Ronan
10e30cccbc feat(models/schedule): null properly remove timezone (#368)
Related to vatesfr/xo-web#1314
2016-07-25 15:54:27 +02:00
Julien Fontanet
806a6b86a2 fix(signin): fix styles when /v4 2016-07-25 13:40:57 +02:00
Julien Fontanet
9719fdf5cc fix(sr.probe*): correctly prepare port param 2016-07-23 16:18:03 +02:00
Julien Fontanet
6d8764f8cb fix(Xapi#createVm): add missing param 2016-07-23 15:49:27 +02:00
Julien Fontanet
d9fd9cb408 fix(vm.create): better VBDs creation (#361)
Fixes vatesfr/xo-web#1257
2016-07-23 15:31:15 +02:00
Julien Fontanet
7710ec0aba feat(schemas): add user schema 2016-07-20 12:10:23 +02:00
Julien Fontanet
c97bd78cd0 fix(VM): cpuCap & cpuWeight are integers 2016-07-20 10:57:15 +02:00
ABHAMON Ronan
728c5aa86e feat(plugins): supports predefined configurations (#365)
See vatesfr/xo-web#1289
2016-07-19 17:28:53 +02:00
Pierre Donias
83d68ca293 feat(vm.set): make cpuWeight and cpuCap nullable (#364) 2016-07-19 16:53:47 +02:00
Julien Fontanet
47d7561db4 fix(VM): cpuCap can be defined when cpuWeight is not 2016-07-19 15:37:07 +02:00
ABHAMON Ronan
7d993e8319 feat(schedules): schedules support timezones (#363)
Fixes vatesfr/xo-web#1258
2016-07-19 13:32:27 +02:00
Julien Fontanet
1d1a597b22 feat(VM): expose cpuCap 2016-07-19 11:02:38 +02:00
Julien Fontanet
23082f9300 feat(vm.set): support for cpuCap (#362) 2016-07-19 10:35:03 +02:00
Julien Fontanet
ea1a7f9376 chore(Xapi#_getXenUpdates): use ensureArray() 2016-07-15 12:57:20 +02:00
Greenkeeper
1796c7bab8 chore(package): update nyc to version 7.0.0 (#358)
https://greenkeeper.io/
2016-07-14 13:09:12 +02:00
Greenkeeper
65ad76479a chore(package): update base64url to version 2.0.0 (#360)
https://greenkeeper.io/
2016-07-14 11:33:12 +02:00
Olivier Lambert
422db04ec8 5.0.5 2016-07-13 15:20:56 +02:00
Olivier Lambert
d12f60fe37 Merge pull request #359 from vatesfr/pierre-fix-create-vm
fix(vm/create): missing single quotes
2016-07-13 09:37:23 +02:00
Pierre Donias
194c1c991c fix(vm/create): missing single quotes 2016-07-12 16:40:32 +02:00
Olivier Lambert
3e8e2222c1 Merge pull request #357 from vatesfr/marsaudf-fix-job-log-error
Add message to job log error
2016-07-07 15:26:15 +02:00
Fabrice Marsaud
1620327a33 Add message to job log error 2016-07-07 14:55:43 +02:00
Olivier Lambert
b1131e3667 5.0.4 2016-07-07 12:12:54 +02:00
Olivier Lambert
db0250ac08 Merge pull request #356 from vatesfr/marsaudf-fix-patch-conflicts
Fix(xapi): handle correctly single XML elements
2016-07-07 11:22:27 +02:00
Fabrice Marsaud
0a6b605760 Handle single patch elements in parsed XML 2016-07-07 10:11:21 +02:00
18 changed files with 204 additions and 85 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server",
"version": "5.0.3",
"version": "5.1.0",
"license": "AGPL-3.0",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -38,7 +38,7 @@
"@marsaud/smb2-promise": "^0.2.0",
"app-conf": "^0.4.0",
"babel-runtime": "^6.5.0",
"base64url": "^1.0.5",
"base64url": "^2.0.0",
"blocked": "^1.1.0",
"bluebird": "^3.1.1",
"body-parser": "^1.13.3",
@@ -79,6 +79,7 @@
"make-error": "^1",
"micromatch": "^2.3.2",
"minimist": "^1.2.0",
"moment-timezone": "^0.5.4",
"ms": "^0.7.1",
"multikey-hash": "^1.0.1",
"ndjson": "^1.4.3",
@@ -121,7 +122,7 @@
"leche": "^2.1.1",
"mocha": "^2.2.1",
"must": "^0.13.1",
"nyc": "^6.4.2",
"nyc": "^7.0.0",
"rimraf": "^2.5.2",
"sinon": "^1.14.1",
"standard": "^7.0.0"

View File

@@ -7,6 +7,7 @@ html
title Xen Orchestra
meta(name = 'author' content = 'Vates SAS')
link(rel = 'stylesheet' href = 'styles/main.css')
link(rel = 'stylesheet' href = 'v4/styles/main.css')
body
.container
.row-login

View File

@@ -3,6 +3,7 @@ const debug = createDebug('xo:api')
import getKeys from 'lodash/keys'
import kindOf from 'kindof'
import moment from 'moment-timezone'
import ms from 'ms'
import schemaInspector from 'schema-inspector'
@@ -188,6 +189,11 @@ methodSignature.description = 'returns the signature of an API method'
// ===================================================================
const getServerTimezone = (tz => () => tz)(moment.tz.guess())
getServerTimezone.description = 'return the timezone server'
// ===================================================================
export default class Api {
constructor ({
context,
@@ -201,6 +207,7 @@ export default class Api {
system: {
getMethodsInfo,
getServerVersion,
getServerTimezone,
getVersion,
listMethods,
methodSignature

View File

@@ -17,8 +17,8 @@ get.params = {
id: {type: 'string'}
}
export async function create ({jobId, cron, enabled, name}) {
return /* await */ this.createSchedule(this.session.get('user_id'), {job: jobId, cron, enabled, name})
export async function create ({ jobId, cron, enabled, name, timezone }) {
return /* await */ this.createSchedule(this.session.get('user_id'), { job: jobId, cron, enabled, name, timezone })
}
create.permission = 'admin'
@@ -30,8 +30,8 @@ create.params = {
name: {type: 'string', optional: true}
}
export async function set ({id, jobId, cron, enabled, name}) {
await this.updateSchedule(id, {job: jobId, cron, enabled, name})
export async function set ({ id, jobId, cron, enabled, name, timezone }) {
await this.updateSchedule(id, { job: jobId, cron, enabled, name, timezone })
}
set.permission = 'admin'

View File

@@ -1,3 +1,4 @@
import { asInteger } from '../xapi/utils'
import {
ensureArray,
forEach,
@@ -344,7 +345,7 @@ export async function createIscsi ({
// if we give another port than default iSCSI
if (port) {
deviceConfig.port = port
deviceConfig.port = asInteger(port)
}
const srRef = await xapi.call(
@@ -405,7 +406,7 @@ export async function probeIscsiIqns ({
// if we give another port than default iSCSI
if (port) {
deviceConfig.port = port
deviceConfig.port = asInteger(port)
}
let xml
@@ -483,7 +484,7 @@ export async function probeIscsiLuns ({
// if we give another port than default iSCSI
if (port) {
deviceConfig.port = port
deviceConfig.port = asInteger(port)
}
let xml
@@ -562,7 +563,7 @@ export async function probeIscsiExists ({
// if we give another port than default iSCSI
if (port) {
deviceConfig.port = port
deviceConfig.port = asInteger(port)
}
const xml = parseXml(await xapi.call('SR.probe', host._xapiRef, deviceConfig, 'lvmoiscsi', {}))

View File

@@ -134,7 +134,7 @@ create = $coroutine (params) ->
installation = extract(params, 'installation')
params.installRepository = installation && installation.repository
resourceSet = extract(params, resourceSet)
resourceSet = extract(params, 'resourceSet')
xapiVm = yield xapi.createVm(template._xapiId, params)
vm = xapi.xo.addObject(xapiVm)
@@ -149,7 +149,7 @@ create = $coroutine (params) ->
if resourceSet
yield Promise.all([
@addAcl(user.id, vm.id, 'admin')
xapi.xo.setData(xapiVm.$id, 'resourceSet')
xapi.xo.setData(xapiVm.$id, 'resourceSet', resourceSet)
])
return vm.id
@@ -455,7 +455,9 @@ set.params = {
# Kernel arguments for PV VM.
PV_args: { type: 'string', optional: true }
cpuWeight: { type: 'integer', optional: true}
cpuWeight: { type: ['integer', 'null'], optional: true }
cpuCap: { type: ['integer', 'null'], optional: true }
}
set.resolve = {

View File

@@ -224,7 +224,8 @@ async function registerPlugin (pluginPath, pluginName) {
// Supports both “normal” CommonJS and Babel's ES2015 modules.
const {
default: factory = plugin,
configurationSchema
configurationSchema,
configurationPresets
} = plugin
// The default export can be either a factory or directly a plugin
@@ -237,6 +238,7 @@ async function registerPlugin (pluginPath, pluginName) {
pluginName,
instance,
configurationSchema,
configurationPresets,
version
)
}

View File

@@ -141,7 +141,7 @@ export default class JobExecutor {
event: 'jobCall.end',
runJobId,
runCallId,
error: reason
error: {...reason, message: reason.message}
})
call.error = reason

View File

@@ -15,13 +15,14 @@ export class Schedules extends Collection {
return 'schedule:'
}
create (userId, job, cron, enabled, name = undefined) {
create (userId, job, cron, enabled, name = undefined, timezone = undefined) {
return this.add(new Schedule({
userId,
job,
cron,
enabled,
name
name,
timezone
}))
}

50
src/schemas/user.js Normal file
View File

@@ -0,0 +1,50 @@
export default {
$schema: 'http://json-schema.org/draft-04/schema#',
type: 'object',
properties: {
id: {
type: 'string',
description: 'unique identifier for this user'
},
email: {
type: 'string',
description: 'email address of this user'
},
groups: {
type: 'array',
items: {
type: 'string'
},
description: 'identifier of groups this user belong to'
},
permission: {
enum: ['none', 'read', 'write', 'admin'],
description: 'root permission for this user, none and admin are the only significant ones'
},
preferences: {
type: 'object',
properties: {
lang: { type: 'string' },
sshKeys: {
type: 'array',
items: {
type: 'object',
properties: {
key: { type: 'string' },
title: { type: 'string' }
},
required: [
'key',
'title'
]
}
}
},
description: 'various user preferences'
}
},
required: [
'id',
'email'
]
}

View File

@@ -12,6 +12,12 @@ import keys from 'lodash/keys'
import kindOf from 'kindof'
import multiKeyHashInt from 'multikey-hash'
import xml2js from 'xml2js'
// Moment timezone can be loaded only one time, it's a workaround to load
// the latest version because cron module uses an old version of moment which
// does not implement `guess` function for example.
import 'moment-timezone'
import { CronJob } from 'cron'
import {
all as pAll,
@@ -439,27 +445,30 @@ export const streamToArray = (stream, {
// -------------------------------------------------------------------
export const scheduleFn = (cronPattern, fn) => {
export const scheduleFn = (cronTime, fn, timeZone) => {
let running = false
const job = new CronJob(cronPattern, async () => {
if (running) {
return
}
const job = new CronJob({
cronTime,
onTick: async () => {
if (running) {
return
}
running = true
running = true
try {
await fn()
} catch (error) {
console.error('[WARN] scheduled function:', error && error.stack || error)
} finally {
running = false
}
try {
await fn()
} catch (error) {
console.error('[WARN] scheduled function:', error && error.stack || error)
} finally {
running = false
}
},
start: true,
timeZone
})
job.start()
return () => {
job.stop()
}

View File

@@ -335,8 +335,10 @@ const TRANSFORMS = {
}
}
if (obj.VCPUs_params && obj.VCPUs_params.weight) {
vm.cpuWeight = obj.VCPUs_params.weight
let tmp
if ((tmp = obj.VCPUs_params)) {
tmp.cap && (vm.cpuCap = +tmp.cap)
tmp.weight && (vm.cpuWeight = +tmp.weight)
}
if (!isHvm) {

View File

@@ -455,7 +455,7 @@ export default class Xapi extends XapiBase {
const resolveVersionPatches = function (uuids) {
const versionPatches = createRawObject()
forEach(uuids, ({uuid}) => {
forEach(ensureArray(uuids), ({uuid}) => {
versionPatches[uuid] = patches[uuid]
})

View File

@@ -50,7 +50,7 @@ export default {
const template = this.getObject(templateId)
// Clones the template.
const vm = await this._getOrWaitObject(
let vm = await this._getOrWaitObject(
await this[clone ? '_cloneVm' : '_copyVm'](template, nameLabel)
)
@@ -137,21 +137,26 @@ export default {
// Creates the user defined VDIs.
//
// TODO: set vm.suspend_SR
vdis && await Promise.all(mapToArray(vdis, (vdiDescription, i) => {
return this._createVdi(
vdiDescription.size, // FIXME: Should not be done in Xapi.
{
name_label: vdiDescription.name_label,
name_description: vdiDescription.name_description,
sr: vdiDescription.sr || vdiDescription.SR
}
)
.then(ref => this._getOrWaitObject(ref))
.then(vdi => this._createVbd(vm, vdi, {
// Only the first VBD if installMethod is not cd is bootable.
bootable: installMethod !== 'cd' && !i
}))
}))
if (vdis) {
const devices = await this.call('VM.get_allowed_VBD_devices', vm.$ref)
await Promise.all(mapToArray(vdis, (vdiDescription, i) => {
return this._createVdi(
vdiDescription.size, // FIXME: Should not be done in Xapi.
{
name_label: vdiDescription.name_label,
name_description: vdiDescription.name_description,
sr: vdiDescription.sr || vdiDescription.SR
}
)
.then(ref => this._getOrWaitObject(ref))
.then(vdi => this._createVbd(vm, vdi, {
// Only the first VBD if installMethod is not cd is bootable.
bootable: installMethod !== 'cd' && !i,
userdevice: devices[i]
}))
}))
}
// Destroys the VIFs cloned from the template.
await Promise.all(mapToArray(vm.$VIFs, vif => this._deleteVif(vif)))
@@ -173,6 +178,9 @@ export default {
// TODO: Assign VGPUs.
if (cloudConfig != null) {
// Refresh the record.
vm = this.getObject(vm.$id)
// Find the SR of the first VDI.
let srRef
forEach(vm.$VBDs, vbd => {
@@ -234,6 +242,14 @@ export default {
]
},
cpuCap: {
addToLimits: true,
get: vm => vm.VCPUs_params.cap && +vm.VCPUs_params.cap,
set (cap, vm) {
return this._updateObjectMapProperty(vm, 'VCPUs_params', { cap })
}
},
cpusMax: 'cpusStaticMax',
cpusStaticMax: {
constraints: {
@@ -245,7 +261,7 @@ export default {
cpuWeight: {
addToLimits: true,
get: vm => +(vm.VCPUs_params.weight || 0),
get: vm => vm.VCPUs_params.weight && +vm.VCPUs_params.weight,
set (weight, vm) {
return this._updateObjectMapProperty(vm, 'VCPUs_params', { weight })
}
@@ -268,6 +284,7 @@ export default {
memory: 'memoryMax',
memoryMax: {
addToLimits: true,
constraints: {
memoryMin: lte,
memoryStaticMax: gte

View File

@@ -268,6 +268,10 @@ export const makeEditObject = specs => {
}
const set = (value, name) => {
if (value === undefined) {
return
}
const spec = specs[name]
if (!spec) {
return

View File

@@ -50,11 +50,13 @@ export default class {
name,
instance,
configurationSchema,
configurationPresets,
version
) {
const id = name
const plugin = this._plugins[id] = {
configured: !configurationSchema,
configurationPresets,
configurationSchema,
id,
instance,
@@ -101,6 +103,7 @@ export default class {
async _getPlugin (id) {
const {
configurationPresets,
configurationSchema,
loaded,
name,
@@ -120,6 +123,7 @@ export default class {
unloadable,
version,
configuration,
configurationPresets,
configurationSchema
}
}

View File

@@ -1,4 +1,5 @@
import every from 'lodash/every'
import keyBy from 'lodash/keyBy'
import remove from 'lodash/remove'
import some from 'lodash/some'
@@ -12,6 +13,7 @@ import {
isObject,
lightSet,
map,
mapToArray,
streamToArray
} from '../utils'
@@ -23,6 +25,33 @@ class NoSuchResourceSet extends NoSuchObject {
}
}
const computeVmResourcesUsage = vm => {
const processed = {}
let disks = 0
let disk = 0
forEach(vm.$VBDs, vbd => {
let vdi, vdiId
if (
vbd.type === 'Disk' &&
!processed[vdiId = vbd.VDI] &&
(vdi = vbd.$VDI)
) {
processed[vdiId] = true
++disks
disk += +vdi.virtual_size
}
})
return {
cpus: vm.VCPUs_at_startup,
disk,
disks,
memory: vm.memory_dynamic_max,
vms: 1
}
}
const normalize = set => ({
id: set.id,
limits: set.limits
@@ -84,29 +113,9 @@ export default class {
}
computeVmResourcesUsage (vm) {
const processed = {}
let disks = 0
let disk = 0
forEach(this._xo.getXapi(vm).getObject(vm._xapiId).$VBDs, (vbd) => {
let vdi, vdiId
if (
vbd.type === 'Disk' &&
!processed[vdiId = vbd.VDI] &&
(vdi = vbd.$VDI)
) {
processed[vdiId] = true
++disks
disk += +vdi.virtual_size
}
})
return {
cpus: vm.CPUs.number,
disk,
disks,
memory: vm.memory.size,
vms: 1
}
return computeVmResourcesUsage(
this._xo.getXapi(vm).getObject(vm._xapiId)
)
}
async createResourceSet (name, subjects = undefined, objects = undefined, limits = undefined) {
@@ -268,7 +277,7 @@ export default class {
}
async recomputeResourceSetsLimits () {
const sets = await this.getAllResourceSets()
const sets = keyBy(await this.getAllResourceSets(), 'id')
forEach(sets, ({ limits }) => {
forEach(limits, (limit, id) => {
limit.available = limit.total
@@ -292,13 +301,15 @@ export default class {
}
const { limits } = set
forEach(this.computeVmResourcesUsage(object), (usage, resource) => {
forEach(computeVmResourcesUsage(object), (usage, resource) => {
const limit = limits[resource]
limit.available -= usage
if (limit) {
limit.available -= usage
}
})
})
})
await Promise.all(map(sets, (set) => this._save(set)))
await Promise.all(mapToArray(sets, set => this._save(set)))
}
}

View File

@@ -74,8 +74,10 @@ export default class {
_enable (schedule) {
const { id } = schedule
const stopSchedule = scheduleFn(schedule.cron, () =>
this.xo.runJobSequence([ schedule.job ])
const stopSchedule = scheduleFn(
schedule.cron,
() => this.xo.runJobSequence([ schedule.job ]),
schedule.timezone
)
this._cronJobs[id] = stopSchedule
@@ -137,8 +139,8 @@ export default class {
return /* await */ this._redisSchedules.get()
}
async createSchedule (userId, {job, cron, enabled, name}) {
const schedule_ = await this._redisSchedules.create(userId, job, cron, enabled, name)
async createSchedule (userId, { job, cron, enabled, name, timezone }) {
const schedule_ = await this._redisSchedules.create(userId, job, cron, enabled, name, timezone)
const schedule = schedule_.properties
this._add(schedule)
@@ -146,13 +148,18 @@ export default class {
return schedule
}
async updateSchedule (id, {job, cron, enabled, name}) {
async updateSchedule (id, { job, cron, enabled, name, timezone }) {
const schedule = await this._getSchedule(id)
if (job) schedule.set('job', job)
if (cron) schedule.set('cron', cron)
if (enabled !== undefined) schedule.set('enabled', enabled)
if (name !== undefined) schedule.set('name', name)
if (timezone === null) {
schedule.set('timezone', undefined) // Remove current timezone
} else if (timezone !== undefined) {
schedule.set('timezone', timezone)
}
await this._redisSchedules.save(schedule)