Compare commits
1 Commits
feat_block
...
fix_openVh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
543f44d1d1 |
@@ -1,10 +1,11 @@
|
||||
import { readFileSync } from 'fs'
|
||||
import getopts from 'getopts'
|
||||
'use strict'
|
||||
|
||||
const { version } = JSON.parse(readFileSync(new URL('package.json', import.meta.url)))
|
||||
const getopts = require('getopts')
|
||||
|
||||
export function composeCommands(commands) {
|
||||
return async function (args, prefix) {
|
||||
const { version } = require('./package.json')
|
||||
|
||||
module.exports = commands =>
|
||||
async function (args, prefix) {
|
||||
const opts = getopts(args, {
|
||||
alias: {
|
||||
help: 'h',
|
||||
@@ -29,6 +30,5 @@ xo-backups v${version}
|
||||
return
|
||||
}
|
||||
|
||||
return (await command.default)(args.slice(1), prefix + ' ' + commandName)
|
||||
return command.main(args.slice(1), prefix + ' ' + commandName)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import fs from 'fs/promises'
|
||||
import { dirname } from 'path'
|
||||
'use strict'
|
||||
|
||||
export * from 'fs/promises'
|
||||
const { dirname } = require('path')
|
||||
|
||||
export const getSize = path =>
|
||||
const fs = require('promise-toolbox/promisifyAll')(require('fs'))
|
||||
module.exports = fs
|
||||
|
||||
fs.getSize = path =>
|
||||
fs.stat(path).then(
|
||||
_ => _.size,
|
||||
error => {
|
||||
@@ -14,7 +16,7 @@ export const getSize = path =>
|
||||
}
|
||||
)
|
||||
|
||||
export async function mktree(path) {
|
||||
fs.mktree = async function mkdirp(path) {
|
||||
try {
|
||||
await fs.mkdir(path)
|
||||
} catch (error) {
|
||||
@@ -24,8 +26,8 @@ export async function mktree(path) {
|
||||
return
|
||||
}
|
||||
if (code === 'ENOENT') {
|
||||
await mktree(dirname(path))
|
||||
return mktree(path)
|
||||
await mkdirp(dirname(path))
|
||||
return mkdirp(path)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
@@ -35,7 +37,7 @@ export async function mktree(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
|
||||
export const readdir2 = path =>
|
||||
fs.readdir2 = path =>
|
||||
fs.readdir(path).then(
|
||||
entries => {
|
||||
entries.forEach((entry, i) => {
|
||||
@@ -57,7 +59,7 @@ export const readdir2 = path =>
|
||||
}
|
||||
)
|
||||
|
||||
export async function symlink2(target, path) {
|
||||
fs.symlink2 = async (target, path) => {
|
||||
try {
|
||||
await fs.symlink(target, path)
|
||||
} catch (error) {
|
||||
40
@xen-orchestra/backups-cli/commands/clean-vms.js
Normal file
40
@xen-orchestra/backups-cli/commands/clean-vms.js
Normal file
@@ -0,0 +1,40 @@
|
||||
'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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
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'
|
||||
'use strict'
|
||||
|
||||
export default async function createSymlinkIndex([backupDir, fieldPath]) {
|
||||
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]) {
|
||||
const indexDir = join(backupDir, 'indexes', filenamify(fieldPath))
|
||||
await mktree(indexDir)
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
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'
|
||||
'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')
|
||||
|
||||
const sha512 = str => createHash('sha512').update(str).digest('hex')
|
||||
const sum = values => values.reduce((a, b) => a + b)
|
||||
|
||||
export default async function info(vmDirs) {
|
||||
module.exports = async function info(vmDirs) {
|
||||
const jsonFiles = (
|
||||
await asyncMap(vmDirs, async vmDir => (await readdir2(vmDir)).filter(_ => _.endsWith('.json')))
|
||||
).flat()
|
||||
@@ -1,12 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
import { composeCommands } from './_composeCommands.mjs'
|
||||
|
||||
const importDefault = async path => (await import(path)).default
|
||||
'use strict'
|
||||
|
||||
composeCommands({
|
||||
require('./_composeCommands')({
|
||||
'clean-vms': {
|
||||
get default() {
|
||||
return importDefault('./commands/clean-vms.mjs')
|
||||
get main() {
|
||||
return require('./commands/clean-vms')
|
||||
},
|
||||
usage: `[--fix] [--merge] [--remove] xo-vm-backups/*
|
||||
|
||||
@@ -19,14 +18,14 @@ composeCommands({
|
||||
`,
|
||||
},
|
||||
'create-symlink-index': {
|
||||
get default() {
|
||||
return importDefault('./commands/create-symlink-index.mjs')
|
||||
get main() {
|
||||
return require('./commands/create-symlink-index')
|
||||
},
|
||||
usage: 'xo-vm-backups <field path>',
|
||||
},
|
||||
info: {
|
||||
get default() {
|
||||
return importDefault('./commands/info.mjs')
|
||||
get main() {
|
||||
return require('./commands/info')
|
||||
},
|
||||
usage: 'xo-vm-backups/*',
|
||||
},
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"bin": {
|
||||
"xo-backups": "index.mjs"
|
||||
"xo-backups": "index.js"
|
||||
},
|
||||
"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": ">=14"
|
||||
"node": ">=7.10.1"
|
||||
},
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/backups-cli",
|
||||
"name": "@xen-orchestra/backups-cli",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const sum = require('lodash/sum')
|
||||
const UUID = require('uuid')
|
||||
const { asyncMap } = require('@xen-orchestra/async-map')
|
||||
const { Constants, openVhd, VhdAbstract, VhdFile } = require('vhd-lib')
|
||||
const { Constants, openVhd, VhdAbstract, VhdFile, BrokenVhdError } = require('vhd-lib')
|
||||
const { isVhdAlias, resolveVhdAlias } = require('vhd-lib/aliases')
|
||||
const { dirname, resolve } = require('path')
|
||||
const { DISK_TYPES } = Constants
|
||||
@@ -242,7 +242,7 @@ exports.cleanVm = async function cleanVm(
|
||||
} catch (error) {
|
||||
vhds.delete(path)
|
||||
logWarn('VHD check error', { path, error })
|
||||
if (error?.code === 'ERR_ASSERTION' && remove) {
|
||||
if (error instanceof BrokenVhdError && remove) {
|
||||
logInfo('deleting broken VHD', { path })
|
||||
return VhdAbstract.unlink(handler, path)
|
||||
}
|
||||
|
||||
@@ -91,7 +91,6 @@ 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)
|
||||
@@ -315,14 +314,6 @@ 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
|
||||
//
|
||||
|
||||
@@ -198,9 +198,4 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,17 +537,4 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,15 @@
|
||||
// Keeping this file to prevent applying the global monorepo config for now
|
||||
module.exports = {};
|
||||
module.exports = {
|
||||
importOrder: [
|
||||
"^[^/]+$",
|
||||
"<THIRD_PARTY_MODULES>",
|
||||
"^@/components/(.*)$",
|
||||
"^@/composables/(.*)$",
|
||||
"^@/libs/(.*)$",
|
||||
"^@/router/(.*)$",
|
||||
"^@/stores/(.*)$",
|
||||
"^@/views/(.*)$",
|
||||
],
|
||||
importOrderSeparation: false,
|
||||
importOrderSortSpecifiers: true,
|
||||
importOrderParserPlugins: ["typescript", "decorators-legacy"],
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"@novnc/novnc": "^1.3.0",
|
||||
"@types/d3-time-format": "^4.0.0",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@vueuse/core": "^9.5.0",
|
||||
"@vueuse/core": "^8.7.5",
|
||||
"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": "^3.2.0",
|
||||
"@vitejs/plugin-vue": "^2.3.3",
|
||||
"@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": "^8.4.19",
|
||||
"postcss-nested": "^6.0.0",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^3.2.4",
|
||||
"vue-tsc": "^1.0.9"
|
||||
"postcss-nested": "^5.0.6",
|
||||
"typescript": "~4.7.4",
|
||||
"vite": "^2.9.12",
|
||||
"vue-tsc": "^0.38.1"
|
||||
},
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/lite",
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="data.length !== 0">
|
||||
<div class="header">
|
||||
<slot name="header" />
|
||||
</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" />
|
||||
<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>
|
||||
</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;
|
||||
@@ -33,7 +29,7 @@ interface Data {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data?: Array<Data>;
|
||||
data: Array<Data>;
|
||||
nItems?: number;
|
||||
}
|
||||
|
||||
@@ -44,7 +40,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 {
|
||||
@@ -76,14 +72,6 @@ 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>
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
:label="$t('vms')"
|
||||
/>
|
||||
</template>
|
||||
<UiSpinner v-else class="spinner" />
|
||||
</UiCard>
|
||||
</template>
|
||||
|
||||
@@ -23,7 +22,6 @@ 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";
|
||||
@@ -47,13 +45,3 @@ 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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<UiCard>
|
||||
<UiTitle type="h4">{{ $t("storage-usage") }}</UiTitle>
|
||||
<UsageBar :data="srStore.isReady ? data.result : undefined" :nItems="5">
|
||||
<UsageBar :data="data.result" :nItems="5">
|
||||
<template #header>
|
||||
<span>{{ $t("storage") }}</span>
|
||||
<span>{{ $t("top-#", { n: 5 }) }}</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<UsageBar :data="statFetched ? data : undefined" :n-items="5">
|
||||
<UsageBar :data="data" :n-items="5">
|
||||
<template #header>
|
||||
<span>{{ $t("hosts") }}</span>
|
||||
<span>{{ $t("top-#", { n: 5 }) }}</span>
|
||||
@@ -42,10 +42,4 @@ 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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<UsageBar :data="statFetched ? data : undefined" :n-items="5">
|
||||
<UsageBar :data="data" :n-items="5">
|
||||
<template #header>
|
||||
<span>{{ $t("vms") }}</span>
|
||||
<span>{{ $t("top-#", { n: 5 }) }}</span>
|
||||
@@ -42,10 +42,4 @@ 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>
|
||||
|
||||
@@ -30,12 +30,7 @@ const props = withDefaults(
|
||||
transparent?: boolean;
|
||||
active?: boolean;
|
||||
}>(),
|
||||
{
|
||||
busy: undefined,
|
||||
disabled: undefined,
|
||||
outlined: undefined,
|
||||
transparent: undefined,
|
||||
}
|
||||
{ busy: undefined, disabled: undefined, outlined: undefined }
|
||||
);
|
||||
|
||||
const isGroupBusy = inject("isButtonGroupBusy", false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="{ merge }" class="ui-button-group">
|
||||
<div class="ui-button-group">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -14,7 +14,6 @@ const props = defineProps<{
|
||||
color?: Color;
|
||||
outlined?: boolean;
|
||||
transparent?: boolean;
|
||||
merge?: boolean;
|
||||
}>();
|
||||
provide(
|
||||
"isButtonGroupBusy",
|
||||
@@ -41,32 +40,8 @@ provide(
|
||||
<style lang="postcss" scoped>
|
||||
.ui-button-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
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>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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,
|
||||
});
|
||||
@@ -15,7 +16,7 @@ export const useHostStore = defineStore("host", () => {
|
||||
if (host === undefined) {
|
||||
throw new Error(`Host ${id} could not be found.`);
|
||||
}
|
||||
return useXenApiStore().getXapiStats()._getAndUpdateStats({
|
||||
return xapiStats._getAndUpdateStats({
|
||||
host,
|
||||
uuid: host.uuid,
|
||||
granularity,
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
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,
|
||||
@@ -41,7 +42,7 @@ export const useVmStore = defineStore("vm", () => {
|
||||
throw new Error(`VM ${id} is halted or host could not be found.`);
|
||||
}
|
||||
|
||||
return useXenApiStore().getXapiStats()._getAndUpdateStats({
|
||||
return xapiStats._getAndUpdateStats({
|
||||
host,
|
||||
uuid: vm.uuid,
|
||||
granularity,
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"lib": [
|
||||
"ES2019",
|
||||
"dom"
|
||||
],
|
||||
"lib": ["ES2019"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
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),
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
'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(''))
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use strict'
|
||||
|
||||
const compileGlobPattern = require('./_compileGlobPattern.js')
|
||||
const createTransport = require('./transports/console')
|
||||
const { LEVELS, resolve } = require('./levels')
|
||||
|
||||
@@ -9,19 +8,8 @@ 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 { 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)
|
||||
}
|
||||
}
|
||||
const level = resolve(process.env.LOG_LEVEL, LEVELS.WARN)
|
||||
global[symbol] = log => log.level >= level && transport(log)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
'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]
|
||||
@@ -10,6 +14,39 @@ 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}`)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const { describe, it } = require('test')
|
||||
const assert = require('assert').strict
|
||||
|
||||
const compileGlobPattern = require('./_compileGlobPattern.js')
|
||||
const { compileGlobPattern } = require('./utils')
|
||||
|
||||
describe('compileGlobPattern()', () => {
|
||||
it('works', () => {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
- 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()
|
||||
```
|
||||
|
||||
@@ -12,29 +12,6 @@ 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
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,19 +1,9 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.76.2** (2022-11-14)
|
||||
## **5.76.1** (2022-11-08)
|
||||
|
||||
<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
|
||||
|
||||
@@ -7,13 +7,12 @@
|
||||
|
||||
> 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
|
||||
|
||||
@@ -30,12 +29,7 @@
|
||||
> 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-->
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use strict'
|
||||
/* eslint-env jest */
|
||||
|
||||
const { describe, it } = require('test')
|
||||
const assert = require('assert').strict
|
||||
'use strict'
|
||||
|
||||
const { ast, pattern } = require('./index.fixtures')
|
||||
const {
|
||||
@@ -18,7 +17,7 @@ const {
|
||||
|
||||
it('getPropertyClausesStrings', () => {
|
||||
const tmp = getPropertyClausesStrings(parse('foo bar:baz baz:|(foo bar /^boo$/ /^far$/) foo:/^bar$/'))
|
||||
assert.deepEqual(tmp, {
|
||||
expect(tmp).toEqual({
|
||||
bar: ['baz'],
|
||||
baz: ['foo', 'bar', 'boo', 'far'],
|
||||
foo: ['bar'],
|
||||
@@ -27,72 +26,72 @@ it('getPropertyClausesStrings', () => {
|
||||
|
||||
describe('parse', () => {
|
||||
it('analyses a string and returns a node/tree', () => {
|
||||
assert.deepEqual(parse(pattern), ast)
|
||||
expect(parse(pattern)).toEqual(ast)
|
||||
})
|
||||
|
||||
it('supports an empty string', () => {
|
||||
assert.deepEqual(parse(''), new Null())
|
||||
expect(parse('')).toEqual(new Null())
|
||||
})
|
||||
|
||||
it('differentiate between numbers and numbers in strings', () => {
|
||||
let node
|
||||
|
||||
node = parse('32')
|
||||
assert.equal(node.match(32), true)
|
||||
assert.equal(node.match('32'), true)
|
||||
assert.equal(node.toString(), '32')
|
||||
expect(node.match(32)).toBe(true)
|
||||
expect(node.match('32')).toBe(true)
|
||||
expect(node.toString()).toBe('32')
|
||||
|
||||
node = parse('"32"')
|
||||
assert.equal(node.match(32), false)
|
||||
assert.equal(node.match('32'), true)
|
||||
assert.equal(node.toString(), '"32"')
|
||||
expect(node.match(32)).toBe(false)
|
||||
expect(node.match('32')).toBe(true)
|
||||
expect(node.toString()).toBe('"32"')
|
||||
})
|
||||
|
||||
it('supports non-ASCII letters in raw strings', () => {
|
||||
assert.deepEqual(parse('åäöé:ÅÄÖÉ'), new Property('åäöé', new StringNode('ÅÄÖÉ')))
|
||||
expect(parse('åäöé:ÅÄÖÉ')).toStrictEqual(new Property('åäöé', new StringNode('ÅÄÖÉ')))
|
||||
})
|
||||
})
|
||||
|
||||
describe('GlobPattern', () => {
|
||||
it('matches a glob pattern recursively', () => {
|
||||
assert.equal(new GlobPattern('b*r').match({ foo: 'bar' }), true)
|
||||
expect(new GlobPattern('b*r').match({ foo: 'bar' })).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Number', () => {
|
||||
it('match a number recursively', () => {
|
||||
assert.equal(new NumberNode(3).match([{ foo: 3 }]), true)
|
||||
expect(new NumberNode(3).match([{ foo: 3 }])).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('NumberOrStringNode', () => {
|
||||
it('match a string', () => {
|
||||
assert.equal(new NumberOrStringNode('123').match([{ foo: '123' }]), true)
|
||||
expect(new NumberOrStringNode('123').match([{ foo: '123' }])).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setPropertyClause', () => {
|
||||
it('creates a node if none passed', () => {
|
||||
assert.equal(setPropertyClause(undefined, 'foo', 'bar').toString(), 'foo:bar')
|
||||
expect(setPropertyClause(undefined, 'foo', 'bar').toString()).toBe('foo:bar')
|
||||
})
|
||||
|
||||
it('adds a property clause if there was none', () => {
|
||||
assert.equal(setPropertyClause(parse('baz'), 'foo', 'bar').toString(), 'baz foo:bar')
|
||||
expect(setPropertyClause(parse('baz'), 'foo', 'bar').toString()).toBe('baz foo:bar')
|
||||
})
|
||||
|
||||
it('replaces the property clause if there was one', () => {
|
||||
assert.equal(setPropertyClause(parse('plip foo:baz plop'), 'foo', 'bar').toString(), 'plip plop foo:bar')
|
||||
expect(setPropertyClause(parse('plip foo:baz plop'), 'foo', 'bar').toString()).toBe('plip plop foo:bar')
|
||||
|
||||
assert.equal(setPropertyClause(parse('foo:|(baz plop)'), 'foo', 'bar').toString(), 'foo:bar')
|
||||
expect(setPropertyClause(parse('foo:|(baz plop)'), 'foo', 'bar').toString()).toBe('foo:bar')
|
||||
})
|
||||
|
||||
it('removes the property clause if no chid is passed', () => {
|
||||
assert.equal(setPropertyClause(parse('foo bar:baz qux'), 'bar', undefined).toString(), 'foo qux')
|
||||
expect(setPropertyClause(parse('foo bar:baz qux'), 'bar', undefined).toString()).toBe('foo qux')
|
||||
|
||||
assert.equal(setPropertyClause(parse('foo bar:baz qux'), 'baz', undefined).toString(), 'foo bar:baz qux')
|
||||
expect(setPropertyClause(parse('foo bar:baz qux'), 'baz', undefined).toString()).toBe('foo bar:baz qux')
|
||||
})
|
||||
})
|
||||
|
||||
it('toString', () => {
|
||||
assert.equal(ast.toString(), pattern)
|
||||
expect(ast.toString()).toBe(pattern)
|
||||
})
|
||||
@@ -26,10 +26,6 @@
|
||||
"lodash": "^4.17.4"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish",
|
||||
"test": "node--test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"test": "^3.2.1"
|
||||
"postversion": "npm publish"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
```
|
||||
> 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
|
||||
|
||||
|
||||
```
|
||||
|
||||
@@ -1,51 +1,30 @@
|
||||
'use strict'
|
||||
|
||||
const { openVhd, VhdSynthetic } = require('vhd-lib')
|
||||
const { VhdFile, checkVhdChain } = require('vhd-lib')
|
||||
const getopts = require('getopts')
|
||||
const { getSyncedHandler } = require('@xen-orchestra/fs')
|
||||
const { resolve } = require('path')
|
||||
const { Disposable } = require('promise-toolbox')
|
||||
|
||||
module.exports = async function check(rawArgs) {
|
||||
const checkVhd = (handler, path) => new VhdFile(handler, path).readHeaderAndFooter()
|
||||
|
||||
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'],
|
||||
module.exports = async function check(rawArgs) {
|
||||
const { chain, _: args } = getopts(rawArgs, {
|
||||
boolean: ['chain'],
|
||||
default: {
|
||||
chain: false,
|
||||
bat: false,
|
||||
blocks: false
|
||||
},
|
||||
})
|
||||
|
||||
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 ++
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
10
packages/vhd-lib/BrokenVhdError.js
Normal file
10
packages/vhd-lib/BrokenVhdError.js
Normal file
@@ -0,0 +1,10 @@
|
||||
'use strict'
|
||||
class BrokenVhdError extends Error {
|
||||
constructor(message, baseError) {
|
||||
super(message)
|
||||
this.code = 'BROKEN_VHD'
|
||||
this.cause = baseError
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BrokenVhdError
|
||||
@@ -394,14 +394,4 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const assert = require('assert')
|
||||
const { synchronized } = require('decorator-synchronized')
|
||||
const promisify = require('promise-toolbox/promisify')
|
||||
const zlib = require('zlib')
|
||||
const BrokenVhdError = require('../BrokenVhdError')
|
||||
|
||||
const { debug } = createLogger('vhd-lib:VhdDirectory')
|
||||
|
||||
@@ -110,7 +111,14 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
|
||||
// EISDIR pathname refers to a directory and the access requested
|
||||
// involved writing (that is, O_WRONLY or O_RDWR is set).
|
||||
// reading the header ensure we have a well formed directory immediatly
|
||||
await vhd.readHeaderAndFooter()
|
||||
try {
|
||||
await vhd.readHeaderAndFooter()
|
||||
} catch (error) {
|
||||
if (error.code === 'ERR_ASSERTION') {
|
||||
throw new BrokenVhdError('Invalid header or footer', error)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
return {
|
||||
dispose: () => {},
|
||||
value: vhd,
|
||||
@@ -185,18 +193,8 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
|
||||
async readHeaderAndFooter() {
|
||||
await this.#readChunkFilters()
|
||||
|
||||
let bufHeader, bufFooter
|
||||
try {
|
||||
bufHeader = (await this._readChunk('header')).buffer
|
||||
bufFooter = (await this._readChunk('footer')).buffer
|
||||
} catch (error) {
|
||||
// emit an AssertionError if the VHD is broken to stay as close as possible to the VhdFile API
|
||||
if (error.code === 'ENOENT') {
|
||||
assert(false, 'Header And Footer should exists')
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
const bufHeader = (await this._readChunk('header')).buffer
|
||||
const bufFooter = (await this._readChunk('footer')).buffer
|
||||
const footer = unpackFooter(bufFooter)
|
||||
const header = unpackHeader(bufHeader, footer)
|
||||
|
||||
@@ -317,9 +315,4 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
|
||||
})
|
||||
this.#compressor = getCompressor(chunkFilters[0])
|
||||
}
|
||||
|
||||
async _checkBlock(blockId){
|
||||
const path = this._getFullBlockPath(blockId)
|
||||
return this._handler.exists(path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ const {
|
||||
} = require('../_constants')
|
||||
const { computeBatSize, sectorsToBytes, unpackHeader, unpackFooter, BUF_BLOCK_UNUSED } = require('./_utils')
|
||||
const { createLogger } = require('@xen-orchestra/log')
|
||||
const BrokenVhdError = require('../BrokenVhdError')
|
||||
const { fuFooter, fuHeader, checksumStruct } = require('../_structs')
|
||||
const { set: mapSetBit } = require('../_bitmap')
|
||||
const { VhdAbstract } = require('./VhdAbstract')
|
||||
@@ -93,7 +94,14 @@ exports.VhdFile = class VhdFile extends VhdAbstract {
|
||||
// EISDIR pathname refers to a directory and the access requested
|
||||
// involved writing (that is, O_WRONLY or O_RDWR is set).
|
||||
// reading the header ensure we have a well formed file immediatly
|
||||
await vhd.readHeaderAndFooter(checkSecondFooter)
|
||||
try {
|
||||
await vhd.readHeaderAndFooter(checkSecondFooter)
|
||||
} catch (error) {
|
||||
if (error.code === 'ERR_ASSERTION') {
|
||||
throw new BrokenVhdError('Invalid header or footer', error)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
return {
|
||||
dispose: () => handler.closeFile(fd),
|
||||
value: vhd,
|
||||
@@ -466,8 +474,4 @@ exports.VhdFile = class VhdFile extends VhdAbstract {
|
||||
async getSize() {
|
||||
return await this._handler.getSize(this._path)
|
||||
}
|
||||
|
||||
_checkBlock(blockId){
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,10 +113,6 @@ 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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
exports.BrokenVhdError = require('./BrokenVhdError')
|
||||
exports.chainVhd = require('./chain')
|
||||
exports.checkFooter = require('./checkFooter')
|
||||
exports.checkVhdChain = require('./checkChain')
|
||||
|
||||
@@ -1,17 +1,37 @@
|
||||
'use strict'
|
||||
|
||||
const { BrokenVhdError } = require('.')
|
||||
const { resolveVhdAlias } = require('./aliases')
|
||||
const { VhdDirectory } = require('./Vhd/VhdDirectory.js')
|
||||
const { VhdFile } = require('./Vhd/VhdFile.js')
|
||||
|
||||
class AggregateError extends Error {
|
||||
constructor(errors, message) {
|
||||
super(message)
|
||||
this.errors = errors
|
||||
}
|
||||
}
|
||||
|
||||
exports.openVhd = async function openVhd(handler, path, opts) {
|
||||
const resolved = await resolveVhdAlias(handler, path)
|
||||
try {
|
||||
return await VhdFile.open(handler, resolved, opts)
|
||||
} catch (e) {
|
||||
if (e.code !== 'EISDIR') {
|
||||
throw e
|
||||
}
|
||||
return await VhdDirectory.open(handler, resolved, opts)
|
||||
} catch (vhdDirectoryError) {
|
||||
// it's a directory, but it's an invalid vhd
|
||||
if (vhdDirectoryError instanceof BrokenVhdError) {
|
||||
throw vhdDirectoryError
|
||||
}
|
||||
|
||||
// it's not a vhd directory, try to open it as a vhd file
|
||||
try {
|
||||
return await VhdFile.open(handler, resolved, opts)
|
||||
} catch (vhdFileError) {
|
||||
// this is really a file that looks like a vhd but is broken
|
||||
if (vhdFileError instanceof BrokenVhdError) {
|
||||
throw vhdFileError
|
||||
}
|
||||
// the errors are not vhd related, throw both error to keep a trace
|
||||
throw new AggregateError([vhdDirectoryError, vhdFileError])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,9 +273,7 @@ async function main(args) {
|
||||
const lines = [error.message]
|
||||
const { errors } = error.data
|
||||
errors.forEach(error => {
|
||||
let { instancePath } = error
|
||||
instancePath = instancePath.length === 0 ? '@' : '@.' + instancePath
|
||||
lines.push(` property ${instancePath}: ${error.message}`)
|
||||
lines.push(` property ${error.property}: ${error.message}`)
|
||||
})
|
||||
throw lines.join('\n')
|
||||
})
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^7.0.2",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"rimraf": "^3.0.0",
|
||||
"test": "^3.2.1"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
@@ -40,7 +39,6 @@
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"postversion": "npm publish",
|
||||
"test": "node--test"
|
||||
"postversion": "npm publish"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
'use strict'
|
||||
/* eslint-env jest */
|
||||
|
||||
const { describe, it } = require('test')
|
||||
const { strict: assert } = require('assert')
|
||||
import deepFreeze from 'deep-freeze'
|
||||
|
||||
const deepFreeze = require('deep-freeze')
|
||||
|
||||
const { parse, format } = require('./')
|
||||
import { parse, format } from './'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -135,7 +132,6 @@ const parseData = deepFreeze({
|
||||
object: {
|
||||
type: 'nfs',
|
||||
host: '192.168.100.225',
|
||||
port: undefined,
|
||||
path: '/media/nfs',
|
||||
},
|
||||
},
|
||||
@@ -207,7 +203,7 @@ describe('format', () => {
|
||||
for (const name in formatData) {
|
||||
const datum = formatData[name]
|
||||
it(name, () => {
|
||||
assert.equal(format(datum.object), datum.string)
|
||||
expect(format(datum.object)).toBe(datum.string)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -216,7 +212,7 @@ describe('parse', () => {
|
||||
for (const name in parseData) {
|
||||
const datum = parseData[name]
|
||||
it(name, () => {
|
||||
assert.deepEqual(parse(datum.string), datum.object)
|
||||
expect(parse(datum.string)).toEqual(datum.object)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.106.1",
|
||||
"version": "5.106.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
|
||||
@@ -25,7 +25,6 @@ register.params = {
|
||||
},
|
||||
authenticationToken: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
}
|
||||
register.resolve = {
|
||||
|
||||
@@ -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, invalidParameters, noSuchObject } from 'xo-common/api-errors.js'
|
||||
import { incorrectState, 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,14 +106,6 @@ 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({
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import asyncMapSettled from '@xen-orchestra/async-map/legacy.js'
|
||||
import { basename } from 'path'
|
||||
import { format, parse } from 'xo-remote-parser'
|
||||
import {
|
||||
DEFAULT_ENCRYPTION_ALGORITHM,
|
||||
@@ -8,7 +7,7 @@ import {
|
||||
UNENCRYPTED_ALGORITHM,
|
||||
} from '@xen-orchestra/fs'
|
||||
import { ignoreErrors, timeout } from 'promise-toolbox'
|
||||
import { invalidParameters, noSuchObject } from 'xo-common/api-errors.js'
|
||||
import { noSuchObject } from 'xo-common/api-errors.js'
|
||||
import { synchronized } from 'decorator-synchronized'
|
||||
|
||||
import * as sensitiveValues from '../sensitive-values.mjs'
|
||||
@@ -23,13 +22,6 @@ 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 }
|
||||
@@ -199,8 +191,6 @@ export default class {
|
||||
}
|
||||
|
||||
async createRemote({ name, options, proxy, url }) {
|
||||
validatePath(url)
|
||||
|
||||
const params = {
|
||||
enabled: false,
|
||||
error: '',
|
||||
@@ -234,10 +224,6 @@ 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
|
||||
|
||||
@@ -591,11 +591,9 @@ 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(isJson ? JSON.stringify(log, null, 2) : log))
|
||||
anchor.download = `${safeDateFormat(date)} - ${type}.${isJson ? 'json' : 'log'}`
|
||||
anchor.href = window.URL.createObjectURL(createBlobFromString(log))
|
||||
anchor.download = `${safeDateFormat(date)} - ${type}.log`
|
||||
anchor.style.display = 'none'
|
||||
document.body.appendChild(anchor)
|
||||
anchor.click()
|
||||
|
||||
@@ -373,8 +373,7 @@ export default decorate([
|
||||
const { tasks } = parent
|
||||
if (tasks !== undefined) {
|
||||
for (const task of tasks) {
|
||||
// parent should not be enumerable as it would create a cycle and break JSON.stringify
|
||||
Object.defineProperty(task, parent, { value: parent })
|
||||
task.parent = parent
|
||||
linkParent(task)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ export default decorate([
|
||||
effects: {
|
||||
_downloadLog:
|
||||
() =>
|
||||
(_, { log }) =>
|
||||
downloadLog({ log, date: log.start, type: 'backup NG' }),
|
||||
({ formattedLog }, { log }) =>
|
||||
downloadLog({ log: formattedLog, date: log.start, type: 'backup NG' }),
|
||||
restartFailedVms:
|
||||
(_, params) =>
|
||||
async (_, { log: { jobId: id, scheduleId: schedule, tasks, infos } }) => {
|
||||
|
||||
@@ -94,7 +94,7 @@ const INDIVIDUAL_ACTIONS = [
|
||||
label: _('logDownload'),
|
||||
handler: task =>
|
||||
downloadLog({
|
||||
log: task,
|
||||
log: JSON.stringify(task, null, 2),
|
||||
date: task.start,
|
||||
type: 'Metadata restore',
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user