Compare commits

...

22 Commits

Author SHA1 Message Date
Florent Beauchamp
0957b5b6b1 doc 2022-11-22 09:23:00 +01:00
Florent Beauchamp
33b758d0b2 feat(vhd-cli): implement deeper checks for vhd 2022-11-22 09:09:47 +01:00
Julien Fontanet
8291124c1f feat(xo-server/remote.{create,set}): prevent xo-vm-backups suffix
Fixes zammad#10930
2022-11-21 16:58:24 +01:00
Julien Fontanet
fc4d9accfd feat(mixin): add usage 2022-11-21 11:04:51 +01:00
Julien Fontanet
80969b785f feat(xo-server/proxy.register): authenticationToken is now optional
It's automatically generated if missing, which can be useful when manually registering a proxy.
2022-11-20 23:51:48 +01:00
Julien Fontanet
3dfd7f1835 fix(xo-server/proxy.register): requires either address or vmUuid 2022-11-20 23:50:51 +01:00
Julien Fontanet
65daa39ebe fix(xo-cli): fix invalid parameters error message
Introduced by d7f29e736

The error format has changed due to the switch of xo-server to Ajv.
2022-11-20 23:44:50 +01:00
Julien Fontanet
5ad94504e3 feat(xo-web/downloadLog): use .json extension for JSON values 2022-11-20 23:20:01 +01:00
Julien Fontanet
4101bf3ba5 fix(xo-web): injected task.parent should not be enumerable
Shared task objects are direclty altered and adding an enumerable cyclic property might break JSON.stringify in other components.
2022-11-20 23:19:35 +01:00
Thierry Goettelmann
e9d52864ef fix(lite): remove @trivago/prettier-plugin-sort-imports package breaking monorepo (#6531) 2022-11-18 11:32:27 +01:00
Julien Fontanet
aef2696426 feat(log): respect env.{DEBUG,NODE_DEBUG} by default
Previously, env.{DEBUG,NODE_DEBUG} were only handled if `log/configure` has been imported, now it's the case by default.
2022-11-18 10:42:13 +01:00
Julien Fontanet
94c755b102 fix(backups-cli/clean-vms): use getSyncedHandler 2022-11-18 10:42:13 +01:00
Gabriel Gunullu
279b457348 test(xo-remote-parser): from Jest to test (#6537) 2022-11-17 14:35:01 +01:00
Julien Fontanet
b5988bb8b7 chore(backups-cli): convert to ESM 2022-11-17 10:44:48 +01:00
Mathieu
f73b1d8b40 feat(lite): add loader in pool dashboard (#6468) 2022-11-17 10:15:03 +01:00
Gabriel Gunullu
b2ccb07a95 test(complex-matcher): from Jest to test (#6535) 2022-11-16 23:24:32 +01:00
Thierry Goettelmann
9560cc4e33 chore(lite): upgrade packages (#6532) 2022-11-16 11:18:04 +01:00
Julien Fontanet
e87c380556 chore: update dev deps 2022-11-15 15:16:29 +01:00
Julien Fontanet
b0846876f7 feat: release 5.76.2 2022-11-14 15:55:02 +01:00
Julien Fontanet
477ed67957 feat(xo-server): 5.106.1 2022-11-14 14:52:01 +01:00
Thierry Goettelmann
5acacd7e1e feat(lite): add merge prop to UiButtonGroup (#6494) 2022-11-14 11:08:26 +01:00
Thierry Goettelmann
8d542fe9c0 fix(lite): UiButton should follow UiButtonGroup transparent prop (#6493) 2022-11-14 11:06:54 +01:00
53 changed files with 2556 additions and 2477 deletions

View File

@@ -1,11 +1,10 @@
'use strict'
import { readFileSync } from 'fs'
import getopts from 'getopts'
const getopts = require('getopts')
const { version } = JSON.parse(readFileSync(new URL('package.json', import.meta.url)))
const { version } = require('./package.json')
module.exports = commands =>
async function (args, prefix) {
export function composeCommands(commands) {
return async function (args, prefix) {
const opts = getopts(args, {
alias: {
help: 'h',
@@ -30,5 +29,6 @@ xo-backups v${version}
return
}
return command.main(args.slice(1), prefix + ' ' + commandName)
return (await command.default)(args.slice(1), prefix + ' ' + commandName)
}
}

View File

@@ -1,11 +1,9 @@
'use strict'
import fs from 'fs/promises'
import { dirname } from 'path'
const { dirname } = require('path')
export * from 'fs/promises'
const fs = require('promise-toolbox/promisifyAll')(require('fs'))
module.exports = fs
fs.getSize = path =>
export const getSize = path =>
fs.stat(path).then(
_ => _.size,
error => {
@@ -16,7 +14,7 @@ fs.getSize = path =>
}
)
fs.mktree = async function mkdirp(path) {
export async function mktree(path) {
try {
await fs.mkdir(path)
} catch (error) {
@@ -26,8 +24,8 @@ fs.mktree = async function mkdirp(path) {
return
}
if (code === 'ENOENT') {
await mkdirp(dirname(path))
return mkdirp(path)
await mktree(dirname(path))
return mktree(path)
}
throw error
}
@@ -37,7 +35,7 @@ fs.mktree = async function mkdirp(path) {
// - single param for direct use in `Array#map`
// - files are prefixed with directory path
// - safer: returns empty array if path is missing or not a directory
fs.readdir2 = path =>
export const readdir2 = path =>
fs.readdir(path).then(
entries => {
entries.forEach((entry, i) => {
@@ -59,7 +57,7 @@ fs.readdir2 = path =>
}
)
fs.symlink2 = async (target, path) => {
export async function symlink2(target, path) {
try {
await fs.symlink(target, path)
} catch (error) {

View File

@@ -1,40 +0,0 @@
'use strict'
// -----------------------------------------------------------------------------
const asyncMap = require('lodash/curryRight')(require('@xen-orchestra/async-map').asyncMap)
const getopts = require('getopts')
const { RemoteAdapter } = require('@xen-orchestra/backups/RemoteAdapter')
const { resolve } = require('path')
const adapter = new RemoteAdapter(require('@xen-orchestra/fs').getHandler({ url: 'file://' }))
module.exports = async function main(args) {
const { _, fix, remove, merge } = getopts(args, {
alias: {
fix: 'f',
remove: 'r',
merge: 'm',
},
boolean: ['fix', 'merge', 'remove'],
default: {
merge: false,
remove: false,
},
})
await asyncMap(_, async vmDir => {
vmDir = resolve(vmDir)
try {
await adapter.cleanVm(vmDir, {
fixMetadata: fix,
remove,
merge,
logInfo: (...args) => console.log(...args),
logWarn: (...args) => console.warn(...args),
})
} catch (error) {
console.error('adapter.cleanVm', vmDir, error)
}
})
}

View File

@@ -0,0 +1,39 @@
import { asyncMap } from '@xen-orchestra/async-map'
import { RemoteAdapter } from '@xen-orchestra/backups/RemoteAdapter.js'
import { getSyncedHandler } from '@xen-orchestra/fs'
import getopts from 'getopts'
import { basename, dirname } from 'path'
import Disposable from 'promise-toolbox/Disposable'
import { pathToFileURL } from 'url'
export default async function cleanVms(args) {
const { _, fix, remove, merge } = getopts(args, {
alias: {
fix: 'f',
remove: 'r',
merge: 'm',
},
boolean: ['fix', 'merge', 'remove'],
default: {
merge: false,
remove: false,
},
})
await asyncMap(_, vmDir =>
Disposable.use(getSyncedHandler({ url: pathToFileURL(dirname(vmDir)).href }), async handler => {
console.log(handler, basename(vmDir))
try {
await new RemoteAdapter(handler).cleanVm(basename(vmDir), {
fixMetadata: fix,
remove,
merge,
logInfo: (...args) => console.log(...args),
logWarn: (...args) => console.warn(...args),
})
} catch (error) {
console.error('adapter.cleanVm', vmDir, error)
}
})
)
}

View File

@@ -1,13 +1,10 @@
'use strict'
import { mktree, readdir2, readFile, symlink2 } from '../_fs.mjs'
import { asyncMap } from '@xen-orchestra/async-map'
import filenamify from 'filenamify'
import get from 'lodash/get.js'
import { dirname, join, relative } from 'path'
const filenamify = require('filenamify')
const get = require('lodash/get')
const { asyncMap } = require('@xen-orchestra/async-map')
const { dirname, join, relative } = require('path')
const { mktree, readdir2, readFile, symlink2 } = require('../_fs')
module.exports = async function createSymlinkIndex([backupDir, fieldPath]) {
export default async function createSymlinkIndex([backupDir, fieldPath]) {
const indexDir = join(backupDir, 'indexes', filenamify(fieldPath))
await mktree(indexDir)

View File

@@ -1,16 +1,13 @@
'use strict'
const groupBy = require('lodash/groupBy')
const { asyncMap } = require('@xen-orchestra/async-map')
const { createHash } = require('crypto')
const { dirname, resolve } = require('path')
const { readdir2, readFile, getSize } = require('../_fs')
import { readdir2, readFile, getSize } from '../_fs.mjs'
import { asyncMap } from '@xen-orchestra/async-map'
import { createHash } from 'crypto'
import groupBy from 'lodash/groupBy.js'
import { dirname, resolve } from 'path'
const sha512 = str => createHash('sha512').update(str).digest('hex')
const sum = values => values.reduce((a, b) => a + b)
module.exports = async function info(vmDirs) {
export default async function info(vmDirs) {
const jsonFiles = (
await asyncMap(vmDirs, async vmDir => (await readdir2(vmDir)).filter(_ => _.endsWith('.json')))
).flat()

View File

@@ -1,11 +1,12 @@
#!/usr/bin/env node
import { composeCommands } from './_composeCommands.mjs'
'use strict'
const importDefault = async path => (await import(path)).default
require('./_composeCommands')({
composeCommands({
'clean-vms': {
get main() {
return require('./commands/clean-vms')
get default() {
return importDefault('./commands/clean-vms.mjs')
},
usage: `[--fix] [--merge] [--remove] xo-vm-backups/*
@@ -18,14 +19,14 @@ require('./_composeCommands')({
`,
},
'create-symlink-index': {
get main() {
return require('./commands/create-symlink-index')
get default() {
return importDefault('./commands/create-symlink-index.mjs')
},
usage: 'xo-vm-backups <field path>',
},
info: {
get main() {
return require('./commands/info')
get default() {
return importDefault('./commands/info.mjs')
},
usage: 'xo-vm-backups/*',
},

View File

@@ -1,7 +1,7 @@
{
"private": false,
"bin": {
"xo-backups": "index.js"
"xo-backups": "index.mjs"
},
"preferGlobal": true,
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
@@ -12,10 +12,10 @@
"filenamify": "^4.1.0",
"getopts": "^2.2.5",
"lodash": "^4.17.15",
"promise-toolbox": "^0.21.0"
"promise-toolbox":"^0.21.0"
},
"engines": {
"node": ">=7.10.1"
"node": ">=14"
},
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/backups-cli",
"name": "@xen-orchestra/backups-cli",

View File

@@ -91,6 +91,7 @@ export default class RemoteHandlerAbstract {
const sharedLimit = limitConcurrency(options.maxParallelOperations ?? DEFAULT_MAX_PARALLEL_OPERATIONS)
this.closeFile = sharedLimit(this.closeFile)
this.copy = sharedLimit(this.copy)
this.exists = sharedLimit(this.exists)
this.getInfo = sharedLimit(this.getInfo)
this.getSize = sharedLimit(this.getSize)
this.list = sharedLimit(this.list)
@@ -314,6 +315,14 @@ export default class RemoteHandlerAbstract {
await this._rmtree(normalizePath(dir))
}
async _exists(file){
throw new Error('not implemented')
}
async exists(file){
return this._exists(normalizePath(file))
}
// Asks the handler to sync the state of the effective remote with its'
// metadata
//

View File

@@ -198,4 +198,9 @@ export default class LocalHandler extends RemoteHandlerAbstract {
_writeFile(file, data, { flags }) {
return this._addSyncStackTrace(fs.writeFile, this._getFilePath(file), data, { flag: flags })
}
async _exists(file){
const exists = await fs.pathExists(this._getFilePath(file))
return exists
}
}

View File

@@ -537,4 +537,17 @@ export default class S3Handler extends RemoteHandlerAbstract {
useVhdDirectory() {
return true
}
async _exists(file){
try{
await this._s3.send(new HeadObjectCommand(this._createParams(file)))
return true
}catch(error){
// normalize this error code
if (error.name === 'NoSuchKey') {
return false
}
throw error
}
}
}

View File

@@ -1,15 +1,2 @@
module.exports = {
importOrder: [
"^[^/]+$",
"<THIRD_PARTY_MODULES>",
"^@/components/(.*)$",
"^@/composables/(.*)$",
"^@/libs/(.*)$",
"^@/router/(.*)$",
"^@/stores/(.*)$",
"^@/views/(.*)$",
],
importOrderSeparation: false,
importOrderSortSpecifiers: true,
importOrderParserPlugins: ["typescript", "decorators-legacy"],
};
// Keeping this file to prevent applying the global monorepo config for now
module.exports = {};

View File

@@ -18,7 +18,7 @@
"@novnc/novnc": "^1.3.0",
"@types/d3-time-format": "^4.0.0",
"@types/lodash-es": "^4.17.6",
"@vueuse/core": "^8.7.5",
"@vueuse/core": "^9.5.0",
"complex-matcher": "^0.7.0",
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",
@@ -40,18 +40,18 @@
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@limegrass/eslint-plugin-import-alias": "^1.0.5",
"@rushstack/eslint-patch": "^1.1.0",
"@trivago/prettier-plugin-sort-imports": "^3.2.0",
"@types/node": "^16.11.41",
"@vitejs/plugin-vue": "^2.3.3",
"@vitejs/plugin-vue": "^3.2.0",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.1.3",
"eslint-plugin-vue": "^9.0.0",
"npm-run-all": "^4.1.5",
"postcss-nested": "^5.0.6",
"typescript": "~4.7.4",
"vite": "^2.9.12",
"vue-tsc": "^0.38.1"
"postcss": "^8.4.19",
"postcss-nested": "^6.0.0",
"typescript": "^4.9.3",
"vite": "^3.2.4",
"vue-tsc": "^1.0.9"
},
"private": true,
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/lite",

View File

@@ -1,24 +1,28 @@
<template>
<div v-if="data.length !== 0">
<div>
<div class="header">
<slot name="header" />
</div>
<ProgressBar
v-for="item in computedData.sortedArray"
:key="item.id"
:value="item.value"
:label="item.label"
:badge-label="item.badgeLabel"
/>
<div class="footer">
<slot name="footer" :total-percent="computedData.totalPercentUsage" />
</div>
<template v-if="data !== undefined">
<ProgressBar
v-for="item in computedData.sortedArray"
:key="item.id"
:value="item.value"
:label="item.label"
:badge-label="item.badgeLabel"
/>
<div class="footer">
<slot name="footer" :total-percent="computedData.totalPercentUsage" />
</div>
</template>
<UiSpinner v-else class="spinner" />
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import ProgressBar from "@/components/ProgressBar.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
interface Data {
id: string;
@@ -29,7 +33,7 @@ interface Data {
}
interface Props {
data: Array<Data>;
data?: Array<Data>;
nItems?: number;
}
@@ -40,7 +44,7 @@ const computedData = computed(() => {
let totalPercentUsage = 0;
return {
sortedArray: _data
.map((item) => {
?.map((item) => {
const value = Math.round((item.value / (item.maxValue ?? 100)) * 100);
totalPercentUsage += value;
return {
@@ -72,6 +76,14 @@ const computedData = computed(() => {
font-size: 14px;
color: var(--color-blue-scale-300);
}
.spinner {
color: var(--color-extra-blue-base);
display: flex;
margin: auto;
width: 40px;
height: 40px;
}
</style>
<style>

View File

@@ -14,6 +14,7 @@
:label="$t('vms')"
/>
</template>
<UiSpinner v-else class="spinner" />
</UiCard>
</template>
@@ -22,6 +23,7 @@ import { computed } from "vue";
import PoolDashboardStatusItem from "@/components/pool/dashboard/PoolDashboardStatusItem.vue";
import UiCard from "@/components/ui/UiCard.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";
@@ -45,3 +47,13 @@ const activeVmsCount = computed(() => {
).length;
});
</script>
<style lang="postcss" scoped>
.spinner {
color: var(--color-extra-blue-base);
display: flex;
margin: auto;
width: 40px;
height: 40px;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<UiCard>
<UiTitle type="h4">{{ $t("storage-usage") }}</UiTitle>
<UsageBar :data="data.result" :nItems="5">
<UsageBar :data="srStore.isReady ? data.result : undefined" :nItems="5">
<template #header>
<span>{{ $t("storage") }}</span>
<span>{{ $t("top-#", { n: 5 }) }}</span>

View File

@@ -1,5 +1,5 @@
<template>
<UsageBar :data="data" :n-items="5">
<UsageBar :data="statFetched ? data : undefined" :n-items="5">
<template #header>
<span>{{ $t("hosts") }}</span>
<span>{{ $t("top-#", { n: 5 }) }}</span>
@@ -42,4 +42,10 @@ const data = computed<{ id: string; label: string; value: number }[]>(() => {
return result;
});
const statFetched: ComputedRef<boolean> = computed(() =>
statFetched.value
? true
: stats.value.length > 0 && stats.value.length === data.value.length
);
</script>

View File

@@ -1,5 +1,5 @@
<template>
<UsageBar :data="data" :n-items="5">
<UsageBar :data="statFetched ? data : undefined" :n-items="5">
<template #header>
<span>{{ $t("vms") }}</span>
<span>{{ $t("top-#", { n: 5 }) }}</span>
@@ -42,4 +42,10 @@ const data = computed<{ id: string; label: string; value: number }[]>(() => {
return result;
});
const statFetched: ComputedRef<boolean> = computed(() =>
statFetched.value
? true
: stats.value.length > 0 && stats.value.length === data.value.length
);
</script>

View File

@@ -30,7 +30,12 @@ const props = withDefaults(
transparent?: boolean;
active?: boolean;
}>(),
{ busy: undefined, disabled: undefined, outlined: undefined }
{
busy: undefined,
disabled: undefined,
outlined: undefined,
transparent: undefined,
}
);
const isGroupBusy = inject("isButtonGroupBusy", false);

View File

@@ -1,5 +1,5 @@
<template>
<div class="ui-button-group">
<div :class="{ merge }" class="ui-button-group">
<slot />
</div>
</template>
@@ -14,6 +14,7 @@ const props = defineProps<{
color?: Color;
outlined?: boolean;
transparent?: boolean;
merge?: boolean;
}>();
provide(
"isButtonGroupBusy",
@@ -40,8 +41,32 @@ provide(
<style lang="postcss" scoped>
.ui-button-group {
display: flex;
justify-content: left;
align-items: center;
justify-content: left;
gap: 1rem;
&.merge {
gap: 0;
:slotted(.ui-button) {
&:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
&.outlined {
border-left: none;
}
}
&:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
&.outlined {
border-right: none;
}
}
}
}
}
</style>

View File

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

View File

@@ -1,15 +1,14 @@
import { defineStore } from "pinia";
import { computed } from "vue";
import { sortRecordsByNameLabel } from "@/libs/utils";
import type { GRANULARITY } from "@/libs/xapi-stats";
import type { XenApiVm } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store";
import { createRecordContext } from "@/stores/index";
import { useXenApiStore } from "@/stores/xen-api.store";
import { defineStore } from "pinia";
import { computed } from "vue";
export const useVmStore = defineStore("vm", () => {
const hostStore = useHostStore();
const xapiStats = useXenApiStore().getXapiStats();
const baseVmContext = createRecordContext<XenApiVm>("VM", {
filter: (vm) =>
!vm.is_a_snapshot && !vm.is_a_template && !vm.is_control_domain,
@@ -42,7 +41,7 @@ export const useVmStore = defineStore("vm", () => {
throw new Error(`VM ${id} is halted or host could not be found.`);
}
return xapiStats._getAndUpdateStats({
return useXenApiStore().getXapiStats()._getAndUpdateStats({
host,
uuid: vm.uuid,
granularity,

View File

@@ -3,7 +3,10 @@
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"experimentalDecorators": true,
"lib": ["ES2019"],
"lib": [
"ES2019",
"dom"
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]

View File

@@ -1,10 +1,14 @@
import { URL, fileURLToPath } from "url";
import { defineConfig } from "vite";
import vueI18n from "@intlify/vite-plugin-vue-i18n";
import vue from "@vitejs/plugin-vue";
import { fileURLToPath, URL } from "url";
import { defineConfig } from "vite";
// https://vitejs.dev/config/
export default defineConfig({
server: {
host: "127.0.0.1",
port: 3000,
},
plugins: [vue(), vueI18n()],
define: {
XO_LITE_VERSION: JSON.stringify(process.env.npm_package_version),

View File

@@ -0,0 +1,33 @@
'use strict'
const escapeRegExp = require('lodash/escapeRegExp')
const compileGlobPatternFragment = pattern => pattern.split('*').map(escapeRegExp).join('.*')
module.exports = function compileGlobPattern(pattern) {
const no = []
const yes = []
pattern.split(/[\s,]+/).forEach(pattern => {
if (pattern[0] === '-') {
no.push(pattern.slice(1))
} else {
yes.push(pattern)
}
})
const raw = ['^']
if (no.length !== 0) {
raw.push('(?!', no.map(compileGlobPatternFragment).join('|'), ')')
}
if (yes.length !== 0) {
raw.push('(?:', yes.map(compileGlobPatternFragment).join('|'), ')')
} else {
raw.push('.*')
}
raw.push('$')
return new RegExp(raw.join(''))
}

View File

@@ -1,8 +1,8 @@
'use strict'
const compileGlobPattern = require('./_compileGlobPattern.js')
const createConsoleTransport = require('./transports/console')
const { LEVELS, resolve } = require('./levels')
const { compileGlobPattern } = require('./utils')
// ===================================================================

View File

@@ -1,5 +1,6 @@
'use strict'
const compileGlobPattern = require('./_compileGlobPattern.js')
const createTransport = require('./transports/console')
const { LEVELS, resolve } = require('./levels')
@@ -8,8 +9,19 @@ if (!(symbol in global)) {
// the default behavior, without requiring `configure` is to avoid
// logging anything unless it's a real error
const transport = createTransport()
const level = resolve(process.env.LOG_LEVEL, LEVELS.WARN)
global[symbol] = log => log.level >= level && transport(log)
const { env } = process
const pattern = [env.DEBUG, env.NODE_DEBUG].filter(Boolean).join(',')
const matchDebug = pattern.length !== 0 ? RegExp.prototype.test.bind(compileGlobPattern(pattern)) : () => false
const level = resolve(env.LOG_LEVEL, LEVELS.WARN)
global[symbol] = function conditionalTransport(log) {
if (log.level >= level || matchDebug(log.namespace)) {
transport(log)
}
}
}
// -------------------------------------------------------------------

View File

@@ -1,9 +1,5 @@
'use strict'
const escapeRegExp = require('lodash/escapeRegExp')
// ===================================================================
const TPL_RE = /\{\{(.+?)\}\}/g
const evalTemplate = (tpl, data) => {
const getData = typeof data === 'function' ? (_, key) => data(key) : (_, key) => data[key]
@@ -14,39 +10,6 @@ exports.evalTemplate = evalTemplate
// -------------------------------------------------------------------
const compileGlobPatternFragment = pattern => pattern.split('*').map(escapeRegExp).join('.*')
const compileGlobPattern = pattern => {
const no = []
const yes = []
pattern.split(/[\s,]+/).forEach(pattern => {
if (pattern[0] === '-') {
no.push(pattern.slice(1))
} else {
yes.push(pattern)
}
})
const raw = ['^']
if (no.length !== 0) {
raw.push('(?!', no.map(compileGlobPatternFragment).join('|'), ')')
}
if (yes.length !== 0) {
raw.push('(?:', yes.map(compileGlobPatternFragment).join('|'), ')')
} else {
raw.push('.*')
}
raw.push('$')
return new RegExp(raw.join(''))
}
exports.compileGlobPattern = compileGlobPattern
// -------------------------------------------------------------------
const required = name => {
throw new Error(`missing required arg ${name}`)
}

View File

@@ -3,7 +3,7 @@
const { describe, it } = require('test')
const assert = require('assert').strict
const { compileGlobPattern } = require('./utils')
const compileGlobPattern = require('./_compileGlobPattern.js')
describe('compileGlobPattern()', () => {
it('works', () => {

View File

@@ -0,0 +1,20 @@
- mixins can depend on each other, they will be instanciated on-demand
```js
import mixin from '@xen-orchestra/mixin'
class MyMixin {
constructor(app, ...mixinParams) {}
foo() {}
}
class App {
constructor() {
mixin(this, { MyMixin }, [...mixinParams])
}
}
app = new App()
app.myMixin.foo()
```

View File

@@ -12,6 +12,29 @@ Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/mixin
> npm install --save @xen-orchestra/mixin
```
## Usage
- mixins can depend on each other, they will be instanciated on-demand
```js
import mixin from '@xen-orchestra/mixin'
class MyMixin {
constructor(app, ...mixinParams) {}
foo() {}
}
class App {
constructor() {
mixin(this, { MyMixin }, [...mixinParams])
}
}
app = new App()
app.myMixin.foo()
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on

View File

@@ -1,9 +1,19 @@
# ChangeLog
## **5.76.1** (2022-11-08)
## **5.76.2** (2022-11-14)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Bug fixes
- [Proxies] Fix `this.getObject is not a function` on upgrade
### Released packages
- xo-server 5.106.1
## **5.76.1** (2022-11-08)
### Enhancements
- [API] `proxy.register` accepts `vmUuid` parameter which can be used when not connected to the XAPI containing the XO Proxy VM

View File

@@ -7,12 +7,13 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [Remotes] Prevent remote path from ending with `xo-vm-backups` as it's usually a mistake
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [Dashboard/Health] Fix `Unknown SR` and `Unknown VDI` in Unhealthy VDIs (PR [#6519](https://github.com/vatesfr/xen-orchestra/pull/6519))
- [Proxies] Fix `this.getObject is not a function` on upgrade
### Packages to release
@@ -29,7 +30,12 @@
> Keep this list alphabetically ordered to avoid merge conflicts
<!--packages-start-->
- vhd-lib minor
- vhd-cli major
- @xen-orchestra/backups-cli major
- @xen-orchestra/log minor
- xo-cli patch
- xo-server minor
- xo-web minor
<!--packages-end-->

View File

@@ -1,7 +1,8 @@
/* eslint-env jest */
'use strict'
const { describe, it } = require('test')
const assert = require('assert').strict
const { ast, pattern } = require('./index.fixtures')
const {
getPropertyClausesStrings,
@@ -17,7 +18,7 @@ const {
it('getPropertyClausesStrings', () => {
const tmp = getPropertyClausesStrings(parse('foo bar:baz baz:|(foo bar /^boo$/ /^far$/) foo:/^bar$/'))
expect(tmp).toEqual({
assert.deepEqual(tmp, {
bar: ['baz'],
baz: ['foo', 'bar', 'boo', 'far'],
foo: ['bar'],
@@ -26,72 +27,72 @@ it('getPropertyClausesStrings', () => {
describe('parse', () => {
it('analyses a string and returns a node/tree', () => {
expect(parse(pattern)).toEqual(ast)
assert.deepEqual(parse(pattern), ast)
})
it('supports an empty string', () => {
expect(parse('')).toEqual(new Null())
assert.deepEqual(parse(''), new Null())
})
it('differentiate between numbers and numbers in strings', () => {
let node
node = parse('32')
expect(node.match(32)).toBe(true)
expect(node.match('32')).toBe(true)
expect(node.toString()).toBe('32')
assert.equal(node.match(32), true)
assert.equal(node.match('32'), true)
assert.equal(node.toString(), '32')
node = parse('"32"')
expect(node.match(32)).toBe(false)
expect(node.match('32')).toBe(true)
expect(node.toString()).toBe('"32"')
assert.equal(node.match(32), false)
assert.equal(node.match('32'), true)
assert.equal(node.toString(), '"32"')
})
it('supports non-ASCII letters in raw strings', () => {
expect(parse('åäöé:ÅÄÖÉ')).toStrictEqual(new Property('åäöé', new StringNode('ÅÄÖÉ')))
assert.deepEqual(parse('åäöé:ÅÄÖÉ'), new Property('åäöé', new StringNode('ÅÄÖÉ')))
})
})
describe('GlobPattern', () => {
it('matches a glob pattern recursively', () => {
expect(new GlobPattern('b*r').match({ foo: 'bar' })).toBe(true)
assert.equal(new GlobPattern('b*r').match({ foo: 'bar' }), true)
})
})
describe('Number', () => {
it('match a number recursively', () => {
expect(new NumberNode(3).match([{ foo: 3 }])).toBe(true)
assert.equal(new NumberNode(3).match([{ foo: 3 }]), true)
})
})
describe('NumberOrStringNode', () => {
it('match a string', () => {
expect(new NumberOrStringNode('123').match([{ foo: '123' }])).toBe(true)
assert.equal(new NumberOrStringNode('123').match([{ foo: '123' }]), true)
})
})
describe('setPropertyClause', () => {
it('creates a node if none passed', () => {
expect(setPropertyClause(undefined, 'foo', 'bar').toString()).toBe('foo:bar')
assert.equal(setPropertyClause(undefined, 'foo', 'bar').toString(), 'foo:bar')
})
it('adds a property clause if there was none', () => {
expect(setPropertyClause(parse('baz'), 'foo', 'bar').toString()).toBe('baz foo:bar')
assert.equal(setPropertyClause(parse('baz'), 'foo', 'bar').toString(), 'baz foo:bar')
})
it('replaces the property clause if there was one', () => {
expect(setPropertyClause(parse('plip foo:baz plop'), 'foo', 'bar').toString()).toBe('plip plop foo:bar')
assert.equal(setPropertyClause(parse('plip foo:baz plop'), 'foo', 'bar').toString(), 'plip plop foo:bar')
expect(setPropertyClause(parse('foo:|(baz plop)'), 'foo', 'bar').toString()).toBe('foo:bar')
assert.equal(setPropertyClause(parse('foo:|(baz plop)'), 'foo', 'bar').toString(), 'foo:bar')
})
it('removes the property clause if no chid is passed', () => {
expect(setPropertyClause(parse('foo bar:baz qux'), 'bar', undefined).toString()).toBe('foo qux')
assert.equal(setPropertyClause(parse('foo bar:baz qux'), 'bar', undefined).toString(), 'foo qux')
expect(setPropertyClause(parse('foo bar:baz qux'), 'baz', undefined).toString()).toBe('foo bar:baz qux')
assert.equal(setPropertyClause(parse('foo bar:baz qux'), 'baz', undefined).toString(), 'foo bar:baz qux')
})
})
it('toString', () => {
expect(ast.toString()).toBe(pattern)
assert.equal(ast.toString(), pattern)
})

View File

@@ -26,6 +26,10 @@
"lodash": "^4.17.4"
},
"scripts": {
"postversion": "npm publish"
"postversion": "npm publish",
"test": "node--test"
},
"devDependencies": {
"test": "^3.2.1"
}
}

View File

@@ -0,0 +1,44 @@
```
> vhd-cli
Usage:
vhd-cli check <path>
Detects issues with VHD. path is relative to the remote url
Options:
--remote <url> the remote url, / if not specified
--bat check if the blocks listed in the bat are present on the file system (vhddirectory only)
--blocks read all the blocks of the vhd
--chain instantiate a vhd with all its parent and check it
vhd-cli compare <sourceRemoteUrl> <source VHD> <destionationRemoteUrl> <destination>
Check if two VHD contains the same data
vhd-cli copy <sourceRemoteUrl> <source VHD> <destionationRemoteUrl> <destination> --directory
Copy a Vhd.
Options:
--directory : the destination vhd will be created as a vhd directory
vhd-cli info <path>
Read informations of a VHD, path is relative to /
vhd-cli merge <child VHD> <parent VHD>
Merge child in parent, paths are relatives to /
vhd-cli raw <path>
extract the raw content of a VHD, path is relative to /
vhd-cli repl
create a REPL environnement in the local folder
```

View File

@@ -1,30 +1,51 @@
'use strict'
const { VhdFile, checkVhdChain } = require('vhd-lib')
const { openVhd, VhdSynthetic } = require('vhd-lib')
const getopts = require('getopts')
const { getSyncedHandler } = require('@xen-orchestra/fs')
const { resolve } = require('path')
const { Disposable } = require('promise-toolbox')
const checkVhd = (handler, path) => new VhdFile(handler, path).readHeaderAndFooter()
module.exports = async function check(rawArgs) {
const { chain, _: args } = getopts(rawArgs, {
boolean: ['chain'],
if (args.length < 2 || args.some(_ => _ === '-h' || _ === '--help')) {
return `Usage: ${this.command} <path> [--remote <remoteURL>] [--chain] [--bat] [--blocks] `
}
const { chain, bat, blocks, remote, _: args } = getopts(rawArgs, {
boolean: ['chain', 'bat', 'blocks'],
default: {
chain: false,
bat: false,
blocks: false
},
})
const check = chain ? checkVhdChain : checkVhd
await Disposable.use(getSyncedHandler({ url: 'file:///' }), async handler => {
for (const vhd of args) {
try {
await check(handler, resolve(vhd))
console.log('ok:', vhd)
} catch (error) {
console.error('nok:', vhd, error)
const vhdPath = args[0]
await Disposable.factory( async function * open(remote, vhdPath) {
const handler = yield getSyncedHandler({url : remote ?? 'file:///'})
const vhd = chain? yield VhdSynthetic.fromVhdChain(handler, vhdPath) : yield openVhd(handler, vhdPath)
await vhd.readBlockAllocationTable()
if(bat){
const nBlocks = vhd.header.maxTableEntries
let nbErrors = 0
for (let blockId = 0; blockId < nBlocks; ++blockId) {
if(!vhd.containsBlock(blockId)){
continue
}
const ok = await vhd.checkBlock(blockId)
if(!ok){
console.warn(`block ${blockId} is invalid`)
nbErrors ++
}
}
console.log('BAT check done ', nbErrors === 0 ? 'OK': `${nbErrors} block(s) faileds`)
}
})
if(blocks){
for await(const _ of vhd.blocks()){
}
console.log('Blocks check done')
}
})(remote, vhdPath)
}

View File

@@ -394,4 +394,14 @@ exports.VhdAbstract = class VhdAbstract {
assert.strictEqual(copied, length, 'invalid length')
return copied
}
_checkBlock() {
throw new Error('not implemented')
}
// check if a block is ok without reading it
// there still can be error when reading the block later (if it's deleted, if right are incorrects,...)
async checkBlock(blockId) {
return this._checkBlock(blockId)
}
}

View File

@@ -317,4 +317,9 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
})
this.#compressor = getCompressor(chunkFilters[0])
}
async _checkBlock(blockId){
const path = this._getFullBlockPath(blockId)
return this._handler.exists(path)
}
}

View File

@@ -466,4 +466,8 @@ exports.VhdFile = class VhdFile extends VhdAbstract {
async getSize() {
return await this._handler.getSize(this._path)
}
_checkBlock(blockId){
return true
}
}

View File

@@ -113,6 +113,10 @@ const VhdSynthetic = class VhdSynthetic extends VhdAbstract {
return vhd?._getFullBlockPath(blockId)
}
_checkBlock(blockId) {
const vhd = this.#getVhdWithBlock(blockId)
return vhd?._checkBlock(blockId) ?? false
}
// return true if all the vhds ar an instance of cls
checkVhdsClass(cls) {
return this.#vhds.every(vhd => vhd instanceof cls)

View File

@@ -273,7 +273,9 @@ async function main(args) {
const lines = [error.message]
const { errors } = error.data
errors.forEach(error => {
lines.push(` property ${error.property}: ${error.message}`)
let { instancePath } = error
instancePath = instancePath.length === 0 ? '@' : '@.' + instancePath
lines.push(` property ${instancePath}: ${error.message}`)
})
throw lines.join('\n')
})

View File

@@ -1,8 +1,11 @@
/* eslint-env jest */
'use strict'
import deepFreeze from 'deep-freeze'
const { describe, it } = require('test')
const { strict: assert } = require('assert')
import { parse, format } from './'
const deepFreeze = require('deep-freeze')
const { parse, format } = require('./')
// ===================================================================
@@ -132,6 +135,7 @@ const parseData = deepFreeze({
object: {
type: 'nfs',
host: '192.168.100.225',
port: undefined,
path: '/media/nfs',
},
},
@@ -203,7 +207,7 @@ describe('format', () => {
for (const name in formatData) {
const datum = formatData[name]
it(name, () => {
expect(format(datum.object)).toBe(datum.string)
assert.equal(format(datum.object), datum.string)
})
}
})
@@ -212,7 +216,7 @@ describe('parse', () => {
for (const name in parseData) {
const datum = parseData[name]
it(name, () => {
expect(parse(datum.string)).toEqual(datum.object)
assert.deepEqual(parse(datum.string), datum.object)
})
}
})

View File

@@ -31,7 +31,8 @@
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^7.0.2",
"deep-freeze": "^0.0.1",
"rimraf": "^3.0.0"
"rimraf": "^3.0.0",
"test": "^3.2.1"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
@@ -39,6 +40,7 @@
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
"postversion": "npm publish",
"test": "node--test"
}
}

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.106.0",
"version": "5.106.1",
"license": "AGPL-3.0-or-later",
"description": "Server part of Xen-Orchestra",
"keywords": [

View File

@@ -25,6 +25,7 @@ register.params = {
},
authenticationToken: {
type: 'string',
optional: true,
},
}
register.resolve = {

View File

@@ -14,7 +14,7 @@ import { createLogger } from '@xen-orchestra/log'
import { decorateWith } from '@vates/decorate-with'
import { defer } from 'golike-defer'
import { format, parse } from 'json-rpc-peer'
import { incorrectState, noSuchObject } from 'xo-common/api-errors.js'
import { incorrectState, invalidParameters, noSuchObject } from 'xo-common/api-errors.js'
import { parseDuration } from '@vates/parse-duration'
import { readChunk } from '@vates/read-chunk'
import { Ref } from 'xen-api'
@@ -106,6 +106,14 @@ export default class Proxy {
@synchronizedWrite
async registerProxy({ address, authenticationToken, name = this._generateDefaultProxyName(), vmUuid }) {
if (address === undefined && vmUuid === undefined) {
throw invalidParameters('at least one of address and vmUuid must be defined')
}
if (authenticationToken === undefined) {
authenticationToken = await generateToken()
}
await this._throwIfRegistered(address, vmUuid)
const { id } = await this._db.add({

View File

@@ -1,4 +1,5 @@
import asyncMapSettled from '@xen-orchestra/async-map/legacy.js'
import { basename } from 'path'
import { format, parse } from 'xo-remote-parser'
import {
DEFAULT_ENCRYPTION_ALGORITHM,
@@ -7,7 +8,7 @@ import {
UNENCRYPTED_ALGORITHM,
} from '@xen-orchestra/fs'
import { ignoreErrors, timeout } from 'promise-toolbox'
import { noSuchObject } from 'xo-common/api-errors.js'
import { invalidParameters, noSuchObject } from 'xo-common/api-errors.js'
import { synchronized } from 'decorator-synchronized'
import * as sensitiveValues from '../sensitive-values.mjs'
@@ -22,6 +23,13 @@ const obfuscateRemote = ({ url, ...remote }) => {
return remote
}
function validatePath(url) {
const { path } = parse(url)
if (path !== undefined && basename(path) === 'xo-vm-backups') {
throw invalidParameters('remote url should not end with xo-vm-backups')
}
}
export default class {
constructor(app) {
this._handlers = { __proto__: null }
@@ -191,6 +199,8 @@ export default class {
}
async createRemote({ name, options, proxy, url }) {
validatePath(url)
const params = {
enabled: false,
error: '',
@@ -224,6 +234,10 @@ export default class {
@synchronized()
async _updateRemote(id, { url, ...props }) {
if (url !== undefined) {
validatePath(url)
}
const remote = await this._getRemote(id)
// url is handled separately to take care of obfuscated values

View File

@@ -591,9 +591,11 @@ export const safeDateFormat = ms => new Date(ms).toISOString().replace(/:/g, '_'
// ===================================================================
export const downloadLog = ({ log, date, type }) => {
const isJson = typeof log !== 'string'
const anchor = document.createElement('a')
anchor.href = window.URL.createObjectURL(createBlobFromString(log))
anchor.download = `${safeDateFormat(date)} - ${type}.log`
anchor.href = window.URL.createObjectURL(createBlobFromString(isJson ? JSON.stringify(log, null, 2) : log))
anchor.download = `${safeDateFormat(date)} - ${type}.${isJson ? 'json' : 'log'}`
anchor.style.display = 'none'
document.body.appendChild(anchor)
anchor.click()

View File

@@ -373,7 +373,8 @@ export default decorate([
const { tasks } = parent
if (tasks !== undefined) {
for (const task of tasks) {
task.parent = parent
// parent should not be enumerable as it would create a cycle and break JSON.stringify
Object.defineProperty(task, parent, { value: parent })
linkParent(task)
}
}

View File

@@ -29,8 +29,8 @@ export default decorate([
effects: {
_downloadLog:
() =>
({ formattedLog }, { log }) =>
downloadLog({ log: formattedLog, date: log.start, type: 'backup NG' }),
(_, { log }) =>
downloadLog({ log, date: log.start, type: 'backup NG' }),
restartFailedVms:
(_, params) =>
async (_, { log: { jobId: id, scheduleId: schedule, tasks, infos } }) => {

View File

@@ -94,7 +94,7 @@ const INDIVIDUAL_ACTIONS = [
label: _('logDownload'),
handler: task =>
downloadLog({
log: JSON.stringify(task, null, 2),
log: task,
date: task.start,
type: 'Metadata restore',
}),

4288
yarn.lock

File diff suppressed because it is too large Load Diff