Compare commits

..

2 Commits

Author SHA1 Message Date
Julien Fontanet
9e695b7aa0 debug 2022-12-01 18:18:50 +01:00
Julien Fontanet
e5797ed4fb feat: run all tests in CI 2022-11-30 14:53:14 +01:00
77 changed files with 402 additions and 1915 deletions

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.29.4",
"@xen-orchestra/backups": "^0.29.1",
"@xen-orchestra/fs": "^3.3.0",
"filenamify": "^4.1.0",
"getopts": "^2.2.5",

View File

@@ -232,23 +232,21 @@ class RemoteAdapter {
return promise
}
async #removeVmBackupsFromCache(backups) {
await asyncEach(
Object.entries(
groupBy(
backups.map(_ => _._filename),
dirname
)
),
([dir, filenames]) =>
// will not reject
this._updateCache(dir + '/cache.json.gz', backups => {
for (const filename of filenames) {
debug('removing cache entry', { entry: filename })
delete backups[filename]
}
})
)
#removeVmBackupsFromCache(backups) {
for (const [dir, filenames] of Object.entries(
groupBy(
backups.map(_ => _._filename),
dirname
)
)) {
// detached async action, will not reject
this._updateCache(dir + '/cache.json.gz', backups => {
for (const filename of filenames) {
debug('removing cache entry', { entry: filename })
delete backups[filename]
}
})
}
}
async deleteDeltaVmBackups(backups) {
@@ -257,7 +255,7 @@ class RemoteAdapter {
// this will delete the json, unused VHDs will be detected by `cleanVm`
await asyncMapSettled(backups, ({ _filename }) => handler.unlink(_filename))
await this.#removeVmBackupsFromCache(backups)
this.#removeVmBackupsFromCache(backups)
}
async deleteMetadataBackup(backupId) {
@@ -286,7 +284,7 @@ class RemoteAdapter {
Promise.all([handler.unlink(_filename), handler.unlink(resolveRelativeFromFile(_filename, xva))])
)
await this.#removeVmBackupsFromCache(backups)
this.#removeVmBackupsFromCache(backups)
}
deleteVmBackup(file) {
@@ -510,7 +508,7 @@ class RemoteAdapter {
return `${BACKUP_DIR}/${vmUuid}/cache.json.gz`
}
async _readCache(path) {
async #readCache(path) {
try {
return JSON.parse(await fromCallback(zlib.gunzip, await this.handler.readFile(path)))
} catch (error) {
@@ -523,15 +521,15 @@ class RemoteAdapter {
_updateCache = synchronized.withKey()(this._updateCache)
// eslint-disable-next-line no-dupe-class-members
async _updateCache(path, fn) {
const cache = await this._readCache(path)
const cache = await this.#readCache(path)
if (cache !== undefined) {
fn(cache)
await this._writeCache(path, cache)
await this.#writeCache(path, cache)
}
}
async _writeCache(path, data) {
async #writeCache(path, data) {
try {
await this.handler.writeFile(path, await fromCallback(zlib.gzip, JSON.stringify(data)), { flags: 'w' })
} catch (error) {
@@ -579,7 +577,7 @@ class RemoteAdapter {
async _readCacheListVmBackups(vmUuid) {
const path = this.#getVmBackupsCache(vmUuid)
const cache = await this._readCache(path)
const cache = await this.#readCache(path)
if (cache !== undefined) {
debug('found VM backups cache, using it', { path })
return cache
@@ -592,7 +590,7 @@ class RemoteAdapter {
}
// detached async action, will not reject
this._writeCache(path, backups)
this.#writeCache(path, backups)
return backups
}
@@ -643,7 +641,7 @@ class RemoteAdapter {
})
// will not throw
await this._updateCache(this.#getVmBackupsCache(vmUuid), backups => {
this._updateCache(this.#getVmBackupsCache(vmUuid), backups => {
debug('adding cache entry', { entry: path })
backups[path] = {
...metadata,

View File

@@ -311,6 +311,7 @@ exports.cleanVm = async function cleanVm(
}
const jsons = new Set()
let mustInvalidateCache = false
const xvas = new Set()
const xvaSums = []
const entries = await handler.list(vmDir, {
@@ -326,20 +327,6 @@ exports.cleanVm = async function cleanVm(
}
})
const cachePath = vmDir + '/cache.json.gz'
let mustRegenerateCache
{
const cache = await this._readCache(cachePath)
const actual = cache === undefined ? 0 : Object.keys(cache).length
const expected = jsons.size
mustRegenerateCache = actual !== expected
if (mustRegenerateCache) {
logWarn('unexpected number of entries in backup cache', { path: cachePath, actual, expected })
}
}
await asyncMap(xvas, async path => {
// check is not good enough to delete the file, the best we can do is report
// it
@@ -351,8 +338,6 @@ exports.cleanVm = async function cleanVm(
const unusedVhds = new Set(vhds)
const unusedXvas = new Set(xvas)
const backups = new Map()
// compile the list of unused XVAs and VHDs, and remove backup metadata which
// reference a missing XVA/VHD
await asyncMap(jsons, async json => {
@@ -365,16 +350,19 @@ exports.cleanVm = async function cleanVm(
return
}
let isBackupComplete
const { mode } = metadata
if (mode === 'full') {
const linkedXva = resolve('/', vmDir, metadata.xva)
isBackupComplete = xvas.has(linkedXva)
if (isBackupComplete) {
if (xvas.has(linkedXva)) {
unusedXvas.delete(linkedXva)
} else {
logWarn('the XVA linked to the backup is missing', { backup: json, xva: linkedXva })
if (remove) {
logInfo('deleting incomplete backup', { path: json })
jsons.delete(json)
mustInvalidateCache = true
await handler.unlink(json)
}
}
} else if (mode === 'delta') {
const linkedVhds = (() => {
@@ -383,28 +371,22 @@ exports.cleanVm = async function cleanVm(
})()
const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
isBackupComplete = missingVhds.length === 0
// FIXME: find better approach by keeping as much of the backup as
// possible (existing disks) even if one disk is missing
if (isBackupComplete) {
if (missingVhds.length === 0) {
linkedVhds.forEach(_ => unusedVhds.delete(_))
linkedVhds.forEach(path => {
vhdsToJSons[path] = json
})
} else {
logWarn('some VHDs linked to the backup are missing', { backup: json, missingVhds })
}
}
if (isBackupComplete) {
backups.set(json, metadata)
} else {
jsons.delete(json)
if (remove) {
logInfo('deleting incomplete backup', { backup: json })
mustRegenerateCache = true
await handler.unlink(json)
if (remove) {
logInfo('deleting incomplete backup', { path: json })
mustInvalidateCache = true
jsons.delete(json)
await handler.unlink(json)
}
}
}
})
@@ -514,7 +496,7 @@ exports.cleanVm = async function cleanVm(
// check for the other that the size is the same as the real file size
await asyncMap(jsons, async metadataPath => {
const metadata = backups.get(metadataPath)
const metadata = JSON.parse(await handler.readFile(metadataPath))
let fileSystemSize
const merged = metadataWithMergedVhd[metadataPath] !== undefined
@@ -556,7 +538,6 @@ exports.cleanVm = async function cleanVm(
// systematically update size after a merge
if ((merged || fixMetadata) && size !== fileSystemSize) {
metadata.size = fileSystemSize
mustRegenerateCache = true
try {
await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
} catch (error) {
@@ -565,16 +546,9 @@ exports.cleanVm = async function cleanVm(
}
})
if (mustRegenerateCache) {
const cache = {}
for (const [path, content] of backups.entries()) {
cache[path] = {
_filename: path,
id: path,
...content,
}
}
await this._writeCache(cachePath, cache)
// purge cache if a metadata file has been deleted
if (mustInvalidateCache) {
await handler.unlink(vmDir + '/cache.json.gz')
}
return {

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.29.4",
"version": "0.29.1",
"engines": {
"node": ">=14.6"
},
@@ -52,7 +52,7 @@
"tmp": "^0.2.1"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^1.6.0"
"@xen-orchestra/xapi": "^1.5.3"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -11,6 +11,7 @@ const { dirname } = require('path')
const { formatFilenameDate } = require('../_filenameDate.js')
const { getOldEntries } = require('../_getOldEntries.js')
const { getVmBackupDir } = require('../_getVmBackupDir.js')
const { Task } = require('../Task.js')
const { MixinBackupWriter } = require('./_MixinBackupWriter.js')
@@ -28,7 +29,8 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
const backup = this._backup
const adapter = this._adapter
const vdisDir = `${this._vmBackupDir}/vdis/${backup.job.id}`
const backupDir = getVmBackupDir(backup.vm.uuid)
const vdisDir = `${backupDir}/vdis/${backup.job.id}`
await asyncMap(baseUuidToSrcVdi, async ([baseUuid, srcVdi]) => {
let found = false
@@ -141,6 +143,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
const jobId = job.id
const handler = adapter.handler
const backupDir = getVmBackupDir(vm.uuid)
// TODO: clean VM backup directory
@@ -174,7 +177,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
const { size } = await Task.run({ name: 'transfer' }, async () => {
await Promise.all(
map(deltaExport.vdis, async (vdi, id) => {
const path = `${this._vmBackupDir}/${vhds[id]}`
const path = `${backupDir}/${vhds[id]}`
const isDelta = vdi.other_config['xo:base_delta'] !== undefined
let parentPath

View File

@@ -2,6 +2,7 @@
const { formatFilenameDate } = require('../_filenameDate.js')
const { getOldEntries } = require('../_getOldEntries.js')
const { getVmBackupDir } = require('../_getVmBackupDir.js')
const { Task } = require('../Task.js')
const { MixinBackupWriter } = require('./_MixinBackupWriter.js')
@@ -33,6 +34,7 @@ exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(Abst
const { job, scheduleId, vm } = backup
const adapter = this._adapter
const backupDir = getVmBackupDir(vm.uuid)
// TODO: clean VM backup directory
@@ -45,7 +47,7 @@ exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(Abst
const basename = formatFilenameDate(timestamp)
const dataBasename = basename + '.xva'
const dataFilename = this._vmBackupDir + '/' + dataBasename
const dataFilename = backupDir + '/' + dataBasename
const metadata = {
jobId: job.id,

View File

@@ -16,6 +16,7 @@ const { info, warn } = createLogger('xo:backups:MixinBackupWriter')
exports.MixinBackupWriter = (BaseClass = Object) =>
class MixinBackupWriter extends BaseClass {
#lock
#vmBackupDir
constructor({ remoteId, ...rest }) {
super(rest)
@@ -23,13 +24,13 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
this._adapter = rest.backup.remoteAdapters[remoteId]
this._remoteId = remoteId
this._vmBackupDir = getVmBackupDir(this._backup.vm.uuid)
this.#vmBackupDir = getVmBackupDir(this._backup.vm.uuid)
}
async _cleanVm(options) {
try {
return await Task.run({ name: 'clean-vm' }, () => {
return this._adapter.cleanVm(this._vmBackupDir, {
return this._adapter.cleanVm(this.#vmBackupDir, {
...options,
fixMetadata: true,
logInfo: info,
@@ -49,7 +50,7 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
async beforeBackup() {
const { handler } = this._adapter
const vmBackupDir = this._vmBackupDir
const vmBackupDir = this.#vmBackupDir
await handler.mktree(vmBackupDir)
this.#lock = await handler.lock(vmBackupDir)
}

View File

@@ -14,7 +14,7 @@ import { basename, dirname, normalize as normalizePath } from './path'
import { createChecksumStream, validChecksumOfReadStream } from './checksum'
import { DEFAULT_ENCRYPTION_ALGORITHM, _getEncryptor } from './_encryptor'
const { info, warn } = createLogger('xo:fs:abstract')
const { info, warn } = createLogger('@xen-orchestra:fs')
const checksumFile = file => file + '.checksum'
const computeRate = (hrtime, size) => {

View File

@@ -6,7 +6,6 @@
- Settings page (PR [#6418](https://github.com/vatesfr/xen-orchestra/pull/6418))
- Uncollapse hosts in the tree by default (PR [#6428](https://github.com/vatesfr/xen-orchestra/pull/6428))
- Display RAM usage in pool dashboard (PR [#6419](https://github.com/vatesfr/xen-orchestra/pull/6419))
- Implement not found page (PR [#6410](https://github.com/vatesfr/xen-orchestra/pull/6410))
## **0.1.0**

View File

@@ -13,12 +13,6 @@
<a :href="url.href" target="_blank" rel="noopener">{{ 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="!xenApiStore.isConnected">
<AppLogin />
@@ -49,7 +43,6 @@ import AppHeader from "@/components/AppHeader.vue";
import AppLogin from "@/components/AppLogin.vue";
import AppTooltips from "@/components/AppTooltips.vue";
import InfraPoolList from "@/components/infra/InfraPoolList.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";
@@ -112,7 +105,6 @@ watch(
);
const isSslModalOpen = computed(() => unreachableHostsUrls.value.length > 0);
const reload = () => window.location.reload();
</script>
<style lang="postcss">

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -2,24 +2,15 @@
<div class="app-login form-container">
<form @submit.prevent="handleSubmit">
<img alt="XO Lite" src="../assets/logo-title.svg" />
<FormInputWrapper>
<FormInput v-model="login" name="login" readonly type="text" />
</FormInputWrapper>
<FormInputWrapper :error="error">
<FormInput
name="password"
ref="passwordRef"
type="password"
v-model="password"
:placeholder="$t('password')"
:readonly="isConnecting"
/>
</FormInputWrapper>
<UiButton
type="submit"
:busy="isConnecting"
:disabled="password.trim().length < 1"
>
<input v-model="login" name="login" readonly type="text" />
<input
v-model="password"
:readonly="isConnecting"
name="password"
:placeholder="$t('password')"
type="password"
/>
<UiButton :busy="isConnecting" type="submit">
{{ $t("login") }}
</UiButton>
</form>
@@ -28,47 +19,21 @@
<script lang="ts" setup>
import { storeToRefs } from "pinia";
import { onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import FormInput from "@/components/form/FormInput.vue";
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
import { onMounted, ref } from "vue";
import UiButton from "@/components/ui/UiButton.vue";
import { useXenApiStore } from "@/stores/xen-api.store";
const { t } = useI18n();
const xenApiStore = useXenApiStore();
const { isConnecting } = storeToRefs(xenApiStore);
const login = ref("root");
const password = ref("");
const error = ref<string>();
const passwordRef = ref<InstanceType<typeof FormInput>>();
const isInvalidPassword = ref(false);
const focusPasswordInput = () => passwordRef.value?.focus();
onMounted(() => {
xenApiStore.reconnect();
focusPasswordInput();
});
watch(password, () => {
isInvalidPassword.value = false;
error.value = undefined;
});
async function handleSubmit() {
try {
await xenApiStore.connect(login.value, password.value);
} catch (err) {
if ((err as Error).message === "SESSION_AUTHENTICATION_FAILED") {
focusPasswordInput();
isInvalidPassword.value = true;
error.value = t("password-invalid");
} else {
error.value = t("error-occured");
console.error(err);
}
}
await xenApiStore.connect(login.value, password.value);
}
</script>
@@ -85,7 +50,6 @@ async function handleSubmit() {
form {
display: flex;
font-size: 2rem;
min-width: 30em;
max-width: 100%;
align-items: center;
@@ -108,6 +72,12 @@ img {
margin-bottom: 5rem;
}
label {
font-size: 120%;
font-weight: bold;
margin: 1.5rem 0 0.5rem 0;
}
input {
width: 45rem;
max-width: 100%;
@@ -119,6 +89,6 @@ input {
}
button {
margin-top: 2rem;
margin-top: 3rem;
}
</style>

View File

@@ -1,47 +0,0 @@
<template>
<div class="wrapper-spinner" v-if="!store.isReady">
<UiSpinner class="spinner" />
</div>
<ObjectNotFoundView :id="id" v-else-if="isRecordNotFound" />
<slot v-else />
</template>
<script lang="ts" setup>
import ObjectNotFoundView from "@/views/ObjectNotFoundView.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import { useHostStore } from "@/stores/host.store";
import { useVmStore } from "@/stores/vm.store";
import { computed } from "vue";
import { useRouter } from "vue-router";
const storeByType = {
vm: useVmStore,
host: useHostStore,
};
const props = defineProps<{ objectType: "vm" | "host"; id?: string }>();
const store = storeByType[props.objectType]();
const { currentRoute } = useRouter();
const id = computed(
() => props.id ?? (currentRoute.value.params.uuid as string)
);
const isRecordNotFound = computed(
() => store.isReady && !store.hasRecordByUuid(id.value)
);
</script>
<style scoped>
.wrapper-spinner {
display: flex;
height: 100%;
}
.spinner {
color: var(--color-extra-blue-base);
display: flex;
margin: auto;
width: 10rem;
height: 10rem;
}
</style>

View File

@@ -0,0 +1,68 @@
<template>
<div class="progress-bar-component">
<div class="progress-bar">
<div class="progress-bar-fill" />
</div>
<div class="legend" v-if="label !== undefined">
<span class="circle" />
{{ label }}
<UiBadge class="badge">{{ badgeLabel ?? progressWithUnit }}</UiBadge>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import UiBadge from "@/components/ui/UiBadge.vue";
interface Props {
value: number;
badgeLabel?: string | number;
label?: string;
maxValue?: number;
}
const props = withDefaults(defineProps<Props>(), {
maxValue: 100,
});
const progressWithUnit = computed(() => {
const progress = Math.round((props.value / props.maxValue) * 100);
return `${progress}%`;
});
</script>
<style lang="postcss" scoped>
.legend {
text-align: right;
margin: 1.6em 0;
}
.badge {
font-size: 0.9em;
font-weight: 700;
}
.circle {
display: inline-block;
height: 10px;
width: 10px;
background-color: #716ac6;
border-radius: 1rem;
}
.progress-bar {
overflow: hidden;
height: 1.2rem;
border-radius: 0.4rem;
background-color: var(--color-blue-scale-400);
margin: 1rem 0;
}
.progress-bar-fill {
transition: width 1s ease-in-out;
width: v-bind(progressWithUnit);
height: 1.2rem;
background-color: var(--color-extra-blue-d20);
}
</style>

View File

@@ -1,22 +1,18 @@
<template>
<div>
<div class="header">
<slot name="header" />
</div>
<template v-if="data !== undefined">
<div
<ProgressBar
v-for="item in computedData.sortedArray"
:key="item.id"
class="progress-item"
>
<UiProgressBar :value="item.value" color="custom" />
<div class="legend">
<span class="circle" />
{{ item.label }}
<UiBadge class="badge">{{
item.badgeLabel ?? `${item.value}%`
}}</UiBadge>
</div>
</div>
:value="item.value"
:label="item.label"
:badge-label="item.badgeLabel"
/>
<div class="footer">
<slot :total-percent="computedData.totalPercentUsage" name="footer" />
<slot name="footer" :total-percent="computedData.totalPercentUsage" />
</div>
</template>
<UiSpinner v-else class="spinner" />
@@ -24,9 +20,8 @@
</template>
<script lang="ts" setup>
import UiBadge from "@/components/ui/UiBadge.vue";
import UiProgressBar from "@/components/ui/UiProgressBar.vue";
import { computed } from "vue";
import ProgressBar from "@/components/ProgressBar.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
interface Data {
@@ -38,7 +33,7 @@ interface Data {
}
interface Props {
data?: Data[];
data?: Array<Data>;
nItems?: number;
}
@@ -65,6 +60,15 @@ const computedData = computed(() => {
</script>
<style scoped>
.header {
color: var(--color-extra-blue-base);
display: flex;
justify-content: space-between;
border-bottom: 1px solid var(--color-extra-blue-base);
margin-bottom: 2rem;
font-size: 16px;
font-weight: 700;
}
.footer {
display: flex;
justify-content: space-between;
@@ -80,43 +84,23 @@ const computedData = computed(() => {
width: 40px;
height: 40px;
}
</style>
.legend {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
margin: 1.6em 0;
<style>
.progress-bar-component:nth-of-type(2) .progress-bar-fill,
.progress-bar-component:nth-of-type(2) .circle {
background-color: var(--color-extra-blue-d60);
}
.badge {
font-size: 0.9em;
font-weight: 700;
.progress-bar-component:nth-of-type(3) .progress-bar-fill,
.progress-bar-component:nth-of-type(3) .circle {
background-color: var(--color-extra-blue-d40);
}
.progress-item {
--progress-bar-height: 1.2rem;
--progress-bar-color: var(--color-extra-blue-l20);
--progress-bar-background-color: var(--color-blue-scale-400);
.progress-bar-component:nth-of-type(4) .progress-bar-fill,
.progress-bar-component:nth-of-type(4) .circle {
background-color: var(--color-extra-blue-d20);
}
.progress-item:nth-child(1) {
--progress-bar-color: var(--color-extra-blue-d60);
}
.progress-item:nth-child(2) {
--progress-bar-color: var(--color-extra-blue-d40);
}
.progress-item:nth-child(3) {
--progress-bar-color: var(--color-extra-blue-d20);
}
.circle {
display: inline-block;
width: 1rem;
height: 1rem;
border-radius: 0.5rem;
background-color: var(--progress-bar-color);
.progress-bar-component .progress-bar-fill,
.progress-bar-component .circle {
background-color: var(--color-extra-blue-l20);
}
</style>

View File

@@ -6,7 +6,6 @@
:class="inputClass"
:disabled="disabled || isLabelDisabled"
class="input"
ref="inputElement"
v-bind="$attrs"
/>
<template v-else>
@@ -15,7 +14,6 @@
:class="inputClass"
:disabled="disabled || isLabelDisabled"
class="select"
ref="inputElement"
v-bind="$attrs"
>
<slot />
@@ -72,8 +70,6 @@ interface Props extends Omit<InputHTMLAttributes, ""> {
const props = withDefaults(defineProps<Props>(), { color: "info" });
const inputElement = ref();
const emit = defineEmits<{
(event: "update:modelValue", value: any): void;
}>();
@@ -82,10 +78,6 @@ const value = useVModel(props, "modelValue", emit);
const empty = computed(() => isEmpty(props.modelValue));
const isSelect = inject("isSelect", false);
const isLabelDisabled = inject("isLabelDisabled", ref(false));
const color = inject(
"color",
computed(() => undefined)
);
const wrapperClass = computed(() => [
isSelect ? "form-select" : "form-input",
@@ -96,19 +88,13 @@ const wrapperClass = computed(() => [
]);
const inputClass = computed(() => [
color.value ?? props.color,
props.color,
{
right: props.right,
"has-before": props.before !== undefined,
"has-after": props.after !== undefined,
},
]);
const focus = () => inputElement.value.focus();
defineExpose({
focus,
});
</script>
<style lang="postcss" scoped>

View File

@@ -1,96 +0,0 @@
<template>
<div class="wrapper">
<label
v-if="$slots.label"
class="form-label"
:class="{ disabled, ...formInputWrapperClass }"
>
<slot />
</label>
<slot />
<p v-if="hasError || hasWarning" :class="formInputWrapperClass">
<UiIcon :icon="faCircleExclamation" v-if="hasError" />{{
error ?? warning
}}
</p>
</div>
</template>
<script lang="ts" setup>
import { computed, provide, useSlots } from "vue";
import { faCircleExclamation } from "@fortawesome/free-solid-svg-icons";
import UiIcon from "@/components/ui/UiIcon.vue";
const slots = useSlots();
const props = defineProps<{
disabled?: boolean;
error?: string;
warning?: string;
}>();
provide("hasLabel", slots.label !== undefined);
provide(
"isLabelDisabled",
computed(() => props.disabled)
);
const hasError = computed(
() => props.error !== undefined && props.error.trim() !== ""
);
const hasWarning = computed(
() => props.warning !== undefined && props.warning.trim() !== ""
);
provide(
"color",
computed(() =>
hasError.value ? "error" : hasWarning.value ? "warning" : undefined
)
);
const formInputWrapperClass = computed(() => ({
error: hasError.value,
warning: !hasError.value && hasWarning.value,
}));
</script>
<style lang="postcss" scoped>
.wrapper {
display: flex;
flex-direction: column;
}
.wrapper :deep(.input) {
margin-bottom: 1rem;
}
.form-label {
font-size: 1.6rem;
display: inline-flex;
align-items: center;
gap: 0.625em;
&.disabled {
cursor: not-allowed;
color: var(--color-blue-scale-300);
}
}
p.error,
p.warning {
font-size: 0.65em;
margin-bottom: 1rem;
}
.error {
color: var(--color-red-vates-base);
}
.warning {
color: var(--color-orange-world-base);
}
p svg {
margin-right: 0.4em;
}
</style>

View File

@@ -0,0 +1,33 @@
<template>
<label :class="{ disabled }" class="form-label">
<slot />
</label>
</template>
<script lang="ts" setup>
import { computed, provide } from "vue";
const props = defineProps<{
disabled?: boolean;
}>();
provide("hasLabel", true);
provide(
"isLabelDisabled",
computed(() => props.disabled)
);
</script>
<style lang="postcss" scoped>
.form-label {
font-size: 1.6rem;
display: inline-flex;
align-items: center;
gap: 0.625em;
&.disabled {
cursor: not-allowed;
color: var(--color-blue-scale-300);
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<UiCard>
<UiCardTitle>{{ $t("cpu-usage") }}</UiCardTitle>
<UiTitle type="h4">{{ $t("cpu-usage") }}</UiTitle>
<HostsCpuUsage />
<VmsCpuUsage />
</UiCard>
@@ -9,5 +9,5 @@
import HostsCpuUsage from "@/components/pool/dashboard/cpuUsage/HostsCpuUsage.vue";
import VmsCpuUsage from "@/components/pool/dashboard/cpuUsage/VmsCpuUsage.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
</script>

View File

@@ -1,6 +1,6 @@
<template>
<UiCard>
<UiCardTitle>{{ $t("ram-usage") }}</UiCardTitle>
<UiTitle type="h4">{{ $t("ram-usage") }}</UiTitle>
<HostsRamUsage />
<VmsRamUsage />
</UiCard>
@@ -10,5 +10,5 @@
import HostsRamUsage from "@/components/pool/dashboard/ramUsage/HostsRamUsage.vue";
import VmsRamUsage from "@/components/pool/dashboard/ramUsage/VmsRamUsage.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
</script>

View File

@@ -1,17 +1,17 @@
<template>
<UiCard>
<UiCardTitle>{{ $t("status") }}</UiCardTitle>
<UiTitle type="h4">{{ $t("status") }}</UiTitle>
<template v-if="isReady">
<PoolDashboardStatusItem
:active="activeHostsCount"
:label="$t('hosts')"
:total="totalHostsCount"
:label="$t('hosts')"
/>
<UiSeparator />
<PoolDashboardStatusItem
:active="activeVmsCount"
:label="$t('vms')"
:total="totalVmsCount"
:label="$t('vms')"
/>
</template>
<UiSpinner v-else class="spinner" />
@@ -19,14 +19,14 @@
</template>
<script lang="ts" setup>
import { computed } from "vue";
import PoolDashboardStatusItem from "@/components/pool/dashboard/PoolDashboardStatusItem.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiSeparator from "@/components/ui/UiSeparator.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import { useHostMetricsStore } from "@/stores/host-metrics.store";
import { useVmStore } from "@/stores/vm.store";
import { computed } from "vue";
const vmStore = useVmStore();
const hostMetricsStore = useHostMetricsStore();

View File

@@ -1,13 +1,11 @@
<template>
<UiCard>
<UiCardTitle
:left="$t('storage-usage')"
:right="$t('top-#', { n: N_ITEMS })"
/>
<UsageBar
:data="srStore.isReady ? data.result : undefined"
:nItems="N_ITEMS"
>
<UiTitle type="h4">{{ $t("storage-usage") }}</UiTitle>
<UsageBar :data="srStore.isReady ? data.result : undefined" :nItems="N_ITEMS">
<template #header>
<span>{{ $t("storage") }}</span>
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
</template>
<template #footer v-if="showFooter">
<div class="footer-card">
<p>{{ $t("total-used") }}:</p>
@@ -33,10 +31,10 @@
</template>
<script lang="ts" setup>
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { computed } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import { formatSize, percent } from "@/libs/utils";
import { useSrStore } from "@/stores/storage.store";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";

View File

@@ -1,20 +1,19 @@
<template>
<UiCardTitle
subtitle
:left="$t('hosts')"
:right="$t('top-#', { n: N_ITEMS })"
/>
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS" />
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
<template #header>
<span>{{ $t("hosts") }}</span>
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
</template>
</UsageBar>
</template>
<script lang="ts" setup>
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { type ComputedRef, computed, inject } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import type { Stat } from "@/composables/fetch-stats.composable";
import { getAvgCpuUsage } from "@/libs/utils";
import type { HostStats } from "@/libs/xapi-stats";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
const stats = inject<ComputedRef<Stat<HostStats>[]>>(
"hostStats",

View File

@@ -1,14 +1,13 @@
<template>
<UiCardTitle
subtitle
:left="$t('vms')"
:right="$t('top-#', { n: N_ITEMS })"
/>
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS" />
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
<template #header>
<span>{{ $t("vms") }}</span>
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
</template>
</UsageBar>
</template>
<script lang="ts" setup>
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { type ComputedRef, computed, inject } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import type { Stat } from "@/composables/fetch-stats.composable";

View File

@@ -1,14 +1,13 @@
<template>
<UiCardTitle
subtitle
:left="$t('hosts')"
:right="$t('top-#', { n: N_ITEMS })"
/>
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS" />
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
<template #header>
<span>{{ $t("hosts") }}</span>
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
</template>
</UsageBar>
</template>
<script lang="ts" setup>
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { type ComputedRef, computed, inject } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import type { Stat } from "@/composables/fetch-stats.composable";

View File

@@ -1,14 +1,13 @@
<template>
<UiCardTitle
subtitle
:left="$t('vms')"
:right="$t('top-#', { n: N_ITEMS })"
/>
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS" />
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
<template #header>
<span>{{ $t("vms") }}</span>
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
</template>
</UsageBar>
</template>
<script lang="ts" setup>
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { type ComputedRef, computed, inject } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import type { Stat } from "@/composables/fetch-stats.composable";

View File

@@ -1,65 +0,0 @@
<template>
<div :class="{ subtitle }" class="ui-section-title">
<component
:is="subtitle ? 'h5' : 'h4'"
v-if="$slots.default || left"
class="left"
>
<slot>{{ left }}</slot>
</component>
<component
:is="subtitle ? 'h6' : 'h5'"
v-if="$slots.right || right"
class="right"
>
<slot name="right">{{ right }}</slot>
</component>
</div>
</template>
<script lang="ts" setup>
defineProps<{
subtitle?: boolean;
left?: string;
right?: string;
}>();
</script>
<style lang="postcss" scoped>
.ui-section-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2rem;
--section-title-left-size: 2rem;
--section-title-left-color: var(--color-blue-scale-100);
--section-title-left-weight: 500;
--section-title-right-size: 1.6rem;
--section-title-right-color: var(--color-extra-blue-base);
--section-title-right-weight: 700;
&.subtitle {
border-bottom: 1px solid var(--color-extra-blue-base);
--section-title-left-size: 1.6rem;
--section-title-left-color: var(--color-extra-blue-base);
--section-title-left-weight: 700;
--section-title-right-size: 1.4rem;
--section-title-right-color: var(--color-extra-blue-base);
--section-title-right-weight: 400;
}
}
.left {
font-size: var(--section-title-left-size);
font-weight: var(--section-title-left-weight);
color: var(--section-title-left-color);
}
.right {
font-size: var(--section-title-right-size);
font-weight: var(--section-title-right-weight);
color: var(--section-title-right-color);
}
</style>

View File

@@ -1,60 +0,0 @@
<template>
<div class="ui-progress-bar" :class="`color-${color}`">
<div class="fill" />
</div>
</template>
<script lang="ts" setup>
import type { Color } from "@/types";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
value: number;
color?: Color | "custom";
maxValue?: number;
}>(),
{ color: "info", maxValue: 100 }
);
const progressWithUnit = computed(() => {
const progress = (props.value / props.maxValue) * 100;
return `${progress}%`;
});
</script>
<style lang="postcss" scoped>
.ui-progress-bar {
overflow: hidden;
height: var(--progress-bar-height, 0.4rem);
margin: 1rem 0;
border-radius: 0.4rem;
background-color: var(
--progress-bar-background-color,
var(--background-color-extra-blue)
);
&.color-info {
--progress-bar-color: var(--color-extra-blue-base);
}
&.color-success {
--progress-bar-color: var(--color-green-infra-base);
}
&.color-warning {
--progress-bar-color: var(--color-orange-world-base);
}
&.color-error {
--progress-bar-color: var(--color-red-vates-base);
}
}
.fill {
width: v-bind(progressWithUnit);
height: var(--progress-bar-height, 0.4rem);
transition: width 1s ease-in-out;
background-color: var(--progress-bar-color);
}
</style>

View File

@@ -1,41 +0,0 @@
# useArrayRemovedItemsHistory composable
This composable allows you to keep a history of each removed item of an array.
## Usage
```typescript
const myArray = ref([]);
const history = useArrayRemovedItemsHistory(myArray)
myArray.push('A'); // myArray = ['A']; history = []
myArray.push('B'); // myArray = ['A', 'B']; history = []
myArray.shift(); // myArray = ['B']; history = ['A']
```
You can limit the number of items to keep in history:
```typescript
const myArray = ref([]);
const history = useArrayRemovedItemsHistory(myArray, 30);
```
Be careful when using an array of objects which is likely to be replaced (instead of being altered):
```typescript
const myArray = ref([]);
const history = useArrayRemovedItemsHistory(myArray);
myArray.value = [{ id: 'foo' }, { id: 'bar' }];
myArray.value = [{ id: 'bar' }, { id: 'baz' }]; // history = [{ id: 'foo' }, { id: 'bar' }]
```
In this case, `{ id: 'bar' }` is detected as removed since in JavaScript `{ id: 'bar' } !== { id: 'bar' }`.
You must therefore use an identity function as third parameter to return the value to be used to detect deletion:
```typescript
const myArray = ref<{ id: string }[]>([]);
const history = useArrayRemovedItemsHistory(myArray, undefined, (item) => item.id);
myArray.value = [{ id: 'foo' }, { id: 'bar' }];
myArray.value = [{ id: 'bar' }, { id: 'baz' }]; // history = [{ id: 'foo' }]
```

View File

@@ -1,30 +0,0 @@
import { differenceBy } from "lodash-es";
import { type Ref, ref, unref, watch } from "vue";
export default function useArrayRemovedItemsHistory<T>(
list: Ref<T[]>,
limit = Infinity,
iteratee: (item: T) => unknown = (item) => item
) {
const currentList: Ref<T[]> = ref([]);
const history: Ref<T[]> = ref([]);
watch(
list,
(updatedList) => {
currentList.value = [...updatedList];
},
{ deep: true }
);
watch(currentList, (nextList, previousList) => {
const removedItems = differenceBy(previousList, nextList, iteratee);
history.value.push(...removedItems);
const currentLimit = unref(limit);
if (history.value.length > currentLimit) {
history.value.slice(-currentLimit);
}
});
return history;
}

View File

@@ -38,12 +38,11 @@ export default function useFetchStats<T extends XenApiHost | XenApiVm, S>(
const newStats = (await STORES_BY_OBJECT_TYPE[type]().getStats(
object.uuid,
granularity
)) as XapiStatsResponse<S> | undefined;
)) as XapiStatsResponse<S>;
if (newStats !== undefined) {
stats.value.get(object.uuid)!.stats = newStats.stats;
await promiseTimeout(newStats.interval * 1000);
}
stats.value.get(object.uuid)!.stats = newStats.stats;
await promiseTimeout(newStats.interval * 1000);
},
0,
{ immediate: true }

View File

@@ -9,7 +9,6 @@
"ascending": "ascending",
"available-properties-for-advanced-filter": "Available properties for advanced filter:",
"backup": "Backup",
"back-pool-dashboard": "Go back to your Pool dashboard",
"cancel": "Cancel",
"change-power-state": "Change power state",
"community": "Community",
@@ -24,7 +23,6 @@
"descending": "descending",
"display": "Display",
"edit-config": "Edit config",
"error-occured": "An error has occurred",
"export": "Export",
"export-table-to": "Export table to {type}",
"export-vms": "Export VMs",
@@ -38,11 +36,8 @@
"network": "Network",
"news": "News",
"news-name": "{name} news",
"object-not-found": "Object {id} can't be found…",
"or": "Or",
"page-not-found": "This page is not to be found…",
"password": "Password",
"password-invalid": "Password invalid",
"property": "Property",
"ram-usage":"RAM usage",
"send-us-feedback": "Send us feedback",
@@ -60,7 +55,6 @@
"total-free": "Total free",
"total-used": "Total used",
"unreachable-hosts": "Unreachable hosts",
"unreachable-hosts-reload-page": "Done, reload the page",
"version": "Version",
"vms": "VMs"
}

View File

@@ -9,7 +9,6 @@
"ascending": "ascendant",
"available-properties-for-advanced-filter": "Propriétés disponibles pour le filtrage avancé :",
"backup": "Sauvegarde",
"back-pool-dashboard": "Revenez au tableau de bord de votre pool",
"cancel": "Annuler",
"change-power-state": "Changer l'état d'alimentation",
"community": "Communauté",
@@ -24,7 +23,6 @@
"descending": "descendant",
"display": "Affichage",
"edit-config": "Modifier config",
"error-occured": "Une erreur est survenue",
"export": "Exporter",
"export-table-to": "Exporter le tableau en {type}",
"export-vms": "Exporter les VMs",
@@ -38,11 +36,8 @@
"network": "Réseau",
"news": "Actualités",
"news-name": "Actualités {name}",
"object-not-found": "L'objet {id} est introuvable…",
"or": "Ou",
"page-not-found": "Cette page est introuvable…",
"password": "Mot de passe",
"password-invalid": "Mot de passe incorrect",
"property": "Propriété",
"ram-usage":"Utilisation de la RAM",
"send-us-feedback": "Envoyez-nous vos commentaires",
@@ -60,7 +55,6 @@
"total-free": "Total libre",
"total-used": "Total utilisé",
"unreachable-hosts": "Hôtes inaccessibles",
"unreachable-hosts-reload-page": "C'est fait. Rafraîchir la page",
"version": "Version",
"vms": "VMs"
}

View File

@@ -1,7 +1,6 @@
import { createRouter, createWebHashHistory } from "vue-router";
import pool from "@/router/pool";
import HomeView from "@/views/HomeView.vue";
import PageNotFoundView from "@/views/PageNotFoundView.vue";
import HostDashboardView from "@/views/host/HostDashboardView.vue";
import HostRootView from "@/views/host/HostRootView.vue";
import SettingsView from "@/views/settings/SettingsView.vue";
@@ -44,11 +43,6 @@ const router = createRouter({
},
],
},
{
path: "/:pathMatch(.*)*",
name: "notFound",
component: PageNotFoundView,
},
],
});

View File

@@ -1,4 +1,3 @@
import { computed } from "vue";
import { sortRecordsByNameLabel } from "@/libs/utils";
import type { GRANULARITY } from "@/libs/xapi-stats";
import type { XenApiHost } from "@/libs/xen-api";
@@ -7,10 +6,6 @@ import { useXenApiStore } from "@/stores/xen-api.store";
import { defineStore } from "pinia";
export const useHostStore = defineStore("host", () => {
const xenApiStore = useXenApiStore();
const xapiStats = computed(() =>
xenApiStore.isConnected ? xenApiStore.getXapiStats() : undefined
);
const recordContext = createRecordContext<XenApiHost>("host", {
sort: sortRecordsByNameLabel,
});
@@ -20,7 +15,7 @@ export const useHostStore = defineStore("host", () => {
if (host === undefined) {
throw new Error(`Host ${id} could not be found.`);
}
return xapiStats.value?._getAndUpdateStats({
return useXenApiStore().getXapiStats()._getAndUpdateStats({
host,
uuid: host.uuid,
granularity,

View File

@@ -62,9 +62,6 @@ export function createRecordContext<T extends XenApiRecord>(
const getRecordByUuid = (uuid: string) =>
useRecordsStore().getRecordByUuid<T>(uuid);
const hasRecordByUuid = (uuid: string) =>
useRecordsStore().hasRecordByUuid(uuid);
return {
init,
opaqueRefs,
@@ -72,6 +69,5 @@ export function createRecordContext<T extends XenApiRecord>(
getRecordByUuid,
isReady,
allRecords,
hasRecordByUuid,
};
}

View File

@@ -77,10 +77,6 @@ export const useRecordsStore = defineStore("records", () => {
return opaqueRefsByObjectType.get(objectType) || new Set();
}
function hasRecordByUuid(uuid: string): boolean {
return uuidToOpaqueRefMapping.has(uuid);
}
return {
loadRecords,
addOrReplaceRecord,
@@ -88,6 +84,5 @@ export const useRecordsStore = defineStore("records", () => {
getRecord,
getRecordsOpaqueRefs,
getRecordByUuid,
hasRecordByUuid,
};
});

View File

@@ -8,11 +8,7 @@ import { defineStore } from "pinia";
import { computed } from "vue";
export const useVmStore = defineStore("vm", () => {
const xenApiStore = useXenApiStore();
const hostStore = useHostStore();
const xapiStats = computed(() =>
xenApiStore.isConnected ? xenApiStore.getXapiStats() : undefined
);
const baseVmContext = createRecordContext<XenApiVm>("VM", {
filter: (vm) =>
!vm.is_a_snapshot && !vm.is_a_template && !vm.is_control_domain,
@@ -45,7 +41,7 @@ export const useVmStore = defineStore("vm", () => {
throw new Error(`VM ${id} is halted or host could not be found.`);
}
return xapiStats.value?._getAndUpdateStats({
return useXenApiStore().getXapiStats()._getAndUpdateStats({
host,
uuid: vm.uuid,
granularity,

View File

@@ -91,7 +91,6 @@ export const useXenApiStore = defineStore("xen-api", () => {
}
async function connect(username: string, password: string) {
isConnecting.value = true;
try {
currentSessionId.value = await xenApi.connectWithPassword(
username,

View File

@@ -1,44 +0,0 @@
<template>
<div>
<img alt="Not found" src="../assets/object-not-found.svg" />
<p class="text">{{ $t("object-not-found", { id }) }}</p>
<UiButton @click="router.push({ name: 'home' })">{{
$t("back-pool-dashboard")
}}</UiButton>
</div>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
import UiButton from "@/components/ui/UiButton.vue";
defineProps<{
id: string;
}>();
const router = useRouter();
</script>
<style lang="postcss" scoped>
div {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
img {
width: 30%;
}
.text {
color: var(--color-extra-blue-base);
font-size: 36px;
font-weight: 400;
line-height: 150%;
margin: 0.5em;
text-align: center;
}
</style>

View File

@@ -1,49 +0,0 @@
<template>
<div>
<img alt="Not found" src="../assets/page-not-found.svg" />
<p class="numeric">404</p>
<p class="text">{{ $t("page-not-found") }}</p>
<UiButton @click="router.push({ name: 'home' })">{{
$t("back-pool-dashboard")
}}</UiButton>
</div>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
import UiButton from "@/components/ui/UiButton.vue";
const router = useRouter();
</script>
<style lang="postcss" scoped>
div {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
img {
width: 30%;
}
.numeric {
color: var(--color-extra-blue-base);
font-size: 96px;
font-weight: 900;
letter-spacing: 1em;
line-height: 100%;
margin-right: -1em;
text-align: center;
}
.text {
color: var(--color-extra-blue-base);
font-size: 36px;
font-weight: 400;
line-height: 150%;
margin: 0.5em;
text-align: center;
}
</style>

View File

@@ -1,9 +1,5 @@
<template>
<ObjectNotFoundWrapper object-type="host">
<RouterView />
</ObjectNotFoundWrapper>
<RouterView />
</template>
<script lang="ts" setup>
import ObjectNotFoundWrapper from "@/components/ObjectNotFoundWrapper.vue";
</script>
<script lang="ts" setup></script>

View File

@@ -2,7 +2,7 @@
<TitleBar :icon="faGear">{{ $t("settings") }}</TitleBar>
<div class="card-view">
<UiCard class="group">
<UiCardTitle>Xen Orchestra Lite</UiCardTitle>
<UiTitle type="h4">Xen Orchestra Lite</UiTitle>
<UiKeyValueList>
<UiKeyValueRow>
<template #key>{{ $t("version") }}</template>
@@ -50,22 +50,24 @@
</UiKeyValueList>
</UiCard>
<UiCard class="group">
<UiCardTitle>{{ $t("display") }}</UiCardTitle>
<UiTitle type="h4">{{ $t("display") }}</UiTitle>
<UiKeyValueList>
<UiKeyValueRow>
<template #key>{{ $t("appearance") }}</template>
<template #value>
<FormSelect v-model="colorMode">
<option value="auto">{{ $t("theme-auto") }}</option>
<option value="dark">{{ $t("theme-dark") }}</option>
<option value="light">{{ $t("theme-light") }}</option>
</FormSelect>
<FormLabel>
<FormSelect v-model="colorMode">
<option value="auto">{{ $t("theme-auto") }}</option>
<option value="dark">{{ $t("theme-dark") }}</option>
<option value="light">{{ $t("theme-light") }}</option>
</FormSelect>
</FormLabel>
</template>
</UiKeyValueRow>
</UiKeyValueList>
</UiCard>
<UiCard class="group">
<UiCardTitle>{{ $t("language") }}</UiCardTitle>
<UiTitle type="h4">{{ $t("language") }}</UiTitle>
<UiKeyValueList>
<UiKeyValueRow>
<template #value>
@@ -89,7 +91,6 @@
<script lang="ts" setup>
import FormSelect from "@/components/form/FormSelect.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useUiStore } from "@/stores/ui.store";
import { storeToRefs } from "pinia";
import { watch } from "vue";
@@ -98,9 +99,11 @@ import { locales } from "@/i18n";
import { faEarthAmericas, faGear } from "@fortawesome/free-solid-svg-icons";
import FormWidget from "@/components/FormWidget.vue";
import TitleBar from "@/components/TitleBar.vue";
import FormLabel from "@/components/form/FormLabel.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiKeyValueList from "@/components/ui/UiKeyValueList.vue";
import UiKeyValueRow from "@/components/ui/UiKeyValueRow.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
const version = XO_LITE_VERSION;
const gitHead = XO_LITE_GIT_HEAD;

View File

@@ -1,11 +1,8 @@
<template>
<ObjectNotFoundWrapper object-type="vm">
<RouterView />
</ObjectNotFoundWrapper>
<RouterView />
</template>
<script lang="ts" setup>
import ObjectNotFoundWrapper from "@/components/ObjectNotFoundWrapper.vue";
import { watchEffect } from "vue";
import { useRoute } from "vue-router";
import { useUiStore } from "@/stores/ui.store";

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@xen-orchestra/proxy",
"version": "0.26.9",
"version": "0.26.5",
"license": "AGPL-3.0-or-later",
"description": "XO Proxy used to remotely execute backup jobs",
"keywords": [
@@ -32,13 +32,13 @@
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.3",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.29.4",
"@xen-orchestra/backups": "^0.29.1",
"@xen-orchestra/fs": "^3.3.0",
"@xen-orchestra/log": "^0.5.0",
"@xen-orchestra/mixin": "^0.1.0",
"@xen-orchestra/mixins": "^0.8.2",
"@xen-orchestra/self-signed": "^0.1.3",
"@xen-orchestra/xapi": "^1.6.0",
"@xen-orchestra/xapi": "^1.5.3",
"ajv": "^8.0.3",
"app-conf": "^2.3.0",
"async-iterator-to-stream": "^1.1.0",

View File

@@ -1 +0,0 @@
../../scripts/npmignore

View File

@@ -1,154 +0,0 @@
import { notEqual, strictEqual } from 'node:assert'
import { VhdAbstract } from 'vhd-lib'
import { createFooter, createHeader } from 'vhd-lib/_createFooterHeader.js'
import _computeGeometryForSize from 'vhd-lib/_computeGeometryForSize.js'
import { DISK_TYPES, FOOTER_SIZE } from 'vhd-lib/_constants.js'
import { unpackFooter, unpackHeader } from 'vhd-lib/Vhd/_utils.js'
export default class VhdCowd extends VhdAbstract {
#esxi
#datastore
#parentFileName
#path
#header
#footer
#grainDirectory
static async open(esxi, datastore, path) {
const vhd = new VhdCowd(esxi, datastore, path)
await vhd.readHeaderAndFooter()
return vhd
}
constructor(esxi, datastore, path, parentFileName) {
super()
this.#esxi = esxi
this.#path = path
this.#datastore = datastore
this.#parentFileName = parentFileName
}
get header() {
return this.#header
}
get footer() {
return this.#footer
}
containsBlock(blockId) {
notEqual(this.#grainDirectory, undefined, "bat must be loaded to use contain blocks'")
// only check if a grain table exist for on of the sector of the block
// the great news is that a grain size has 4096 entries of 512B = 2M
// and a vhd block is also 2M
// so we only need to check if a grain table exists (it's not created without data)
return this.#grainDirectory.readInt32LE(blockId * 4) !== 0
}
async #read(start, end) {
return (await this.#esxi.download(this.#datastore, this.#path, `${start}-${end}`)).buffer()
}
async readHeaderAndFooter(checkSecondFooter = true) {
const buffer = await this.#read(0, 2048)
strictEqual(buffer.slice(0, 4).toString('ascii'), 'COWD')
strictEqual(buffer.readInt32LE(4), 1) // version
strictEqual(buffer.readInt32LE(8), 3) // flags
const sectorCapacity = buffer.readInt32LE(12)
// const sectorGrainNumber = buffer.readInt32LE(16)
strictEqual(buffer.readInt32LE(20), 4) // grain directory position in sectors
// const nbGrainDirectoryEntries = buffer.readInt32LE(24)
// const nextFreeSector = buffer.readInt32LE(28)
const size = sectorCapacity * 512
// a grain directory entry represent a grain table
// a grain table can adresse, at most 4096 grain of 512 B
this.#header = unpackHeader(createHeader(Math.ceil(size / (4096 * 512))))
this.#header.parentUnicodeName = this.#parentFileName
const geometry = _computeGeometryForSize(size)
const actualSize = geometry.actualSize
this.#footer = unpackFooter(
createFooter(
actualSize,
Math.floor(Date.now() / 1000),
geometry,
FOOTER_SIZE,
this.#parentFileName ? DISK_TYPES.DIFFERENCING : DISK_TYPES.DYNAMIC
)
)
}
async readBlockAllocationTable() {
const nbBlocks = this.header.maxTableEntries
this.#grainDirectory = await this.#read(2048, 2048 + nbBlocks * 4 - 1)
}
async readBlock(blockId) {
const sectorOffset = this.#grainDirectory.readInt32LE(blockId * 4)
if (sectorOffset === 1) {
return Promise.resolve(Buffer.alloc(4096 * 512, 0))
}
const offset = sectorOffset * 512
const graintable = await this.#read(offset, offset + 2048 - 1)
const buf = Buffer.concat([
Buffer.alloc(512, 255), // vhd block bitmap,
Buffer.alloc(512 * 4096, 0), // empty data
])
// we have no guaranty that data are order or contiguous
// let's construct ranges to limit the number of queries
const fileOffsetToIndexInGrainTable = {}
let nbNonEmptyGrain = 0
for (let i = 0; i < graintable.length / 4; i++) {
const grainOffset = graintable.readInt32LE(i * 4)
if (grainOffset !== 0) {
// non empty grain
fileOffsetToIndexInGrainTable[grainOffset] = i
nbNonEmptyGrain++
}
}
// grain table exists but only contains empty grains
if (nbNonEmptyGrain === 0) {
return {
id: blockId,
bitmap: buf.slice(0, this.bitmapSize),
data: buf.slice(this.bitmapSize),
buffer: buf,
}
}
const offsets = Object.keys(fileOffsetToIndexInGrainTable).map(offset => parseInt(offset))
offsets.sort((a, b) => a - b)
let startOffset = offsets[0]
const ranges = []
const OVERPROVISION = 3
for (let i = 1; i < offsets.length; i++) {
if (offsets[i - 1] + OVERPROVISION < offsets[i]) {
ranges.push({ startOffset, endOffset: offsets[i - 1] })
startOffset = offsets[i]
}
}
ranges.push({ startOffset, endOffset: offsets[offsets.length - 1] })
for (const { startOffset, endOffset } of ranges) {
const startIndex = fileOffsetToIndexInGrainTable[startOffset]
const startInBlock = startIndex * 512 + 512 /* block bitmap */
const sectors = await this.#read(startOffset * 512, endOffset * 512 - 1)
// @todo : if overprovision > 1 , it may copy random data from the vmdk
sectors.copy(buf, startInBlock)
}
return {
id: blockId,
bitmap: buf.slice(0, 512),
data: buf.slice(512),
buffer: buf,
}
}
}

View File

@@ -1,131 +0,0 @@
import _computeGeometryForSize from 'vhd-lib/_computeGeometryForSize.js'
import { createFooter, createHeader } from 'vhd-lib/_createFooterHeader.js'
import { DISK_TYPES, FOOTER_SIZE } from 'vhd-lib/_constants.js'
import { readChunk } from '@vates/read-chunk'
import { unpackFooter, unpackHeader } from 'vhd-lib/Vhd/_utils.js'
import { VhdAbstract } from 'vhd-lib'
import assert from 'node:assert'
const VHD_BLOCK_LENGTH = 2 * 1024 * 1024
export default class VhdEsxiRaw extends VhdAbstract {
#esxi
#datastore
#path
#bat
#header
#footer
#stream
#bytesRead = 0
static async open(esxi, datastore, path) {
const vhd = new VhdEsxiRaw(esxi, datastore, path)
await vhd.readHeaderAndFooter()
return vhd
}
get header() {
return this.#header
}
get footer() {
return this.#footer
}
constructor(esxi, datastore, path) {
super()
this.#esxi = esxi
this.#path = path
this.#datastore = datastore
}
async readHeaderAndFooter(checkSecondFooter = true) {
const res = await this.#esxi.download(this.#datastore, this.#path)
const length = res.headers.get('content-length')
this.#header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
const geometry = _computeGeometryForSize(length)
const actualSize = geometry.actualSize
this.#footer = unpackFooter(
createFooter(actualSize, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DYNAMIC)
)
}
containsBlock(blockId) {
assert.notEqual(this.#bat, undefined, "bat is not loaded")
return this.#bat.has(blockId)
}
async readBlock(blockId) {
const start = blockId * VHD_BLOCK_LENGTH
if (!this.#stream) {
this.#stream = (await this.#esxi.download(this.#datastore, this.#path)).body
this.#bytesRead = 0
}
if (this.#bytesRead > start) {
this.#stream.destroy()
this.#stream = (
await this.#esxi.download(this.#datastore, this.#path, `${start}-${this.footer.currentSize}`)
).body
this.#bytesRead = start
}
if (start - this.#bytesRead > 0) {
this.#stream.destroy()
this.#stream = (
await this.#esxi.download(this.#datastore, this.#path, `${start}-${this.footer.currentSize}`)
).body
this.#bytesRead = start
}
const data = await readChunk(this.#stream, VHD_BLOCK_LENGTH)
this.#bytesRead += data.length
const bitmap = Buffer.alloc(512, 255)
return {
id: blockId,
bitmap,
data,
buffer: Buffer.concat([bitmap, data]),
}
}
async readBlockAllocationTable() {
const res = await this.#esxi.download(this.#datastore, this.#path)
const length = res.headers.get('content-length')
const stream = res.body
const empty = Buffer.alloc(VHD_BLOCK_LENGTH, 0)
let pos = 0
this.#bat = new Set()
let nextChunkLength = Math.min(VHD_BLOCK_LENGTH, length)
const progress = setInterval(() => {
console.log("reading blocks", pos / VHD_BLOCK_LENGTH, "/", length/ VHD_BLOCK_LENGTH)
}, 30 * 1000)
while (nextChunkLength > 0) {
try{
const chunk = await readChunk(stream, nextChunkLength)
let isEmpty
if (nextChunkLength === VHD_BLOCK_LENGTH) {
isEmpty = empty.equals(chunk)
} else {
// last block can be smaller
isEmpty = Buffer.alloc(nextChunkLength, 0).equals(chunk)
}
if (!isEmpty) {
this.#bat.add(pos / VHD_BLOCK_LENGTH)
}
pos += VHD_BLOCK_LENGTH
nextChunkLength = Math.min(VHD_BLOCK_LENGTH, length - pos)
}catch(error){
clearInterval(progress)
throw error
}
}
console.log("BAT reading done, remaining ", this.#bat.size, "/", Math.ceil(length / VHD_BLOCK_LENGTH))
clearInterval(progress)
}
}

View File

@@ -1,315 +0,0 @@
import { Client } from 'node-vsphere-soap'
import { dirname } from 'node:path'
import { EventEmitter } from 'node:events'
import { strictEqual } from 'node:assert'
import fetch from 'node-fetch'
import parseVmdk from './parsers/vmdk.mjs'
import parseVmsd from './parsers/vmsd.mjs'
import parseVmx from './parsers/vmx.mjs'
import VhdCowd from './VhdEsxiCowd.mjs'
import VhdEsxiRaw from './VhdEsxiRaw.mjs'
const MAX_SCSI = 9
const MAX_ETHERNET = 9
export default class Esxi extends EventEmitter {
#client
#cookies
#host
#user
#password
#ready = false
constructor(host, user, password, sslVerify = true) {
super()
this.#host = host
this.#user = user
this.#password = password
this.#client = new Client(host, user, password, sslVerify)
process.on('warning', this.#eatTlsWarning )
this.#client.once('ready', () => {
process.off('warning', this.#eatTlsWarning )
this.#ready = true
this.emit('ready')
})
this.#client.on('error', err => {
process.off('warning', this.#eatTlsWarning )
console.error({
in:'ERROR',
code: err.code,
message: err.message
})
this.emit('error', err)
})
}
#eatTlsWarning (/* err */){
// console.log('yummy', err.code, err.message)
}
#exec(cmd, args) {
strictEqual(this.#ready, true)
const client = this.#client
return new Promise(function (resolve, reject) {
client.once('error', function (error) {
client.off('result', resolve)
reject(error)
})
client.runCommand(cmd, args).once('result', function () {
client.off('error', reject)
resolve(...arguments)
})
})
}
async download(dataStore, path, range) {
strictEqual(this.#ready, true)
const url = `https://${this.#host}/folder/${path}?dsName=${dataStore}`
const headers = {}
if(this.#cookies){
headers.cookie= this.#cookies
} else {
headers.Authorization = 'Basic ' + Buffer.from(this.#user + ':' + this.#password).toString('base64')
}
if (range) {
headers['content-type'] = 'multipart/byteranges'
headers.Range = 'bytes=' + range
}
const res = await fetch(url, {
method: 'GET',
headers,
highWaterMark: 10 * 1024 * 1024,
})
if (res.status < 200 || res.status >= 300) {
const error = new Error(res.status + ' ' + res.statusText + ' ' + url)
error.cause = res
throw error
}
if(res.headers.raw()['set-cookie']){
this.#cookies = res.headers.raw()['set-cookie']
}
return res
}
async search(type, properties) {
// get property collector
const propertyCollector = this.#client.serviceContent.propertyCollector
// get view manager
const viewManager = this.#client.serviceContent.viewManager
// get root folder
const rootFolder = this.#client.serviceContent.rootFolder
let result = await this.#exec('CreateContainerView', {
_this: viewManager,
container: rootFolder,
type: [type],
recursive: true,
})
// build all the data structures needed to query all the vm names
const containerView = result.returnval
const objectSpec = {
attributes: { 'xsi:type': 'ObjectSpec' }, // setting attributes xsi:type is important or else the server may mis-recognize types!
obj: containerView,
skip: true,
selectSet: [
{
attributes: { 'xsi:type': 'TraversalSpec' },
name: 'traverseEntities',
type: 'ContainerView',
path: 'view',
skip: false,
},
],
}
const propertyFilterSpec = {
attributes: { 'xsi:type': 'PropertyFilterSpec' },
propSet: properties.map(p => ({
attributes: { 'xsi:type': 'PropertySpec' },
type,
pathSet: [p],
})),
objectSet: [objectSpec],
}
result = await this.#exec('RetrievePropertiesEx', {
_this: propertyCollector,
specSet: [propertyFilterSpec],
options: { attributes: { type: 'RetrieveOptions' } },
})
const objects = {}
const returnObj = Array.isArray(result.returnval.objects) ? result.returnval.objects : [result.returnval.objects]
returnObj.forEach(({ obj, propSet }) => {
objects[obj.$value] = {}
propSet = Array.isArray(propSet) ? propSet : [propSet]
propSet.forEach(({ name, val }) => {
// don't care about the type for now
delete val.attributes
// a scalar value : simplify it
if (val.$value) {
objects[obj.$value][name] = val.$value
} else {
objects[obj.$value][name] = val
}
})
})
return objects
}
async #inspectVmdk(dataStores, currentDataStore, currentPath, filePath) {
let diskDataStore, diskPath
if (filePath.startsWith('/')) {
// disk is on another datastore
Object.keys(dataStores).forEach(dataStoreUrl => {
if (filePath.startsWith(dataStoreUrl)) {
diskDataStore = dataStores[dataStoreUrl].name
diskPath = filePath.substring(dataStoreUrl.length + 1)
}
})
} else {
diskDataStore = currentDataStore
diskPath = currentPath + '/' + filePath
}
const vmdkRes = await this.download(diskDataStore, diskPath)
const text = await vmdkRes.text()
const parsed = parseVmdk(text)
const { fileName, parentFileName, capacity } = parsed
return {
...parsed,
datastore: diskDataStore,
path: dirname(diskPath),
descriptionLabel: ' from esxi',
vhd: async () => {
if (fileName.endsWith('-flat.vmdk')) {
const vhd = await VhdEsxiRaw.open(this, diskDataStore, dirname(diskPath) + '/' + fileName)
await vhd.readBlockAllocationTable()
return vhd.stream()
}
// last snasphot only works when vm is powered off
const vhd = await VhdCowd.open(this, diskDataStore, dirname(diskPath) + '/' + fileName, parentFileName)
await vhd.readBlockAllocationTable()
return vhd.stream()
},
rawStream: async () => {
if (!fileName.endsWith('-flat.vmdk')) {
return
}
// @todo : only if vm is powered off
const stream = (await this.download(diskDataStore, dirname(diskPath) + '/' + fileName)).body
stream.length = capacity
return stream
},
}
}
async getTransferableVmMetadata(vmId) {
const search = await this.search('VirtualMachine', ['name', 'config', 'storage', 'runtime', 'snapshot'])
if (search[vmId] === undefined) {
throw new Error(`VM ${vmId} not found `)
}
const { config, runtime } = search[vmId]
const [, dataStore, vmxPath] = config.files.vmPathName.match(/^\[(.*)\] (.+.vmx)$/)
const res = await this.download(dataStore, vmxPath)
const vmx = parseVmx(await res.text())
// list datastores
const dataStores = {}
Object.values(await this.search('Datastore', ['summary'])).forEach(({ summary }) => {
dataStores[summary.url] = summary
})
const disks = []
for (let scsiIndex = 0; scsiIndex < MAX_SCSI; scsiIndex++) {
const scsiChannel = vmx[`scsi${scsiIndex}`]
if (scsiChannel === undefined) {
continue
}
for (const diskIndex in Object.values(scsiChannel)) {
const disk = scsiChannel[diskIndex]
if (typeof disk !== 'object' || disk.deviceType !== 'scsi-hardDisk') {
continue
}
disks.push({
...(await this.#inspectVmdk(dataStores, dataStore, dirname(vmxPath), disk.fileName)),
node: `scsi${scsiIndex}:${diskIndex}`,
})
}
}
const networks = []
for (let ethernetIndex = 0; ethernetIndex < MAX_ETHERNET; ethernetIndex++) {
const ethernet = vmx[`ethernet${ethernetIndex}`]
if (ethernet === undefined) {
continue
}
networks.push({
label: ethernet.networkName,
macAddress: ethernet.generatedAddress,
isGenerated: ethernet.addressType === 'generated',
})
}
const vmsd = await (await this.download(dataStore, vmxPath.replace('.vmx', '.vmsd'))).text()
let snapshots
if(vmsd){
snapshots = parseVmsd(vmsd)
for (const snapshotIndex in snapshots?.snapshots) {
const snapshot = snapshots.snapshots[snapshotIndex]
for (const diskIndex in snapshot.disks) {
const fileName = snapshot.disks[diskIndex].fileName
snapshot.disks[diskIndex] = {
node: snapshot.disks[diskIndex]?.node, // 'scsi0:0',
...(await this.#inspectVmdk(dataStores, dataStore, dirname(vmxPath), fileName)),
}
}
}
}
return {
name_label: config.name,
memory: parseInt(config.hardware.memoryMB) * 1024 * 1024,
numCpu: parseInt(config.hardware.numCPU),
guestToolsInstalled: false,
firmware: config.firmware, // bios or uefi
powerState: runtime.powerState,
snapshots,
disks: disks.map(({ fileName, rawDiskFileName, datastore, path, parentFileName, ...other }) => {
return {
...other,
vhd: async () => {
if (fileName.endsWith('-flat.vmdk')) {
const vhd = await VhdEsxiRaw.open(this, datastore, path + '/' + fileName)
await vhd.readBlockAllocationTable()
return vhd.stream()
}
// last snasphot only works when vm is powered off
const vhd = await VhdCowd.open(this, datastore, path + '/' + fileName, parentFileName)
await vhd.readBlockAllocationTable()
return vhd.stream()
},
rawStream: async () => {
if (fileName.endsWith('-flat.vmdk')) {
return
}
// @todo : only if vm is powered off
const stream = (await this.download(datastore, path + '/' + fileName)).body
stream.length = other.capacity
return stream
},
}
}),
networks,
}
}
}

View File

@@ -1,14 +0,0 @@
import Esxi from './esxi.mjs'
const host = '10.10.0.62'
const user = 'root'
const password = ''
const sslVerify = false
console.log(Esxi)
const esxi = new Esxi(host, user, password, sslVerify)
console.log(esxi)
esxi.on('ready', async function (){
const metadata = await esxi.getTransferableVmMetadata('4')
console.log('metadata', metadata)
})

View File

@@ -1,30 +0,0 @@
{
"license": "ISC",
"private": false,
"version": "0.0.2",
"name": "@xen-orchestra/vmware-explorer",
"dependencies": {
"@vates/read-chunk": "^1.0.1",
"lodash": "^4.17.21",
"node-fetch": "^3.3.0",
"node-vsphere-soap": "^0.0.2-5",
"vhd-lib": "^4.2.0"
},
"engines": {
"node": ">=18"
},
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/vmware-explorer",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/vmware-explorer",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"scripts": {
"postversion": "npm publish --access public"
}
}

View File

@@ -1,45 +0,0 @@
import { strictEqual } from 'node:assert'
// this file contains the disk metadata
export function parseDescriptor(text) {
const descriptorText = text.toString('ascii').replace(/\x00+$/, '') // eslint-disable-line no-control-regex
strictEqual(descriptorText.substr(0, 21), '# Disk DescriptorFile')
const descriptorDict = {}
const extentList = []
const lines = descriptorText.split(/\r?\n/).filter(line => {
return line.trim().length > 0 && line[0] !== '#'
})
for (const line of lines) {
const defLine = line.split('=')
// the wonky quote test is to avoid having an equal sign in the name of an extent
if (defLine.length === 2 && defLine[0].indexOf('"') === -1) {
descriptorDict[defLine[0].toLowerCase()] = defLine[1].replace(/['"]+/g, '')
} else {
const [, access, sizeSectors, type, name, offset] = line.match(/([A-Z]+) ([0-9]+) ([A-Z]+) "(.*)" ?(.*)$/)
extentList.push({ access, sizeSectors, type, name, offset })
}
}
strictEqual(extentList.length, 1, 'only one extent per vmdk is supported')
return { ...descriptorDict, extent: extentList[0] }
}
// https://github.com/libyal/libvmdk/blob/main/documentation/VMWare%20Virtual%20Disk%20Format%20(VMDK).asciidoc#5-the-cowd-sparse-extent-data-file
// vmdk file can be only a descriptor, or a
export default function parseVmdk(raw) {
strictEqual(typeof raw, 'string')
const descriptor = parseDescriptor(raw)
const isFull = !descriptor.parentfilenamehint
return {
capacity: descriptor.extent.sizeSectors * 512,
isFull,
uid: descriptor.cid,
fileName: descriptor.extent.name,
parentId: descriptor.parentcid,
parentFileName: descriptor.parentfilenamehint,
vmdFormat: descriptor.extent.type,
nameLabel: descriptor.extent.name,
}
}

View File

@@ -1,53 +0,0 @@
// these files contains the snapshot history of the VM
function set(obj, keyPath, val) {
const [key, ...other] = keyPath
const match = key.match(/^(.+)([0-9])$/)
if (match) {
// an array
let [, label, index] = match
label += 's'
if (!obj[label]) {
obj[label] = []
}
if (other.length) {
if (!obj[label][index]) {
obj[label][parseInt(index)] = {}
}
set(obj[label][index], other, val)
} else {
obj[label][index] = val
}
} else {
if (other.length) {
// an object
if (!obj[key]) {
// first time
obj[key] = {}
}
set(obj[key], other, val)
} else {
// a scalar
obj[key] = val
}
}
}
export default function parseVmsd(text) {
const parsed = {}
text.split('\n').forEach(line => {
const [key, val] = line.split(' = ')
if (!key.startsWith('snapshot')) {
return
}
set(parsed, key.split('.'), val?.substring(1, val.length - 1))
})
return {
lastUID: parsed.snapshot.current,
current: parsed.snapshot.current,
numSnapshots: parsed.snapshot.numSnapshots,
snapshots: Object.values(parsed.snapshots) || [],
}
}

View File

@@ -1,48 +0,0 @@
function set(obj, keyPath, val) {
let [key, ...other] = keyPath
if (key.includes(':')) {
// it's an array
let index
;[key, index] = key.split(':')
index = parseInt(index)
if (!obj[key]) {
// first time on this array
obj[key] = []
}
if (!other.length) {
// without descendant
obj[key][index] = val
} else {
// with descendant
if (!obj[key][index]) {
// first time on this descendant
obj[key][index] = {}
}
set(obj[key][index], other, val)
}
} else {
// it's an object
if (!other.length) {
// wihtout descendant
obj[key] = val
} else {
// with descendant
if (obj[key] === undefined) {
// first time
obj[key] = {}
}
set(obj[key], other, val)
}
}
}
// this file contains the vm configuration
export default function parseVmx(text) {
const vmx = {}
text.split('\n').forEach(line => {
const [key, val] = line.split(' = ')
set(vmx, key.split('.'), val?.substring(1, val.length - 1))
})
return vmx
}

View File

@@ -211,10 +211,9 @@ class Xapi extends Base {
})
if (timeout !== undefined) {
const error = new Error(`waitObjectState: timeout reached before ${refOrUuid} in expected state`)
timeoutHandle = setTimeout(() => {
stop()
reject(error)
reject(new Error(`waitObjectState: timeout reached before ${refOrUuid} in expected state`))
}, timeout)
}
})

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/xapi",
"version": "1.6.0",
"version": "1.5.3",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/xapi",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {

View File

@@ -7,6 +7,7 @@ const ignoreErrors = require('promise-toolbox/ignoreErrors')
const pickBy = require('lodash/pickBy.js')
const omit = require('lodash/omit.js')
const pCatch = require('promise-toolbox/catch')
const pRetry = require('promise-toolbox/retry')
const { asyncMap } = require('@xen-orchestra/async-map')
const { createLogger } = require('@xen-orchestra/log')
const { decorateClass } = require('@vates/decorate-with')
@@ -636,44 +637,92 @@ class Vm {
}
}
let ignoredVbds
if (ignoreNobakVdis) {
ignoredVbds = await listNobakVbds(this, vm.VBDs)
ignoreNobakVdis = ignoredVbds.length !== 0
}
const params = [cancelToken, 'VM.snapshot', vmRef, name_label ?? vm.name_label]
if (ignoreNobakVdis) {
params.push(ignoredVbds.map(_ => _.VDI))
}
let destroyNobakVdis = false
let result
try {
result = await this.callAsync(...params)
} catch (error) {
if (error.code !== 'MESSAGE_PARAMETER_COUNT_MISMATCH') {
throw error
if (ignoreNobakVdis) {
if (isHalted) {
await asyncMap(await listNobakVbds(this, vm.VBDs), async vbd => {
await this.VBD_destroy(vbd.$ref)
$defer.call(this, 'VBD_create', vbd)
})
} else {
// cannot unplug VBDs on Running, Paused and Suspended VMs
destroyNobakVdis = true
}
}
if (ignoreNobakVdis) {
if (isHalted) {
await asyncMap(ignoredVbds, async vbd => {
await this.VBD_destroy(vbd.$ref)
$defer.call(this, 'VBD_create', vbd)
if (name_label === undefined) {
name_label = vm.name_label
}
let ref
do {
if (!vm.tags.includes('xo-disable-quiesce')) {
try {
let { snapshots } = vm
ref = await pRetry(
async () => {
try {
return await this.callAsync(cancelToken, 'VM.snapshot_with_quiesce', vmRef, name_label)
} catch (error) {
if (error == null || error.code !== 'VM_SNAPSHOT_WITH_QUIESCE_FAILED') {
throw pRetry.bail(error)
}
// detect and remove new broken snapshots
//
// see https://github.com/vatesfr/xen-orchestra/issues/3936
const prevSnapshotRefs = new Set(snapshots)
const snapshotNameLabelPrefix = `Snapshot of ${vm.uuid} [`
snapshots = await this.getField('VM', vm.$ref, 'snapshots')
const createdSnapshots = (
await this.getRecords(
'VM',
snapshots.filter(_ => !prevSnapshotRefs.has(_))
)
).filter(_ => _.name_label.startsWith(snapshotNameLabelPrefix))
// be safe: only delete if there was a single match
if (createdSnapshots.length === 1) {
const snapshotRef = createdSnapshots[0]
this.VM_destroy(snapshotRef).catch(error => {
warn('VM_sapshot: failed to destroy broken snapshot', {
error,
snapshotRef,
vmRef,
})
})
}
throw error
}
},
{
delay: 60e3,
tries: 3,
}
).then(extractOpaqueRef)
this.call('VM.add_tags', ref, 'quiesce').catch(error => {
warn('VM_snapshot: failed to add quiesce tag', {
vmRef,
snapshotRef: ref,
error,
})
})
} else {
// cannot unplug VBDs on Running, Paused and Suspended VMs
destroyNobakVdis = true
break
} catch (error) {
const { code } = error
if (
// removed in CH 8.1
code !== 'MESSAGE_REMOVED' &&
code !== 'VM_SNAPSHOT_WITH_QUIESCE_NOT_SUPPORTED' &&
// quiesce only work on a running VM
code !== 'VM_BAD_POWER_STATE' &&
// quiesce failed, fallback on standard snapshot
// TODO: emit warning
code !== 'VM_SNAPSHOT_WITH_QUIESCE_FAILED'
) {
throw error
}
}
}
params.pop()
result = await this.callAsync(...params)
}
const ref = extractOpaqueRef(result)
ref = await this.callAsync(cancelToken, 'VM.snapshot', vmRef, name_label).then(extractOpaqueRef)
} while (false)
// detached async
this._httpHook(vm, '/post-sync').catch(noop)

View File

@@ -1,56 +1,9 @@
# ChangeLog
## **next**
### Enhancements
- [Snapshot] Use the new [`ignore_vdis` feature](https://github.com/xapi-project/xen-api/pull/4563) of XCP-ng/XenServer 8.3
- [Hub/Recipes/Kubernetes] Now use the [Flannel](https://github.com/flannel-io/flannel) Container Network Interface plugin to handle network
### Bug fixes
- [Nagios] Fix reporting, broken in 5.77.2
### Released packages
- @xen-orchestra/xapi 1.6.0
- @xen-orchestra/backups 0.29.4
- @xen-orchestra/proxy 0.26.9
- xo-server 5.107.5
- xo-web 5.109.0
## **5.77.2** (2022-12-12)
## **5.77.0** (2022-11-30)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Bug fixes
- [Backups] Fixes most of the _unexpected number of entries in backup cache_ errors
### Released packages
- @xen-orchestra/backups 0.29.3
- @xen-orchestra/proxy 0.26.7
- xo-server 5.107.3
## **5.77.1** (2022-12-07)
### Enhancements
- [Backups] Automatically detect, report and fix cache inconsistencies
### Bug fixes
- [Warm migration] Fix start and delete VMs after a warm migration [#6568](https://github.com/vatesfr/xen-orchestra/issues/6568)
### Released packages
- @xen-orchestra/backups 0.29.2
- @xen-orchestra/proxy 0.26.6
- xo-server 0.107.2
## **5.77.0** (2022-11-30)
### Highlights
- [Proxies] Ability to register an existing proxy (PR [#6556](https://github.com/vatesfr/xen-orchestra/pull/6556))

View File

@@ -15,7 +15,7 @@
> When modifying a package, add it here with its release type.
>
> The format is the following: `- $packageName $releaseType`
> The format is the following: - `$packageName` `$releaseType`
>
> Where `$releaseType` is
>
@@ -26,5 +26,4 @@
> Keep this list alphabetically ordered to avoid merge conflicts
<!--packages-start-->
<!--packages-end-->

View File

@@ -15,10 +15,8 @@ RUN /bin/bash -c "source $NVM_DIR/nvm.sh && nvm install $NODE_VERSION && nvm use
ENV NODE_PATH $NVM_DIR/versions/node/$NODE_VERSION/lib/node_modules
ENV PATH $NVM_DIR/versions/node/$NODE_VERSION/bin:$PATH
RUN npm install -g yarn
RUN npm install -g yarn
WORKDIR /xen-orchestra
# invalidate build on package change
COPY ./yarn.lock /xen-orchestra/yarn.lock
ENTRYPOINT yarn ci
ENTRYPOINT yarn ci

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -570,17 +570,6 @@ If your hosts are already in a pool you only need to add your pool master host t
Don't add pool slaves to your XOA server list! XOA will automatically find them from the master you add.
:::
### Remove a host from an existing pool
To remove one host from a pool, you can go to the "Advanced" tab of the host page for the host you wish to remove, and click on "Detach"
![](./assets/detach-host.png)
:::warning
- Detaching a host will remove all the VM disks stored on the Local Storage of this host, and reboot the host.
- The host you want to remove must be a slave, not the master!
:::
## Visualizations
Visualizations can help you to understand your XenServer infrastructure, as well as correlate events and detect bottlenecks.

View File

@@ -27,7 +27,6 @@
"promise-toolbox": "^0.21.0",
"semver": "^7.3.6",
"sorted-object": "^2.0.1",
"vue": "^2.7.14",
"vuepress": "^1.4.1"
},
"engines": {
@@ -82,15 +81,15 @@
"private": true,
"scripts": {
"build": "scripts/run-script.js --parallel --concurrency 2 build",
"ci": "yarn && yarn build && yarn test-lint && yarn test-integration",
"ci": "yarn && yarn build && yarn test && yarn test-integration",
"clean": "scripts/run-script.js --parallel clean",
"dev": "scripts/run-script.js --parallel --concurrency 0 --verbose dev",
"dev-test": "jest --bail --watch \"^(?!.*\\.integ\\.spec\\.js$)\"",
"docs:dev": "NODE_OPTIONS=--openssl-legacy-provider vuepress dev docs",
"docs:build": "NODE_OPTIONS=--openssl-legacy-provider vuepress build docs",
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs",
"prepare": "husky install",
"prettify": "prettier --ignore-path .gitignore --write '**/*.{cjs,js,jsx,md,mjs,ts,tsx}'",
"test": "npm run test-lint && npm run test-unit",
"test": "yarn run test-lint && yarn run test-unit",
"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"
@@ -98,6 +97,5 @@
"workspaces": [
"@*/*",
"packages/*"
],
"packageManager": "yarn@1.22.19"
]
}

View File

@@ -103,7 +103,7 @@ writeBlockConcurrency = 16
# This is a work-around.
#
# See https://github.com/vatesfr/xen-orchestra/pull/4674
maxMergedDeltasPerRun = inf
maxMergedDeltasPerRun = 2
# https://github.com/naugtur/blocked-at#params-and-return-value
[blockedAtOptions]
@@ -175,7 +175,7 @@ poolMarkingPrefix = 'xo:clientInfo:'
[xo-proxy]
callTimeout = '1 min'
channel = 'xo-proxy-appliance-latest'
channel = 'xo-proxy-appliance-{xoChannel}'
namespace = 'xoProxyAppliance'

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.107.5",
"version": "5.107.1",
"license": "AGPL-3.0-or-later",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -35,13 +35,13 @@
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.3",
"@vates/event-listeners-manager": "^1.0.1",
"@vates/multi-key-map": "^0.1.0",
"@vates/otp": "^1.0.0",
"@vates/multi-key-map": "^0.1.0",
"@vates/parse-duration": "^0.1.1",
"@vates/predicates": "^1.1.0",
"@vates/read-chunk": "^1.0.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.29.4",
"@xen-orchestra/backups": "^0.29.1",
"@xen-orchestra/cron": "^1.0.6",
"@xen-orchestra/defined": "^0.0.1",
"@xen-orchestra/emit-async": "^1.0.0",
@@ -51,8 +51,7 @@
"@xen-orchestra/mixins": "^0.8.2",
"@xen-orchestra/self-signed": "^0.1.3",
"@xen-orchestra/template": "^0.1.0",
"@xen-orchestra/vmware-explorer": "^0.0.2",
"@xen-orchestra/xapi": "^1.6.0",
"@xen-orchestra/xapi": "^1.5.3",
"ajv": "^8.0.3",
"app-conf": "^2.3.0",
"async-iterator-to-stream": "^1.0.1",
@@ -158,7 +157,7 @@
"dev": "cross-env NODE_ENV=development yarn run _build --watch",
"prepublishOnly": "yarn run build",
"start": "node dist/cli.mjs",
"pretest": "yarn run build",
"pretest": "id >&2; yarn run build",
"test": "tap 'dist/**/*.spec.mjs'"
},
"author": {

View File

@@ -5,11 +5,6 @@ import { createLogger } from '@xen-orchestra/log'
const { warn } = createLogger('xo:server:handleBackupLog')
async function sendToNagios(app, jobName, vmBackupInfo) {
if (app.sendPassiveCheck === undefined) {
// Nagios plugin is not loaded
return
}
try {
const messageToNagios = {
id: vmBackupInfo.id,

View File

@@ -596,8 +596,8 @@ migrate.resolve = {
migrationNetwork: ['migrationNetwork', 'network', 'administrate'],
}
export async function warmMigration({ vm, sr, startDestinationVm, deleteSourceVm }) {
await this.warmMigrateVm(vm, sr, startDestinationVm, deleteSourceVm)
export async function warmMigration({ vm, sr, startVm, deleteSource }) {
await this.warmMigrateVm(vm, sr, startVm, deleteSource)
}
warmMigration.permission = 'admin'
@@ -1298,25 +1298,6 @@ import_.resolve = {
export { import_ as import }
export async function importFomEsxi({host, user, password, sslVerify=true, sr, network, vm, thin=false}){
return await this.migrationfromEsxi({host, user, password, sslVerify, thin, vm, sr, network})
}
importFomEsxi.params = {
host: { type: 'string' },
network: { type: 'string' },
password: { type: 'string' },
user: { type: 'string' },
sr: { type: 'string' },
sslVerify: {type: 'boolean', optional: true},
vm:{type: 'string'},
thin:{type: 'boolean', optional: true}
}
// -------------------------------------------------------------------
// FIXME: if position is used, all other disks after this position

View File

@@ -1,10 +1,5 @@
import { Backup } from '@xen-orchestra/backups/Backup.js'
import { v4 as generateUuid } from 'uuid'
import Esxi from '@xen-orchestra/vmware-explorer/esxi.mjs'
import OTHER_CONFIG_TEMPLATE from '../xapi/other-config-template.mjs'
import asyncMapSettled from '@xen-orchestra/async-map/legacy.js'
import { fromEvent } from 'promise-toolbox'
import { VDI_FORMAT_RAW, VDI_FORMAT_VHD } from '@xen-orchestra/xapi'
export default class MigrateVm {
constructor(app) {
@@ -111,116 +106,4 @@ export default class MigrateVm {
}
}
}
async migrationfromEsxi({ host, user, password, sslVerify, sr: srId, network: networkId, vm: vmId, thin }) {
const esxi = new Esxi(host, user, password, sslVerify)
const app = this._app
const sr = app.getXapiObject(srId)
const xapi = sr.$xapi
await fromEvent(esxi, 'ready')
const esxiVmMetadata = await esxi.getTransferableVmMetadata(vmId)
const { memory, name_label, networks, numCpu } = esxiVmMetadata
const vm = await xapi._getOrWaitObject(
await xapi.VM_create({
...OTHER_CONFIG_TEMPLATE,
memory_dynamic_max: memory,
memory_dynamic_min: memory,
memory_static_max: memory,
memory_static_min: memory,
name_description: 'from esxi',
name_label,
VCPUs_at_startup: numCpu,
VCPUs_max: numCpu,
})
)
await Promise.all([
asyncMapSettled(['start', 'start_on'], op => vm.update_blocked_operations(op, 'OVA import in progress...')),
vm.set_name_label(`[Importing...] ${name_label}`),
])
const vifDevices = await xapi.call('VM.get_allowed_VIF_devices', vm.$ref)
await Promise.all(
networks.map((network, i) =>
xapi.VIF_create({
device: vifDevices[i],
network: xapi.getObject(networkId).$ref,
VM: vm.$ref,
})
)
)
// get the snapshot to migrate
const snapshots = esxiVmMetadata.snapshots
let chain =[]
if(snapshots && snapshots.current){
const currentSnapshotId = snapshots.current
let currentSnapshot = snapshots.snapshots.find(({ uid }) => uid === currentSnapshotId)
chain = [currentSnapshot.disks]
while ((currentSnapshot = snapshots.snapshots.find(({ uid }) => uid === currentSnapshot.parent))) {
chain.push(currentSnapshot.disks)
}
chain.reverse()
}
chain.push(esxiVmMetadata.disks)
const chainsByNodes = {}
chain.forEach(disks => {
disks.forEach(disk => {
chainsByNodes[disk.node] = chainsByNodes[disk.node] || []
chainsByNodes[disk.node].push(disk)
})
})
let userdevice = 0
for (const node in chainsByNodes) {
const chainByNode = chainsByNodes[node]
const vdi = await xapi._getOrWaitObject(
await xapi.VDI_create({
name_description: 'fromESXI' + chainByNode[0].descriptionLabel,
name_label: '[ESXI]' + chainByNode[0].nameLabel,
SR: sr.$ref,
virtual_size: chainByNode[0].capacity,
})
)
console.log('vdi created')
await xapi.VBD_create({
userdevice: String(userdevice),
VDI: vdi.$ref,
VM: vm.$ref,
})
console.log('vbd created')
for (const disk of chainByNode) {
// the first one is a RAW disk ( full )
console.log('will import ', { disk })
let format = VDI_FORMAT_VHD
let stream
if (!thin) {
stream = await disk.rawStream()
format = VDI_FORMAT_RAW
}
if (!stream) {
stream = await disk.vhd()
}
console.log('will import in format ', { disk, format })
await vdi.$importContent(stream, { format })
// for now we don't handle snapshots
break
}
userdevice ++
}
console.log('disks created')
// remove the importing in label
await vm.set_name_label(esxiVmMetadata.name_label)
// remove lock on start
await asyncMapSettled(['start', 'start_on'], op => vm.update_blocked_operations(op, null))
return vm.uuid
}
}

View File

@@ -58,7 +58,6 @@ export default class {
const handlers = this._handlers
for (const id in handlers) {
try {
delete handlers[id]
await handlers[id].forget()
} catch (_) {}
}
@@ -252,10 +251,8 @@ export default class {
}
async removeRemote(id) {
const handlers = this._handlers
const handler = handlers[id]
const handler = this._handlers[id]
if (handler !== undefined) {
delete handlers[id]
ignoreErrors.call(handler.forget())
}

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-web",
"version": "5.109.0",
"version": "5.108.0",
"license": "AGPL-3.0-or-later",
"description": "Web interface client for Xen-Orchestra",
"keywords": [

View File

@@ -5671,6 +5671,9 @@ export default {
// Original text: 'SSH key'
recipeSshKeyLabel: 'Chiave SSH',
// Original text: 'Network CIDR'
recipeNetworkCidr: 'Rete CIDR',
// Original text: 'Action/Event'
auditActionEvent: 'Azione/Evento',

View File

@@ -2392,6 +2392,7 @@ const messages = {
recipeMasterNameLabel: 'Master name',
recipeNumberOfNodesLabel: 'Number of nodes',
recipeSshKeyLabel: 'SSH key',
recipeNetworkCidr: 'Network CIDR',
// Audit
auditActionEvent: 'Action/Event',

View File

@@ -112,6 +112,18 @@ export default decorate([
value={value.sshKey}
/>
</FormGrid.Row>
<FormGrid.Row>
<label>{_('recipeNetworkCidr')}</label>
<input
className='form-control'
name='cidr'
onChange={effects.onChangeValue}
placeholder={formatMessage(messages.recipeNetworkCidr)}
required
type='text'
value={value.cidr}
/>
</FormGrid.Row>
</Container>
),
])

View File

@@ -51,10 +51,11 @@ export default decorate([
size: 'medium',
})
const { masterName, nbNodes, network, sr, sshKey } = recipeParams
const { cidr, masterName, nbNodes, network, sr, sshKey } = recipeParams
markRecipeAsCreating(RECIPE_INFO.id)
const tag = await createKubernetesCluster({
cidr,
masterName,
nbNodes: +nbNodes,
network: network.id,

View File

@@ -9,7 +9,6 @@ const escapeRegExp = require('lodash/escapeRegExp')
const invert = require('lodash/invert')
const keyBy = require('lodash/keyBy')
const { debug } = require('../@xen-orchestra/log').createLogger('gen-deps-list')
const computeDepOrder = require('./_computeDepOrder.js')
const changelogConfig = {
@@ -31,8 +30,6 @@ async function main(args, scriptName) {
const testMode = args[0] === '--test'
if (testMode) {
debug('reading packages from CLI')
args.shift()
for (const arg of args) {
@@ -94,8 +91,6 @@ async function main(args, scriptName) {
}
async function readPackagesFromChangelog(toRelease) {
debug('reading packages from CHANGELOG.unreleased.md')
const content = await fs.readFile(changelogConfig.path)
const changelogRegex = new RegExp(
`${escapeRegExp(changelogConfig.startTag)}(.*)${escapeRegExp(changelogConfig.endTag)}`,
@@ -140,20 +135,8 @@ function handlePackageDependencies(packageName, packageNextVersion) {
shouldPackageBeReleased(name, { ...dependencies, ...optionalDependencies }, packageName, packageNextVersion)
) {
releaseWeight = RELEASE_WEIGHT.PATCH
debug('new compatible release due to dependency update', {
package: name,
dependency: packageName,
version: getNextVersion(version, releaseWeight),
})
} else if (shouldPackageBeReleased(name, peerDependencies || {}, packageName, packageNextVersion)) {
releaseWeight = versionToReleaseWeight(version)
debug('new breaking release due to peer dependency update', {
package: name,
peerDependency: packageName,
version: getNextVersion(version, releaseWeight),
})
}
if (releaseWeight !== undefined) {
@@ -181,10 +164,6 @@ function shouldPackageBeReleased(name, dependencies, depName, depVersion) {
}
if (['xo-web', 'xo-server', '@xen-orchestra/proxy'].includes(name)) {
debug('forced release due to dependency update', {
package: name,
dependency: depName,
})
return true
}

View File

@@ -7128,11 +7128,6 @@ data-uri-to-buffer@3:
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636"
integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==
data-uri-to-buffer@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b"
integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==
dateformat@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062"
@@ -9128,14 +9123,6 @@ feature-policy@0.3.0:
resolved "https://registry.yarnpkg.com/feature-policy/-/feature-policy-0.3.0.tgz#7430e8e54a40da01156ca30aaec1a381ce536069"
integrity sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ==
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
version "3.2.0"
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9"
integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==
dependencies:
node-domexception "^1.0.0"
web-streams-polyfill "^3.0.3"
fifolock@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fifolock/-/fifolock-1.0.0.tgz#a37e54f3ebe69d13480d95a82abc42b7a5c1792d"
@@ -9323,11 +9310,6 @@ fined@^1.0.1:
object.pick "^1.2.0"
parse-filepath "^1.0.1"
first-chunk-stream@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-0.1.0.tgz#755d3ec14d49a86e3d2fcc08beead5c0ca2b9c0a"
integrity sha512-o7kVqimu9cl+XNeEGqDPI8Ms4IViicBnjIDZ5uU+7aegfDhJJiU1Da9y52Qt0TfBO3rpKA5hW2cqwp4EkCfl9w==
first-chunk-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz#1bdecdb8e083c0664b91945581577a43a9f31d70"
@@ -9431,13 +9413,6 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
formdata-polyfill@^4.0.10:
version "4.0.10"
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
dependencies:
fetch-blob "^3.1.2"
forwarded@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
@@ -13061,21 +13036,11 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
lodash@3.x.x:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
integrity sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ==
lodash@^4.13.1, lodash@^4.15.0, lodash@^4.16.2, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.6.1:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lodash@~2.4.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-2.4.2.tgz#fadd834b9683073da179b3eae6d9c0d15053f73e"
integrity sha512-Kak1hi6/hYHGVPmdyiZijoQyz5x2iGVzs6w9GYB/HiXEtylY7tIoYEROMjvM1d9nXJqPOrG2MNPMn01bJ+S0Rw==
log-symbols@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
@@ -13912,11 +13877,6 @@ node-addon-api@^5.0.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.0.0.tgz#7d7e6f9ef89043befdb20c1989c905ebde18c501"
integrity sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==
node-domexception@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
node-fetch@^1.0.1:
version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
@@ -13932,15 +13892,6 @@ node-fetch@^2.6.7:
dependencies:
whatwg-url "^5.0.0"
node-fetch@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.0.tgz#37e71db4ecc257057af828d523a7243d651d91e4"
integrity sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==
dependencies:
data-uri-to-buffer "^4.0.0"
fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10"
node-forge@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
@@ -14016,15 +13967,6 @@ node-version@^1.0.0:
resolved "https://registry.yarnpkg.com/node-version/-/node-version-1.2.0.tgz#34fde3ffa8e1149bd323983479dda620e1b5060d"
integrity sha512-ma6oU4Sk0qOoKEAymVoTvk8EdXEobdS7m/mAGhDJ8Rouugho48crHBORAmy5BoOcv8wraPM6xumapQp5hl4iIQ==
node-vsphere-soap@^0.0.2-5:
version "0.0.2-5"
resolved "https://registry.yarnpkg.com/node-vsphere-soap/-/node-vsphere-soap-0.0.2-5.tgz#e055a17d23452276b0755949b163e16b4214755c"
integrity sha512-FC0QHZMV1QWuCPKdUmYWAX2yVnHNybEGblKOwkFow6DS6xAejIAqRn+hd5imK+VLt2yBT+0XprP44zl2+eTfzw==
dependencies:
lodash "3.x.x"
soap "0.8.0"
soap-cookie "0.10.x"
node-xmpp-client@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/node-xmpp-client/-/node-xmpp-client-3.2.0.tgz#af4527df0cc5abd2690cba2139cc1ecdc81ea189"
@@ -16939,7 +16881,7 @@ replace-homedir@^1.0.0:
is-absolute "^1.0.0"
remove-trailing-separator "^1.1.0"
request@>=2.9.0, request@^2.65.0, request@^2.74.0, request@^2.87.0:
request@^2.65.0, request@^2.74.0, request@^2.87.0:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@@ -17259,7 +17201,7 @@ sass@^1.38.1:
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
sax@1.2.x, sax@>=0.6, sax@>=0.6.0, sax@~1.2.4:
sax@1.2.x, sax@>=0.6.0, sax@~1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
@@ -17652,21 +17594,6 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
soap-cookie@0.10.x:
version "0.10.1"
resolved "https://registry.yarnpkg.com/soap-cookie/-/soap-cookie-0.10.1.tgz#7c7e62f3779b3e42e6b584d35ecabee92121a23f"
integrity sha512-lG3/Vozl7otPEFbEWLIDOyArDMAUZBJhMQBXW/L5cfGh88GMBtBItOw28zcMLO0o6Y7RC2vkUtZ7CWauTv0a7w==
soap@0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/soap/-/soap-0.8.0.tgz#ab2766a7515fa5069f264a094e087e3fe74e2a78"
integrity sha512-rQpzOrol1pQpvhn9CdxDJg/D0scOwlr+DcdMFMAm/Q1cWbjaYKEMCl1dcfW5JjMFdf5esKUqGplDPEEB0Z2RZA==
dependencies:
lodash "~2.4.1"
request ">=2.9.0"
sax ">=0.6"
strip-bom "~0.3.1"
sockjs-client@^1.5.0:
version "1.6.1"
resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.6.1.tgz#350b8eda42d6d52ddc030c39943364c11dcad806"
@@ -18250,14 +18177,6 @@ strip-bom@^4.0.0:
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
strip-bom@~0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-0.3.1.tgz#9e8a39eff456ff9abc2f059f5f2225bb0f3f7ca5"
integrity sha512-8m24eJUyKXllSCydAwFVbr4QRZrRb82T2QfwtbO9gTLWhWIOxoDEZESzCGMgperFNyLhly6SDOs+LPH6/seBfw==
dependencies:
first-chunk-stream "^0.1.0"
is-utf8 "^0.2.0"
strip-eof@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
@@ -19721,7 +19640,7 @@ vue-tsc@^1.0.9:
"@volar/vue-language-core" "1.0.9"
"@volar/vue-typescript" "1.0.9"
vue@^2.6.10, vue@^2.7.14:
vue@^2.6.10:
version "2.7.14"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.14.tgz#3743dcd248fd3a34d421ae456b864a0246bafb17"
integrity sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==
@@ -19854,11 +19773,6 @@ wcwidth@^1.0.1:
dependencies:
defaults "^1.0.3"
web-streams-polyfill@^3.0.3:
version "3.2.1"
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"