Compare commits

..

1 Commits

Author SHA1 Message Date
Pierre Donias
bd04345569 feat(xo-server/self): ignore snapshots CPU and RAM usage 2023-04-26 15:52:04 +02:00
94 changed files with 328 additions and 1411 deletions

View File

@@ -28,7 +28,7 @@ module.exports = {
},
},
{
files: ['*.{integ,spec,test}.{,c,m}js'],
files: ['*.{spec,test}.{,c,m}js'],
rules: {
'n/no-unpublished-require': 'off',
'n/no-unpublished-import': 'off',

View File

@@ -23,7 +23,7 @@
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"promise-toolbox": "^0.21.0",
"xen-api": "^1.3.1"
"xen-api": "^1.3.0"
},
"devDependencies": {
"tap": "^16.3.0",
@@ -31,6 +31,6 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test-integration": "tap --lines 70 --functions 36 --branches 54 --statements 69 *.integ.js"
"test-integration": "tap *.spec.js"
}
}

View File

@@ -48,7 +48,7 @@ exports.makeOnProgress = function ({ onRootTaskEnd = noop, onRootTaskStart = noo
assert.notEqual(parent, undefined)
// 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)
}
} else {

View File

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

View File

@@ -83,7 +83,7 @@ exports.Task = class Task {
return this.#status
}
constructor({ data = {}, onProgress } = {}) {
constructor({ data = {}, onProgress }) {
this.#startData = data
if (onProgress !== undefined) {
@@ -106,8 +106,6 @@ exports.Task = class Task {
const { signal } = this.#abortController
signal.addEventListener('abort', () => {
if (this.status === PENDING && !this.#running) {
this.#maybeStart()
const status = ABORTED
this.#status = status
this.#emit('end', { result: signal.reason, status })
@@ -120,18 +118,16 @@ exports.Task = class Task {
}
#emit(type, data) {
data.id = this.id
data.timestamp = Date.now()
data.type = type
this.#onProgress(data)
}
#maybeStart() {
const startData = this.#startData
if (startData !== undefined) {
this.#startData = undefined
this.#emit('start', startData)
}
data.id = this.id
data.timestamp = Date.now()
data.type = type
this.#onProgress(data)
}
async run(fn) {
@@ -149,8 +145,6 @@ exports.Task = class Task {
assert.equal(this.#running, false)
this.#running = true
this.#maybeStart()
try {
const result = await asyncStorage.run(this, fn)
this.#running = false

View File

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

View File

@@ -13,16 +13,12 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.1.2",
"version": "0.1.0",
"engines": {
"node": ">=14"
},
"devDependencies": {
"test": "^3.3.0"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
"postversion": "npm publish --access public"
},
"exports": {
".": "./index.js",

View File

@@ -7,7 +7,7 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@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",
"filenamify": "^4.1.0",
"getopts": "^2.2.5",
@@ -27,7 +27,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "1.0.6",
"version": "1.0.5",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -3,14 +3,12 @@
const { Task } = require('./Task')
exports.HealthCheckVmBackup = class HealthCheckVmBackup {
#restoredVm
#timeout
#xapi
#restoredVm
constructor({ restoredVm, timeout = 10 * 60 * 1000, xapi }) {
constructor({ restoredVm, xapi }) {
this.#restoredVm = restoredVm
this.#xapi = xapi
this.#timeout = timeout
}
async run() {
@@ -25,12 +23,7 @@ exports.HealthCheckVmBackup = class HealthCheckVmBackup {
// remove vifs
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()
// start Vm
@@ -41,7 +34,7 @@ exports.HealthCheckVmBackup = class HealthCheckVmBackup {
false // Skip pre-boot checks?
)
const started = new Date()
const timeout = this.#timeout
const timeout = 10 * 60 * 1000
const startDuration = started - start
let remainingTimeout = timeout - startDuration
@@ -59,52 +52,12 @@ exports.HealthCheckVmBackup = class HealthCheckVmBackup {
remainingTimeout -= running - started
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
await xapi.waitObjectState(restoredVm.guest_metrics, gm => gm?.PV_drivers_version?.major !== undefined, {
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')
}
}
)
}

View File

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

View File

@@ -8,13 +8,13 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.36.1",
"version": "0.35.0",
"engines": {
"node": ">=14.6"
},
"scripts": {
"postversion": "npm publish --access public",
"test-integration": "node--test *.integ.js"
"test": "node--test"
},
"dependencies": {
"@kldzj/stream-throttle": "^1.1.1",

View File

@@ -50,8 +50,7 @@ exports.DeltaReplicationWriter = class DeltaReplicationWriter extends MixinRepli
},
})
this.transfer = task.wrapFn(this.transfer)
this.cleanup = task.wrapFn(this.cleanup)
this.healthCheck = task.wrapFn(this.healthCheck, true)
this.cleanup = task.wrapFn(this.cleanup, true)
return task.run(() => this._prepare())
}

View File

@@ -80,7 +80,7 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
assert.notStrictEqual(
this._metadataFileName,
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(
{

View File

@@ -18,7 +18,7 @@
"preferGlobal": true,
"dependencies": {
"golike-defer": "^0.5.1",
"xen-api": "^1.3.1"
"xen-api": "^1.3.0"
},
"scripts": {
"postversion": "npm publish"

View File

@@ -1,5 +1,25 @@
<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">
<AppLogin />
</div>
@@ -21,14 +41,21 @@ import AppHeader from "@/components/AppHeader.vue";
import AppLogin from "@/components/AppLogin.vue";
import AppNavigation from "@/components/AppNavigation.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 { useHostStore } from "@/stores/host.store";
import { usePoolStore } from "@/stores/pool.store";
import { useUiStore } from "@/stores/ui.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import { faServer } from "@fortawesome/free-solid-svg-icons";
import { useActiveElement, useMagicKeys, whenever } from "@vueuse/core";
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(
"link[rel~='icon']"
@@ -43,6 +70,7 @@ link.href = favicon;
document.title = "XO Lite";
const xenApiStore = useXenApiStore();
const { records: hosts } = useHostStore().subscribe();
const { pool } = usePoolStore().subscribe();
useChartTheme();
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(
() => pool.value?.$ref,
async (poolRef) => {
@@ -73,6 +112,9 @@ whenever(
await xenApi.startWatch();
}
);
const isSslModalOpen = computed(() => unreachableHostsUrls.value.length > 0);
const reload = () => window.location.reload();
</script>
<style lang="postcss">

View File

@@ -1,15 +1,15 @@
<template>
<div v-if="!isDisabled" ref="tooltipElement" class="app-tooltip">
<span class="triangle" />
<span class="label">{{ options.content }}</span>
<span class="label">{{ content }}</span>
</div>
</template>
<script lang="ts" setup>
import type { TooltipOptions } from "@/stores/tooltip.store";
import { isString } from "lodash-es";
import { isEmpty, isFunction, isString } from "lodash-es";
import place from "placement.js";
import { computed, ref, watchEffect } from "vue";
import type { TooltipOptions } from "@/stores/tooltip.store";
const props = defineProps<{
target: HTMLElement;
@@ -18,13 +18,29 @@ const props = defineProps<{
const tooltipElement = ref<HTMLElement>();
const isDisabled = computed(() =>
isString(props.options.content)
? props.options.content.trim() === ""
: props.options.content === false
const content = computed(() =>
isString(props.options) ? props.options : props.options.content
);
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(() => {
if (tooltipElement.value) {

View File

@@ -14,12 +14,7 @@
</UiActionButton>
</UiFilterGroup>
<UiModal
v-if="isOpen"
:icon="faFilter"
@submit.prevent="handleSubmit"
@close="handleCancel"
>
<UiModal v-if="isOpen" :icon="faFilter" @submit.prevent="handleSubmit">
<div class="rows">
<CollectionFilterRow
v-for="(newFilter, index) in newFilters"

View File

@@ -17,12 +17,7 @@
</UiActionButton>
</UiFilterGroup>
<UiModal
v-if="isOpen"
:icon="faSort"
@submit.prevent="handleSubmit"
@close="handleCancel"
>
<UiModal v-if="isOpen" :icon="faSort" @submit.prevent="handleSubmit">
<div class="form-widgets">
<FormWidget :label="$t('sort-by')">
<select v-model="newSortProperty">

View File

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

View File

@@ -1,5 +1,12 @@
<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
:active="isCurrentHost"
:icon="faServer"
@@ -29,6 +36,7 @@ import InfraAction from "@/components/infra/InfraAction.vue";
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
import InfraVmList from "@/components/infra/InfraVmList.vue";
import { vTooltip } from "@/directives/tooltip.directive";
import { hasEllipsis } from "@/libs/utils";
import { useHostStore } from "@/stores/host.store";
import { usePoolStore } from "@/stores/pool.store";
import { useUiStore } from "@/stores/ui.store";
@@ -58,6 +66,9 @@ const isCurrentHost = computed(
() => props.hostOpaqueRef === uiStore.currentHostOpaqueRef
);
const [isExpanded, toggle] = useToggle(true);
const isTooltipDisabled = (target: HTMLElement) =>
!hasEllipsis(target.querySelector(".text"));
</script>
<style lang="postcss" scoped>

View File

@@ -7,9 +7,9 @@
class="infra-item-label"
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" />
<div ref="textElement" class="text">
<div class="text">
<slot />
</div>
</a>
@@ -22,10 +22,7 @@
<script lang="ts" setup>
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 { computed, ref } from "vue";
import type { RouteLocationRaw } from "vue-router";
defineProps<{
@@ -33,9 +30,6 @@ defineProps<{
route: RouteLocationRaw;
active?: boolean;
}>();
const textElement = ref<HTMLElement>();
const hasTooltip = computed(() => hasEllipsis(textElement.value));
</script>
<style lang="postcss" scoped>

View File

@@ -1,5 +1,13 @@
<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
v-if="isVisible"
:icon="faDisplay"
@@ -19,6 +27,8 @@
import InfraAction from "@/components/infra/InfraAction.vue";
import InfraItemLabel from "@/components/infra/InfraItemLabel.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 { faDisplay } from "@fortawesome/free-solid-svg-icons";
import { useIntersectionObserver } from "@vueuse/core";
@@ -39,6 +49,9 @@ const { stop } = useIntersectionObserver(rootElement, ([entry]) => {
stop();
}
});
const isTooltipDisabled = (target: HTMLElement) =>
!hasEllipsis(target.querySelector(".text"));
</script>
<style lang="postcss" scoped>

View File

@@ -16,7 +16,6 @@ defineProps<{
<style lang="postcss" scoped>
.ui-badge {
white-space: nowrap;
display: inline-flex;
align-items: center;
gap: 0.4rem;

View File

@@ -1,13 +1,7 @@
<template>
<div class="legend">
<template v-if="$slots.label || label">
<span class="circle" />
<div class="label-container">
<div ref="labelElement" v-tooltip="isTooltipEnabled" class="label">
<slot name="label">{{ label }}</slot>
</div>
</div>
</template>
<span class="circle" />
<slot name="label">{{ label }}</slot>
<UiBadge class="badge">
<slot name="value">{{ value }}</slot>
</UiBadge>
@@ -16,23 +10,14 @@
<script lang="ts" setup>
import UiBadge from "@/components/ui/UiBadge.vue";
import { vTooltip } from "@/directives/tooltip.directive";
import { hasEllipsis } from "@/libs/utils";
import { computed, ref } from "vue";
defineProps<{
label?: string;
value?: string;
}>();
const labelElement = ref<HTMLElement>();
const isTooltipEnabled = computed(() =>
hasEllipsis(labelElement.value, { vertical: true })
);
</script>
<style lang="postcss" scoped>
<style scoped lang="postcss">
.badge {
font-size: 0.9em;
font-weight: 700;
@@ -40,8 +25,8 @@ const isTooltipEnabled = computed(() =>
.circle {
display: inline-block;
min-width: 1rem;
min-height: 1rem;
width: 1rem;
height: 1rem;
border-radius: 0.5rem;
background-color: var(--progress-bar-color);
}
@@ -53,14 +38,4 @@ const isTooltipEnabled = computed(() =>
gap: 0.5rem;
margin: 1.6em 0;
}
.label-container {
overflow: hidden;
}
.label {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
</style>

View File

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

View File

@@ -1,71 +1,36 @@
# Tooltip Directive
By default, the 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`
By default, tooltip will appear centered above the target element.
## Usage
```vue
<template>
<!-- Boolean / Undefined -->
<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 -->
<!-- Static -->
<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>
<!-- Dynamic -->
<span v-tooltip="myTooltip">Item</span>
<!-- Disabling (variable) -->
<span v-tooltip="{ content: 'Foobar', disabled: isDisabled }">Item</span>
<!-- Conditional -->
<span v-tooltip="isTooltipEnabled && 'Foobar'">Item</span>
<!-- Disabling (function) -->
<span v-tooltip="{ content: 'Foobar', disabled: isDisabledFn }">Item</span>
</template>
<script setup>
import { ref } from "vue";
import { vTooltip } from "@/directives/tooltip.directive";
const myTooltip = ref("Content"); // or ref({ content: "Content", placement: "left-end" })
const isTooltipEnabled = ref(true);
const myTooltipContent = ref("Content");
const isDisabled = ref(true);
const isDisabledFn = (target: Element) => {
// return boolean;
};
</script>
```

View File

@@ -1,36 +1,8 @@
import type { Directive } from "vue";
import type { TooltipEvents, TooltipOptions } 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;
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> = {
export const vTooltip: Directive<HTMLElement, TooltipOptions> = {
mounted(target, binding) {
const store = useTooltipStore();
@@ -38,11 +10,11 @@ export const vTooltip: Directive<HTMLElement, TooltipDirectiveOptions> = {
? { on: "focusin", off: "focusout" }
: { on: "mouseenter", off: "mouseleave" };
store.register(target, parseOptions(binding.value, target), events);
store.register(target, binding.value, events);
},
updated(target, binding) {
const store = useTooltipStore();
store.updateOptions(target, parseOptions(binding.value, target));
store.updateOptions(target, binding.value);
},
beforeUnmount(target) {
const store = useTooltipStore();

View File

@@ -71,20 +71,8 @@ export function parseDateTime(dateTime: string) {
return date.getTime();
}
export const hasEllipsis = (
target: Element | undefined | null,
{ vertical = false }: { vertical?: boolean } = {}
) => {
if (target == null) {
return false;
}
if (vertical) {
return target.clientHeight < target.scrollHeight;
}
return target.clientWidth < target.scrollWidth;
};
export const hasEllipsis = (target: Element | undefined | null) =>
target != undefined && target.clientWidth < target.scrollWidth;
export function percent(currentValue: number, maxValue: number, precision = 2) {
return round((currentValue / maxValue) * 100, precision);

View File

@@ -17,7 +17,6 @@
"coming-soon": "Coming soon!",
"community": "Community",
"community-name": "{name} community",
"console": "Console",
"copy": "Copy",
"cpu-provisioning": "CPU provisioning",
"cpu-usage": "CPU usage",

View File

@@ -17,7 +17,6 @@
"coming-soon": "Bientôt disponible !",
"community": "Communauté",
"community-name": "Communauté {name}",
"console": "Console",
"copy": "Copier",
"cpu-provisioning": "Provisionnement CPU",
"cpu-usage": "Utilisation CPU",

View File

@@ -1,11 +1,12 @@
import pool from "@/router/pool";
import vm from "@/router/vm";
import HomeView from "@/views/HomeView.vue";
import HostDashboardView from "@/views/host/HostDashboardView.vue";
import HostRootView from "@/views/host/HostRootView.vue";
import PageNotFoundView from "@/views/PageNotFoundView.vue";
import SettingsView from "@/views/settings/SettingsView.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 { createRouter, createWebHashHistory } from "vue-router";
@@ -30,7 +31,6 @@ const router = createRouter({
component: SettingsView,
},
pool,
vm,
{
path: "/host/:uuid",
component: HostRootView,
@@ -42,6 +42,17 @@ const router = createRouter({
},
],
},
{
path: "/vm/:uuid",
component: VmRootView,
children: [
{
path: "console",
name: "vm.console",
component: VmConsoleView,
},
],
},
{
path: "/:pathMatch(.*)*",
name: "notFound",

View File

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

View File

@@ -4,10 +4,13 @@ import type { Options } from "placement.js";
import { type EffectScope, computed, effectScope, ref } from "vue";
import { type WindowEventName, useEventListener } from "@vueuse/core";
export type TooltipOptions = {
content: string | false;
placement: Options["placement"];
};
export type TooltipOptions =
| string
| {
content: string;
placement?: Options["placement"];
disabled?: boolean | ((target: HTMLElement) => boolean);
};
export type TooltipEvents = { on: WindowEventName; off: WindowEventName };

View File

@@ -13,7 +13,7 @@
<PoolDashboardRamUsage />
</div>
<div class="item">
<PoolDashboardCpuProvisioning />
<PoolDashboardCpuProvisionning />
</div>
<div class="item">
<PoolDashboardNetworkChart />
@@ -38,7 +38,7 @@ import { provide, watch } from "vue";
import PoolCpuUsageChart from "@/components/pool/dashboard/cpuUsage/PoolCpuUsageChart.vue";
import PoolDashboardCpuUsage from "@/components/pool/dashboard/PoolDashboardCpuUsage.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 PoolDashboardRamUsageChart from "@/components/pool/dashboard/ramUsage/PoolRamUsage.vue";
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";

View File

@@ -1,49 +1,32 @@
<template>
<UiCard class="home-view">
<UiCardTitle>Component Story skeleton generator</UiCardTitle>
<div class="row">
Choose a component
<FormSelect v-model="componentPath">
<div class="home-view">
<UiTitle type="h4">
This helper will generate a basic story component
</UiTitle>
<div>
Choose a component:
<select v-model="componentPath">
<option value="" />
<option v-for="path in componentPaths" :key="path">
<option v-for="(component, path) in componentsWithProps" :key="path">
{{ path }}
</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 class="row">
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>
<CodeHighlight v-if="componentPath" :code="template" />
</div>
</template>
<script lang="ts" setup>
import CodeHighlight from "@/components/CodeHighlight.vue";
import FormInput from "@/components/form/FormInput.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 UiTitle from "@/components/ui/UiTitle.vue";
import { type ComponentOptions, computed, ref, watch } from "vue";
const componentPath = ref("");
@@ -61,14 +44,10 @@ const componentsWithProps = Object.fromEntries(
)
);
const componentPaths = Object.keys(componentsWithProps);
const lines = ref<string[]>([]);
const slots = ref("");
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 widgetsToImport = ref(new Set<string>());
@@ -82,15 +61,13 @@ const template = computed(() => {
.filter((name) => name !== "");
for (const slotName of slotsNames) {
paramsLines.push(
`slot(${slotName === "default" ? "" : quote(camel(slotName))})`
);
paramsLines.push(`slot(${slotName === "default" ? "" : quote(slotName)})`);
}
for (const slotName of slotsNames) {
paramsLines.push(
`setting(${quote(
`${camel(slotName)}SlotContent`
`${slotName}SlotContent`
)}).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 scriptEndTag = "</" + "script>";
return `<template>
<ComponentStory
v-slot="{ properties, settings }"
@@ -114,10 +91,8 @@ const template = computed(() => {
? `>\n ${slotsNames
.map((name) =>
name === "default"
? `{{ settings.${camel(name)}SlotContent }}`
: `<template #${name}>{{ settings.${camel(
name
)}SlotContent }}</template>`
? `{{ settings.${name}SlotContent }}`
: `<template #${name}>{{ settings.${name}SlotContent }}</template>`
)
.join("\n ")}
</${componentName}>`
@@ -143,30 +118,10 @@ ${
)} } 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(
componentPath,
(path: string) => {
@@ -178,7 +133,6 @@ watch(
slots.value = "";
widgetsToImport.value = new Set();
paramsToImport.value = new Set();
warnings.value = new Set();
lines.value = [];
for (const propName in component.props) {
@@ -193,14 +147,12 @@ watch(
current.push(`default(${quote(prop.default)})`);
}
if (prop.type !== undefined) {
const type = castArray(prop.type)
.map((ctor) => extractTypeFromConstructor(ctor, propName))
.join(" | ");
if (prop.type) {
const type = prop.type();
if (type !== "unknown") {
current.push(`type(${quote(type)})`);
}
current.push(
`type(${quote(Array.isArray(type) ? "array" : typeof type)})`
);
}
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("."));
}
let shouldImportEvent = false;
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");
for (const eventName of component.emits) {
lines.value.push(`event("${eventName}")`);
}
}
},
{ immediate: true }
@@ -245,28 +185,11 @@ watch(
margin: 1rem;
}
.ui-title {
margin-bottom: 1rem;
}
.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;
}
</style>

View File

@@ -1,7 +0,0 @@
<template>
<PageUnderConstruction />
</template>
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
</script>

View File

@@ -1,7 +0,0 @@
<template>
<PageUnderConstruction />
</template>
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
</script>

View File

@@ -1,7 +0,0 @@
<template>
<PageUnderConstruction />
</template>
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
</script>

View File

@@ -1,7 +1,6 @@
<template>
<ObjectNotFoundWrapper :is-ready="isReady" :uuid-checker="hasUuid">
<VmHeader />
<VmTabBar :uuid="vm!.uuid" />
<RouterView />
</ObjectNotFoundWrapper>
</template>
@@ -9,16 +8,18 @@
<script lang="ts" setup>
import ObjectNotFoundWrapper from "@/components/ObjectNotFoundWrapper.vue";
import VmHeader from "@/components/vm/VmHeader.vue";
import VmTabBar from "@/components/vm/VmTabBar.vue";
import { useUiStore } from "@/stores/ui.store";
import { useVmStore } from "@/stores/vm.store";
import { whenever } from "@vueuse/core";
import { computed } from "vue";
import { watchEffect } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
const { getByUuid, hasUuid, isReady } = useVmStore().subscribe();
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>

View File

@@ -1,7 +0,0 @@
<template>
<PageUnderConstruction />
</template>
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
</script>

View File

@@ -1,7 +0,0 @@
<template>
<PageUnderConstruction />
</template>
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
</script>

View File

@@ -1,7 +0,0 @@
<template>
<PageUnderConstruction />
</template>
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
</script>

View File

@@ -1,7 +0,0 @@
<template>
<PageUnderConstruction />
</template>
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
</script>

View File

@@ -21,7 +21,7 @@
"dependencies": {
"@vates/event-listeners-manager": "^1.0.1",
"@vates/parse-duration": "^0.1.1",
"@vates/task": "^0.1.2",
"@vates/task": "^0.1.0",
"@xen-orchestra/log": "^0.6.0",
"acme-client": "^5.0.0",
"app-conf": "^2.3.0",

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@xen-orchestra/proxy",
"version": "0.26.23",
"version": "0.26.20",
"license": "AGPL-3.0-or-later",
"description": "XO Proxy used to remotely execute backup jobs",
"keywords": [
@@ -32,7 +32,7 @@
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.4",
"@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/log": "^0.6.0",
"@xen-orchestra/mixin": "^0.1.0",
@@ -60,7 +60,7 @@
"source-map-support": "^0.5.16",
"stoppable": "^1.0.6",
"xdg-basedir": "^5.1.0",
"xen-api": "^1.3.1",
"xen-api": "^1.3.0",
"xo-common": "^0.8.0"
},
"devDependencies": {

View File

@@ -43,7 +43,7 @@
"pw": "^0.0.4",
"xdg-basedir": "^4.0.0",
"xo-lib": "^0.11.1",
"xo-vmdk-to-vhd": "^2.5.4"
"xo-vmdk-to-vhd": "^2.5.3"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -4,7 +4,7 @@
"version": "0.2.2",
"name": "@xen-orchestra/vmware-explorer",
"dependencies": {
"@vates/task": "^0.1.2",
"@vates/task": "^0.1.0",
"@vates/read-chunk": "^1.1.1",
"lodash": "^4.17.21",
"node-fetch": "^3.3.0",

View File

@@ -15,7 +15,7 @@
"node": ">=14"
},
"peerDependencies": {
"xen-api": "^1.3.1"
"xen-api": "^1.3.0"
},
"scripts": {
"postversion": "npm publish --access public",

View File

@@ -2,57 +2,13 @@
## **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
- [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))
- [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
@@ -73,21 +29,21 @@
- vhd-lib 4.4.0
- xen-api 1.3.0
- @vates/nbd-client 1.2.0
- @vates/task 0.1.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/proxy 0.26.20
- @xen-orchestra/vmware-explorer 0.2.2
- xo-cli 0.18.0
- xo-server 5.112.0
- xo-server-usage-report 0.10.4
- @vates/task 0.1.1
- @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
- xo-web 5.115.0
## **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
@@ -126,6 +82,8 @@
## **5.80.2** (2023-03-16)
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
### Enhancements
- [Plugin/auth-oidc] Support `email` for _username field_ setting [Forum#59587](https://xcp-ng.org/forum/post/59587)

View File

@@ -7,15 +7,10 @@
> 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
> 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
> When modifying a package, add it here with its release type.
@@ -32,6 +27,4 @@
<!--packages-start-->
- xo-web minor
<!--packages-end-->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -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.
:::tip
- 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**:
@@ -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-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.
@@ -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:
- Full: _Backup_ and _Disaster Recovery_ (DR)
- Deltas: _Delta Backup_ and _Continuous Replication_ (CR)
- Full: *Backup* and *Disaster Recovery* (DR)
- 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
@@ -368,56 +366,3 @@ It is often a good idea to configure retention of older backups with decreasing
- 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.
## 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.
![](./assets/restorehealthcheck.png)
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.
![](./assets/scheduled_healthcheck.png)
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.

View File

@@ -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:
```
username ALL=(root)NOPASSWD: /bin/mount, /bin/umount, /bin/findmnt
username ALL=(root)NOPASSWD: /bin/mount, /bin/umount
```

View File

@@ -96,7 +96,7 @@
"prepare": "husky install",
"prettify": "prettier --ignore-path .gitignore --ignore-unknown --write .",
"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-unit": "jest \"^(?!.*\\.integ\\.spec\\.js$)\" && scripts/run-script.js --bail test"
},

View File

@@ -35,7 +35,7 @@
},
"scripts": {
"postversion": "npm publish",
"test-integration": "node--test *.integ.js"
"test": "node--test"
},
"devDependencies": {
"execa": "^4.0.0",

View File

@@ -40,7 +40,7 @@
"human-format": "^1.0.0",
"lodash": "^4.17.4",
"pw": "^0.0.4",
"xen-api": "^1.3.1"
"xen-api": "^1.3.0"
},
"devDependencies": {
"@babel/cli": "^7.1.5",

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xen-api",
"version": "1.3.1",
"version": "1.3.0",
"license": "ISC",
"description": "Connector to the Xen API",
"keywords": [
@@ -35,7 +35,7 @@
"bind-property-descriptor": "^2.0.0",
"blocked": "^1.2.1",
"debug": "^4.0.1",
"http-request-plus": "^1.0.2",
"http-request-plus": "^1.0.0",
"jest-diff": "^29.0.3",
"json-rpc-protocol": "^0.13.1",
"kindof": "^2.0.0",

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.114.2",
"version": "5.112.0",
"license": "AGPL-3.0-or-later",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -41,7 +41,7 @@
"@vates/predicates": "^1.1.0",
"@vates/read-chunk": "^1.1.1",
"@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/defined": "^0.0.1",
"@xen-orchestra/emit-async": "^1.0.0",
@@ -131,12 +131,12 @@
"vhd-lib": "^4.4.0",
"ws": "^8.2.3",
"xdg-basedir": "^5.1.0",
"xen-api": "^1.3.1",
"xen-api": "^1.3.0",
"xo-acl-resolver": "^0.4.1",
"xo-collection": "^0.5.0",
"xo-common": "^0.8.0",
"xo-remote-parser": "^0.9.2",
"xo-vmdk-to-vhd": "^2.5.4"
"xo-vmdk-to-vhd": "^2.5.3"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -10,7 +10,7 @@ export async function create({ pool, name, description, pif, mtu = 1500, vlan =
description,
pifId: pif && this.getObject(pif, 'PIF')._xapiId,
mtu: +mtu,
vlan,
vlan: +vlan,
})
if (nbd) {
@@ -27,7 +27,7 @@ create.params = {
description: { type: 'string', minLength: 0, optional: true },
pif: { type: 'string', optional: true },
mtu: { type: 'integer', optional: true },
vlan: { type: 'integer', optional: true },
vlan: { type: ['integer', 'string'], optional: true },
}
create.resolve = {

View File

@@ -84,10 +84,10 @@ export async function reconfigureIp({ pif, mode = 'DHCP', ip = '', netmask = '',
reconfigureIp.params = {
id: { type: 'string', optional: true },
mode: { type: 'string', optional: true },
ip: { type: 'string', minLength: 0, optional: true },
netmask: { type: 'string', minLength: 0, optional: true },
gateway: { type: 'string', minLength: 0, optional: true },
dns: { type: 'string', minLength: 0, optional: true },
ip: { type: 'string', optional: true },
netmask: { type: 'string', optional: true },
gateway: { type: 'string', optional: true },
dns: { type: 'string', optional: true },
}
reconfigureIp.resolve = {

View File

@@ -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)
$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
const ignoreNotRemovable = error => {
const { code } = error
if (code !== 'VBD_IS_EMPTY' && code !== 'VBD_NOT_REMOVABLE_MEDIA') {
if (error.code !== 'VBD_NOT_REMOVABLE_MEDIA') {
throw error
}
}
@@ -1376,7 +1369,7 @@ export async function importMultipleFromEsxi({
await asyncEach(
vms,
async vm => {
await Task.run({ data: { name: `importing vm ${vm}` } }, async () => {
await new Task({ name: `importing vm ${vm}` }).run(async () => {
try {
const vmUuid = await this.migrationfromEsxi({
host,

View File

@@ -191,7 +191,7 @@ export default class BackupNg {
vmIds.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) {
handleRecord(job.settings[schedule.id].healthCheckSr)
}

View File

@@ -154,7 +154,7 @@ export default class MigrateVm {
}
#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)
await fromEvent(esxi, 'ready')
return esxi
@@ -174,24 +174,21 @@ export default class MigrateVm {
const app = this._app
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)
})
const { disks, firmware, memory, name_label, networks, nCpus, powerState, snapshots } = esxiVmMetadata
const isRunning = powerState !== 'poweredOff'
const chainsByNodes = await Task.run(
{ data: { name: `build disks and snapshots chains for ${vmId}` } },
async () => {
return this.#buildDiskChainByNode(disks, snapshots)
}
)
const chainsByNodes = await new Task({ name: `build disks and snapshots chains for ${vmId}` }).run(async () => {
return this.#buildDiskChainByNode(disks, snapshots)
})
const sr = app.getXapiObject(srId)
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
const vm = await xapi._getOrWaitObject(
await xapi.VM_create({
@@ -236,7 +233,7 @@ export default class MigrateVm {
const vhds = await Promise.all(
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 vdi = await xapi._getOrWaitObject(
await xapi.VDI_create({
@@ -271,11 +268,9 @@ export default class MigrateVm {
}
parentVhd = vhd
}
if (vhd !== undefined) {
// it can be empty if the VM don't have a snapshot and is running
const stream = vhd.stream()
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
}
const stream = vhd.stream()
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
return { vdi, vhd }
})
)
@@ -283,26 +278,20 @@ export default class MigrateVm {
if (isRunning && stopSource) {
// 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(
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 disk = chainByNode[chainByNode.length - 1]
const { fileName, path, datastore, isFull } = disk
const { vdi, vhd: parentVhd } = vhds[userdevice]
let vhd
if (vdi === undefined) {
throw new Error(`Can't import delta of a running VM without its parent vdi`)
}
if (isFull) {
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName, { thin })
await vhd.readBlockAllocationTable()
} 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
vhd = await openDeltaVmdkasVhd(esxi, datastore, path + '/' + fileName, parentVhd, {
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
await vm.set_name_label(esxiVmMetadata.name_label)

View File

@@ -31,6 +31,8 @@ const computeVmXapiResourcesUsage = vm => {
let disks = 0
let disk = 0
const canUseLiveResources = !(vm.is_a_snapshot || vm.is_a_template)
forEach(vm.$VBDs, vbd => {
let vdi, vdiId
if (vbd.type === 'Disk' && !processed[(vdiId = vbd.VDI)] && (vdi = vbd.$VDI)) {
@@ -41,11 +43,11 @@ const computeVmXapiResourcesUsage = vm => {
})
return {
cpus: vm.VCPUs_at_startup,
cpus: canUseLiveResources ? vm.VCPUs_at_startup : 0,
disk,
disks,
memory: vm.memory_dynamic_max,
vms: 1,
memory: canUseLiveResources ? vm.memory_dynamic_max : 0,
vms: canUseLiveResources ? 1 : 0,
}
}

View File

@@ -149,7 +149,6 @@ export default class RestApi {
collections.backups = { id: 'backups' }
collections.restore = { id: 'restore' }
collections.tasks = { id: 'tasks' }
collections.vms.actions = {
__proto__: null,

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-vmdk-to-vhd",
"version": "2.5.4",
"version": "2.5.3",
"license": "AGPL-3.0-or-later",
"description": "JS lib reading and writing .vmdk and .ova files",
"keywords": [
@@ -39,8 +39,6 @@
"fs-extra": "^11.1.1",
"get-stream": "^6.0.0",
"rimraf": "^4.1.1",
"sinon": "^15.0.4",
"test": "^3.3.0",
"tmp": "^0.2.1"
},
"scripts": {
@@ -50,9 +48,7 @@
"prebuild": "yarn run clean",
"predev": "yarn run clean",
"prepublishOnly": "yarn run build",
"pretest-integration": "yarn run build",
"postversion": "npm publish",
"test-integration": "node--test ./dist/*.integ.js"
"postversion": "npm publish"
},
"author": {
"name": "Vates SAS",

View File

@@ -1,6 +1,4 @@
import { afterEach, beforeEach, test } from 'test'
import sinon from 'sinon'
/* eslint-env jest */
import { spawn } from 'child_process'
import { createReadStream, createWriteStream, readFile } from 'fs-extra'
import execa from 'execa'
@@ -12,8 +10,7 @@ import { writeOvaOn } from './ova-generate'
import { parseOVF } from './ova-read'
const initialDir = process.cwd()
const clock = sinon.useFakeTimers()
clock.tick(100000)
jest.setTimeout(100000)
beforeEach(async () => {
const dir = await pFromCallback(cb => tmp.dir(cb))
process.chdir(dir)
@@ -102,7 +99,7 @@ test('An ova file is generated correctly', async () => {
try {
await execXmllint(xml, [
'--schema',
path.join(__dirname, '../src/ova-schema', 'dsp8023_1.1.1.xsd'),
path.join(__dirname, 'ova-schema', 'dsp8023_1.1.1.xsd'),
'--noout',
'--nonet',
'-',

View File

@@ -26,7 +26,7 @@ export async function writeOvaOn(
async function writeDisk(entry, blockIterator) {
for await (const block of blockIterator) {
await fromCallback.call(entry, entry.write, block)
entry.write(block)
}
}

View File

@@ -1,5 +1,4 @@
import { afterEach, beforeEach, test } from 'test'
import { strict as assert } from 'assert'
/* eslint-env jest */
import { exec } from 'child-process-promise'
import { createReadStream } from 'fs'
@@ -67,14 +66,14 @@ test('An ova file is parsed correctly', async () => {
const directGrainTableFetch = await readVmdkGrainTable(async (start, end) =>
vmdkParsableFile.slice(start, end).read()
)
assert.deepEqual(directGrainTableFetch, expectedResult.tables[vmdkFileName])
expect(directGrainTableFetch).toEqual(expectedResult.tables[vmdkFileName])
const data = await parseOVAFile(new NodeParsableFile(ovaName), (buffer, encoder) => {
return Buffer.from(buffer).toString(encoder)
})
for (const fileName in data.tables) {
data.tables[fileName] = await data.tables[fileName]
}
assert.deepEqual(data, expectedResult)
expect(data).toEqual(expectedResult)
})
function arrayToBuffer(array) {

View File

@@ -1,5 +1,4 @@
import { afterEach, beforeEach, test } from 'test'
import { strict as assert } from 'assert'
/* eslint-env jest */
import { createReadStream, readFile } from 'fs-extra'
import { exec } from 'child-process-promise'
@@ -29,5 +28,5 @@ test('Virtual Buffer can read a file correctly', async () => {
const part1 = await buffer.readChunk(10)
const part2 = await buffer.readChunk(1038)
const original = await readFile(rawFileName)
assert.equal(Buffer.concat([part1, part2]).toString('ascii'), original.toString('ascii'))
expect(Buffer.concat([part1, part2]).toString('ascii')).toEqual(original.toString('ascii'))
})

View File

@@ -1,6 +1,4 @@
import { afterEach, beforeEach, test } from 'test'
import { strict as assert } from 'assert'
import sinon from 'sinon'
/* eslint-env jest */
import { createReadStream, stat } from 'fs-extra'
import { exec } from 'child-process-promise'
@@ -36,8 +34,7 @@ function bufferToArray(buffer) {
return res
}
const clock = sinon.useFakeTimers()
clock.tick(10000)
jest.setTimeout(10000)
const initialDir = process.cwd()
@@ -70,8 +67,8 @@ test('VMDKDirectParser reads OK', async () => {
for await (const res of parser.blockIterator()) {
harvested.push(res)
}
assert.equal(harvested.length, 2)
assert.equal(harvested[0].logicalAddressBytes, 0)
assert.equal(harvested[0].data.length, header.grainSizeSectors * 512)
assert.equal(harvested[1].logicalAddressBytes, header.grainSizeSectors * 512)
expect(harvested.length).toEqual(2)
expect(harvested[0].logicalAddressBytes).toEqual(0)
expect(harvested[0].data.length).toEqual(header.grainSizeSectors * 512)
expect(harvested[1].logicalAddressBytes).toEqual(header.grainSizeSectors * 512)
})

View File

@@ -1,6 +1,4 @@
import { afterEach, beforeEach, test } from 'test'
import { strict as assert } from 'assert'
import sinon from 'sinon'
/* eslint-env jest */
import execa from 'execa'
import fromEvent from 'promise-toolbox/fromEvent'
@@ -17,8 +15,7 @@ import asyncIteratorToStream from 'async-iterator-to-stream'
import fs from 'fs'
const initialDir = process.cwd()
const clock = sinon.useFakeTimers()
clock.tick(100000)
jest.setTimeout(100000)
beforeEach(async () => {
const dir = await pFromCallback(cb => tmp.dir(cb))
@@ -127,7 +124,7 @@ test('Can generate a small VMDK file', async () => {
}
}
const data = await readVmdkGrainTable(createFileAccessor(fileName))
assert.deepEqual(bufferToArray(data.grainLogicalAddressList), expectedLBAs)
expect(bufferToArray(data.grainLogicalAddressList)).toEqual(expectedLBAs)
const grainFileOffsetList = bufferToArray(data.grainFileOffsetList)
const parser = new VMDKDirectParser(
createReadStream(fileName),
@@ -145,8 +142,8 @@ test('Can generate a small VMDK file', async () => {
}
const resultBuffer = Buffer.concat(resBuffers)
const startingBuffer = Buffer.concat([b1, b2])
assert.deepEqual(resultBuffer, startingBuffer)
assert.deepEqual(resLbas, expectedLBAs)
expect(resultBuffer).toEqual(startingBuffer)
expect(resLbas).toEqual(expectedLBAs)
await execa('qemu-img', ['check', 'result.vmdk'])
})

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-web",
"version": "5.117.1",
"version": "5.115.0",
"license": "AGPL-3.0-or-later",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -137,7 +137,7 @@
"xo-common": "^0.8.0",
"xo-lib": "^0.11.1",
"xo-remote-parser": "^0.9.2",
"xo-vmdk-to-vhd": "^2.5.4"
"xo-vmdk-to-vhd": "^2.5.3"
},
"scripts": {
"build": "GIT_HEAD=$(git rev-parse HEAD) NODE_ENV=production gulp build",

View File

@@ -917,7 +917,6 @@ const messages = {
// ----- Host item ------
host: 'Host',
hostHvmDisabled: 'Hardware-assisted virtualization is not enabled on this host',
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',
hostNoSupport: 'No XCP-ng Pro Support enabled on this host',
@@ -2434,9 +2433,6 @@ const messages = {
recipeNumberOfNodesLabel: 'Number of worker nodes',
recipeSshKeyLabel: 'SSH key',
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',
recipeWorkerIpAddress: 'Worker node { i, number } IP address/subnet mask',
recipeGatewayIpAddress: 'Gateway IP address',

View File

@@ -168,12 +168,8 @@ const handleFnProps = (prop, items, userData) => (typeof prop === 'function' ? p
const CollapsedActions = decorate([
withRouter,
provideState({
initialState: () => ({
runningActions: [],
}),
effects: {
async execute(state, { handler, label, redirectOnSuccess }) {
this.state.runningActions = [...this.state.runningActions, label]
try {
await handler()
ifDef(redirectOnSuccess, this.props.router.push)
@@ -187,25 +183,18 @@ const CollapsedActions = decorate([
_error(label, defined(error.message, String(error)))
}
}
} finally {
this.state.runningActions = this.state.runningActions.filter(action => action !== label)
}
},
},
computed: {
wrappedActions: ({ runningActions }, { actions }) =>
actions.map(action => {
action.isRunning = runningActions.includes(action.label)
return action
}),
dropdownId: generateId,
actions: ({ wrappedActions: actions }, { items, userData }) =>
actions.map(({ disabled, grouped, handler, icon, isRunning, label, level, redirectOnSuccess }) => {
actions: (_, { actions, items, userData }) =>
actions.map(({ disabled, grouped, handler, icon, label, level, redirectOnSuccess }) => {
const actionItems = Array.isArray(items) || !grouped ? items : [items]
return {
disabled: isRunning || handleFnProps(disabled, actionItems, userData),
disabled: handleFnProps(disabled, actionItems, userData),
handler: () => handler(actionItems, userData),
icon: isRunning ? 'loading' : handleFnProps(icon, actionItems, userData),
icon: handleFnProps(icon, actionItems, userData),
label: handleFnProps(label, actionItems, userData),
level: handleFnProps(level, actionItems, userData),
redirectOnSuccess: handleFnProps(redirectOnSuccess, actionItems, userData),

View File

@@ -136,17 +136,7 @@ const New = decorate([
)}
<FormGroup>
<label>
<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>{' '}
<strong>{_('healthCheck')}</strong>{' '}
{conditionalTooltip(
<input
checked={schedule.healthCheckVmsWithTags !== undefined}
@@ -167,7 +157,7 @@ const New = decorate([
<p className='h2'>
<Tags labels={schedule.healthCheckVmsWithTags} onChange={effects.setHealthCheckTags} />
</p>
<strong>{_('healthCheckChooseSr')}</strong>
<strong>{_('sr')}</strong>
<SelectSr
onChange={effects.setHealthCheckSr}
placeholder={_('healthCheckChooseSr')}

View File

@@ -201,14 +201,7 @@ export default class Restore extends Component {
_restoreHealthCheck = data =>
confirm({
title: _('checkVmBackupsTitle', { vm: data.last.vm.name_label }),
body: (
<RestoreBackupsModalBody
data={data}
showGenerateNewMacAddress={false}
showStartAfterBackup={false}
backupHealthCheck
/>
),
body: <RestoreBackupsModalBody data={data} showGenerateNewMacAddress={false} showStartAfterBackup={false} />,
icon: 'restore',
})
.then(({ backup, targetSrs: { mainSr, mapVdisSrs } }) => {

View File

@@ -1,5 +1,4 @@
import _ from 'intl'
import Icon from 'icon'
import React from 'react'
import ChooseSrForEachVdisModal from 'xo/choose-sr-for-each-vdis-modal'
import Component from 'base-component'
@@ -34,16 +33,6 @@ export default class RestoreBackupsModalBody extends Component {
render() {
return (
<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'>
<Select
optionRenderer={BACKUP_RENDERER}
@@ -88,7 +77,6 @@ export default class RestoreBackupsModalBody extends Component {
RestoreBackupsModalBody.defaultProps = {
showGenerateNewMacAddress: true,
showStartAfterBackup: true,
backupHealthCheck: false,
}
export class RestoreBackupsBulkModalBody extends Component {

View File

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

View File

@@ -21,7 +21,6 @@ import { FormattedRelative, FormattedTime } from 'react-intl'
import { Sr } from 'render-xo-item'
import { Text } from 'editable'
import { Toggle, Select, SizeInput } from 'form'
import * as xoaPlans from 'xoa-plans'
import {
detachHost,
disableHost,
@@ -45,7 +44,6 @@ import {
import { installCertificate } from './install-certificate'
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 = [
{
@@ -64,9 +62,7 @@ const SCHED_GRAN_TYPE_OPTIONS = [
const forceReboot = host => restartHost(host, true)
const smartReboot = ALLOW_SMART_REBOOT
? host => restartHost(host, false, true) // don't force, suspend resident VMs
: () => {}
const smartReboot = host => restartHost(host, false, true) // don't force, suspend resident VMs
const formatPack = ({ name, author, description, version }, key) => (
<tr key={key}>
@@ -263,12 +259,11 @@ export default class extends Component {
<TabButton
key='smart-reboot'
btnStyle='warning'
disabled={!ALLOW_SMART_REBOOT}
handler={smartReboot}
handlerParam={host}
icon='freeze'
labelId='smartRebootHostLabel'
tooltip={ALLOW_SMART_REBOOT ? _('smartRebootHostTooltip') : _('availableXoaPremium')}
tooltip={_('smartRebootHostTooltip')}
/>,
<TabButton
key='force-reboot'

View File

@@ -53,17 +53,6 @@ export default decorate([
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) {
const { value } = ev.target
const { onChange, value: prevValue } = this.props
@@ -148,19 +137,6 @@ export default decorate([
value={value.nbNodes}
/>
</FormGrid.Row>
<FormGrid.Row>
<label>
<input
className='mt-1'
name='highAvailability'
onChange={effects.toggleStaticIpAddress}
type='checkbox'
value={value.highAvailability}
/>
&nbsp;
{_('recipeHighAvailability')}
</label>
</FormGrid.Row>
<FormGrid.Row>
<label>
<input
@@ -176,48 +152,18 @@ export default decorate([
</FormGrid.Row>
{value.nbNodes > 0 &&
value.staticIpAddress && [
value.highAvailability && [
Array.from({ length: 3 }).map((v, i) => (
<FormGrid.Row key={i}>
<label>{_('recipeHaControPlaneIpAddress', { i: i + 1 })}</label>
<input
className='form-control'
name={`controlPlaneIpAddress.${i}`}
onChange={effects.onChangeCpIp}
placeholder={formatMessage(messages.recipeHaControPlaneIpAddress, { i: i + 1 })}
required
type='text'
value={value[`controlPlaneIpAddress.${i}`]}
/>
</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='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'>
<label>{_('recipeGatewayIpAddress')}</label>
<input

View File

@@ -53,34 +53,27 @@ export default decorate([
const {
clusterName,
controlPlaneIpAddress,
controlPlaneIpAddresses,
gatewayIpAddress,
highAvailability,
nameservers,
nbNodes,
network,
searches,
sr,
sshKey,
vipAddress,
workerNodeIpAddresses,
} = recipeParams
markRecipeAsCreating(RECIPE_INFO.id)
const tag = await createKubernetesCluster({
clusterName,
clusterName,
controlPlaneIpAddress,
controlPlaneIpAddresses,
gatewayIpAddress,
highAvailability,
nameservers,
nbNodes: +nbNodes,
network: network.id,
searches,
sr: sr.id,
sshKey,
vipAddress,
workerNodeIpAddresses,
})
markRecipeAsDone(RECIPE_INFO.id)

View File

@@ -319,7 +319,6 @@ const HealthCheckTask = ({ children, className, task }) => (
const HealthCheckVmStartTask = ({ children, className, task }) => (
<li className={className}>
<Icon icon='run' /> {task.message} <TaskStateInfos status={task.status} />
<TaskInfos infos={task.infos} />
<TaskStart task={task} />
<TaskEnd task={task} />
<TaskError task={task} />

View File

@@ -197,11 +197,11 @@ const NewNetwork = decorate([
networks,
pif,
pifs,
vlan,
} = state
let { mtu, vlan } = state
let { mtu } = state
mtu = mtu === '' ? undefined : +mtu
vlan = vlan === '' ? undefined : +vlan
return bonded
? createBondedNetwork({

View File

@@ -37,13 +37,7 @@ import { updateApplianceSettings } from './update-appliance-settings'
import Tooltip from '../../common/tooltip'
import { getXoaPlan, SOURCES } from '../../common/xoa-plans'
const _editProxy = (value, { name, proxy }) => {
if (typeof value === 'string') {
value = value.trim()
value = value === '' ? null : value
}
return editProxyAppliance(proxy, { [name]: value })
}
const _editProxy = (value, { name, proxy }) => editProxyAppliance(proxy, { [name]: value })
const HEADER = (
<h2>
@@ -149,12 +143,6 @@ const COLUMNS = [
itemRenderer: proxy => <Vm id={proxy.vmUuid} link />,
name: _('vm'),
},
{
itemRenderer: proxy => (
<Text data-name='address' data-proxy={proxy} value={proxy.address ?? ''} onChange={_editProxy} />
),
name: _('address'),
},
{
name: _('license'),
itemRenderer: (proxy, { isAdmin, licensesByVmUuid }) => {

View File

@@ -5,11 +5,9 @@ import ActionToggle from 'action-toggle'
import Button from 'button'
import Component from 'base-component'
import decorate from 'apply-decorators'
import escapeRegExp from 'lodash/escapeRegExp'
import GenericInput from 'json-schema-input'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import Link from 'link'
import map from 'lodash/map'
import orderBy from 'lodash/orderBy'
import pFinally from 'promise-toolbox/finally'
@@ -41,17 +39,6 @@ class Plugin extends Component {
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)
_updateExpanded = () => {
@@ -138,8 +125,9 @@ class Plugin extends Component {
<Row>
<Col mediumSize={8}>
<h5 className='form-inline clearfix'>
<ActionToggle disabled={loaded && props.unloadable === false} handler={this._updateLoad} value={loaded} />{' '}
<Link to={this._getPluginLink()}>{props.name}</Link> <span>{`(v${props.version}) `}</span>
<ActionToggle disabled={loaded && props.unloadable === false} handler={this._updateLoad} value={loaded} />
<span className='text-primary'>{` ${props.name} `}</span>
<span>{`(v${props.version}) `}</span>
{description !== undefined && description !== '' && (
<span className='text-muted small'> - {description}</span>
)}

View File

@@ -22,8 +22,6 @@ example.{,c,m}js.map
/test/
/tests/
*.integ.{,c,m}js
*.integ.{,c,m}js.map
*.spec.{,c,m}js
*.spec.{,c,m}js.map
*.test.{,c,m}js

View File

@@ -11236,13 +11236,6 @@ http-request-plus@^1.0.0:
dependencies:
"@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:
version "1.0.0"
resolved "https://registry.yarnpkg.com/http-server-plus/-/http-server-plus-1.0.0.tgz#cb7adc1ca3e679e8728286a6c9b5b1bd012ccbfd"