Compare commits
1 Commits
persian-lo
...
pierre-sel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd04345569 |
@@ -28,7 +28,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['*.{integ,spec,test}.{,c,m}js'],
|
files: ['*.{spec,test}.{,c,m}js'],
|
||||||
rules: {
|
rules: {
|
||||||
'n/no-unpublished-require': 'off',
|
'n/no-unpublished-require': 'off',
|
||||||
'n/no-unpublished-import': 'off',
|
'n/no-unpublished-import': 'off',
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"@xen-orchestra/async-map": "^0.1.2",
|
"@xen-orchestra/async-map": "^0.1.2",
|
||||||
"@xen-orchestra/log": "^0.6.0",
|
"@xen-orchestra/log": "^0.6.0",
|
||||||
"promise-toolbox": "^0.21.0",
|
"promise-toolbox": "^0.21.0",
|
||||||
"xen-api": "^1.3.1"
|
"xen-api": "^1.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tap": "^16.3.0",
|
"tap": "^16.3.0",
|
||||||
@@ -31,6 +31,6 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postversion": "npm publish --access public",
|
"postversion": "npm publish --access public",
|
||||||
"test-integration": "tap --lines 70 --functions 36 --branches 54 --statements 69 *.integ.js"
|
"test-integration": "tap *.spec.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ exports.makeOnProgress = function ({ onRootTaskEnd = noop, onRootTaskStart = noo
|
|||||||
assert.notEqual(parent, undefined)
|
assert.notEqual(parent, undefined)
|
||||||
|
|
||||||
// inject a (non-enumerable) reference to the parent and the root task
|
// inject a (non-enumerable) reference to the parent and the root task
|
||||||
Object.defineProperties(taskLog, { $parent: { value: parent }, $root: { value: parent.$root } })
|
Object.defineProperty(taskLog, { $parent: { value: parent }, $root: { value: parent.$root } })
|
||||||
;(parent.tasks ?? (parent.tasks = [])).push(taskLog)
|
;(parent.tasks ?? (parent.tasks = [])).push(taskLog)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const assert = require('node:assert').strict
|
|
||||||
const { describe, it } = require('test')
|
|
||||||
|
|
||||||
const { makeOnProgress } = require('./combineEvents.js')
|
|
||||||
const { Task } = require('./index.js')
|
|
||||||
|
|
||||||
describe('makeOnProgress()', function () {
|
|
||||||
it('works', async function () {
|
|
||||||
const events = []
|
|
||||||
let log
|
|
||||||
const task = new Task({
|
|
||||||
data: { name: 'task' },
|
|
||||||
onProgress: makeOnProgress({
|
|
||||||
onRootTaskStart(log_) {
|
|
||||||
assert.equal(log, undefined)
|
|
||||||
log = log_
|
|
||||||
events.push('onRootTaskStart')
|
|
||||||
},
|
|
||||||
onRootTaskEnd(log_) {
|
|
||||||
assert.equal(log_, log)
|
|
||||||
events.push('onRootTaskEnd')
|
|
||||||
},
|
|
||||||
|
|
||||||
onTaskUpdate(log_) {
|
|
||||||
assert.equal(log_.$root, log)
|
|
||||||
events.push('onTaskUpdate')
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.equal(events.length, 0)
|
|
||||||
|
|
||||||
await task.run(async () => {
|
|
||||||
assert.equal(events[0], 'onRootTaskStart')
|
|
||||||
assert.equal(events[1], 'onTaskUpdate')
|
|
||||||
assert.equal(log.name, 'task')
|
|
||||||
|
|
||||||
Task.set('progress', 0)
|
|
||||||
assert.equal(events[2], 'onTaskUpdate')
|
|
||||||
assert.equal(log.properties.progress, 0)
|
|
||||||
|
|
||||||
Task.info('foo', {})
|
|
||||||
assert.equal(events[3], 'onTaskUpdate')
|
|
||||||
assert.deepEqual(log.infos, [{ data: {}, message: 'foo' }])
|
|
||||||
|
|
||||||
await Task.run({ data: { name: 'subtask' } }, () => {
|
|
||||||
assert.equal(events[4], 'onTaskUpdate')
|
|
||||||
assert.equal(log.tasks[0].name, 'subtask')
|
|
||||||
|
|
||||||
Task.warning('bar', {})
|
|
||||||
assert.equal(events[5], 'onTaskUpdate')
|
|
||||||
assert.deepEqual(log.tasks[0].warnings, [{ data: {}, message: 'bar' }])
|
|
||||||
})
|
|
||||||
assert.equal(events[6], 'onTaskUpdate')
|
|
||||||
assert.equal(log.tasks[0].status, 'success')
|
|
||||||
|
|
||||||
Task.set('progress', 100)
|
|
||||||
assert.equal(events[7], 'onTaskUpdate')
|
|
||||||
assert.equal(log.properties.progress, 100)
|
|
||||||
})
|
|
||||||
assert.equal(events[8], 'onRootTaskEnd')
|
|
||||||
assert.equal(events[9], 'onTaskUpdate')
|
|
||||||
assert.equal(log.status, 'success')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -83,7 +83,7 @@ exports.Task = class Task {
|
|||||||
return this.#status
|
return this.#status
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor({ data = {}, onProgress } = {}) {
|
constructor({ data = {}, onProgress }) {
|
||||||
this.#startData = data
|
this.#startData = data
|
||||||
|
|
||||||
if (onProgress !== undefined) {
|
if (onProgress !== undefined) {
|
||||||
@@ -106,8 +106,6 @@ exports.Task = class Task {
|
|||||||
const { signal } = this.#abortController
|
const { signal } = this.#abortController
|
||||||
signal.addEventListener('abort', () => {
|
signal.addEventListener('abort', () => {
|
||||||
if (this.status === PENDING && !this.#running) {
|
if (this.status === PENDING && !this.#running) {
|
||||||
this.#maybeStart()
|
|
||||||
|
|
||||||
const status = ABORTED
|
const status = ABORTED
|
||||||
this.#status = status
|
this.#status = status
|
||||||
this.#emit('end', { result: signal.reason, status })
|
this.#emit('end', { result: signal.reason, status })
|
||||||
@@ -120,18 +118,16 @@ exports.Task = class Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#emit(type, data) {
|
#emit(type, data) {
|
||||||
data.id = this.id
|
|
||||||
data.timestamp = Date.now()
|
|
||||||
data.type = type
|
|
||||||
this.#onProgress(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
#maybeStart() {
|
|
||||||
const startData = this.#startData
|
const startData = this.#startData
|
||||||
if (startData !== undefined) {
|
if (startData !== undefined) {
|
||||||
this.#startData = undefined
|
this.#startData = undefined
|
||||||
this.#emit('start', startData)
|
this.#emit('start', startData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.id = this.id
|
||||||
|
data.timestamp = Date.now()
|
||||||
|
data.type = type
|
||||||
|
this.#onProgress(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(fn) {
|
async run(fn) {
|
||||||
@@ -149,8 +145,6 @@ exports.Task = class Task {
|
|||||||
assert.equal(this.#running, false)
|
assert.equal(this.#running, false)
|
||||||
this.#running = true
|
this.#running = true
|
||||||
|
|
||||||
this.#maybeStart()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await asyncStorage.run(this, fn)
|
const result = await asyncStorage.run(this, fn)
|
||||||
this.#running = false
|
this.#running = false
|
||||||
|
|||||||
@@ -1,341 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const assert = require('node:assert').strict
|
|
||||||
const { describe, it } = require('test')
|
|
||||||
|
|
||||||
const { Task } = require('./index.js')
|
|
||||||
|
|
||||||
const noop = Function.prototype
|
|
||||||
|
|
||||||
function assertEvent(task, expected, eventIndex = -1) {
|
|
||||||
const logs = task.$events
|
|
||||||
const actual = logs[eventIndex < 0 ? logs.length + eventIndex : eventIndex]
|
|
||||||
|
|
||||||
assert.equal(typeof actual, 'object')
|
|
||||||
assert.equal(typeof actual.id, 'string')
|
|
||||||
assert.equal(typeof actual.timestamp, 'number')
|
|
||||||
for (const keys of Object.keys(expected)) {
|
|
||||||
assert.equal(actual[keys], expected[keys])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// like new Task() but with a custom onProgress which adds event to task.$events
|
|
||||||
function createTask(opts) {
|
|
||||||
const events = []
|
|
||||||
const task = new Task({ ...opts, onProgress: events.push.bind(events) })
|
|
||||||
task.$events = events
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Task', function () {
|
|
||||||
describe('contructor', function () {
|
|
||||||
it('data properties are passed to the start event', async function () {
|
|
||||||
const data = { foo: 0, bar: 1 }
|
|
||||||
const task = createTask({ data })
|
|
||||||
await task.run(noop)
|
|
||||||
assertEvent(task, { ...data, type: 'start' }, 0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('subtasks events are passed to root task', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
const result = {}
|
|
||||||
|
|
||||||
await task.run(async () => {
|
|
||||||
await new Task().run(() => result)
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.equal(task.$events.length, 4)
|
|
||||||
assertEvent(task, { type: 'start', parentId: task.id }, 1)
|
|
||||||
assertEvent(task, { type: 'end', status: 'success', result }, 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('.abortSignal', function () {
|
|
||||||
it('is undefined when run outside a task', function () {
|
|
||||||
assert.equal(Task.abortSignal, undefined)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('is the current abort signal when run inside a task', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
await task.run(() => {
|
|
||||||
const { abortSignal } = Task
|
|
||||||
assert.equal(abortSignal.aborted, false)
|
|
||||||
task.abort()
|
|
||||||
assert.equal(abortSignal.aborted, true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('.abort()', function () {
|
|
||||||
it('aborts if the task throws fails with the abort reason', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
const reason = {}
|
|
||||||
|
|
||||||
await task
|
|
||||||
.run(() => {
|
|
||||||
task.abort(reason)
|
|
||||||
|
|
||||||
Task.abortSignal.throwIfAborted()
|
|
||||||
})
|
|
||||||
.catch(noop)
|
|
||||||
|
|
||||||
assert.equal(task.status, 'aborted')
|
|
||||||
|
|
||||||
assert.equal(task.$events.length, 2)
|
|
||||||
assertEvent(task, { type: 'start' }, 0)
|
|
||||||
assertEvent(task, { type: 'end', status: 'aborted', result: reason }, 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not abort if the task fails without the abort reason', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
const result = new Error()
|
|
||||||
|
|
||||||
await task
|
|
||||||
.run(() => {
|
|
||||||
task.abort({})
|
|
||||||
|
|
||||||
throw result
|
|
||||||
})
|
|
||||||
.catch(noop)
|
|
||||||
|
|
||||||
assert.equal(task.status, 'failure')
|
|
||||||
|
|
||||||
assert.equal(task.$events.length, 2)
|
|
||||||
assertEvent(task, { type: 'start' }, 0)
|
|
||||||
assertEvent(task, { type: 'end', status: 'failure', result }, 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not abort if the task succeed', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
const result = {}
|
|
||||||
|
|
||||||
await task
|
|
||||||
.run(() => {
|
|
||||||
task.abort({})
|
|
||||||
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
.catch(noop)
|
|
||||||
|
|
||||||
assert.equal(task.status, 'success')
|
|
||||||
|
|
||||||
assert.equal(task.$events.length, 2)
|
|
||||||
assertEvent(task, { type: 'start' }, 0)
|
|
||||||
assertEvent(task, { type: 'end', status: 'success', result }, 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('aborts before task is running', function () {
|
|
||||||
const task = createTask()
|
|
||||||
const reason = {}
|
|
||||||
|
|
||||||
task.abort(reason)
|
|
||||||
|
|
||||||
assert.equal(task.status, 'aborted')
|
|
||||||
|
|
||||||
assert.equal(task.$events.length, 2)
|
|
||||||
assertEvent(task, { type: 'start' }, 0)
|
|
||||||
assertEvent(task, { type: 'end', status: 'aborted', result: reason }, 1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('.info()', function () {
|
|
||||||
it('does nothing when run outside a task', function () {
|
|
||||||
Task.info('foo')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('emits an info message when run inside a task', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
await task.run(() => {
|
|
||||||
Task.info('foo')
|
|
||||||
assertEvent(task, {
|
|
||||||
data: undefined,
|
|
||||||
message: 'foo',
|
|
||||||
type: 'info',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('.set()', function () {
|
|
||||||
it('does nothing when run outside a task', function () {
|
|
||||||
Task.set('progress', 10)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('emits an info message when run inside a task', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
await task.run(() => {
|
|
||||||
Task.set('progress', 10)
|
|
||||||
assertEvent(task, {
|
|
||||||
name: 'progress',
|
|
||||||
type: 'property',
|
|
||||||
value: 10,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('.warning()', function () {
|
|
||||||
it('does nothing when run outside a task', function () {
|
|
||||||
Task.warning('foo')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('emits an warning message when run inside a task', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
await task.run(() => {
|
|
||||||
Task.warning('foo')
|
|
||||||
assertEvent(task, {
|
|
||||||
data: undefined,
|
|
||||||
message: 'foo',
|
|
||||||
type: 'warning',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#id', function () {
|
|
||||||
it('can be set', function () {
|
|
||||||
const task = createTask()
|
|
||||||
task.id = 'foo'
|
|
||||||
assert.equal(task.id, 'foo')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cannot be set more than once', function () {
|
|
||||||
const task = createTask()
|
|
||||||
task.id = 'foo'
|
|
||||||
|
|
||||||
assert.throws(() => {
|
|
||||||
task.id = 'bar'
|
|
||||||
}, TypeError)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('is randomly generated if not set', function () {
|
|
||||||
assert.notEqual(createTask().id, createTask().id)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cannot be set after being observed', function () {
|
|
||||||
const task = createTask()
|
|
||||||
noop(task.id)
|
|
||||||
|
|
||||||
assert.throws(() => {
|
|
||||||
task.id = 'bar'
|
|
||||||
}, TypeError)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#status', function () {
|
|
||||||
it('starts as pending', function () {
|
|
||||||
assert.equal(createTask().status, 'pending')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('changes to success when finish without error', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
await task.run(noop)
|
|
||||||
assert.equal(task.status, 'success')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('changes to failure when finish with error', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
await task
|
|
||||||
.run(() => {
|
|
||||||
throw Error()
|
|
||||||
})
|
|
||||||
.catch(noop)
|
|
||||||
assert.equal(task.status, 'failure')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('changes to aborted after run is complete', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
await task
|
|
||||||
.run(() => {
|
|
||||||
task.abort()
|
|
||||||
assert.equal(task.status, 'pending')
|
|
||||||
Task.abortSignal.throwIfAborted()
|
|
||||||
})
|
|
||||||
.catch(noop)
|
|
||||||
assert.equal(task.status, 'aborted')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('changes to aborted if aborted when not running', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
task.abort()
|
|
||||||
assert.equal(task.status, 'aborted')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function makeRunTests(run) {
|
|
||||||
it('starts the task', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
await run(task, () => {
|
|
||||||
assertEvent(task, { type: 'start' })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('finishes the task on success', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
await run(task, () => 'foo')
|
|
||||||
assert.equal(task.status, 'success')
|
|
||||||
assertEvent(task, {
|
|
||||||
status: 'success',
|
|
||||||
result: 'foo',
|
|
||||||
type: 'end',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('fails the task on error', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
const e = new Error()
|
|
||||||
await run(task, () => {
|
|
||||||
throw e
|
|
||||||
}).catch(noop)
|
|
||||||
|
|
||||||
assert.equal(task.status, 'failure')
|
|
||||||
assertEvent(task, {
|
|
||||||
status: 'failure',
|
|
||||||
result: e,
|
|
||||||
type: 'end',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
describe('.run', function () {
|
|
||||||
makeRunTests((task, fn) => task.run(fn))
|
|
||||||
})
|
|
||||||
describe('.wrap', function () {
|
|
||||||
makeRunTests((task, fn) => task.wrap(fn)())
|
|
||||||
})
|
|
||||||
|
|
||||||
function makeRunInsideTests(run) {
|
|
||||||
it('starts the task', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
await run(task, () => {
|
|
||||||
assertEvent(task, { type: 'start' })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not finish the task on success', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
await run(task, () => 'foo')
|
|
||||||
assert.equal(task.status, 'pending')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('fails the task on error', async function () {
|
|
||||||
const task = createTask()
|
|
||||||
const e = new Error()
|
|
||||||
await run(task, () => {
|
|
||||||
throw e
|
|
||||||
}).catch(noop)
|
|
||||||
|
|
||||||
assert.equal(task.status, 'failure')
|
|
||||||
assertEvent(task, {
|
|
||||||
status: 'failure',
|
|
||||||
result: e,
|
|
||||||
type: 'end',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
describe('.runInside', function () {
|
|
||||||
makeRunInsideTests((task, fn) => task.runInside(fn))
|
|
||||||
})
|
|
||||||
describe('.wrapInside', function () {
|
|
||||||
makeRunInsideTests((task, fn) => task.wrapInside(fn)())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -13,16 +13,12 @@
|
|||||||
"url": "https://vates.fr"
|
"url": "https://vates.fr"
|
||||||
},
|
},
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"version": "0.1.2",
|
"version": "0.1.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
|
||||||
"test": "^3.3.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postversion": "npm publish --access public",
|
"postversion": "npm publish --access public"
|
||||||
"test": "node--test"
|
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.js",
|
".": "./index.js",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xen-orchestra/async-map": "^0.1.2",
|
"@xen-orchestra/async-map": "^0.1.2",
|
||||||
"@xen-orchestra/backups": "^0.36.1",
|
"@xen-orchestra/backups": "^0.35.0",
|
||||||
"@xen-orchestra/fs": "^3.3.4",
|
"@xen-orchestra/fs": "^3.3.4",
|
||||||
"filenamify": "^4.1.0",
|
"filenamify": "^4.1.0",
|
||||||
"getopts": "^2.2.5",
|
"getopts": "^2.2.5",
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"postversion": "npm publish --access public"
|
"postversion": "npm publish --access public"
|
||||||
},
|
},
|
||||||
"version": "1.0.6",
|
"version": "1.0.5",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Vates SAS",
|
"name": "Vates SAS",
|
||||||
|
|||||||
@@ -3,14 +3,12 @@
|
|||||||
const { Task } = require('./Task')
|
const { Task } = require('./Task')
|
||||||
|
|
||||||
exports.HealthCheckVmBackup = class HealthCheckVmBackup {
|
exports.HealthCheckVmBackup = class HealthCheckVmBackup {
|
||||||
#restoredVm
|
|
||||||
#timeout
|
|
||||||
#xapi
|
#xapi
|
||||||
|
#restoredVm
|
||||||
|
|
||||||
constructor({ restoredVm, timeout = 10 * 60 * 1000, xapi }) {
|
constructor({ restoredVm, xapi }) {
|
||||||
this.#restoredVm = restoredVm
|
this.#restoredVm = restoredVm
|
||||||
this.#xapi = xapi
|
this.#xapi = xapi
|
||||||
this.#timeout = timeout
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
@@ -25,12 +23,7 @@ exports.HealthCheckVmBackup = class HealthCheckVmBackup {
|
|||||||
|
|
||||||
// remove vifs
|
// remove vifs
|
||||||
await Promise.all(restoredVm.$VIFs.map(vif => xapi.callAsync('VIF.destroy', vif.$ref)))
|
await Promise.all(restoredVm.$VIFs.map(vif => xapi.callAsync('VIF.destroy', vif.$ref)))
|
||||||
const waitForScript = restoredVm.tags.includes('xo-backup-health-check-xenstore')
|
|
||||||
if (waitForScript) {
|
|
||||||
await restoredVm.set_xenstore_data({
|
|
||||||
'vm-data/xo-backup-health-check': 'planned',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const start = new Date()
|
const start = new Date()
|
||||||
// start Vm
|
// start Vm
|
||||||
|
|
||||||
@@ -41,7 +34,7 @@ exports.HealthCheckVmBackup = class HealthCheckVmBackup {
|
|||||||
false // Skip pre-boot checks?
|
false // Skip pre-boot checks?
|
||||||
)
|
)
|
||||||
const started = new Date()
|
const started = new Date()
|
||||||
const timeout = this.#timeout
|
const timeout = 10 * 60 * 1000
|
||||||
const startDuration = started - start
|
const startDuration = started - start
|
||||||
|
|
||||||
let remainingTimeout = timeout - startDuration
|
let remainingTimeout = timeout - startDuration
|
||||||
@@ -59,52 +52,12 @@ exports.HealthCheckVmBackup = class HealthCheckVmBackup {
|
|||||||
remainingTimeout -= running - started
|
remainingTimeout -= running - started
|
||||||
|
|
||||||
if (remainingTimeout < 0) {
|
if (remainingTimeout < 0) {
|
||||||
throw new Error(`local xapi did not get Running state for VM ${restoredId} after ${timeout / 1000} second`)
|
throw new Error(`local xapi did not get Runnig state for VM ${restoredId} after ${timeout / 1000} second`)
|
||||||
}
|
}
|
||||||
// wait for the guest tool version to be defined
|
// wait for the guest tool version to be defined
|
||||||
await xapi.waitObjectState(restoredVm.guest_metrics, gm => gm?.PV_drivers_version?.major !== undefined, {
|
await xapi.waitObjectState(restoredVm.guest_metrics, gm => gm?.PV_drivers_version?.major !== undefined, {
|
||||||
timeout: remainingTimeout,
|
timeout: remainingTimeout,
|
||||||
})
|
})
|
||||||
|
|
||||||
const guestToolsReady = new Date()
|
|
||||||
remainingTimeout -= guestToolsReady - running
|
|
||||||
if (remainingTimeout < 0) {
|
|
||||||
throw new Error(`local xapi did not get he guest tools check ${restoredId} after ${timeout / 1000} second`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (waitForScript) {
|
|
||||||
const startedRestoredVm = await xapi.waitObjectState(
|
|
||||||
restoredVm.$ref,
|
|
||||||
vm =>
|
|
||||||
vm?.xenstore_data !== undefined &&
|
|
||||||
(vm.xenstore_data['vm-data/xo-backup-health-check'] === 'success' ||
|
|
||||||
vm.xenstore_data['vm-data/xo-backup-health-check'] === 'failure'),
|
|
||||||
{
|
|
||||||
timeout: remainingTimeout,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const scriptOk = new Date()
|
|
||||||
remainingTimeout -= scriptOk - guestToolsReady
|
|
||||||
if (remainingTimeout < 0) {
|
|
||||||
throw new Error(
|
|
||||||
`Backup health check script did not update vm-data/xo-backup-health-check of ${restoredId} after ${
|
|
||||||
timeout / 1000
|
|
||||||
} second, got ${
|
|
||||||
startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check']
|
|
||||||
} instead of 'success' or 'failure'`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check'] !== 'success') {
|
|
||||||
const message = startedRestoredVm.xenstore_data['vm-data/xo-backup-health-check-error']
|
|
||||||
if (message) {
|
|
||||||
throw new Error(`Backup health check script failed with message ${message} for VM ${restoredId} `)
|
|
||||||
} else {
|
|
||||||
throw new Error(`Backup health check script failed for VM ${restoredId} `)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Task.info('Backup health check script successfully executed')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# This script must be executed at the start of the machine.
|
|
||||||
#
|
|
||||||
# It must run as root to be able to use xenstore-read and xenstore-write
|
|
||||||
|
|
||||||
# fail in case of error or undefined variable
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
# stop there if a health check is not in progress
|
|
||||||
if [ "$(xenstore-read vm-data/xo-backup-health-check 2>&1)" != planned ]
|
|
||||||
then
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
# not necessary, but informs XO that this script has started which helps diagnose issues
|
|
||||||
xenstore-write vm-data/xo-backup-health-check running
|
|
||||||
|
|
||||||
# put your test here
|
|
||||||
#
|
|
||||||
# in this example, the command `sqlite3` is used to validate the health of a database
|
|
||||||
# and its output is captured and passed to XO via the XenStore in case of error
|
|
||||||
if output=$(sqlite3 ~/my-database.sqlite3 .table 2>&1)
|
|
||||||
then
|
|
||||||
# inform XO everything is ok
|
|
||||||
xenstore-write vm-data/xo-backup-health-check success
|
|
||||||
else
|
|
||||||
# inform XO there is an issue
|
|
||||||
xenstore-write vm-data/xo-backup-health-check failure
|
|
||||||
|
|
||||||
# more info about the issue can be written to `vm-data/health-check-error`
|
|
||||||
#
|
|
||||||
# it will be shown in XO
|
|
||||||
xenstore-write vm-data/xo-backup-health-check-error "$output"
|
|
||||||
fi
|
|
||||||
@@ -8,13 +8,13 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||||
},
|
},
|
||||||
"version": "0.36.1",
|
"version": "0.35.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.6"
|
"node": ">=14.6"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postversion": "npm publish --access public",
|
"postversion": "npm publish --access public",
|
||||||
"test-integration": "node--test *.integ.js"
|
"test": "node--test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kldzj/stream-throttle": "^1.1.1",
|
"@kldzj/stream-throttle": "^1.1.1",
|
||||||
|
|||||||
@@ -50,8 +50,7 @@ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinRepli
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.transfer = task.wrapFn(this.transfer)
|
this.transfer = task.wrapFn(this.transfer)
|
||||||
this.cleanup = task.wrapFn(this.cleanup)
|
this.cleanup = task.wrapFn(this.cleanup, true)
|
||||||
this.healthCheck = task.wrapFn(this.healthCheck, true)
|
|
||||||
|
|
||||||
return task.run(() => this._prepare())
|
return task.run(() => this._prepare())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
|
|||||||
assert.notStrictEqual(
|
assert.notStrictEqual(
|
||||||
this._metadataFileName,
|
this._metadataFileName,
|
||||||
undefined,
|
undefined,
|
||||||
'Metadata file name should be defined before making a health check'
|
'Metadata file name should be defined before making a healthcheck'
|
||||||
)
|
)
|
||||||
return Task.run(
|
return Task.run(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"preferGlobal": true,
|
"preferGlobal": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"golike-defer": "^0.5.1",
|
"golike-defer": "^0.5.1",
|
||||||
"xen-api": "^1.3.1"
|
"xen-api": "^1.3.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postversion": "npm publish"
|
"postversion": "npm publish"
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<UnreachableHostsModal />
|
<UiModal
|
||||||
|
v-if="isSslModalOpen"
|
||||||
|
:icon="faServer"
|
||||||
|
color="error"
|
||||||
|
@close="clearUnreachableHostsUrls"
|
||||||
|
>
|
||||||
|
<template #title>{{ $t("unreachable-hosts") }}</template>
|
||||||
|
<template #subtitle>{{ $t("following-hosts-unreachable") }}</template>
|
||||||
|
<p>{{ $t("allow-self-signed-ssl") }}</p>
|
||||||
|
<ul>
|
||||||
|
<li v-for="url in unreachableHostsUrls" :key="url.hostname">
|
||||||
|
<a :href="url.href" rel="noopener" target="_blank">{{ url.href }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<template #buttons>
|
||||||
|
<UiButton color="success" @click="reload">
|
||||||
|
{{ $t("unreachable-hosts-reload-page") }}
|
||||||
|
</UiButton>
|
||||||
|
<UiButton @click="clearUnreachableHostsUrls">{{ $t("cancel") }}</UiButton>
|
||||||
|
</template>
|
||||||
|
</UiModal>
|
||||||
<div v-if="!$route.meta.hasStoryNav && !xenApiStore.isConnected">
|
<div v-if="!$route.meta.hasStoryNav && !xenApiStore.isConnected">
|
||||||
<AppLogin />
|
<AppLogin />
|
||||||
</div>
|
</div>
|
||||||
@@ -21,14 +41,21 @@ import AppHeader from "@/components/AppHeader.vue";
|
|||||||
import AppLogin from "@/components/AppLogin.vue";
|
import AppLogin from "@/components/AppLogin.vue";
|
||||||
import AppNavigation from "@/components/AppNavigation.vue";
|
import AppNavigation from "@/components/AppNavigation.vue";
|
||||||
import AppTooltips from "@/components/AppTooltips.vue";
|
import AppTooltips from "@/components/AppTooltips.vue";
|
||||||
import UnreachableHostsModal from "@/components/UnreachableHostsModal.vue";
|
import UiButton from "@/components/ui/UiButton.vue";
|
||||||
|
import UiModal from "@/components/ui/UiModal.vue";
|
||||||
import { useChartTheme } from "@/composables/chart-theme.composable";
|
import { useChartTheme } from "@/composables/chart-theme.composable";
|
||||||
|
import { useHostStore } from "@/stores/host.store";
|
||||||
import { usePoolStore } from "@/stores/pool.store";
|
import { usePoolStore } from "@/stores/pool.store";
|
||||||
import { useUiStore } from "@/stores/ui.store";
|
import { useUiStore } from "@/stores/ui.store";
|
||||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||||
|
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useActiveElement, useMagicKeys, whenever } from "@vueuse/core";
|
import { useActiveElement, useMagicKeys, whenever } from "@vueuse/core";
|
||||||
import { logicAnd } from "@vueuse/math";
|
import { logicAnd } from "@vueuse/math";
|
||||||
import { computed } from "vue";
|
import { difference } from "lodash-es";
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
|
||||||
|
const unreachableHostsUrls = ref<URL[]>([]);
|
||||||
|
const clearUnreachableHostsUrls = () => (unreachableHostsUrls.value = []);
|
||||||
|
|
||||||
let link = document.querySelector(
|
let link = document.querySelector(
|
||||||
"link[rel~='icon']"
|
"link[rel~='icon']"
|
||||||
@@ -43,6 +70,7 @@ link.href = favicon;
|
|||||||
document.title = "XO Lite";
|
document.title = "XO Lite";
|
||||||
|
|
||||||
const xenApiStore = useXenApiStore();
|
const xenApiStore = useXenApiStore();
|
||||||
|
const { records: hosts } = useHostStore().subscribe();
|
||||||
const { pool } = usePoolStore().subscribe();
|
const { pool } = usePoolStore().subscribe();
|
||||||
useChartTheme();
|
useChartTheme();
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
@@ -65,6 +93,17 @@ if (import.meta.env.DEV) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(hosts, (hosts, previousHosts) => {
|
||||||
|
difference(hosts, previousHosts).forEach((host) => {
|
||||||
|
const url = new URL("http://localhost");
|
||||||
|
url.protocol = window.location.protocol;
|
||||||
|
url.hostname = host.address;
|
||||||
|
fetch(url, { mode: "no-cors" }).catch(() =>
|
||||||
|
unreachableHostsUrls.value.push(url)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
whenever(
|
whenever(
|
||||||
() => pool.value?.$ref,
|
() => pool.value?.$ref,
|
||||||
async (poolRef) => {
|
async (poolRef) => {
|
||||||
@@ -73,6 +112,9 @@ whenever(
|
|||||||
await xenApi.startWatch();
|
await xenApi.startWatch();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isSslModalOpen = computed(() => unreachableHostsUrls.value.length > 0);
|
||||||
|
const reload = () => window.location.reload();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!isDisabled" ref="tooltipElement" class="app-tooltip">
|
<div v-if="!isDisabled" ref="tooltipElement" class="app-tooltip">
|
||||||
<span class="triangle" />
|
<span class="triangle" />
|
||||||
<span class="label">{{ options.content }}</span>
|
<span class="label">{{ content }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { TooltipOptions } from "@/stores/tooltip.store";
|
import { isEmpty, isFunction, isString } from "lodash-es";
|
||||||
import { isString } from "lodash-es";
|
|
||||||
import place from "placement.js";
|
import place from "placement.js";
|
||||||
import { computed, ref, watchEffect } from "vue";
|
import { computed, ref, watchEffect } from "vue";
|
||||||
|
import type { TooltipOptions } from "@/stores/tooltip.store";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
target: HTMLElement;
|
target: HTMLElement;
|
||||||
@@ -18,13 +18,29 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const tooltipElement = ref<HTMLElement>();
|
const tooltipElement = ref<HTMLElement>();
|
||||||
|
|
||||||
const isDisabled = computed(() =>
|
const content = computed(() =>
|
||||||
isString(props.options.content)
|
isString(props.options) ? props.options : props.options.content
|
||||||
? props.options.content.trim() === ""
|
|
||||||
: props.options.content === false
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const placement = computed(() => props.options.placement ?? "top");
|
const isDisabled = computed(() => {
|
||||||
|
if (isEmpty(content.value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isString(props.options)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFunction(props.options.disabled)) {
|
||||||
|
return props.options.disabled(props.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.options.disabled ?? false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const placement = computed(() =>
|
||||||
|
isString(props.options) ? "top" : props.options.placement ?? "top"
|
||||||
|
);
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (tooltipElement.value) {
|
if (tooltipElement.value) {
|
||||||
|
|||||||
@@ -14,12 +14,7 @@
|
|||||||
</UiActionButton>
|
</UiActionButton>
|
||||||
</UiFilterGroup>
|
</UiFilterGroup>
|
||||||
|
|
||||||
<UiModal
|
<UiModal v-if="isOpen" :icon="faFilter" @submit.prevent="handleSubmit">
|
||||||
v-if="isOpen"
|
|
||||||
:icon="faFilter"
|
|
||||||
@submit.prevent="handleSubmit"
|
|
||||||
@close="handleCancel"
|
|
||||||
>
|
|
||||||
<div class="rows">
|
<div class="rows">
|
||||||
<CollectionFilterRow
|
<CollectionFilterRow
|
||||||
v-for="(newFilter, index) in newFilters"
|
v-for="(newFilter, index) in newFilters"
|
||||||
|
|||||||
@@ -17,12 +17,7 @@
|
|||||||
</UiActionButton>
|
</UiActionButton>
|
||||||
</UiFilterGroup>
|
</UiFilterGroup>
|
||||||
|
|
||||||
<UiModal
|
<UiModal v-if="isOpen" :icon="faSort" @submit.prevent="handleSubmit">
|
||||||
v-if="isOpen"
|
|
||||||
:icon="faSort"
|
|
||||||
@submit.prevent="handleSubmit"
|
|
||||||
@close="handleCancel"
|
|
||||||
>
|
|
||||||
<div class="form-widgets">
|
<div class="form-widgets">
|
||||||
<FormWidget :label="$t('sort-by')">
|
<FormWidget :label="$t('sort-by')">
|
||||||
<select v-model="newSortProperty">
|
<select v-model="newSortProperty">
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
<template>
|
|
||||||
<UiModal
|
|
||||||
v-if="isSslModalOpen"
|
|
||||||
:icon="faServer"
|
|
||||||
color="error"
|
|
||||||
@close="clearUnreachableHostsUrls"
|
|
||||||
>
|
|
||||||
<template #title>{{ $t("unreachable-hosts") }}</template>
|
|
||||||
<div class="description">
|
|
||||||
<p>{{ $t("following-hosts-unreachable") }}</p>
|
|
||||||
<p>{{ $t("allow-self-signed-ssl") }}</p>
|
|
||||||
<ul>
|
|
||||||
<li v-for="url in unreachableHostsUrls" :key="url">
|
|
||||||
<a :href="url" class="link" rel="noopener" target="_blank">{{
|
|
||||||
url
|
|
||||||
}}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<template #buttons>
|
|
||||||
<UiButton color="success" @click="reload">
|
|
||||||
{{ $t("unreachable-hosts-reload-page") }}
|
|
||||||
</UiButton>
|
|
||||||
<UiButton @click="clearUnreachableHostsUrls">{{ $t("cancel") }}</UiButton>
|
|
||||||
</template>
|
|
||||||
</UiModal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import UiModal from "@/components/ui/UiModal.vue";
|
|
||||||
import UiButton from "@/components/ui/UiButton.vue";
|
|
||||||
import { computed, ref, watch } from "vue";
|
|
||||||
import { difference } from "lodash";
|
|
||||||
import { useHostStore } from "@/stores/host.store";
|
|
||||||
|
|
||||||
const { records: hosts } = useHostStore().subscribe();
|
|
||||||
const unreachableHostsUrls = ref<Set<string>>(new Set());
|
|
||||||
const clearUnreachableHostsUrls = () => unreachableHostsUrls.value.clear();
|
|
||||||
const isSslModalOpen = computed(() => unreachableHostsUrls.value.size > 0);
|
|
||||||
const reload = () => window.location.reload();
|
|
||||||
|
|
||||||
watch(hosts, (nextHosts, previousHosts) => {
|
|
||||||
difference(nextHosts, previousHosts).forEach((host) => {
|
|
||||||
const url = new URL("http://localhost");
|
|
||||||
url.protocol = window.location.protocol;
|
|
||||||
url.hostname = host.address;
|
|
||||||
fetch(url, { mode: "no-cors" }).catch(() =>
|
|
||||||
unreachableHostsUrls.value.add(url.toString())
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
|
||||||
.description p {
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<li v-if="host !== undefined" class="infra-host-item">
|
<li
|
||||||
|
v-if="host !== undefined"
|
||||||
|
v-tooltip="{
|
||||||
|
content: host.name_label,
|
||||||
|
disabled: isTooltipDisabled,
|
||||||
|
}"
|
||||||
|
class="infra-host-item"
|
||||||
|
>
|
||||||
<InfraItemLabel
|
<InfraItemLabel
|
||||||
:active="isCurrentHost"
|
:active="isCurrentHost"
|
||||||
:icon="faServer"
|
:icon="faServer"
|
||||||
@@ -29,6 +36,7 @@ import InfraAction from "@/components/infra/InfraAction.vue";
|
|||||||
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
|
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
|
||||||
import InfraVmList from "@/components/infra/InfraVmList.vue";
|
import InfraVmList from "@/components/infra/InfraVmList.vue";
|
||||||
import { vTooltip } from "@/directives/tooltip.directive";
|
import { vTooltip } from "@/directives/tooltip.directive";
|
||||||
|
import { hasEllipsis } from "@/libs/utils";
|
||||||
import { useHostStore } from "@/stores/host.store";
|
import { useHostStore } from "@/stores/host.store";
|
||||||
import { usePoolStore } from "@/stores/pool.store";
|
import { usePoolStore } from "@/stores/pool.store";
|
||||||
import { useUiStore } from "@/stores/ui.store";
|
import { useUiStore } from "@/stores/ui.store";
|
||||||
@@ -58,6 +66,9 @@ const isCurrentHost = computed(
|
|||||||
() => props.hostOpaqueRef === uiStore.currentHostOpaqueRef
|
() => props.hostOpaqueRef === uiStore.currentHostOpaqueRef
|
||||||
);
|
);
|
||||||
const [isExpanded, toggle] = useToggle(true);
|
const [isExpanded, toggle] = useToggle(true);
|
||||||
|
|
||||||
|
const isTooltipDisabled = (target: HTMLElement) =>
|
||||||
|
!hasEllipsis(target.querySelector(".text"));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
class="infra-item-label"
|
class="infra-item-label"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
>
|
>
|
||||||
<a :href="href" class="link" @click="navigate" v-tooltip="hasTooltip">
|
<a :href="href" class="link" @click="navigate">
|
||||||
<UiIcon :icon="icon" class="icon" />
|
<UiIcon :icon="icon" class="icon" />
|
||||||
<div ref="textElement" class="text">
|
<div class="text">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -22,10 +22,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||||
import { vTooltip } from "@/directives/tooltip.directive";
|
|
||||||
import { hasEllipsis } from "@/libs/utils";
|
|
||||||
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
||||||
import { computed, ref } from "vue";
|
|
||||||
import type { RouteLocationRaw } from "vue-router";
|
import type { RouteLocationRaw } from "vue-router";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -33,9 +30,6 @@ defineProps<{
|
|||||||
route: RouteLocationRaw;
|
route: RouteLocationRaw;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const textElement = ref<HTMLElement>();
|
|
||||||
const hasTooltip = computed(() => hasEllipsis(textElement.value));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<li v-if="vm !== undefined" ref="rootElement" class="infra-vm-item">
|
<li
|
||||||
|
v-if="vm !== undefined"
|
||||||
|
ref="rootElement"
|
||||||
|
v-tooltip="{
|
||||||
|
content: vm.name_label,
|
||||||
|
disabled: isTooltipDisabled,
|
||||||
|
}"
|
||||||
|
class="infra-vm-item"
|
||||||
|
>
|
||||||
<InfraItemLabel
|
<InfraItemLabel
|
||||||
v-if="isVisible"
|
v-if="isVisible"
|
||||||
:icon="faDisplay"
|
:icon="faDisplay"
|
||||||
@@ -19,6 +27,8 @@
|
|||||||
import InfraAction from "@/components/infra/InfraAction.vue";
|
import InfraAction from "@/components/infra/InfraAction.vue";
|
||||||
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
|
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
|
||||||
import PowerStateIcon from "@/components/PowerStateIcon.vue";
|
import PowerStateIcon from "@/components/PowerStateIcon.vue";
|
||||||
|
import { vTooltip } from "@/directives/tooltip.directive";
|
||||||
|
import { hasEllipsis } from "@/libs/utils";
|
||||||
import { useVmStore } from "@/stores/vm.store";
|
import { useVmStore } from "@/stores/vm.store";
|
||||||
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
|
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useIntersectionObserver } from "@vueuse/core";
|
import { useIntersectionObserver } from "@vueuse/core";
|
||||||
@@ -39,6 +49,9 @@ const { stop } = useIntersectionObserver(rootElement, ([entry]) => {
|
|||||||
stop();
|
stop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isTooltipDisabled = (target: HTMLElement) =>
|
||||||
|
!hasEllipsis(target.querySelector(".text"));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ defineProps<{
|
|||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
.ui-badge {
|
.ui-badge {
|
||||||
white-space: nowrap;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="legend">
|
<div class="legend">
|
||||||
<template v-if="$slots.label || label">
|
<span class="circle" />
|
||||||
<span class="circle" />
|
<slot name="label">{{ label }}</slot>
|
||||||
<div class="label-container">
|
|
||||||
<div ref="labelElement" v-tooltip="isTooltipEnabled" class="label">
|
|
||||||
<slot name="label">{{ label }}</slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<UiBadge class="badge">
|
<UiBadge class="badge">
|
||||||
<slot name="value">{{ value }}</slot>
|
<slot name="value">{{ value }}</slot>
|
||||||
</UiBadge>
|
</UiBadge>
|
||||||
@@ -16,23 +10,14 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import UiBadge from "@/components/ui/UiBadge.vue";
|
import UiBadge from "@/components/ui/UiBadge.vue";
|
||||||
import { vTooltip } from "@/directives/tooltip.directive";
|
|
||||||
import { hasEllipsis } from "@/libs/utils";
|
|
||||||
import { computed, ref } from "vue";
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
label?: string;
|
label?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const labelElement = ref<HTMLElement>();
|
|
||||||
|
|
||||||
const isTooltipEnabled = computed(() =>
|
|
||||||
hasEllipsis(labelElement.value, { vertical: true })
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style scoped lang="postcss">
|
||||||
.badge {
|
.badge {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -40,8 +25,8 @@ const isTooltipEnabled = computed(() =>
|
|||||||
|
|
||||||
.circle {
|
.circle {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 1rem;
|
width: 1rem;
|
||||||
min-height: 1rem;
|
height: 1rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background-color: var(--progress-bar-color);
|
background-color: var(--progress-bar-color);
|
||||||
}
|
}
|
||||||
@@ -53,14 +38,4 @@ const isTooltipEnabled = computed(() =>
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin: 1.6em 0;
|
margin: 1.6em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-container {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
<template>
|
|
||||||
<UiTabBar>
|
|
||||||
<RouterTab :to="{ name: 'vm.dashboard', params: { uuid } }">
|
|
||||||
{{ $t("dashboard") }}
|
|
||||||
</RouterTab>
|
|
||||||
<RouterTab :to="{ name: 'vm.console', params: { uuid } }">
|
|
||||||
{{ $t("console") }}
|
|
||||||
</RouterTab>
|
|
||||||
<RouterTab :to="{ name: 'vm.alarms', params: { uuid } }">
|
|
||||||
{{ $t("alarms") }}
|
|
||||||
</RouterTab>
|
|
||||||
<RouterTab :to="{ name: 'vm.stats', params: { uuid } }">
|
|
||||||
{{ $t("stats") }}
|
|
||||||
</RouterTab>
|
|
||||||
<RouterTab :to="{ name: 'vm.system', params: { uuid } }">
|
|
||||||
{{ $t("system") }}
|
|
||||||
</RouterTab>
|
|
||||||
<RouterTab :to="{ name: 'vm.network', params: { uuid } }">
|
|
||||||
{{ $t("network") }}
|
|
||||||
</RouterTab>
|
|
||||||
<RouterTab :to="{ name: 'vm.storage', params: { uuid } }">
|
|
||||||
{{ $t("storage") }}
|
|
||||||
</RouterTab>
|
|
||||||
<RouterTab :to="{ name: 'vm.tasks', params: { uuid } }">
|
|
||||||
{{ $t("tasks") }}
|
|
||||||
</RouterTab>
|
|
||||||
</UiTabBar>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import RouterTab from "@/components/RouterTab.vue";
|
|
||||||
import UiTabBar from "@/components/ui/UiTabBar.vue";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
uuid: string;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
@@ -1,71 +1,36 @@
|
|||||||
# Tooltip Directive
|
# Tooltip Directive
|
||||||
|
|
||||||
By default, the tooltip will appear centered above the target element.
|
By default, tooltip will appear centered above the target element.
|
||||||
|
|
||||||
## Directive argument
|
|
||||||
|
|
||||||
The directive argument can be either:
|
|
||||||
|
|
||||||
- The tooltip content
|
|
||||||
- An object containing the tooltip content and/or placement: `{ content: "...", placement: "..." }` (both optional)
|
|
||||||
|
|
||||||
## Tooltip content
|
|
||||||
|
|
||||||
The tooltip content can be either:
|
|
||||||
|
|
||||||
- `false` or an empty-string to disable the tooltip
|
|
||||||
- `true` or `undefined` to enable the tooltip and extract its content from the element's innerText.
|
|
||||||
- Non-empty string to enable the tooltip and use the string as content.
|
|
||||||
|
|
||||||
## Tooltip placement
|
|
||||||
|
|
||||||
Tooltip can be placed on the following positions:
|
|
||||||
|
|
||||||
- `top`
|
|
||||||
- `top-start`
|
|
||||||
- `top-end`
|
|
||||||
- `bottom`
|
|
||||||
- `bottom-start`
|
|
||||||
- `bottom-end`
|
|
||||||
- `left`
|
|
||||||
- `left-start`
|
|
||||||
- `left-end`
|
|
||||||
- `right`
|
|
||||||
- `right-start`
|
|
||||||
- `right-end`
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<template>
|
<template>
|
||||||
<!-- Boolean / Undefined -->
|
<!-- Static -->
|
||||||
<span v-tooltip="true"
|
|
||||||
>This content will be ellipsized by CSS but displayed entirely in the
|
|
||||||
tooltip</span
|
|
||||||
>
|
|
||||||
<span v-tooltip
|
|
||||||
>This content will be ellipsized by CSS but displayed entirely in the
|
|
||||||
tooltip</span
|
|
||||||
>
|
|
||||||
|
|
||||||
<!-- String -->
|
|
||||||
<span v-tooltip="'Tooltip content'">Item</span>
|
<span v-tooltip="'Tooltip content'">Item</span>
|
||||||
|
|
||||||
<!-- Object -->
|
<!-- Dynamic -->
|
||||||
|
<span v-tooltip="myTooltipContent">Item</span>
|
||||||
|
|
||||||
|
<!-- Placement -->
|
||||||
<span v-tooltip="{ content: 'Foobar', placement: 'left-end' }">Item</span>
|
<span v-tooltip="{ content: 'Foobar', placement: 'left-end' }">Item</span>
|
||||||
|
|
||||||
<!-- Dynamic -->
|
<!-- Disabling (variable) -->
|
||||||
<span v-tooltip="myTooltip">Item</span>
|
<span v-tooltip="{ content: 'Foobar', disabled: isDisabled }">Item</span>
|
||||||
|
|
||||||
<!-- Conditional -->
|
<!-- Disabling (function) -->
|
||||||
<span v-tooltip="isTooltipEnabled && 'Foobar'">Item</span>
|
<span v-tooltip="{ content: 'Foobar', disabled: isDisabledFn }">Item</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { vTooltip } from "@/directives/tooltip.directive";
|
import { vTooltip } from "@/directives/tooltip.directive";
|
||||||
|
|
||||||
const myTooltip = ref("Content"); // or ref({ content: "Content", placement: "left-end" })
|
const myTooltipContent = ref("Content");
|
||||||
const isTooltipEnabled = ref(true);
|
const isDisabled = ref(true);
|
||||||
|
|
||||||
|
const isDisabledFn = (target: Element) => {
|
||||||
|
// return boolean;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,36 +1,8 @@
|
|||||||
|
import type { Directive } from "vue";
|
||||||
import type { TooltipEvents, TooltipOptions } from "@/stores/tooltip.store";
|
import type { TooltipEvents, TooltipOptions } from "@/stores/tooltip.store";
|
||||||
import { useTooltipStore } from "@/stores/tooltip.store";
|
import { useTooltipStore } from "@/stores/tooltip.store";
|
||||||
import { isObject } from "lodash-es";
|
|
||||||
import type { Options } from "placement.js";
|
|
||||||
import type { Directive } from "vue";
|
|
||||||
|
|
||||||
type TooltipDirectiveContent = undefined | boolean | string;
|
export const vTooltip: Directive<HTMLElement, TooltipOptions> = {
|
||||||
|
|
||||||
type TooltipDirectiveOptions =
|
|
||||||
| TooltipDirectiveContent
|
|
||||||
| {
|
|
||||||
content?: TooltipDirectiveContent;
|
|
||||||
placement?: Options["placement"];
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseOptions = (
|
|
||||||
options: TooltipDirectiveOptions,
|
|
||||||
target: HTMLElement
|
|
||||||
): TooltipOptions => {
|
|
||||||
const { placement, content } = isObject(options)
|
|
||||||
? options
|
|
||||||
: { placement: undefined, content: options };
|
|
||||||
|
|
||||||
return {
|
|
||||||
placement,
|
|
||||||
content:
|
|
||||||
content === true || content === undefined
|
|
||||||
? target.innerText.trim()
|
|
||||||
: content,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const vTooltip: Directive<HTMLElement, TooltipDirectiveOptions> = {
|
|
||||||
mounted(target, binding) {
|
mounted(target, binding) {
|
||||||
const store = useTooltipStore();
|
const store = useTooltipStore();
|
||||||
|
|
||||||
@@ -38,11 +10,11 @@ export const vTooltip: Directive<HTMLElement, TooltipDirectiveOptions> = {
|
|||||||
? { on: "focusin", off: "focusout" }
|
? { on: "focusin", off: "focusout" }
|
||||||
: { on: "mouseenter", off: "mouseleave" };
|
: { on: "mouseenter", off: "mouseleave" };
|
||||||
|
|
||||||
store.register(target, parseOptions(binding.value, target), events);
|
store.register(target, binding.value, events);
|
||||||
},
|
},
|
||||||
updated(target, binding) {
|
updated(target, binding) {
|
||||||
const store = useTooltipStore();
|
const store = useTooltipStore();
|
||||||
store.updateOptions(target, parseOptions(binding.value, target));
|
store.updateOptions(target, binding.value);
|
||||||
},
|
},
|
||||||
beforeUnmount(target) {
|
beforeUnmount(target) {
|
||||||
const store = useTooltipStore();
|
const store = useTooltipStore();
|
||||||
|
|||||||
@@ -71,20 +71,8 @@ export function parseDateTime(dateTime: string) {
|
|||||||
return date.getTime();
|
return date.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hasEllipsis = (
|
export const hasEllipsis = (target: Element | undefined | null) =>
|
||||||
target: Element | undefined | null,
|
target != undefined && target.clientWidth < target.scrollWidth;
|
||||||
{ vertical = false }: { vertical?: boolean } = {}
|
|
||||||
) => {
|
|
||||||
if (target == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vertical) {
|
|
||||||
return target.clientHeight < target.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return target.clientWidth < target.scrollWidth;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function percent(currentValue: number, maxValue: number, precision = 2) {
|
export function percent(currentValue: number, maxValue: number, precision = 2) {
|
||||||
return round((currentValue / maxValue) * 100, precision);
|
return round((currentValue / maxValue) * 100, precision);
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
"coming-soon": "Coming soon!",
|
"coming-soon": "Coming soon!",
|
||||||
"community": "Community",
|
"community": "Community",
|
||||||
"community-name": "{name} community",
|
"community-name": "{name} community",
|
||||||
"console": "Console",
|
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"cpu-provisioning": "CPU provisioning",
|
"cpu-provisioning": "CPU provisioning",
|
||||||
"cpu-usage": "CPU usage",
|
"cpu-usage": "CPU usage",
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
"coming-soon": "Bientôt disponible !",
|
"coming-soon": "Bientôt disponible !",
|
||||||
"community": "Communauté",
|
"community": "Communauté",
|
||||||
"community-name": "Communauté {name}",
|
"community-name": "Communauté {name}",
|
||||||
"console": "Console",
|
|
||||||
"copy": "Copier",
|
"copy": "Copier",
|
||||||
"cpu-provisioning": "Provisionnement CPU",
|
"cpu-provisioning": "Provisionnement CPU",
|
||||||
"cpu-usage": "Utilisation CPU",
|
"cpu-usage": "Utilisation CPU",
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import pool from "@/router/pool";
|
import pool from "@/router/pool";
|
||||||
import vm from "@/router/vm";
|
|
||||||
import HomeView from "@/views/HomeView.vue";
|
import HomeView from "@/views/HomeView.vue";
|
||||||
import HostDashboardView from "@/views/host/HostDashboardView.vue";
|
import HostDashboardView from "@/views/host/HostDashboardView.vue";
|
||||||
import HostRootView from "@/views/host/HostRootView.vue";
|
import HostRootView from "@/views/host/HostRootView.vue";
|
||||||
import PageNotFoundView from "@/views/PageNotFoundView.vue";
|
import PageNotFoundView from "@/views/PageNotFoundView.vue";
|
||||||
import SettingsView from "@/views/settings/SettingsView.vue";
|
import SettingsView from "@/views/settings/SettingsView.vue";
|
||||||
import StoryView from "@/views/StoryView.vue";
|
import StoryView from "@/views/StoryView.vue";
|
||||||
|
import VmConsoleView from "@/views/vm/VmConsoleView.vue";
|
||||||
|
import VmRootView from "@/views/vm/VmRootView.vue";
|
||||||
import storiesRoutes from "virtual:stories";
|
import storiesRoutes from "virtual:stories";
|
||||||
import { createRouter, createWebHashHistory } from "vue-router";
|
import { createRouter, createWebHashHistory } from "vue-router";
|
||||||
|
|
||||||
@@ -30,7 +31,6 @@ const router = createRouter({
|
|||||||
component: SettingsView,
|
component: SettingsView,
|
||||||
},
|
},
|
||||||
pool,
|
pool,
|
||||||
vm,
|
|
||||||
{
|
{
|
||||||
path: "/host/:uuid",
|
path: "/host/:uuid",
|
||||||
component: HostRootView,
|
component: HostRootView,
|
||||||
@@ -42,6 +42,17 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/vm/:uuid",
|
||||||
|
component: VmRootView,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "console",
|
||||||
|
name: "vm.console",
|
||||||
|
component: VmConsoleView,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/:pathMatch(.*)*",
|
path: "/:pathMatch(.*)*",
|
||||||
name: "notFound",
|
name: "notFound",
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
export default {
|
|
||||||
path: "/vm/:uuid",
|
|
||||||
component: () => import("@/views/vm/VmRootView.vue"),
|
|
||||||
redirect: { name: "vm.console" },
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "dashboard",
|
|
||||||
name: "vm.dashboard",
|
|
||||||
component: () => import("@/views/vm/VmDashboardView.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "console",
|
|
||||||
name: "vm.console",
|
|
||||||
component: () => import("@/views/vm/VmConsoleView.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "alarms",
|
|
||||||
name: "vm.alarms",
|
|
||||||
component: () => import("@/views/vm/VmAlarmsView.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "stats",
|
|
||||||
name: "vm.stats",
|
|
||||||
component: () => import("@/views/vm/VmStatsView.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "system",
|
|
||||||
name: "vm.system",
|
|
||||||
component: () => import("@/views/vm/VmSystemView.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "network",
|
|
||||||
name: "vm.network",
|
|
||||||
component: () => import("@/views/vm/VmNetworkView.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "storage",
|
|
||||||
name: "vm.storage",
|
|
||||||
component: () => import("@/views/vm/VmStorageView.vue"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "tasks",
|
|
||||||
name: "vm.tasks",
|
|
||||||
component: () => import("@/views/vm/VmTasksView.vue"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -4,10 +4,13 @@ import type { Options } from "placement.js";
|
|||||||
import { type EffectScope, computed, effectScope, ref } from "vue";
|
import { type EffectScope, computed, effectScope, ref } from "vue";
|
||||||
import { type WindowEventName, useEventListener } from "@vueuse/core";
|
import { type WindowEventName, useEventListener } from "@vueuse/core";
|
||||||
|
|
||||||
export type TooltipOptions = {
|
export type TooltipOptions =
|
||||||
content: string | false;
|
| string
|
||||||
placement: Options["placement"];
|
| {
|
||||||
};
|
content: string;
|
||||||
|
placement?: Options["placement"];
|
||||||
|
disabled?: boolean | ((target: HTMLElement) => boolean);
|
||||||
|
};
|
||||||
|
|
||||||
export type TooltipEvents = { on: WindowEventName; off: WindowEventName };
|
export type TooltipEvents = { on: WindowEventName; off: WindowEventName };
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<PoolDashboardRamUsage />
|
<PoolDashboardRamUsage />
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<PoolDashboardCpuProvisioning />
|
<PoolDashboardCpuProvisionning />
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<PoolDashboardNetworkChart />
|
<PoolDashboardNetworkChart />
|
||||||
@@ -38,7 +38,7 @@ import { provide, watch } from "vue";
|
|||||||
import PoolCpuUsageChart from "@/components/pool/dashboard/cpuUsage/PoolCpuUsageChart.vue";
|
import PoolCpuUsageChart from "@/components/pool/dashboard/cpuUsage/PoolCpuUsageChart.vue";
|
||||||
import PoolDashboardCpuUsage from "@/components/pool/dashboard/PoolDashboardCpuUsage.vue";
|
import PoolDashboardCpuUsage from "@/components/pool/dashboard/PoolDashboardCpuUsage.vue";
|
||||||
import PoolDashboardNetworkChart from "@/components/pool/dashboard/PoolDashboardNetworkChart.vue";
|
import PoolDashboardNetworkChart from "@/components/pool/dashboard/PoolDashboardNetworkChart.vue";
|
||||||
import PoolDashboardCpuProvisioning from "@/components/pool/dashboard/PoolDashboardCpuProvisioning.vue";
|
import PoolDashboardCpuProvisionning from "@/components/pool/dashboard/PoolDashboardCpuProvisionning.vue";
|
||||||
import PoolDashboardRamUsage from "@/components/pool/dashboard/PoolDashboardRamUsage.vue";
|
import PoolDashboardRamUsage from "@/components/pool/dashboard/PoolDashboardRamUsage.vue";
|
||||||
import PoolDashboardRamUsageChart from "@/components/pool/dashboard/ramUsage/PoolRamUsage.vue";
|
import PoolDashboardRamUsageChart from "@/components/pool/dashboard/ramUsage/PoolRamUsage.vue";
|
||||||
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
|
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
|
||||||
|
|||||||
@@ -1,49 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<UiCard class="home-view">
|
<div class="home-view">
|
||||||
<UiCardTitle>Component Story skeleton generator</UiCardTitle>
|
<UiTitle type="h4">
|
||||||
|
This helper will generate a basic story component
|
||||||
<div class="row">
|
</UiTitle>
|
||||||
Choose a component
|
<div>
|
||||||
<FormSelect v-model="componentPath">
|
Choose a component:
|
||||||
|
<select v-model="componentPath">
|
||||||
<option value="" />
|
<option value="" />
|
||||||
<option v-for="path in componentPaths" :key="path">
|
<option v-for="(component, path) in componentsWithProps" :key="path">
|
||||||
{{ path }}
|
{{ path }}
|
||||||
</option>
|
</option>
|
||||||
</FormSelect>
|
</select>
|
||||||
|
<div class="slots">
|
||||||
|
<label>
|
||||||
|
Slots names, separated by a comma
|
||||||
|
<input v-model="slots" />
|
||||||
|
</label>
|
||||||
|
<button @click="slots = 'default'">Default</button>
|
||||||
|
<button @click="slots = ''">Clear</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<CodeHighlight v-if="componentPath" :code="template" />
|
||||||
<div class="row">
|
</div>
|
||||||
Slot names, separated by comma
|
|
||||||
<span class="slots">
|
|
||||||
<FormInput v-model="slots" />
|
|
||||||
<UiButton @click="slots = 'default'">Default</UiButton>
|
|
||||||
<UiButton outlined @click="slots = ''">Clear</UiButton>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p v-for="warning in warnings" :key="warning" class="row warning">
|
|
||||||
<UiIcon :icon="faWarning" />
|
|
||||||
{{ warning }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<CodeHighlight
|
|
||||||
class="code-highlight"
|
|
||||||
v-if="componentPath"
|
|
||||||
:code="template"
|
|
||||||
/>
|
|
||||||
</UiCard>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import CodeHighlight from "@/components/CodeHighlight.vue";
|
import CodeHighlight from "@/components/CodeHighlight.vue";
|
||||||
import FormInput from "@/components/form/FormInput.vue";
|
import UiTitle from "@/components/ui/UiTitle.vue";
|
||||||
import FormSelect from "@/components/form/FormSelect.vue";
|
|
||||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
|
||||||
import UiButton from "@/components/ui/UiButton.vue";
|
|
||||||
import UiCard from "@/components/ui/UiCard.vue";
|
|
||||||
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
|
|
||||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { castArray } from "lodash-es";
|
|
||||||
import { type ComponentOptions, computed, ref, watch } from "vue";
|
import { type ComponentOptions, computed, ref, watch } from "vue";
|
||||||
|
|
||||||
const componentPath = ref("");
|
const componentPath = ref("");
|
||||||
@@ -61,14 +44,10 @@ const componentsWithProps = Object.fromEntries(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const componentPaths = Object.keys(componentsWithProps);
|
|
||||||
|
|
||||||
const lines = ref<string[]>([]);
|
const lines = ref<string[]>([]);
|
||||||
const slots = ref("");
|
const slots = ref("");
|
||||||
|
|
||||||
const quote = (str: string) => `'${str}'`;
|
const quote = (str: string) => `'${str}'`;
|
||||||
const camel = (str: string) =>
|
|
||||||
str.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
|
|
||||||
const paramsToImport = ref(new Set<string>());
|
const paramsToImport = ref(new Set<string>());
|
||||||
const widgetsToImport = ref(new Set<string>());
|
const widgetsToImport = ref(new Set<string>());
|
||||||
|
|
||||||
@@ -82,15 +61,13 @@ const template = computed(() => {
|
|||||||
.filter((name) => name !== "");
|
.filter((name) => name !== "");
|
||||||
|
|
||||||
for (const slotName of slotsNames) {
|
for (const slotName of slotsNames) {
|
||||||
paramsLines.push(
|
paramsLines.push(`slot(${slotName === "default" ? "" : quote(slotName)})`);
|
||||||
`slot(${slotName === "default" ? "" : quote(camel(slotName))})`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const slotName of slotsNames) {
|
for (const slotName of slotsNames) {
|
||||||
paramsLines.push(
|
paramsLines.push(
|
||||||
`setting(${quote(
|
`setting(${quote(
|
||||||
`${camel(slotName)}SlotContent`
|
`${slotName}SlotContent`
|
||||||
)}).preset('Example content for ${slotName} slot').widget(text()).help('Content for ${slotName} slot')`
|
)}).preset('Example content for ${slotName} slot').widget(text()).help('Content for ${slotName} slot')`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -101,7 +78,7 @@ const template = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const paramsStr = paramsLines.join(",\n ");
|
const paramsStr = paramsLines.join(",\n ");
|
||||||
const scriptEndTag = "</" + "script>";
|
|
||||||
return `<template>
|
return `<template>
|
||||||
<ComponentStory
|
<ComponentStory
|
||||||
v-slot="{ properties, settings }"
|
v-slot="{ properties, settings }"
|
||||||
@@ -114,10 +91,8 @@ const template = computed(() => {
|
|||||||
? `>\n ${slotsNames
|
? `>\n ${slotsNames
|
||||||
.map((name) =>
|
.map((name) =>
|
||||||
name === "default"
|
name === "default"
|
||||||
? `{{ settings.${camel(name)}SlotContent }}`
|
? `{{ settings.${name}SlotContent }}`
|
||||||
: `<template #${name}>{{ settings.${camel(
|
: `<template #${name}>{{ settings.${name}SlotContent }}</template>`
|
||||||
name
|
|
||||||
)}SlotContent }}</template>`
|
|
||||||
)
|
)
|
||||||
.join("\n ")}
|
.join("\n ")}
|
||||||
</${componentName}>`
|
</${componentName}>`
|
||||||
@@ -143,30 +118,10 @@ ${
|
|||||||
)} } from "@/libs/story/story-widget"`
|
)} } from "@/libs/story/story-widget"`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
${scriptEndTag}
|
${"<"}/script>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const warnings = ref(new Set<string>());
|
|
||||||
|
|
||||||
const extractTypeFromConstructor = (
|
|
||||||
ctor: null | (new () => unknown),
|
|
||||||
propName: string
|
|
||||||
) => {
|
|
||||||
if (ctor == null) {
|
|
||||||
warnings.value.add(
|
|
||||||
`An unknown type has been detected for prop "${propName}"`
|
|
||||||
);
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctor === Date) {
|
|
||||||
return "Date";
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctor.name.toLocaleLowerCase();
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
componentPath,
|
componentPath,
|
||||||
(path: string) => {
|
(path: string) => {
|
||||||
@@ -178,7 +133,6 @@ watch(
|
|||||||
slots.value = "";
|
slots.value = "";
|
||||||
widgetsToImport.value = new Set();
|
widgetsToImport.value = new Set();
|
||||||
paramsToImport.value = new Set();
|
paramsToImport.value = new Set();
|
||||||
warnings.value = new Set();
|
|
||||||
lines.value = [];
|
lines.value = [];
|
||||||
|
|
||||||
for (const propName in component.props) {
|
for (const propName in component.props) {
|
||||||
@@ -193,14 +147,12 @@ watch(
|
|||||||
current.push(`default(${quote(prop.default)})`);
|
current.push(`default(${quote(prop.default)})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prop.type !== undefined) {
|
if (prop.type) {
|
||||||
const type = castArray(prop.type)
|
const type = prop.type();
|
||||||
.map((ctor) => extractTypeFromConstructor(ctor, propName))
|
|
||||||
.join(" | ");
|
|
||||||
|
|
||||||
if (type !== "unknown") {
|
current.push(
|
||||||
current.push(`type(${quote(type)})`);
|
`type(${quote(Array.isArray(type) ? "array" : typeof type)})`
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isModel = component.emits?.includes(`update:${propName}`);
|
const isModel = component.emits?.includes(`update:${propName}`);
|
||||||
@@ -212,28 +164,16 @@ watch(
|
|||||||
})`
|
})`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isModel) {
|
current.push("widget()");
|
||||||
current.push("widget()");
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.value.push(current.join("."));
|
lines.value.push(current.join("."));
|
||||||
}
|
}
|
||||||
|
|
||||||
let shouldImportEvent = false;
|
|
||||||
|
|
||||||
if (component.emits) {
|
if (component.emits) {
|
||||||
for (const eventName of component.emits) {
|
|
||||||
if (eventName.startsWith("update:")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldImportEvent = true;
|
|
||||||
lines.value.push(`event(${quote(eventName)})`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldImportEvent) {
|
|
||||||
paramsToImport.value.add("event");
|
paramsToImport.value.add("event");
|
||||||
|
for (const eventName of component.emits) {
|
||||||
|
lines.value.push(`event("${eventName}")`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
@@ -245,28 +185,11 @@ watch(
|
|||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-title {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.slots {
|
.slots {
|
||||||
display: inline-flex;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
:deep(input) {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
font-size: 1.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
font-size: 1.6rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-orange-world-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-highlight {
|
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageUnderConstruction />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
|
|
||||||
</script>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageUnderConstruction />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
|
|
||||||
</script>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageUnderConstruction />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
|
|
||||||
</script>
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<ObjectNotFoundWrapper :is-ready="isReady" :uuid-checker="hasUuid">
|
<ObjectNotFoundWrapper :is-ready="isReady" :uuid-checker="hasUuid">
|
||||||
<VmHeader />
|
<VmHeader />
|
||||||
<VmTabBar :uuid="vm!.uuid" />
|
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</ObjectNotFoundWrapper>
|
</ObjectNotFoundWrapper>
|
||||||
</template>
|
</template>
|
||||||
@@ -9,16 +8,18 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import ObjectNotFoundWrapper from "@/components/ObjectNotFoundWrapper.vue";
|
import ObjectNotFoundWrapper from "@/components/ObjectNotFoundWrapper.vue";
|
||||||
import VmHeader from "@/components/vm/VmHeader.vue";
|
import VmHeader from "@/components/vm/VmHeader.vue";
|
||||||
import VmTabBar from "@/components/vm/VmTabBar.vue";
|
|
||||||
import { useUiStore } from "@/stores/ui.store";
|
import { useUiStore } from "@/stores/ui.store";
|
||||||
import { useVmStore } from "@/stores/vm.store";
|
import { useVmStore } from "@/stores/vm.store";
|
||||||
import { whenever } from "@vueuse/core";
|
import { watchEffect } from "vue";
|
||||||
import { computed } from "vue";
|
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { getByUuid, hasUuid, isReady } = useVmStore().subscribe();
|
const { getByUuid, hasUuid, isReady } = useVmStore().subscribe();
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
const vm = computed(() => getByUuid(route.params.uuid as string));
|
|
||||||
whenever(vm, (vm) => (uiStore.currentHostOpaqueRef = vm.resident_on));
|
watchEffect(() => {
|
||||||
|
uiStore.currentHostOpaqueRef = getByUuid(
|
||||||
|
route.params.uuid as string
|
||||||
|
)?.resident_on;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageUnderConstruction />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
|
|
||||||
</script>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageUnderConstruction />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
|
|
||||||
</script>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageUnderConstruction />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
|
|
||||||
</script>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<template>
|
|
||||||
<PageUnderConstruction />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
|
|
||||||
</script>
|
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vates/event-listeners-manager": "^1.0.1",
|
"@vates/event-listeners-manager": "^1.0.1",
|
||||||
"@vates/parse-duration": "^0.1.1",
|
"@vates/parse-duration": "^0.1.1",
|
||||||
"@vates/task": "^0.1.2",
|
"@vates/task": "^0.1.0",
|
||||||
"@xen-orchestra/log": "^0.6.0",
|
"@xen-orchestra/log": "^0.6.0",
|
||||||
"acme-client": "^5.0.0",
|
"acme-client": "^5.0.0",
|
||||||
"app-conf": "^2.3.0",
|
"app-conf": "^2.3.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "@xen-orchestra/proxy",
|
"name": "@xen-orchestra/proxy",
|
||||||
"version": "0.26.23",
|
"version": "0.26.20",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"description": "XO Proxy used to remotely execute backup jobs",
|
"description": "XO Proxy used to remotely execute backup jobs",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"@vates/decorate-with": "^2.0.0",
|
"@vates/decorate-with": "^2.0.0",
|
||||||
"@vates/disposable": "^0.1.4",
|
"@vates/disposable": "^0.1.4",
|
||||||
"@xen-orchestra/async-map": "^0.1.2",
|
"@xen-orchestra/async-map": "^0.1.2",
|
||||||
"@xen-orchestra/backups": "^0.36.1",
|
"@xen-orchestra/backups": "^0.35.0",
|
||||||
"@xen-orchestra/fs": "^3.3.4",
|
"@xen-orchestra/fs": "^3.3.4",
|
||||||
"@xen-orchestra/log": "^0.6.0",
|
"@xen-orchestra/log": "^0.6.0",
|
||||||
"@xen-orchestra/mixin": "^0.1.0",
|
"@xen-orchestra/mixin": "^0.1.0",
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"source-map-support": "^0.5.16",
|
"source-map-support": "^0.5.16",
|
||||||
"stoppable": "^1.0.6",
|
"stoppable": "^1.0.6",
|
||||||
"xdg-basedir": "^5.1.0",
|
"xdg-basedir": "^5.1.0",
|
||||||
"xen-api": "^1.3.1",
|
"xen-api": "^1.3.0",
|
||||||
"xo-common": "^0.8.0"
|
"xo-common": "^0.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
"pw": "^0.0.4",
|
"pw": "^0.0.4",
|
||||||
"xdg-basedir": "^4.0.0",
|
"xdg-basedir": "^4.0.0",
|
||||||
"xo-lib": "^0.11.1",
|
"xo-lib": "^0.11.1",
|
||||||
"xo-vmdk-to-vhd": "^2.5.4"
|
"xo-vmdk-to-vhd": "^2.5.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.0.0",
|
"@babel/cli": "^7.0.0",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"name": "@xen-orchestra/vmware-explorer",
|
"name": "@xen-orchestra/vmware-explorer",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vates/task": "^0.1.2",
|
"@vates/task": "^0.1.0",
|
||||||
"@vates/read-chunk": "^1.1.1",
|
"@vates/read-chunk": "^1.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"node-fetch": "^3.3.0",
|
"node-fetch": "^3.3.0",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"xen-api": "^1.3.1"
|
"xen-api": "^1.3.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postversion": "npm publish --access public",
|
"postversion": "npm publish --access public",
|
||||||
|
|||||||
64
CHANGELOG.md
64
CHANGELOG.md
@@ -2,57 +2,13 @@
|
|||||||
|
|
||||||
## **next**
|
## **next**
|
||||||
|
|
||||||
### Bug fixes
|
|
||||||
|
|
||||||
- [New/VM] Fix stuck Cloud Config import ([GitHub comment](https://github.com/vatesfr/xen-orchestra/issues/5896#issuecomment-1465253774))
|
|
||||||
|
|
||||||
### Released packages
|
|
||||||
|
|
||||||
- xen-api 1.3.1
|
|
||||||
- @xen-orchestra/proxy 0.26.23
|
|
||||||
- xo-server 5.114.2
|
|
||||||
|
|
||||||
## **5.82.1** (2023-05-12)
|
|
||||||
|
|
||||||
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
|
||||||
|
|
||||||
### Enhancements
|
|
||||||
|
|
||||||
- [Plugins] Clicking on a plugin name now filters out other plugins
|
|
||||||
|
|
||||||
### Bug fixes
|
|
||||||
|
|
||||||
- [Host/Network] Fix IP configuration not working with empty fields
|
|
||||||
- [Import/VM/From VMware] Fix `Property description must be an object: undefined` [Forum#61834](https://xcp-ng.org/forum/post/61834) [Forum#61900](https://xcp-ng.org/forum/post/61900)
|
|
||||||
- [Import/VM/From VMware] Fix `Cannot read properties of undefined (reading 'stream')` [Forum#59879](https://xcp-ng.org/forum/post/59879) (PR [#6825](https://github.com/vatesfr/xen-orchestra/pull/6825))
|
|
||||||
- [OVA export] Fix major memory leak which may lead to xo-server crash [Forum#56051](https://xcp-ng.org/forum/post/56051) (PR [#6800](https://github.com/vatesfr/xen-orchestra/pull/6800))
|
|
||||||
- [VM] Fix `VBD_IS_EMPTY` error when converting to template [Forum#61653](https://xcp-ng.org/forum/post/61653) (PR [#6808](https://github.com/vatesfr/xen-orchestra/pull/6808))
|
|
||||||
- [New/Network] Fix `invalid parameter error` when not providing a VLAN [Forum#62090](https://xcp-ng.org/forum/post/62090) (PR [#6829](https://github.com/vatesfr/xen-orchestra/pull/6829))
|
|
||||||
- [Backup/Health check] Fix `task has already ended` error during a healthcheck in continous replication [Forum#62073](https://xcp-ng.org/forum/post/62073) (PR [#6830](https://github.com/vatesfr/xen-orchestra/pull/6830))
|
|
||||||
|
|
||||||
### Released packages
|
|
||||||
|
|
||||||
- @vates/task 0.1.2
|
|
||||||
- xo-vmdk-to-vhd 2.5.4
|
|
||||||
- @xen-orchestra/backups 0.36.1
|
|
||||||
- @xen-orchestra/proxy 0.26.22
|
|
||||||
- xo-server 5.114.1
|
|
||||||
- xo-web 5.117.1
|
|
||||||
|
|
||||||
## **5.82.0** (2023-04-28)
|
|
||||||
|
|
||||||
### Highlights
|
|
||||||
|
|
||||||
- [Host] Smart reboot: suspend resident VMs, restart host and resume VMs [#6750](https://github.com/vatesfr/xen-orchestra/issues/6750) (PR [#6795](https://github.com/vatesfr/xen-orchestra/pull/6795))
|
|
||||||
- [Backup/exports] Retry when failing to read a data block during Delta Backup, Continuous Replication, disk and OVA export when NBD is enabled [PR #6763](https://github.com/vatesfr/xen-orchestra/pull/6763)
|
|
||||||
- [Backup/Health check] [Opt-in XenStore API](https://xen-orchestra.com/docs/backups.html#backup-health-check) to execute custom checks inside the VM (PR [#6784](https://github.com/vatesfr/xen-orchestra/pull/6784))
|
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
- [VM/Advanced] Automatically eject removable medias when converting a VM to a template [#6752](https://github.com/vatesfr/xen-orchestra/issues/6752) (PR [#6769](https://github.com/vatesfr/xen-orchestra/pull/6769))
|
- [VM/Advanced] Automatically eject removable medias when converting a VM to a template [#6752](https://github.com/vatesfr/xen-orchestra/issues/6752) (PR [#6769](https://github.com/vatesfr/xen-orchestra/pull/6769))
|
||||||
- [Dashboard/Health] Add free space column for storage state table (PR [#6778](https://github.com/vatesfr/xen-orchestra/pull/6778))
|
- [Dashboard/Health] Add free space column for storage state table (PR [#6778](https://github.com/vatesfr/xen-orchestra/pull/6778))
|
||||||
- [VM/General] Displays the template name used to create the VM, as well as the email address of the VM creator for admin users (PR [#6771](https://github.com/vatesfr/xen-orchestra/pull/6771))
|
- [VM/General] Displays the template name used to create the VM, as well as the email address of the VM creator for admin users (PR [#6771](https://github.com/vatesfr/xen-orchestra/pull/6771))
|
||||||
- [Kubernetes] Give the possibility to create an high availability cluster (PR [#6794](https://github.com/vatesfr/xen-orchestra/pull/6794))
|
- [Backup/exports] Retry when failing to read a data block during Delta Backup, Continuous Replication, disk and OVA export when NBD is enabled [PR #6763](https://github.com/vatesfr/xen-orchestra/pull/6763)
|
||||||
|
- [Host] Smart reboot: suspend resident VMs, restart host and resume VMs [#6750](https://github.com/vatesfr/xen-orchestra/issues/6750) (PR [#6795](https://github.com/vatesfr/xen-orchestra/pull/6795))
|
||||||
|
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
|
|
||||||
@@ -73,21 +29,21 @@
|
|||||||
- vhd-lib 4.4.0
|
- vhd-lib 4.4.0
|
||||||
- xen-api 1.3.0
|
- xen-api 1.3.0
|
||||||
- @vates/nbd-client 1.2.0
|
- @vates/nbd-client 1.2.0
|
||||||
|
- @vates/task 0.1.0
|
||||||
- @xen-orchestra/xapi 2.2.0
|
- @xen-orchestra/xapi 2.2.0
|
||||||
|
- @xen-orchestra/backups 0.35.0
|
||||||
|
- @xen-orchestra/backups-cli 1.0.5
|
||||||
- @xen-orchestra/mixins 0.10.0
|
- @xen-orchestra/mixins 0.10.0
|
||||||
|
- @xen-orchestra/proxy 0.26.20
|
||||||
- @xen-orchestra/vmware-explorer 0.2.2
|
- @xen-orchestra/vmware-explorer 0.2.2
|
||||||
- xo-cli 0.18.0
|
- xo-cli 0.18.0
|
||||||
|
- xo-server 5.112.0
|
||||||
- xo-server-usage-report 0.10.4
|
- xo-server-usage-report 0.10.4
|
||||||
- @vates/task 0.1.1
|
- xo-web 5.115.0
|
||||||
- @xen-orchestra/backups 0.36.0
|
|
||||||
- @xen-orchestra/backups-cli 1.0.6
|
|
||||||
- @xen-orchestra/proxy 0.26.21
|
|
||||||
- xo-server 5.113.0
|
|
||||||
- xo-web 5.116.1
|
|
||||||
|
|
||||||
## **5.81** (2023-03-31)
|
## **5.81** (2023-03-31)
|
||||||
|
|
||||||
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
||||||
|
|
||||||
### Highlights
|
### Highlights
|
||||||
|
|
||||||
@@ -126,6 +82,8 @@
|
|||||||
|
|
||||||
## **5.80.2** (2023-03-16)
|
## **5.80.2** (2023-03-16)
|
||||||
|
|
||||||
|
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
- [Plugin/auth-oidc] Support `email` for _username field_ setting [Forum#59587](https://xcp-ng.org/forum/post/59587)
|
- [Plugin/auth-oidc] Support `email` for _username field_ setting [Forum#59587](https://xcp-ng.org/forum/post/59587)
|
||||||
|
|||||||
@@ -7,15 +7,10 @@
|
|||||||
|
|
||||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||||
|
|
||||||
- [Proxy] Make proxy address editable (PR [#6816](https://github.com/vatesfr/xen-orchestra/pull/6816))
|
|
||||||
- [Home/Host] Displays a warning for hosts with HVM disabled [#6823](https://github.com/vatesfr/xen-orchestra/issues/6823) (PR [#6834](https://github.com/vatesfr/xen-orchestra/pull/6834))
|
|
||||||
|
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
|
|
||||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||||
|
|
||||||
- [Sorted table] In collapsed actions, a spinner is displayed during the action time (PR [#6831](https://github.com/vatesfr/xen-orchestra/pull/6831))
|
|
||||||
|
|
||||||
### Packages to release
|
### Packages to release
|
||||||
|
|
||||||
> When modifying a package, add it here with its release type.
|
> When modifying a package, add it here with its release type.
|
||||||
@@ -32,6 +27,4 @@
|
|||||||
|
|
||||||
<!--packages-start-->
|
<!--packages-start-->
|
||||||
|
|
||||||
- xo-web minor
|
|
||||||
|
|
||||||
<!--packages-end-->
|
<!--packages-end-->
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 55 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 88 KiB |
@@ -296,9 +296,8 @@ When it's done exporting, we'll remove the snapshot. Note: this operation will t
|
|||||||
Concurrency is a parameter that let you define how many VMs your backup job will manage simultaneously.
|
Concurrency is a parameter that let you define how many VMs your backup job will manage simultaneously.
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
|
|
||||||
- Default concurrency value is 2 if left empty.
|
- Default concurrency value is 2 if left empty.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Let's say you want to backup 50 VMs (each with 1x disk) at 3:00 AM. There are **2 different strategies**:
|
Let's say you want to backup 50 VMs (each with 1x disk) at 3:00 AM. There are **2 different strategies**:
|
||||||
|
|
||||||
@@ -336,7 +335,6 @@ When a backup job is configured using Normal snapshot mode, it's possible to use
|
|||||||
|
|
||||||
- **xo-offline-backup** to apply offline snapshotting mode (VM with be shut down prior to snapshot)
|
- **xo-offline-backup** to apply offline snapshotting mode (VM with be shut down prior to snapshot)
|
||||||
- **xo-memory-backup** to apply RAM-enabled snapshotting
|
- **xo-memory-backup** to apply RAM-enabled snapshotting
|
||||||
- **xo-backup-healthcheck-xenstore** to use a script during [backup healthcheck](#backup-health-check)
|
|
||||||
|
|
||||||
For example, you could have a regular backup job with 10 VMs configured with Normal snapshotting, including two which are database servers. Since database servers are generally more sensitive to being restored from snapshots, you could apply the **xo-memory-backup** tag to those two VMs and only those will be backed up in RAM-enabled mode. This will avoid the need to manage a separate backup job and schedule.
|
For example, you could have a regular backup job with 10 VMs configured with Normal snapshotting, including two which are database servers. Since database servers are generally more sensitive to being restored from snapshots, you could apply the **xo-memory-backup** tag to those two VMs and only those will be backed up in RAM-enabled mode. This will avoid the need to manage a separate backup job and schedule.
|
||||||
|
|
||||||
@@ -344,10 +342,10 @@ For example, you could have a regular backup job with 10 VMs configured with Nor
|
|||||||
|
|
||||||
Just a refresher/summary: You can select multiple backup methods for the same job:
|
Just a refresher/summary: You can select multiple backup methods for the same job:
|
||||||
|
|
||||||
- Full: _Backup_ and _Disaster Recovery_ (DR)
|
- Full: *Backup* and *Disaster Recovery* (DR)
|
||||||
- Deltas: _Delta Backup_ and _Continuous Replication_ (CR)
|
- Deltas: *Delta Backup* and *Continuous Replication* (CR)
|
||||||
|
|
||||||
The Full and Delta options are mutually exclusive; Rolling Snapshots are compatible with both. The Backup and Delta Backup go to a remote Target (e.g, NFS); DR and CR back up to another XCP-ng storage repository (i.e., not the one on which the VM's being backed up reside). In the Schedule configuration, you will have the option to select the number of "Backup Retention" if your backup includes a _Backup_ (or _Delta Backup_); you will have the option to select the number "Replication Retention" if you have selected _DR_ or _CR_ in the backup configuration.
|
The Full and Delta options are mutually exclusive; Rolling Snapshots are compatible with both. The Backup and Delta Backup go to a remote Target (e.g, NFS); DR and CR back up to another XCP-ng storage repository (i.e., not the one on which the VM's being backed up reside). In the Schedule configuration, you will have the option to select the number of "Backup Retention" if your backup includes a *Backup* (or *Delta Backup*); you will have the option to select the number "Replication Retention" if you have selected *DR* or *CR* in the backup configuration.
|
||||||
|
|
||||||
### Rolling Snapshots
|
### Rolling Snapshots
|
||||||
|
|
||||||
@@ -368,56 +366,3 @@ It is often a good idea to configure retention of older backups with decreasing
|
|||||||
- a monthly backup (retaining 12)
|
- a monthly backup (retaining 12)
|
||||||
|
|
||||||
Again, all of these can be assigned to the same backup job. Note that if you do a weekly and a monthly backup, at some point, these will fall on the same day. Xen Orchestra is designed to fail gracefully (with an error message) if a backup job for a VM is already running. For this reason, you will want to set the time on the monthly job to run before the weekly job so that if one fails, it will be the weekly rather than the monthly one; if the weekly one fails, the monthly will be there for that spot in the retention plan; if the monthly one fails, the weekly one will only be retained for 4 weeks, and then there will be a gap in the monthly retention.
|
Again, all of these can be assigned to the same backup job. Note that if you do a weekly and a monthly backup, at some point, these will fall on the same day. Xen Orchestra is designed to fail gracefully (with an error message) if a backup job for a VM is already running. For this reason, you will want to set the time on the monthly job to run before the weekly job so that if one fails, it will be the weekly rather than the monthly one; if the weekly one fails, the monthly will be there for that spot in the retention plan; if the monthly one fails, the weekly one will only be retained for 4 weeks, and then there will be a gap in the monthly retention.
|
||||||
|
|
||||||
## Backup Health Check
|
|
||||||
|
|
||||||
Backup health check ensures the backups are ready to be restored.
|
|
||||||
|
|
||||||
### Different level of checking
|
|
||||||
|
|
||||||
#### Check for boot
|
|
||||||
|
|
||||||
XO will restore the VM, either by downloading it for a delta/full backup or by cloning it for a disaster recovery of continous replication and then wait for the guest tools to be loaded before the end of a timeout of 10 minutes (boot + guest tools).
|
|
||||||
|
|
||||||
A VM without guest tools will fail its health check.
|
|
||||||
|
|
||||||
The restored VM is then deleted.
|
|
||||||
|
|
||||||
#### Execute a script
|
|
||||||
|
|
||||||
If a VM has the tag **xo-backup-healthcheck-xenstore** during a backup health check, then XO will wait for a script to change the value of the xenstore `vm-data/xo-backup-health-check` key to be either `success` or `failure`.
|
|
||||||
|
|
||||||
In case of `failure`, it will mark the health check as failed, and will show the (optional) message contained in `vm-data/xo-backup-health-check-error`
|
|
||||||
|
|
||||||
The script needs to be planned on boot. It can check if the record `vm-data/xo-backup-health-check` of the local xenstore contains `planned`
|
|
||||||
to differenciate a normal boot and a boot during health check.
|
|
||||||
On success it must write `success`in `vm-data/xo-backup-health-check`.
|
|
||||||
On failure it must write `failure` in `vm-data/xo-backup-health-check`, and may optionally add details in `vm-data/xo-backup-health-check-error` .
|
|
||||||
|
|
||||||
The total timeout of a backup health check (boot + guest tools + scripts) is 10min.
|
|
||||||
|
|
||||||
The restored VM is then deleted.
|
|
||||||
|
|
||||||
An example in bash is shown in `@xen-orchestra/backups/docs/healtcheck example/wait30seconds.sh`
|
|
||||||
|
|
||||||
### Running Health checks
|
|
||||||
|
|
||||||
#### Checking a backup
|
|
||||||
|
|
||||||
Go to backup > restore and click on the tick to launch a health check.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Then, you will select the backup to be checked and a destination SR, which must have enough space for the full restore.
|
|
||||||
|
|
||||||
#### Scheduling health check after backups
|
|
||||||
|
|
||||||
Go to Backup > overview > edit.
|
|
||||||
|
|
||||||
Then edit the schedule and check the healthcheck box.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
You will then need to select the SR used, which must have enough space to restore the VMs. Healthcheck will be done after each VM backup, before starting the next one.
|
|
||||||
|
|
||||||
You can filter the VMs list by providing tags, only the VMs with these tags will be checked.
|
|
||||||
|
|||||||
@@ -336,5 +336,5 @@ useSudo = true
|
|||||||
You need to configure `sudo` to allow the user of your choice to run mount/umount commands without asking for a password. Depending on your operating system / sudo version, the location of this configuration may change. Regardless, you can use:
|
You need to configure `sudo` to allow the user of your choice to run mount/umount commands without asking for a password. Depending on your operating system / sudo version, the location of this configuration may change. Regardless, you can use:
|
||||||
|
|
||||||
```
|
```
|
||||||
username ALL=(root)NOPASSWD: /bin/mount, /bin/umount, /bin/findmnt
|
username ALL=(root)NOPASSWD: /bin/mount, /bin/umount
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"prettify": "prettier --ignore-path .gitignore --ignore-unknown --write .",
|
"prettify": "prettier --ignore-path .gitignore --ignore-unknown --write .",
|
||||||
"test": "npm run test-lint && npm run test-unit",
|
"test": "npm run test-lint && npm run test-unit",
|
||||||
"test-integration": "jest \".integ\\.spec\\.js$\" && scripts/run-script.js --parallel test-integration",
|
"test-integration": "jest \".integ\\.spec\\.js$\"",
|
||||||
"test-lint": "eslint --ignore-path .gitignore --ignore-pattern packages/xo-web .",
|
"test-lint": "eslint --ignore-path .gitignore --ignore-pattern packages/xo-web .",
|
||||||
"test-unit": "jest \"^(?!.*\\.integ\\.spec\\.js$)\" && scripts/run-script.js --bail test"
|
"test-unit": "jest \"^(?!.*\\.integ\\.spec\\.js$)\" && scripts/run-script.js --bail test"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postversion": "npm publish",
|
"postversion": "npm publish",
|
||||||
"test-integration": "node--test *.integ.js"
|
"test": "node--test"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"execa": "^4.0.0",
|
"execa": "^4.0.0",
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
"human-format": "^1.0.0",
|
"human-format": "^1.0.0",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
"pw": "^0.0.4",
|
"pw": "^0.0.4",
|
||||||
"xen-api": "^1.3.1"
|
"xen-api": "^1.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.1.5",
|
"@babel/cli": "^7.1.5",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": false,
|
"private": false,
|
||||||
"name": "xen-api",
|
"name": "xen-api",
|
||||||
"version": "1.3.1",
|
"version": "1.3.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "Connector to the Xen API",
|
"description": "Connector to the Xen API",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"bind-property-descriptor": "^2.0.0",
|
"bind-property-descriptor": "^2.0.0",
|
||||||
"blocked": "^1.2.1",
|
"blocked": "^1.2.1",
|
||||||
"debug": "^4.0.1",
|
"debug": "^4.0.1",
|
||||||
"http-request-plus": "^1.0.2",
|
"http-request-plus": "^1.0.0",
|
||||||
"jest-diff": "^29.0.3",
|
"jest-diff": "^29.0.3",
|
||||||
"json-rpc-protocol": "^0.13.1",
|
"json-rpc-protocol": "^0.13.1",
|
||||||
"kindof": "^2.0.0",
|
"kindof": "^2.0.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "xo-server",
|
"name": "xo-server",
|
||||||
"version": "5.114.2",
|
"version": "5.112.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"description": "Server part of Xen-Orchestra",
|
"description": "Server part of Xen-Orchestra",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
"@vates/predicates": "^1.1.0",
|
"@vates/predicates": "^1.1.0",
|
||||||
"@vates/read-chunk": "^1.1.1",
|
"@vates/read-chunk": "^1.1.1",
|
||||||
"@xen-orchestra/async-map": "^0.1.2",
|
"@xen-orchestra/async-map": "^0.1.2",
|
||||||
"@xen-orchestra/backups": "^0.36.1",
|
"@xen-orchestra/backups": "^0.35.0",
|
||||||
"@xen-orchestra/cron": "^1.0.6",
|
"@xen-orchestra/cron": "^1.0.6",
|
||||||
"@xen-orchestra/defined": "^0.0.1",
|
"@xen-orchestra/defined": "^0.0.1",
|
||||||
"@xen-orchestra/emit-async": "^1.0.0",
|
"@xen-orchestra/emit-async": "^1.0.0",
|
||||||
@@ -131,12 +131,12 @@
|
|||||||
"vhd-lib": "^4.4.0",
|
"vhd-lib": "^4.4.0",
|
||||||
"ws": "^8.2.3",
|
"ws": "^8.2.3",
|
||||||
"xdg-basedir": "^5.1.0",
|
"xdg-basedir": "^5.1.0",
|
||||||
"xen-api": "^1.3.1",
|
"xen-api": "^1.3.0",
|
||||||
"xo-acl-resolver": "^0.4.1",
|
"xo-acl-resolver": "^0.4.1",
|
||||||
"xo-collection": "^0.5.0",
|
"xo-collection": "^0.5.0",
|
||||||
"xo-common": "^0.8.0",
|
"xo-common": "^0.8.0",
|
||||||
"xo-remote-parser": "^0.9.2",
|
"xo-remote-parser": "^0.9.2",
|
||||||
"xo-vmdk-to-vhd": "^2.5.4"
|
"xo-vmdk-to-vhd": "^2.5.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.0.0",
|
"@babel/cli": "^7.0.0",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export async function create({ pool, name, description, pif, mtu = 1500, vlan =
|
|||||||
description,
|
description,
|
||||||
pifId: pif && this.getObject(pif, 'PIF')._xapiId,
|
pifId: pif && this.getObject(pif, 'PIF')._xapiId,
|
||||||
mtu: +mtu,
|
mtu: +mtu,
|
||||||
vlan,
|
vlan: +vlan,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (nbd) {
|
if (nbd) {
|
||||||
@@ -27,7 +27,7 @@ create.params = {
|
|||||||
description: { type: 'string', minLength: 0, optional: true },
|
description: { type: 'string', minLength: 0, optional: true },
|
||||||
pif: { type: 'string', optional: true },
|
pif: { type: 'string', optional: true },
|
||||||
mtu: { type: 'integer', optional: true },
|
mtu: { type: 'integer', optional: true },
|
||||||
vlan: { type: 'integer', optional: true },
|
vlan: { type: ['integer', 'string'], optional: true },
|
||||||
}
|
}
|
||||||
|
|
||||||
create.resolve = {
|
create.resolve = {
|
||||||
|
|||||||
@@ -84,10 +84,10 @@ export async function reconfigureIp({ pif, mode = 'DHCP', ip = '', netmask = '',
|
|||||||
reconfigureIp.params = {
|
reconfigureIp.params = {
|
||||||
id: { type: 'string', optional: true },
|
id: { type: 'string', optional: true },
|
||||||
mode: { type: 'string', optional: true },
|
mode: { type: 'string', optional: true },
|
||||||
ip: { type: 'string', minLength: 0, optional: true },
|
ip: { type: 'string', optional: true },
|
||||||
netmask: { type: 'string', minLength: 0, optional: true },
|
netmask: { type: 'string', optional: true },
|
||||||
gateway: { type: 'string', minLength: 0, optional: true },
|
gateway: { type: 'string', optional: true },
|
||||||
dns: { type: 'string', minLength: 0, optional: true },
|
dns: { type: 'string', optional: true },
|
||||||
}
|
}
|
||||||
|
|
||||||
reconfigureIp.resolve = {
|
reconfigureIp.resolve = {
|
||||||
|
|||||||
@@ -186,12 +186,6 @@ export const create = defer(async function ($defer, params) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceSetTags = resourceSet !== undefined ? (await this.getResourceSet(resourceSet)).tags : undefined
|
|
||||||
const paramsTags = params.tags
|
|
||||||
if (resourceSetTags !== undefined) {
|
|
||||||
params.tags = paramsTags !== undefined ? paramsTags.concat(resourceSetTags) : resourceSetTags
|
|
||||||
}
|
|
||||||
|
|
||||||
const xapiVm = await xapi.createVm(template._xapiId, params, checkLimits, user.id)
|
const xapiVm = await xapi.createVm(template._xapiId, params, checkLimits, user.id)
|
||||||
$defer.onFailure(() => xapi.VM_destroy(xapiVm.$ref, { deleteDisks: true, force: true }))
|
$defer.onFailure(() => xapi.VM_destroy(xapiVm.$ref, { deleteDisks: true, force: true }))
|
||||||
|
|
||||||
@@ -883,8 +877,7 @@ export async function convertToTemplate({ vm }) {
|
|||||||
|
|
||||||
// Attempts to eject all removable media
|
// Attempts to eject all removable media
|
||||||
const ignoreNotRemovable = error => {
|
const ignoreNotRemovable = error => {
|
||||||
const { code } = error
|
if (error.code !== 'VBD_NOT_REMOVABLE_MEDIA') {
|
||||||
if (code !== 'VBD_IS_EMPTY' && code !== 'VBD_NOT_REMOVABLE_MEDIA') {
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1376,7 +1369,7 @@ export async function importMultipleFromEsxi({
|
|||||||
await asyncEach(
|
await asyncEach(
|
||||||
vms,
|
vms,
|
||||||
async vm => {
|
async vm => {
|
||||||
await Task.run({ data: { name: `importing vm ${vm}` } }, async () => {
|
await new Task({ name: `importing vm ${vm}` }).run(async () => {
|
||||||
try {
|
try {
|
||||||
const vmUuid = await this.migrationfromEsxi({
|
const vmUuid = await this.migrationfromEsxi({
|
||||||
host,
|
host,
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ export default class BackupNg {
|
|||||||
vmIds.forEach(handleRecord)
|
vmIds.forEach(handleRecord)
|
||||||
unboxIdsFromPattern(job.srs).forEach(handleRecord)
|
unboxIdsFromPattern(job.srs).forEach(handleRecord)
|
||||||
|
|
||||||
// add xapi specific to the health check SR if needed
|
// add xapi specific to the healthcheck SR if needed
|
||||||
if (job.settings[schedule.id].healthCheckSr !== undefined) {
|
if (job.settings[schedule.id].healthCheckSr !== undefined) {
|
||||||
handleRecord(job.settings[schedule.id].healthCheckSr)
|
handleRecord(job.settings[schedule.id].healthCheckSr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export default class MigrateVm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#connectToEsxi(host, user, password, sslVerify) {
|
#connectToEsxi(host, user, password, sslVerify) {
|
||||||
return Task.run({ data: { name: `connecting to ${host}` } }, async () => {
|
return new Task({ name: `connecting to ${host}` }).run(async () => {
|
||||||
const esxi = new Esxi(host, user, password, sslVerify)
|
const esxi = new Esxi(host, user, password, sslVerify)
|
||||||
await fromEvent(esxi, 'ready')
|
await fromEvent(esxi, 'ready')
|
||||||
return esxi
|
return esxi
|
||||||
@@ -174,24 +174,21 @@ export default class MigrateVm {
|
|||||||
const app = this._app
|
const app = this._app
|
||||||
const esxi = await this.#connectToEsxi(host, user, password, sslVerify)
|
const esxi = await this.#connectToEsxi(host, user, password, sslVerify)
|
||||||
|
|
||||||
const esxiVmMetadata = await Task.run({ data: { name: `get metadata of ${vmId}` } }, async () => {
|
const esxiVmMetadata = await new Task({ name: `get metadata of ${vmId}` }).run(async () => {
|
||||||
return esxi.getTransferableVmMetadata(vmId)
|
return esxi.getTransferableVmMetadata(vmId)
|
||||||
})
|
})
|
||||||
|
|
||||||
const { disks, firmware, memory, name_label, networks, nCpus, powerState, snapshots } = esxiVmMetadata
|
const { disks, firmware, memory, name_label, networks, nCpus, powerState, snapshots } = esxiVmMetadata
|
||||||
const isRunning = powerState !== 'poweredOff'
|
const isRunning = powerState !== 'poweredOff'
|
||||||
|
|
||||||
const chainsByNodes = await Task.run(
|
const chainsByNodes = await new Task({ name: `build disks and snapshots chains for ${vmId}` }).run(async () => {
|
||||||
{ data: { name: `build disks and snapshots chains for ${vmId}` } },
|
return this.#buildDiskChainByNode(disks, snapshots)
|
||||||
async () => {
|
})
|
||||||
return this.#buildDiskChainByNode(disks, snapshots)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const sr = app.getXapiObject(srId)
|
const sr = app.getXapiObject(srId)
|
||||||
const xapi = sr.$xapi
|
const xapi = sr.$xapi
|
||||||
|
|
||||||
const vm = await Task.run({ data: { name: 'creating MV on XCP side' } }, async () => {
|
const vm = await new Task({ name: 'creating MV on XCP side ' }).run(async () => {
|
||||||
// got data, ready to start creating
|
// got data, ready to start creating
|
||||||
const vm = await xapi._getOrWaitObject(
|
const vm = await xapi._getOrWaitObject(
|
||||||
await xapi.VM_create({
|
await xapi.VM_create({
|
||||||
@@ -236,7 +233,7 @@ export default class MigrateVm {
|
|||||||
|
|
||||||
const vhds = await Promise.all(
|
const vhds = await Promise.all(
|
||||||
Object.keys(chainsByNodes).map(async (node, userdevice) =>
|
Object.keys(chainsByNodes).map(async (node, userdevice) =>
|
||||||
Task.run({ data: { name: `Cold import of disks ${node}` } }, async () => {
|
new Task({ name: `Cold import of disks ${node} ` }).run(async () => {
|
||||||
const chainByNode = chainsByNodes[node]
|
const chainByNode = chainsByNodes[node]
|
||||||
const vdi = await xapi._getOrWaitObject(
|
const vdi = await xapi._getOrWaitObject(
|
||||||
await xapi.VDI_create({
|
await xapi.VDI_create({
|
||||||
@@ -271,11 +268,9 @@ export default class MigrateVm {
|
|||||||
}
|
}
|
||||||
parentVhd = vhd
|
parentVhd = vhd
|
||||||
}
|
}
|
||||||
if (vhd !== undefined) {
|
|
||||||
// it can be empty if the VM don't have a snapshot and is running
|
const stream = vhd.stream()
|
||||||
const stream = vhd.stream()
|
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
|
||||||
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
|
|
||||||
}
|
|
||||||
return { vdi, vhd }
|
return { vdi, vhd }
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -283,26 +278,20 @@ export default class MigrateVm {
|
|||||||
|
|
||||||
if (isRunning && stopSource) {
|
if (isRunning && stopSource) {
|
||||||
// it the vm was running, we stop it and transfer the data in the active disk
|
// it the vm was running, we stop it and transfer the data in the active disk
|
||||||
await Task.run({ data: { name: 'powering down source VM' } }, () => esxi.powerOff(vmId))
|
await new Task({ name: 'powering down source VM' }).run(() => esxi.powerOff(vmId))
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.keys(chainsByNodes).map(async (node, userdevice) => {
|
Object.keys(chainsByNodes).map(async (node, userdevice) => {
|
||||||
await Task.run({ data: { name: `Transfering deltas of ${userdevice}` } }, async () => {
|
await new Task({ name: `Transfering deltas of ${userdevice}` }).run(async () => {
|
||||||
const chainByNode = chainsByNodes[node]
|
const chainByNode = chainsByNodes[node]
|
||||||
const disk = chainByNode[chainByNode.length - 1]
|
const disk = chainByNode[chainByNode.length - 1]
|
||||||
const { fileName, path, datastore, isFull } = disk
|
const { fileName, path, datastore, isFull } = disk
|
||||||
const { vdi, vhd: parentVhd } = vhds[userdevice]
|
const { vdi, vhd: parentVhd } = vhds[userdevice]
|
||||||
let vhd
|
let vhd
|
||||||
if (vdi === undefined) {
|
|
||||||
throw new Error(`Can't import delta of a running VM without its parent vdi`)
|
|
||||||
}
|
|
||||||
if (isFull) {
|
if (isFull) {
|
||||||
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName, { thin })
|
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName, { thin })
|
||||||
await vhd.readBlockAllocationTable()
|
await vhd.readBlockAllocationTable()
|
||||||
} else {
|
} else {
|
||||||
if (parentVhd === undefined) {
|
|
||||||
throw new Error(`Can't import delta of a running VM without its parent VHD`)
|
|
||||||
}
|
|
||||||
// we only want to transfer blocks present in the delta vhd, not the full vhd chain
|
// we only want to transfer blocks present in the delta vhd, not the full vhd chain
|
||||||
vhd = await openDeltaVmdkasVhd(esxi, datastore, path + '/' + fileName, parentVhd, {
|
vhd = await openDeltaVmdkasVhd(esxi, datastore, path + '/' + fileName, parentVhd, {
|
||||||
lookMissingBlockInParent: false,
|
lookMissingBlockInParent: false,
|
||||||
@@ -316,7 +305,7 @@ export default class MigrateVm {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.run({ data: { name: 'Finishing transfer' } }, async () => {
|
await new Task({ name: 'Finishing transfer' }).run(async () => {
|
||||||
// remove the importing in label
|
// remove the importing in label
|
||||||
await vm.set_name_label(esxiVmMetadata.name_label)
|
await vm.set_name_label(esxiVmMetadata.name_label)
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ const computeVmXapiResourcesUsage = vm => {
|
|||||||
let disks = 0
|
let disks = 0
|
||||||
let disk = 0
|
let disk = 0
|
||||||
|
|
||||||
|
const canUseLiveResources = !(vm.is_a_snapshot || vm.is_a_template)
|
||||||
|
|
||||||
forEach(vm.$VBDs, vbd => {
|
forEach(vm.$VBDs, vbd => {
|
||||||
let vdi, vdiId
|
let vdi, vdiId
|
||||||
if (vbd.type === 'Disk' && !processed[(vdiId = vbd.VDI)] && (vdi = vbd.$VDI)) {
|
if (vbd.type === 'Disk' && !processed[(vdiId = vbd.VDI)] && (vdi = vbd.$VDI)) {
|
||||||
@@ -41,11 +43,11 @@ const computeVmXapiResourcesUsage = vm => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cpus: vm.VCPUs_at_startup,
|
cpus: canUseLiveResources ? vm.VCPUs_at_startup : 0,
|
||||||
disk,
|
disk,
|
||||||
disks,
|
disks,
|
||||||
memory: vm.memory_dynamic_max,
|
memory: canUseLiveResources ? vm.memory_dynamic_max : 0,
|
||||||
vms: 1,
|
vms: canUseLiveResources ? 1 : 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,6 @@ export default class RestApi {
|
|||||||
|
|
||||||
collections.backups = { id: 'backups' }
|
collections.backups = { id: 'backups' }
|
||||||
collections.restore = { id: 'restore' }
|
collections.restore = { id: 'restore' }
|
||||||
collections.tasks = { id: 'tasks' }
|
|
||||||
|
|
||||||
collections.vms.actions = {
|
collections.vms.actions = {
|
||||||
__proto__: null,
|
__proto__: null,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": false,
|
"private": false,
|
||||||
"name": "xo-vmdk-to-vhd",
|
"name": "xo-vmdk-to-vhd",
|
||||||
"version": "2.5.4",
|
"version": "2.5.3",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"description": "JS lib reading and writing .vmdk and .ova files",
|
"description": "JS lib reading and writing .vmdk and .ova files",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export async function writeOvaOn(
|
|||||||
|
|
||||||
async function writeDisk(entry, blockIterator) {
|
async function writeDisk(entry, blockIterator) {
|
||||||
for await (const block of blockIterator) {
|
for await (const block of blockIterator) {
|
||||||
await fromCallback.call(entry, entry.write, block)
|
entry.write(block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "xo-web",
|
"name": "xo-web",
|
||||||
"version": "5.117.1",
|
"version": "5.115.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"description": "Web interface client for Xen-Orchestra",
|
"description": "Web interface client for Xen-Orchestra",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
"xo-common": "^0.8.0",
|
"xo-common": "^0.8.0",
|
||||||
"xo-lib": "^0.11.1",
|
"xo-lib": "^0.11.1",
|
||||||
"xo-remote-parser": "^0.9.2",
|
"xo-remote-parser": "^0.9.2",
|
||||||
"xo-vmdk-to-vhd": "^2.5.4"
|
"xo-vmdk-to-vhd": "^2.5.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "GIT_HEAD=$(git rev-parse HEAD) NODE_ENV=production gulp build",
|
"build": "GIT_HEAD=$(git rev-parse HEAD) NODE_ENV=production gulp build",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -917,7 +917,6 @@ const messages = {
|
|||||||
|
|
||||||
// ----- Host item ------
|
// ----- Host item ------
|
||||||
host: 'Host',
|
host: 'Host',
|
||||||
hostHvmDisabled: 'Hardware-assisted virtualization is not enabled on this host',
|
|
||||||
hostNoLicensePartialProSupport:
|
hostNoLicensePartialProSupport:
|
||||||
'This host does not have an active license, even though it is in a pool with licensed hosts. In order for XCP-ng Pro Support to be enabled on a pool, all hosts within the pool must have an active license',
|
'This host does not have an active license, even though it is in a pool with licensed hosts. In order for XCP-ng Pro Support to be enabled on a pool, all hosts within the pool must have an active license',
|
||||||
hostNoSupport: 'No XCP-ng Pro Support enabled on this host',
|
hostNoSupport: 'No XCP-ng Pro Support enabled on this host',
|
||||||
@@ -2434,9 +2433,6 @@ const messages = {
|
|||||||
recipeNumberOfNodesLabel: 'Number of worker nodes',
|
recipeNumberOfNodesLabel: 'Number of worker nodes',
|
||||||
recipeSshKeyLabel: 'SSH key',
|
recipeSshKeyLabel: 'SSH key',
|
||||||
recipeStaticIpAddresses: 'Static IP addresses',
|
recipeStaticIpAddresses: 'Static IP addresses',
|
||||||
recipeHighAvailability: 'High Availability Cluster',
|
|
||||||
recipeHaControPlaneIpAddress: 'Control plane { i, number } node IP address/subnet mask',
|
|
||||||
recipeVip: 'VIP address',
|
|
||||||
recipeControlPlaneIpAddress: 'Control plane node IP address/subnet mask',
|
recipeControlPlaneIpAddress: 'Control plane node IP address/subnet mask',
|
||||||
recipeWorkerIpAddress: 'Worker node { i, number } IP address/subnet mask',
|
recipeWorkerIpAddress: 'Worker node { i, number } IP address/subnet mask',
|
||||||
recipeGatewayIpAddress: 'Gateway IP address',
|
recipeGatewayIpAddress: 'Gateway IP address',
|
||||||
|
|||||||
@@ -168,12 +168,8 @@ const handleFnProps = (prop, items, userData) => (typeof prop === 'function' ? p
|
|||||||
const CollapsedActions = decorate([
|
const CollapsedActions = decorate([
|
||||||
withRouter,
|
withRouter,
|
||||||
provideState({
|
provideState({
|
||||||
initialState: () => ({
|
|
||||||
runningActions: [],
|
|
||||||
}),
|
|
||||||
effects: {
|
effects: {
|
||||||
async execute(state, { handler, label, redirectOnSuccess }) {
|
async execute(state, { handler, label, redirectOnSuccess }) {
|
||||||
this.state.runningActions = [...this.state.runningActions, label]
|
|
||||||
try {
|
try {
|
||||||
await handler()
|
await handler()
|
||||||
ifDef(redirectOnSuccess, this.props.router.push)
|
ifDef(redirectOnSuccess, this.props.router.push)
|
||||||
@@ -187,25 +183,18 @@ const CollapsedActions = decorate([
|
|||||||
_error(label, defined(error.message, String(error)))
|
_error(label, defined(error.message, String(error)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
this.state.runningActions = this.state.runningActions.filter(action => action !== label)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
wrappedActions: ({ runningActions }, { actions }) =>
|
|
||||||
actions.map(action => {
|
|
||||||
action.isRunning = runningActions.includes(action.label)
|
|
||||||
return action
|
|
||||||
}),
|
|
||||||
dropdownId: generateId,
|
dropdownId: generateId,
|
||||||
actions: ({ wrappedActions: actions }, { items, userData }) =>
|
actions: (_, { actions, items, userData }) =>
|
||||||
actions.map(({ disabled, grouped, handler, icon, isRunning, label, level, redirectOnSuccess }) => {
|
actions.map(({ disabled, grouped, handler, icon, label, level, redirectOnSuccess }) => {
|
||||||
const actionItems = Array.isArray(items) || !grouped ? items : [items]
|
const actionItems = Array.isArray(items) || !grouped ? items : [items]
|
||||||
return {
|
return {
|
||||||
disabled: isRunning || handleFnProps(disabled, actionItems, userData),
|
disabled: handleFnProps(disabled, actionItems, userData),
|
||||||
handler: () => handler(actionItems, userData),
|
handler: () => handler(actionItems, userData),
|
||||||
icon: isRunning ? 'loading' : handleFnProps(icon, actionItems, userData),
|
icon: handleFnProps(icon, actionItems, userData),
|
||||||
label: handleFnProps(label, actionItems, userData),
|
label: handleFnProps(label, actionItems, userData),
|
||||||
level: handleFnProps(level, actionItems, userData),
|
level: handleFnProps(level, actionItems, userData),
|
||||||
redirectOnSuccess: handleFnProps(redirectOnSuccess, actionItems, userData),
|
redirectOnSuccess: handleFnProps(redirectOnSuccess, actionItems, userData),
|
||||||
|
|||||||
@@ -136,17 +136,7 @@ const New = decorate([
|
|||||||
)}
|
)}
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<label>
|
<label>
|
||||||
<strong>
|
<strong>{_('healthCheck')}</strong>{' '}
|
||||||
<a
|
|
||||||
className='text-info'
|
|
||||||
rel='noreferrer'
|
|
||||||
href='https://xen-orchestra.com/docs/backups.html#backup-health-check'
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
<Icon icon='info' />
|
|
||||||
</a>{' '}
|
|
||||||
{_('healthCheck')}
|
|
||||||
</strong>{' '}
|
|
||||||
{conditionalTooltip(
|
{conditionalTooltip(
|
||||||
<input
|
<input
|
||||||
checked={schedule.healthCheckVmsWithTags !== undefined}
|
checked={schedule.healthCheckVmsWithTags !== undefined}
|
||||||
@@ -167,7 +157,7 @@ const New = decorate([
|
|||||||
<p className='h2'>
|
<p className='h2'>
|
||||||
<Tags labels={schedule.healthCheckVmsWithTags} onChange={effects.setHealthCheckTags} />
|
<Tags labels={schedule.healthCheckVmsWithTags} onChange={effects.setHealthCheckTags} />
|
||||||
</p>
|
</p>
|
||||||
<strong>{_('healthCheckChooseSr')}</strong>
|
<strong>{_('sr')}</strong>
|
||||||
<SelectSr
|
<SelectSr
|
||||||
onChange={effects.setHealthCheckSr}
|
onChange={effects.setHealthCheckSr}
|
||||||
placeholder={_('healthCheckChooseSr')}
|
placeholder={_('healthCheckChooseSr')}
|
||||||
|
|||||||
@@ -201,14 +201,7 @@ export default class Restore extends Component {
|
|||||||
_restoreHealthCheck = data =>
|
_restoreHealthCheck = data =>
|
||||||
confirm({
|
confirm({
|
||||||
title: _('checkVmBackupsTitle', { vm: data.last.vm.name_label }),
|
title: _('checkVmBackupsTitle', { vm: data.last.vm.name_label }),
|
||||||
body: (
|
body: <RestoreBackupsModalBody data={data} showGenerateNewMacAddress={false} showStartAfterBackup={false} />,
|
||||||
<RestoreBackupsModalBody
|
|
||||||
data={data}
|
|
||||||
showGenerateNewMacAddress={false}
|
|
||||||
showStartAfterBackup={false}
|
|
||||||
backupHealthCheck
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
icon: 'restore',
|
icon: 'restore',
|
||||||
})
|
})
|
||||||
.then(({ backup, targetSrs: { mainSr, mapVdisSrs } }) => {
|
.then(({ backup, targetSrs: { mainSr, mapVdisSrs } }) => {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import _ from 'intl'
|
import _ from 'intl'
|
||||||
import Icon from 'icon'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ChooseSrForEachVdisModal from 'xo/choose-sr-for-each-vdis-modal'
|
import ChooseSrForEachVdisModal from 'xo/choose-sr-for-each-vdis-modal'
|
||||||
import Component from 'base-component'
|
import Component from 'base-component'
|
||||||
@@ -34,16 +33,6 @@ export default class RestoreBackupsModalBody extends Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.props.backupHealthCheck && (
|
|
||||||
<a
|
|
||||||
className='text-info'
|
|
||||||
rel='noreferrer'
|
|
||||||
href='https://xen-orchestra.com/docs/backups.html#backup-health-check'
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
<Icon icon='info' />
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
<div className='mb-1'>
|
<div className='mb-1'>
|
||||||
<Select
|
<Select
|
||||||
optionRenderer={BACKUP_RENDERER}
|
optionRenderer={BACKUP_RENDERER}
|
||||||
@@ -88,7 +77,6 @@ export default class RestoreBackupsModalBody extends Component {
|
|||||||
RestoreBackupsModalBody.defaultProps = {
|
RestoreBackupsModalBody.defaultProps = {
|
||||||
showGenerateNewMacAddress: true,
|
showGenerateNewMacAddress: true,
|
||||||
showStartAfterBackup: true,
|
showStartAfterBackup: true,
|
||||||
backupHealthCheck: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RestoreBackupsBulkModalBody extends Component {
|
export class RestoreBackupsBulkModalBody extends Component {
|
||||||
|
|||||||
@@ -177,17 +177,6 @@ export default class HostItem extends Component {
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!host.hvmCapable) {
|
|
||||||
alerts.push({
|
|
||||||
level: 'warning',
|
|
||||||
render: (
|
|
||||||
<span>
|
|
||||||
<Icon icon='alarm' /> {_('hostHvmDisabled')}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return alerts
|
return alerts
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import { FormattedRelative, FormattedTime } from 'react-intl'
|
|||||||
import { Sr } from 'render-xo-item'
|
import { Sr } from 'render-xo-item'
|
||||||
import { Text } from 'editable'
|
import { Text } from 'editable'
|
||||||
import { Toggle, Select, SizeInput } from 'form'
|
import { Toggle, Select, SizeInput } from 'form'
|
||||||
import * as xoaPlans from 'xoa-plans'
|
|
||||||
import {
|
import {
|
||||||
detachHost,
|
detachHost,
|
||||||
disableHost,
|
disableHost,
|
||||||
@@ -45,7 +44,6 @@ import {
|
|||||||
import { installCertificate } from './install-certificate'
|
import { installCertificate } from './install-certificate'
|
||||||
|
|
||||||
const ALLOW_INSTALL_SUPP_PACK = process.env.XOA_PLAN > 1
|
const ALLOW_INSTALL_SUPP_PACK = process.env.XOA_PLAN > 1
|
||||||
const ALLOW_SMART_REBOOT = xoaPlans.CURRENT.value >= xoaPlans.PREMIUM.value
|
|
||||||
|
|
||||||
const SCHED_GRAN_TYPE_OPTIONS = [
|
const SCHED_GRAN_TYPE_OPTIONS = [
|
||||||
{
|
{
|
||||||
@@ -64,9 +62,7 @@ const SCHED_GRAN_TYPE_OPTIONS = [
|
|||||||
|
|
||||||
const forceReboot = host => restartHost(host, true)
|
const forceReboot = host => restartHost(host, true)
|
||||||
|
|
||||||
const smartReboot = ALLOW_SMART_REBOOT
|
const smartReboot = host => restartHost(host, false, true) // don't force, suspend resident VMs
|
||||||
? host => restartHost(host, false, true) // don't force, suspend resident VMs
|
|
||||||
: () => {}
|
|
||||||
|
|
||||||
const formatPack = ({ name, author, description, version }, key) => (
|
const formatPack = ({ name, author, description, version }, key) => (
|
||||||
<tr key={key}>
|
<tr key={key}>
|
||||||
@@ -263,12 +259,11 @@ export default class extends Component {
|
|||||||
<TabButton
|
<TabButton
|
||||||
key='smart-reboot'
|
key='smart-reboot'
|
||||||
btnStyle='warning'
|
btnStyle='warning'
|
||||||
disabled={!ALLOW_SMART_REBOOT}
|
|
||||||
handler={smartReboot}
|
handler={smartReboot}
|
||||||
handlerParam={host}
|
handlerParam={host}
|
||||||
icon='freeze'
|
icon='freeze'
|
||||||
labelId='smartRebootHostLabel'
|
labelId='smartRebootHostLabel'
|
||||||
tooltip={ALLOW_SMART_REBOOT ? _('smartRebootHostTooltip') : _('availableXoaPremium')}
|
tooltip={_('smartRebootHostTooltip')}
|
||||||
/>,
|
/>,
|
||||||
<TabButton
|
<TabButton
|
||||||
key='force-reboot'
|
key='force-reboot'
|
||||||
|
|||||||
@@ -53,17 +53,6 @@ export default decorate([
|
|||||||
workerNodeIpAddresses,
|
workerNodeIpAddresses,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onChangeCpIp(__, ev) {
|
|
||||||
const { name, value } = ev.target
|
|
||||||
const { onChange, value: prevValue } = this.props
|
|
||||||
const controlPlaneIpAddresses = prevValue.controlPlaneIpAddresses ?? []
|
|
||||||
|
|
||||||
controlPlaneIpAddresses[name.split('.')[1]] = value
|
|
||||||
onChange({
|
|
||||||
...prevValue,
|
|
||||||
controlPlaneIpAddresses,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onChangeNameserver(__, ev) {
|
onChangeNameserver(__, ev) {
|
||||||
const { value } = ev.target
|
const { value } = ev.target
|
||||||
const { onChange, value: prevValue } = this.props
|
const { onChange, value: prevValue } = this.props
|
||||||
@@ -148,19 +137,6 @@ export default decorate([
|
|||||||
value={value.nbNodes}
|
value={value.nbNodes}
|
||||||
/>
|
/>
|
||||||
</FormGrid.Row>
|
</FormGrid.Row>
|
||||||
<FormGrid.Row>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
className='mt-1'
|
|
||||||
name='highAvailability'
|
|
||||||
onChange={effects.toggleStaticIpAddress}
|
|
||||||
type='checkbox'
|
|
||||||
value={value.highAvailability}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{_('recipeHighAvailability')}
|
|
||||||
</label>
|
|
||||||
</FormGrid.Row>
|
|
||||||
<FormGrid.Row>
|
<FormGrid.Row>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
@@ -176,48 +152,18 @@ export default decorate([
|
|||||||
</FormGrid.Row>
|
</FormGrid.Row>
|
||||||
{value.nbNodes > 0 &&
|
{value.nbNodes > 0 &&
|
||||||
value.staticIpAddress && [
|
value.staticIpAddress && [
|
||||||
value.highAvailability && [
|
<FormGrid.Row key='controlPlaneIpAddrRow'>
|
||||||
Array.from({ length: 3 }).map((v, i) => (
|
<label>{_('recipeControlPlaneIpAddress')}</label>
|
||||||
<FormGrid.Row key={i}>
|
<input
|
||||||
<label>{_('recipeHaControPlaneIpAddress', { i: i + 1 })}</label>
|
className='form-control'
|
||||||
<input
|
name='controlPlaneIpAddress'
|
||||||
className='form-control'
|
onChange={effects.onChangeValue}
|
||||||
name={`controlPlaneIpAddress.${i}`}
|
placeholder={formatMessage(messages.recipeControlPlaneIpAddress)}
|
||||||
onChange={effects.onChangeCpIp}
|
required
|
||||||
placeholder={formatMessage(messages.recipeHaControPlaneIpAddress, { i: i + 1 })}
|
type='text'
|
||||||
required
|
value={value.controlPlaneIpAddress}
|
||||||
type='text'
|
/>
|
||||||
value={value[`controlPlaneIpAddress.${i}`]}
|
</FormGrid.Row>,
|
||||||
/>
|
|
||||||
</FormGrid.Row>
|
|
||||||
)),
|
|
||||||
<FormGrid.Row key='vipAddrRow'>
|
|
||||||
<label>{_('recipeVip')}</label>
|
|
||||||
<input
|
|
||||||
className='form-control'
|
|
||||||
name='vipAddress'
|
|
||||||
onChange={effects.onChangeValue}
|
|
||||||
placeholder={formatMessage(messages.recipeVip)}
|
|
||||||
required
|
|
||||||
type='text'
|
|
||||||
value={value.vipAddress}
|
|
||||||
/>
|
|
||||||
</FormGrid.Row>,
|
|
||||||
],
|
|
||||||
!value.highAvailability && [
|
|
||||||
<FormGrid.Row key='controlPlaneIpAddrRow'>
|
|
||||||
<label>{_('recipeControlPlaneIpAddress')}</label>
|
|
||||||
<input
|
|
||||||
className='form-control'
|
|
||||||
name='controlPlaneIpAddress'
|
|
||||||
onChange={effects.onChangeValue}
|
|
||||||
placeholder={formatMessage(messages.recipeControlPlaneIpAddress)}
|
|
||||||
required
|
|
||||||
type='text'
|
|
||||||
value={value.controlPlaneIpAddress}
|
|
||||||
/>
|
|
||||||
</FormGrid.Row>,
|
|
||||||
],
|
|
||||||
<FormGrid.Row key='gatewayRow'>
|
<FormGrid.Row key='gatewayRow'>
|
||||||
<label>{_('recipeGatewayIpAddress')}</label>
|
<label>{_('recipeGatewayIpAddress')}</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -53,34 +53,27 @@ export default decorate([
|
|||||||
const {
|
const {
|
||||||
clusterName,
|
clusterName,
|
||||||
controlPlaneIpAddress,
|
controlPlaneIpAddress,
|
||||||
controlPlaneIpAddresses,
|
|
||||||
gatewayIpAddress,
|
gatewayIpAddress,
|
||||||
highAvailability,
|
|
||||||
nameservers,
|
nameservers,
|
||||||
nbNodes,
|
nbNodes,
|
||||||
network,
|
network,
|
||||||
searches,
|
searches,
|
||||||
sr,
|
sr,
|
||||||
sshKey,
|
sshKey,
|
||||||
vipAddress,
|
|
||||||
workerNodeIpAddresses,
|
workerNodeIpAddresses,
|
||||||
} = recipeParams
|
} = recipeParams
|
||||||
|
|
||||||
markRecipeAsCreating(RECIPE_INFO.id)
|
markRecipeAsCreating(RECIPE_INFO.id)
|
||||||
const tag = await createKubernetesCluster({
|
const tag = await createKubernetesCluster({
|
||||||
clusterName,
|
|
||||||
clusterName,
|
clusterName,
|
||||||
controlPlaneIpAddress,
|
controlPlaneIpAddress,
|
||||||
controlPlaneIpAddresses,
|
|
||||||
gatewayIpAddress,
|
gatewayIpAddress,
|
||||||
highAvailability,
|
|
||||||
nameservers,
|
nameservers,
|
||||||
nbNodes: +nbNodes,
|
nbNodes: +nbNodes,
|
||||||
network: network.id,
|
network: network.id,
|
||||||
searches,
|
searches,
|
||||||
sr: sr.id,
|
sr: sr.id,
|
||||||
sshKey,
|
sshKey,
|
||||||
vipAddress,
|
|
||||||
workerNodeIpAddresses,
|
workerNodeIpAddresses,
|
||||||
})
|
})
|
||||||
markRecipeAsDone(RECIPE_INFO.id)
|
markRecipeAsDone(RECIPE_INFO.id)
|
||||||
|
|||||||
@@ -319,7 +319,6 @@ const HealthCheckTask = ({ children, className, task }) => (
|
|||||||
const HealthCheckVmStartTask = ({ children, className, task }) => (
|
const HealthCheckVmStartTask = ({ children, className, task }) => (
|
||||||
<li className={className}>
|
<li className={className}>
|
||||||
<Icon icon='run' /> {task.message} <TaskStateInfos status={task.status} />
|
<Icon icon='run' /> {task.message} <TaskStateInfos status={task.status} />
|
||||||
<TaskInfos infos={task.infos} />
|
|
||||||
<TaskStart task={task} />
|
<TaskStart task={task} />
|
||||||
<TaskEnd task={task} />
|
<TaskEnd task={task} />
|
||||||
<TaskError task={task} />
|
<TaskError task={task} />
|
||||||
|
|||||||
@@ -197,11 +197,11 @@ const NewNetwork = decorate([
|
|||||||
networks,
|
networks,
|
||||||
pif,
|
pif,
|
||||||
pifs,
|
pifs,
|
||||||
|
vlan,
|
||||||
} = state
|
} = state
|
||||||
|
|
||||||
let { mtu, vlan } = state
|
let { mtu } = state
|
||||||
mtu = mtu === '' ? undefined : +mtu
|
mtu = mtu === '' ? undefined : +mtu
|
||||||
vlan = vlan === '' ? undefined : +vlan
|
|
||||||
|
|
||||||
return bonded
|
return bonded
|
||||||
? createBondedNetwork({
|
? createBondedNetwork({
|
||||||
|
|||||||
@@ -37,13 +37,7 @@ import { updateApplianceSettings } from './update-appliance-settings'
|
|||||||
import Tooltip from '../../common/tooltip'
|
import Tooltip from '../../common/tooltip'
|
||||||
import { getXoaPlan, SOURCES } from '../../common/xoa-plans'
|
import { getXoaPlan, SOURCES } from '../../common/xoa-plans'
|
||||||
|
|
||||||
const _editProxy = (value, { name, proxy }) => {
|
const _editProxy = (value, { name, proxy }) => editProxyAppliance(proxy, { [name]: value })
|
||||||
if (typeof value === 'string') {
|
|
||||||
value = value.trim()
|
|
||||||
value = value === '' ? null : value
|
|
||||||
}
|
|
||||||
return editProxyAppliance(proxy, { [name]: value })
|
|
||||||
}
|
|
||||||
|
|
||||||
const HEADER = (
|
const HEADER = (
|
||||||
<h2>
|
<h2>
|
||||||
@@ -149,12 +143,6 @@ const COLUMNS = [
|
|||||||
itemRenderer: proxy => <Vm id={proxy.vmUuid} link />,
|
itemRenderer: proxy => <Vm id={proxy.vmUuid} link />,
|
||||||
name: _('vm'),
|
name: _('vm'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
itemRenderer: proxy => (
|
|
||||||
<Text data-name='address' data-proxy={proxy} value={proxy.address ?? ''} onChange={_editProxy} />
|
|
||||||
),
|
|
||||||
name: _('address'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: _('license'),
|
name: _('license'),
|
||||||
itemRenderer: (proxy, { isAdmin, licensesByVmUuid }) => {
|
itemRenderer: (proxy, { isAdmin, licensesByVmUuid }) => {
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ import ActionToggle from 'action-toggle'
|
|||||||
import Button from 'button'
|
import Button from 'button'
|
||||||
import Component from 'base-component'
|
import Component from 'base-component'
|
||||||
import decorate from 'apply-decorators'
|
import decorate from 'apply-decorators'
|
||||||
import escapeRegExp from 'lodash/escapeRegExp'
|
|
||||||
import GenericInput from 'json-schema-input'
|
import GenericInput from 'json-schema-input'
|
||||||
import Icon from 'icon'
|
import Icon from 'icon'
|
||||||
import isEmpty from 'lodash/isEmpty'
|
import isEmpty from 'lodash/isEmpty'
|
||||||
import Link from 'link'
|
|
||||||
import map from 'lodash/map'
|
import map from 'lodash/map'
|
||||||
import orderBy from 'lodash/orderBy'
|
import orderBy from 'lodash/orderBy'
|
||||||
import pFinally from 'promise-toolbox/finally'
|
import pFinally from 'promise-toolbox/finally'
|
||||||
@@ -41,17 +39,6 @@ class Plugin extends Component {
|
|||||||
this.testFormId = `form-test-${props.id}`
|
this.testFormId = `form-test-${props.id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
_getPluginLink = createSelector(
|
|
||||||
() => this.props.name,
|
|
||||||
name => {
|
|
||||||
const s = new ComplexMatcher.Property(
|
|
||||||
'name',
|
|
||||||
new ComplexMatcher.RegExp('^' + escapeRegExp(name) + '$', 'i')
|
|
||||||
).toString()
|
|
||||||
return location => ({ ...location, query: { ...location.query, s } })
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
_getUiSchema = createSelector(() => this.props.configurationSchema, generateUiSchema)
|
_getUiSchema = createSelector(() => this.props.configurationSchema, generateUiSchema)
|
||||||
|
|
||||||
_updateExpanded = () => {
|
_updateExpanded = () => {
|
||||||
@@ -138,8 +125,9 @@ class Plugin extends Component {
|
|||||||
<Row>
|
<Row>
|
||||||
<Col mediumSize={8}>
|
<Col mediumSize={8}>
|
||||||
<h5 className='form-inline clearfix'>
|
<h5 className='form-inline clearfix'>
|
||||||
<ActionToggle disabled={loaded && props.unloadable === false} handler={this._updateLoad} value={loaded} />{' '}
|
<ActionToggle disabled={loaded && props.unloadable === false} handler={this._updateLoad} value={loaded} />
|
||||||
<Link to={this._getPluginLink()}>{props.name}</Link> <span>{`(v${props.version}) `}</span>
|
<span className='text-primary'>{` ${props.name} `}</span>
|
||||||
|
<span>{`(v${props.version}) `}</span>
|
||||||
{description !== undefined && description !== '' && (
|
{description !== undefined && description !== '' && (
|
||||||
<span className='text-muted small'> - {description}</span>
|
<span className='text-muted small'> - {description}</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -485,7 +485,6 @@ export default class User extends Component {
|
|||||||
<option value='tr'>Türkçe</option>
|
<option value='tr'>Türkçe</option>
|
||||||
<option value='he'>עברי</option>
|
<option value='he'>עברי</option>
|
||||||
<option value='zh'>简体中文</option>
|
<option value='zh'>简体中文</option>
|
||||||
<option value='fa'>فارسی</option>
|
|
||||||
</select>
|
</select>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ example.{,c,m}js.map
|
|||||||
|
|
||||||
/test/
|
/test/
|
||||||
/tests/
|
/tests/
|
||||||
*.integ.{,c,m}js
|
|
||||||
*.integ.{,c,m}js.map
|
|
||||||
*.spec.{,c,m}js
|
*.spec.{,c,m}js
|
||||||
*.spec.{,c,m}js.map
|
*.spec.{,c,m}js.map
|
||||||
*.test.{,c,m}js
|
*.test.{,c,m}js
|
||||||
|
|||||||
@@ -11236,13 +11236,6 @@ http-request-plus@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@xen-orchestra/log" "^0.6.0"
|
"@xen-orchestra/log" "^0.6.0"
|
||||||
|
|
||||||
http-request-plus@^1.0.2:
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/http-request-plus/-/http-request-plus-1.0.2.tgz#e0d6b4fa79c82f1f9df7dcd1ce62c26934fa20ca"
|
|
||||||
integrity sha512-no2XjPTwCGwzgF+abs3o76e+J2Fm2w9xnUu/nlMbOIPrqooHGPKfpM7uv+QsGMKilFrchJQpvc1NLBfb9VI14Q==
|
|
||||||
dependencies:
|
|
||||||
"@xen-orchestra/log" "^0.6.0"
|
|
||||||
|
|
||||||
http-server-plus@^1.0.0:
|
http-server-plus@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/http-server-plus/-/http-server-plus-1.0.0.tgz#cb7adc1ca3e679e8728286a6c9b5b1bd012ccbfd"
|
resolved "https://registry.yarnpkg.com/http-server-plus/-/http-server-plus-1.0.0.tgz#cb7adc1ca3e679e8728286a6c9b5b1bd012ccbfd"
|
||||||
|
|||||||
Reference in New Issue
Block a user