Compare commits

..

37 Commits

Author SHA1 Message Date
florent Beauchamp
a15428ac88 fix(@xen-orchestra/vmware-explorer): cleanup 2023-02-07 10:54:28 +01:00
ggunullu
85a23c68f2 remove ignore-pattern for vmware-explorer 2023-02-07 10:42:56 +01:00
ggunullu
c16c1f8eb9 remove checkFile from util.js 2023-02-07 10:42:56 +01:00
ggunullu
8af95b41fd test 2023-02-07 10:42:56 +01:00
ggunullu
d0e3603663 upgrade node version in package 2023-02-07 10:42:56 +01:00
ggunullu
2e755ec083 test 2023-02-07 10:42:56 +01:00
ggunullu
724195d66d use unlink and move test file 2023-02-07 10:42:56 +01:00
ggunullu
b132ff4fd0 remove unused test 2023-02-07 10:42:56 +01:00
ggunullu
6f1054e2d1 remove ignore-pattern on vmware-explorer 2023-02-07 10:42:56 +01:00
ggunullu
60c59a0529 test 2023-02-07 10:42:56 +01:00
ggunullu
d382f262fd change file to remove 2023-02-07 10:42:56 +01:00
ggunullu
f6baef3bd6 test 2023-02-07 10:42:56 +01:00
ggunullu
4a27fd35bf remove ignore-pattern on vmware-explorer 2023-02-07 10:42:56 +01:00
ggunullu
edd37be295 test 2023-02-07 10:42:56 +01:00
ggunullu
e38f00c18b test 2023-02-07 10:42:56 +01:00
ggunullu
24b08037f9 test 2023-02-07 10:42:56 +01:00
ggunullu
1d9bc390bb test 2023-02-07 10:42:56 +01:00
ggunullu
44ba19990e test 2023-02-07 10:42:56 +01:00
ggunullu
5571a1c262 test 2023-02-07 10:42:56 +01:00
ggunullu
9617241b6d test 2023-02-07 10:42:56 +01:00
ggunullu
4b5eadcf88 test 2023-02-07 10:42:56 +01:00
ggunullu
c76295e5c9 test 2023-02-07 10:42:56 +01:00
ggunullu
b61ab4c79a test 2023-02-07 10:42:56 +01:00
ggunullu
2d01192204 Test 2023-02-07 10:42:56 +01:00
ggunullu
eb6763b0bb test 2023-02-07 10:42:56 +01:00
ggunullu
2bb935e9ca test 2023-02-07 10:42:56 +01:00
ggunullu
1e72e9d749 test 2023-02-07 10:42:56 +01:00
ggunullu
59700834cc test 2023-02-07 10:42:56 +01:00
ggunullu
95d6ed0376 test 2023-02-07 10:42:56 +01:00
ggunullu
5dfc8b2e0a test 2023-02-07 10:42:56 +01:00
ggunullu
6961361cf8 test 2023-02-07 10:42:56 +01:00
ggunullu
c105057b91 test 2023-02-07 10:42:56 +01:00
ggunullu
29b20753e9 test 2023-02-07 10:42:56 +01:00
ggunullu
f0b93dc7fe test 2023-02-07 10:42:56 +01:00
ggunullu
dd2b054b35 set back vmware-explorer test 2023-02-07 10:42:56 +01:00
ggunullu
bc09387f5e ignore vmware-explorer 2023-02-07 10:42:56 +01:00
ggunullu
6e8e725a94 chore(test): remove vhd-util check 2023-02-07 10:42:56 +01:00
116 changed files with 2223 additions and 3573 deletions

View File

@@ -16,7 +16,6 @@ const {
NBD_REPLY_MAGIC,
NBD_REQUEST_MAGIC,
OPTS_MAGIC,
NBD_CMD_DISC,
} = require('./constants.js')
const { fromCallback } = require('promise-toolbox')
const { readChunkStrict } = require('@vates/read-chunk')
@@ -90,11 +89,6 @@ module.exports = class NbdClient {
}
async disconnect() {
const buffer = Buffer.alloc(28)
buffer.writeInt32BE(NBD_REQUEST_MAGIC, 0) // it is a nbd request
buffer.writeInt16BE(0, 4) // no command flags for a disconnect
buffer.writeInt16BE(NBD_CMD_DISC, 6) // we want to disconnect from nbd server
await this.#write(buffer)
await this.#serverSocket.destroy()
}

View File

@@ -13,7 +13,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "1.0.1",
"version": "1.0.0",
"engines": {
"node": ">=14.0"
},
@@ -22,7 +22,7 @@
"@vates/read-chunk": "^1.0.1",
"@xen-orchestra/async-map": "^0.1.2",
"promise-toolbox": "^0.21.0",
"xen-api": "^1.2.5"
"xen-api": "^1.2.2"
},
"devDependencies": {
"tap": "^16.3.0",

View File

@@ -1,12 +1,5 @@
'use strict'
/**
* Read a chunk of data from a stream.
*
* @param {Readable} stream - A readable stream to read from.
* @param {number} size - The number of bytes to read.
* @returns {Promise<Buffer|null>} - A Promise that resolves to a Buffer of up to size bytes if available, or null if end of stream is reached. The Promise is rejected if there is an error while reading from the stream.
*/
const readChunk = (stream, size) =>
stream.closed || stream.readableEnded
? Promise.resolve(null)
@@ -40,13 +33,6 @@ const readChunk = (stream, size) =>
})
exports.readChunk = readChunk
/**
* Read a chunk of data from a stream.
*
* @param {Readable} stream - A readable stream to read from.
* @param {number} size - The number of bytes to read.
* @returns {Promise<Buffer>} - A Promise that resolves to a Buffer of size bytes. The Promise is rejected if there is an error while reading from the stream.
*/
exports.readChunkStrict = async function readChunkStrict(stream, size) {
const chunk = await readChunk(stream, size)
if (chunk === null) {

View File

@@ -7,7 +7,7 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.29.6",
"@xen-orchestra/backups": "^0.29.5",
"@xen-orchestra/fs": "^3.3.1",
"filenamify": "^4.1.0",
"getopts": "^2.2.5",

View File

@@ -1,6 +1,7 @@
'use strict'
const { finished, PassThrough } = require('node:stream')
const eos = require('end-of-stream')
const { PassThrough } = require('stream')
const { debug } = require('@xen-orchestra/log').createLogger('xo:backups:forkStreamUnpipe')
@@ -8,29 +9,29 @@ const { debug } = require('@xen-orchestra/log').createLogger('xo:backups:forkStr
//
// in case of error in the new readable stream, it will simply be unpiped
// from the original one
exports.forkStreamUnpipe = function forkStreamUnpipe(source) {
const { forks = 0 } = source
source.forks = forks + 1
exports.forkStreamUnpipe = function forkStreamUnpipe(stream) {
const { forks = 0 } = stream
stream.forks = forks + 1
debug('forking', { forks: source.forks })
debug('forking', { forks: stream.forks })
const fork = new PassThrough()
source.pipe(fork)
finished(source, { writable: false }, error => {
const proxy = new PassThrough()
stream.pipe(proxy)
eos(stream, error => {
if (error !== undefined) {
debug('error on original stream, destroying fork', { error })
fork.destroy(error)
proxy.destroy(error)
}
})
finished(fork, { readable: false }, error => {
debug('end of stream, unpiping', { error, forks: --source.forks })
eos(proxy, error => {
debug('end of stream, unpiping', { error, forks: --stream.forks })
source.unpipe(fork)
stream.unpipe(proxy)
if (source.forks === 0) {
if (stream.forks === 0) {
debug('no more forks, destroying original stream')
source.destroy(new Error('no more consumers for this stream'))
stream.destroy(new Error('no more consumers for this stream'))
}
})
return fork
return proxy
}

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.29.6",
"version": "0.29.5",
"engines": {
"node": ">=14.6"
},
@@ -23,7 +23,7 @@
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.4",
"@vates/fuse-vhd": "^1.0.0",
"@vates/nbd-client": "^1.0.1",
"@vates/nbd-client": "*",
"@vates/parse-duration": "^0.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^3.3.1",
@@ -32,6 +32,7 @@
"compare-versions": "^5.0.1",
"d3-time-format": "^3.0.0",
"decorator-synchronized": "^0.6.0",
"end-of-stream": "^1.4.4",
"fs-extra": "^11.1.0",
"golike-defer": "^0.5.1",
"limit-concurrency-decorator": "^0.5.0",

View File

@@ -7,8 +7,6 @@ const ignoreErrors = require('promise-toolbox/ignoreErrors')
const { asyncMap } = require('@xen-orchestra/async-map')
const { chainVhd, checkVhdChain, openVhd, VhdAbstract } = require('vhd-lib')
const { createLogger } = require('@xen-orchestra/log')
const { decorateClass } = require('@vates/decorate-with')
const { defer } = require('golike-defer')
const { dirname } = require('path')
const { formatFilenameDate } = require('../_filenameDate.js')
@@ -24,7 +22,7 @@ const NbdClient = require('@vates/nbd-client')
const { debug, warn, info } = createLogger('xo:backups:DeltaBackupWriter')
class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
async checkBaseVdis(baseUuidToSrcVdi) {
const { handler } = this._adapter
const backup = this._backup
@@ -135,7 +133,7 @@ class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
}
}
async _transfer($defer, { timestamp, deltaExport }) {
async _transfer({ timestamp, deltaExport }) {
const adapter = this._adapter
const backup = this._backup
@@ -212,11 +210,6 @@ class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
debug('got NBD info', { nbdInfo, vdi: id, path })
nbdClient = new NbdClient(nbdInfo)
await nbdClient.connect()
// this will inform the xapi that we don't need this anymore
// and will detach the vdi from dom0
$defer(() => nbdClient.disconnect())
info('NBD client ready', { vdi: id, path })
} catch (error) {
nbdClient = undefined
@@ -255,6 +248,3 @@ class DeltaBackupWriter extends MixinBackupWriter(AbstractDeltaWriter) {
// TODO: run cleanup?
}
}
exports.DeltaBackupWriter = decorateClass(DeltaBackupWriter, {
_transfer: defer,
})

View File

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

View File

@@ -24,14 +24,12 @@
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",
"echarts": "^5.3.3",
"highlight.js": "^11.6.0",
"human-format": "^1.0.0",
"json-rpc-2.0": "^1.3.0",
"json5": "^2.2.1",
"limit-concurrency-decorator": "^0.5.0",
"lodash-es": "^4.17.21",
"make-error": "^1.3.6",
"markdown-it": "^13.0.1",
"pinia": "^2.0.14",
"placement.js": "^1.0.0-beta.5",
"vue": "^3.2.37",

View File

@@ -16,8 +16,8 @@ a {
color: var(--color-extra-blue-base);
}
code, code * {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
code {
font-family: monospace;
}
.card-view {

View File

@@ -1,114 +0,0 @@
<template>
<div ref="rootElement" class="app-markdown" v-html="html" />
</template>
<script lang="ts" setup>
import { type Ref, computed, ref } from "vue";
import { useEventListener } from "@vueuse/core";
import "highlight.js/styles/github-dark.css";
import { markdown } from "@/libs/markdown";
const rootElement = ref() as Ref<HTMLElement>;
const props = defineProps<{
content: string;
}>();
const html = computed(() => markdown.render(props.content ?? ""));
useEventListener(
rootElement,
"click",
(event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!target.classList.contains("copy-button")) {
return;
}
const copyable =
target.parentElement!.querySelector<HTMLElement>(".copyable");
if (copyable !== null) {
navigator.clipboard.writeText(copyable.innerText);
}
},
{ capture: true }
);
</script>
<style lang="postcss" scoped>
.app-markdown {
font-size: 1.6rem;
:deep() {
p,
h1,
h2,
h3,
h4,
h5,
h6,
pre {
margin: 1rem 0;
}
pre {
width: 100%;
padding: 0.8rem 1.4rem;
border-radius: 1rem;
}
code:not(.hljs-code) {
background-color: var(--background-color-extra-blue);
padding: 0.3rem 0.6rem;
border-radius: 0.6rem;
}
ul,
ol {
margin: revert;
padding-left: 2rem;
list-style-type: revert;
}
table {
border-spacing: 0;
th,
td {
padding: 0.5rem 1rem;
}
thead th {
border-bottom: 2px solid var(--color-blue-scale-400);
background-color: var(--background-color-secondary);
}
tbody td {
border-bottom: 1px solid var(--color-blue-scale-400);
}
}
.copy-button {
font-size: 1.6rem;
font-weight: 400;
position: absolute;
z-index: 1;
right: 1rem;
cursor: pointer;
color: white;
border: none;
background-color: transparent;
&:hover {
color: var(--color-extra-blue-base);
}
&:active {
color: var(--color-extra-blue-d20);
}
}
}
}
</style>

View File

@@ -1,41 +0,0 @@
<template>
<pre class="code-highlight hljs"><code v-html="codeAsHtml"></code></pre>
</template>
<script lang="ts" setup>
import HLJS from "highlight.js";
import { computed } from "vue";
import "highlight.js/styles/github-dark.css";
const props = withDefaults(
defineProps<{
code?: any;
lang?: string;
}>(),
{ lang: "typescript" }
);
const codeAsText = computed(() => {
switch (typeof props.code) {
case "string":
return props.code;
case "function":
return String(props.code);
default:
return JSON.stringify(props.code, undefined, 2);
}
});
const codeAsHtml = computed(
() => HLJS.highlight(codeAsText.value, { language: props.lang }).value
);
</script>
<style lang="postcss" scoped>
.code-highlight {
display: inline-block;
padding: 0.3rem 0.6rem;
text-align: left;
border-radius: 0.6rem;
}
</style>

View File

@@ -7,7 +7,7 @@
@remove="emit('removeSort', property)"
>
<span class="property">
<UiIcon :icon="isAscending ? faCaretUp : faCaretDown" />
<FontAwesomeIcon :icon="isAscending ? faCaretUp : faCaretDown" />
{{ property }}
</span>
</UiFilter>
@@ -17,7 +17,7 @@
</UiActionButton>
</UiFilterGroup>
<UiModal v-if="isOpen" :icon="faSort" @submit.prevent="handleSubmit">
<UiModal v-if="isOpen" @submit.prevent="handleSubmit" :icon="faSort">
<div class="form-widgets">
<FormWidget :label="$t('sort-by')">
<select v-model="newSortProperty">
@@ -48,14 +48,7 @@
</template>
<script lang="ts" setup>
import FormWidget from "@/components/FormWidget.vue";
import UiActionButton from "@/components/ui/UiActionButton.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiFilter from "@/components/ui/UiFilter.vue";
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
import UiIcon from "@/components/ui/UiIcon.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import { ref } from "vue";
import type { ActiveSorts, Sorts } from "@/types/sort";
import {
faCaretDown,
@@ -63,7 +56,13 @@ import {
faPlus,
faSort,
} from "@fortawesome/free-solid-svg-icons";
import { ref } from "vue";
import FormWidget from "@/components/FormWidget.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiActionButton from "@/components/ui/UiActionButton.vue";
import UiFilter from "@/components/ui/UiFilter.vue";
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
defineProps<{
availableSorts: Sorts;

View File

@@ -2,7 +2,7 @@
<th>
<div class="content">
<span class="label">
<UiIcon :icon="icon" />
<FontAwesomeIcon v-if="icon" :icon="icon" />
<slot />
</span>
</div>
@@ -10,7 +10,6 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineProps<{

View File

@@ -8,7 +8,7 @@
<span class="widget">
<span v-if="before || $slots.before" class="before">
<slot name="before">
<UiIcon v-if="isIcon(before)" :icon="before" fixed-width />
<FontAwesomeIcon v-if="isIcon(before)" :icon="before" fixed-width />
<template v-else>{{ before }}</template>
</slot>
</span>
@@ -17,7 +17,7 @@
</span>
<span v-if="after || $slots.after" class="after">
<slot name="after">
<UiIcon v-if="isIcon(after)" :icon="after" fixed-width />
<FontAwesomeIcon v-if="isIcon(after)" :icon="after" fixed-width />
<template v-else>{{ after }}</template>
</slot>
</span>
@@ -26,7 +26,6 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineProps<{

View File

@@ -1,10 +1,9 @@
<template>
<UiIcon :class="className" :icon="icon" class="power-state-icon" />
<FontAwesomeIcon class="power-state-icon" :class="className" :icon="icon" />
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import type { PowerState } from "@/libs/xen-api";
import { computed } from "vue";
import {
faMoon,
faPause,
@@ -12,7 +11,7 @@ import {
faQuestion,
faStop,
} from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
import type { PowerState } from "@/libs/xen-api";
const props = defineProps<{
state: PowerState;
@@ -30,7 +29,7 @@ const icon = computed(() => icons[props.state] ?? faQuestion);
const className = computed(() => `state-${props.state.toLocaleLowerCase()}`);
</script>
<style lang="postcss" scoped>
<style scoped lang="postcss">
.power-state-icon {
color: var(--color-extra-blue-d60);

View File

@@ -1,29 +0,0 @@
<template>
<RouterLink
v-slot="{ isActive, href }"
:to="disabled || isTabBarDisabled ? '' : to"
custom
>
<UiTab :active="isActive" :disabled="disabled" :href="href" tag="a">
<slot />
</UiTab>
</RouterLink>
</template>
<script lang="ts" setup>
import { type ComputedRef, computed, inject } from "vue";
import type { RouteLocationRaw } from "vue-router";
import UiTab from "@/components/ui/UiTab.vue";
const props = defineProps<{
to: RouteLocationRaw;
disabled?: boolean;
}>();
const isTabBarDisabled = inject<ComputedRef<boolean>>(
"isTabBarDisabled",
computed(() => false)
);
</script>
<style lang="postcss" scoped></style>

View File

@@ -1,29 +1,16 @@
<template>
<div class="ui-tab-bar">
<div class="tab-bar">
<slot />
</div>
</template>
<script lang="ts" setup>
import { computed, provide } from "vue";
const props = defineProps<{
disabled?: boolean;
}>();
provide(
"isTabBarDisabled",
computed(() => props.disabled)
);
</script>
<style lang="postcss" scoped>
.ui-tab-bar {
.tab-bar {
display: flex;
align-items: stretch;
height: 6.5rem;
border-bottom: 1px solid var(--color-blue-scale-400);
background-color: var(--background-color-primary);
border-bottom: 1px solid var(--color-blue-scale-400);
max-width: 100%;
overflow: auto;
}

View File

@@ -0,0 +1,54 @@
<template>
<span v-if="disabled" class="tab-bar-item disabled">
<slot />
</span>
<RouterLink v-else class="tab-bar-item" v-bind="$props">
<slot />
</RouterLink>
</template>
<script lang="ts" setup>
import type { RouterLinkProps } from "vue-router";
// https://vuejs.org/api/sfc-script-setup.html#type-only-props-emit-declarations
interface Props extends RouterLinkProps {
disabled?: boolean;
}
defineProps<Props>();
</script>
<style lang="postcss" scoped>
.tab-bar-item {
font-size: 1.8rem;
font-weight: 600;
display: flex;
align-items: center;
padding: 0 1.2em;
text-decoration: none;
text-transform: uppercase;
color: var(--color-blue-scale-100);
border-bottom: 2px solid transparent;
&:hover:not(.disabled) {
border-bottom-color: var(--color-extra-blue-base);
background-color: var(--background-color-secondary);
}
&:active:not(.disabled) {
color: var(--color-extra-blue-base);
border-bottom-color: var(--color-extra-blue-base);
background-color: var(--background-color-secondary);
}
&.router-link-active {
color: var(--color-extra-blue-base);
border-bottom-color: var(--color-extra-blue-base);
background-color: var(--background-color-primary);
}
&.disabled {
color: var(--color-blue-scale-400);
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div class="title-bar">
<UiIcon :icon="icon" class="icon" />
<FontAwesomeIcon :icon="icon" class="icon" />
<div class="title">
<slot />
</div>
@@ -11,7 +11,6 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineProps<{

View File

@@ -20,8 +20,8 @@
</template>
<script lang="ts" setup>
import { computed, inject } from "vue";
import { percent } from "@/libs/utils";
import { computed, type ComputedRef, inject } from "vue";
const props = defineProps<{
total: number;
@@ -34,9 +34,7 @@ const freePercent = computed(() =>
percent(props.total - props.used, props.total)
);
const valueFormatter = inject("valueFormatter") as ComputedRef<
(value: number) => string
>;
const valueFormatter = inject("valueFormatter") as (value: number) => string;
</script>
<style lang="postcss" scoped>

View File

@@ -33,17 +33,8 @@ const props = defineProps<{
maxValue?: number;
}>();
const valueFormatter = computed(() => {
const formatter = props.valueFormatter;
return (value: OptionDataValue | OptionDataValue[]) => {
if (formatter) {
return formatter(value as number);
}
return value.toString();
};
});
const valueFormatter = (value: OptionDataValue | OptionDataValue[]) =>
props.valueFormatter?.(value as number) ?? `${value}`;
provide("valueFormatter", valueFormatter);
@@ -65,7 +56,7 @@ const option = computed<EChartsOption>(() => ({
data: props.data.map((series) => series.label),
},
tooltip: {
valueFormatter: valueFormatter.value,
valueFormatter,
},
xAxis: {
type: "time",
@@ -79,9 +70,9 @@ const option = computed<EChartsOption>(() => ({
yAxis: {
type: "value",
axisLabel: {
formatter: valueFormatter.value,
formatter: valueFormatter,
},
max: props.maxValue ?? Y_AXIS_MAX_VALUE,
max: () => props.maxValue ?? Y_AXIS_MAX_VALUE,
},
series: props.data.map((series, index) => ({
type: "line",

View File

@@ -6,7 +6,6 @@
>
<input
v-model="value"
:class="{ indeterminate: type === 'checkbox' && value === undefined }"
:disabled="isLabelDisabled || disabled"
:type="type === 'radio' ? 'radio' : 'checkbox'"
class="input"
@@ -33,7 +32,7 @@ import {
inject,
ref,
} from "vue";
import { faCheck, faCircle, faMinus } from "@fortawesome/free-solid-svg-icons";
import { faCheck, faCircle } from "@fortawesome/free-solid-svg-icons";
import { useVModel } from "@vueuse/core";
import UiIcon from "@/components/ui/UiIcon.vue";
@@ -54,17 +53,7 @@ const value = useVModel(props, "modelValue", emit);
const type = inject<"checkbox" | "radio" | "toggle">("inputType", "checkbox");
const hasLabel = inject("hasLabel", false);
const isLabelDisabled = inject("isLabelDisabled", ref(false));
const icon = computed(() => {
if (type !== "checkbox") {
return faCircle;
}
if (value.value === undefined) {
return faMinus;
}
return faCheck;
});
const icon = computed(() => (type === "checkbox" ? faCheck : faCircle));
</script>
<style lang="postcss" scoped>
@@ -85,11 +74,6 @@ const icon = computed(() => {
.form-checkbox {
--checkbox-border-radius: 0.25em;
--checkbox-icon-size: 1em;
.input.indeterminate + .fake-checkbox > .icon {
opacity: 1;
color: var(--color-blue-scale-300);
}
}
.form-checkbox,
@@ -125,8 +109,8 @@ const icon = computed(() => {
}
.icon {
transition: transform 0.125s ease-in-out;
transform: translateX(-0.7em);
transition: transform 0.125s ease-in-out;
}
.input:checked + .fake-checkbox > .icon {
@@ -156,12 +140,12 @@ const icon = computed(() => {
align-items: center;
justify-content: center;
height: 1.25em;
transition: background-color 0.125s ease-in-out,
border-color 0.125s ease-in-out;
border: var(--checkbox-border-width) solid var(--border-color);
border-radius: var(--checkbox-border-radius);
background-color: var(--background-color);
box-shadow: var(--shadow-100);
transition: background-color 0.125s ease-in-out,
border-color 0.125s ease-in-out;
--border-color: var(--color-blue-scale-400);
}

View File

@@ -1,6 +1,15 @@
<template>
<span :class="wrapperClass" v-bind="wrapperAttrs">
<template v-if="inputType === 'select'">
<input
v-if="!isSelect"
v-model="value"
:class="inputClass"
:disabled="disabled || isLabelDisabled"
class="input"
ref="inputElement"
v-bind="$attrs"
/>
<template v-else>
<select
v-model="value"
:class="inputClass"
@@ -15,24 +24,6 @@
<UiIcon :fixed-width="false" :icon="faAngleDown" />
</span>
</template>
<textarea
v-else-if="inputType === 'textarea'"
ref="textarea"
v-model="value"
:class="inputClass"
:disabled="disabled || isLabelDisabled"
class="textarea"
v-bind="$attrs"
/>
<input
v-else
v-model="value"
:class="inputClass"
:disabled="disabled || isLabelDisabled"
class="input"
ref="inputElement"
v-bind="$attrs"
/>
<span v-if="before !== undefined" class="before">
<template v-if="typeof before === 'string'">{{ before }}</template>
<UiIcon v-else :icon="before" class="before" />
@@ -52,19 +43,18 @@ export default {
</script>
<script lang="ts" setup>
import { isEmpty } from "lodash-es";
import {
type HTMLAttributes,
type InputHTMLAttributes,
computed,
inject,
nextTick,
ref,
watch,
} from "vue";
import type { Color } from "@/types";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
import { useTextareaAutosize, useVModel } from "@vueuse/core";
import { useVModel } from "@vueuse/core";
import UiIcon from "@/components/ui/UiIcon.vue";
// Temporary workaround for https://github.com/vuejs/core/issues/4294
@@ -89,10 +79,8 @@ const emit = defineEmits<{
}>();
const value = useVModel(props, "modelValue", emit);
const isEmpty = computed(
() => props.modelValue == null || String(props.modelValue).trim() === ""
);
const inputType = inject("inputType", "input");
const empty = computed(() => isEmpty(props.modelValue));
const isSelect = inject("isSelect", false);
const isLabelDisabled = inject("isLabelDisabled", ref(false));
const color = inject(
"color",
@@ -100,10 +88,10 @@ const color = inject(
);
const wrapperClass = computed(() => [
`form-${inputType}`,
isSelect ? "form-select" : "form-input",
{
disabled: props.disabled || isLabelDisabled.value,
empty: isEmpty.value,
empty: empty.value,
},
]);
@@ -116,12 +104,6 @@ const inputClass = computed(() => [
},
]);
const { textarea, triggerResize } = useTextareaAutosize();
watch(value, () => nextTick(() => triggerResize()), {
immediate: true,
});
const focus = () => inputElement.value.focus();
defineExpose({
@@ -131,13 +113,12 @@ defineExpose({
<style lang="postcss" scoped>
.form-input,
.form-select,
.form-textarea {
.form-select {
display: inline-grid;
align-items: stretch;
--before-width: v-bind('beforeWidth || "1.75em"');
--after-width: v-bind('afterWidth || "1.625em"');
--before-width: v-bind('beforeWidth ?? "1.75em"');
--after-width: v-bind('afterWidth ?? "1.625em"');
--caret-width: 1.5em;
--text-color: var(--color-blue-scale-100);
@@ -151,8 +132,7 @@ defineExpose({
}
}
.form-input,
.form-textarea {
.form-input {
grid-template-columns: var(--before-width) auto var(--after-width);
}
@@ -165,10 +145,8 @@ defineExpose({
}
.input,
.textarea,
.select {
font-size: 1em;
width: 100%;
height: 2em;
margin: 0;
color: var(--text-color);
@@ -257,19 +235,8 @@ defineExpose({
}
}
.textarea {
height: auto;
min-height: 2em;
}
.input {
padding: 0;
}
.input,
.textarea {
padding-right: 0.625em;
padding-left: 0.625em;
padding: 0 0.625em 0 0.625em;
&.has-before {
padding-left: calc(var(--before-width) + 0.25em);

View File

@@ -1,93 +0,0 @@
<template>
<UiModal
@submit.prevent="saveJson"
:color="isJsonValid ? 'success' : 'error'"
v-if="isCodeModalOpen"
:icon="faCode"
@close="closeCodeModal"
>
<FormTextarea class="modal-textarea" v-model="editedJson" />
<template #buttons>
<UiButton transparent @click="formatJson">{{ $t("reformat") }}</UiButton>
<UiButton outlined @click="closeCodeModal">{{ $t("cancel") }}</UiButton>
<UiButton :disabled="!isJsonValid" type="submit"
>{{ $t("save") }}
</UiButton>
</template>
</UiModal>
<FormInput
@click="openCodeModal"
:model-value="jsonValue"
:before="faCode"
readonly
/>
</template>
<script lang="ts" setup>
import FormInput from "@/components/form/FormInput.vue";
import FormTextarea from "@/components/form/FormTextarea.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import { faCode } from "@fortawesome/free-solid-svg-icons";
import { useVModel, whenever } from "@vueuse/core";
import { computed, ref } from "vue";
const props = defineProps<{
modelValue: any;
}>();
const emit = defineEmits<{
(event: "update:modelValue", value: any): void;
}>();
const model = useVModel(props, "modelValue", emit);
const {
open: openCodeModal,
close: closeCodeModal,
isOpen: isCodeModalOpen,
} = useModal();
const jsonValue = computed(() => JSON.stringify(model.value, undefined, 2));
const isJsonValid = computed(() => {
try {
JSON.parse(editedJson.value);
return true;
} catch {
return false;
}
});
const formatJson = () => {
if (!isJsonValid.value) {
return;
}
editedJson.value = JSON.stringify(JSON.parse(editedJson.value), undefined, 2);
};
const saveJson = () => {
if (!isJsonValid.value) {
return;
}
formatJson();
model.value = JSON.parse(editedJson.value);
closeCodeModal();
};
whenever(isCodeModalOpen, () => (editedJson.value = jsonValue.value));
const editedJson = ref();
</script>
<style lang="postcss" scoped>
:deep(.modal-textarea) {
min-width: 50rem;
min-height: 20rem;
}
</style>

View File

@@ -8,7 +8,7 @@
import { provide } from "vue";
import FormInput from "@/components/form/FormInput.vue";
provide("inputType", "select");
provide("isSelect", true);
</script>
<style lang="postcss" scoped></style>

View File

@@ -1,12 +0,0 @@
<template>
<FormInput />
</template>
<script lang="ts" setup>
import { provide } from "vue";
import FormInput from "@/components/form/FormInput.vue";
provide("inputType", "textarea");
</script>
<style lang="postcss" scoped></style>

View File

@@ -1,13 +1,12 @@
<template>
<div class="infra-action">
<slot>
<UiIcon :icon="icon" fixed-width />
<FontAwesomeIcon :icon="icon" fixed-width />
</slot>
</div>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineProps<{

View File

@@ -8,7 +8,7 @@
v-bind="$attrs"
>
<a :href="href" class="link" @click="navigate">
<UiIcon :icon="icon" class="icon" />
<FontAwesomeIcon :icon="icon" class="icon" />
<div class="text">
<slot />
</div>
@@ -21,9 +21,8 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import type { RouteLocationRaw } from "vue-router";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineProps<{
icon: IconDefinition;

View File

@@ -2,7 +2,7 @@
<li class="infra-loading-item">
<div class="infra-item-label-placeholder">
<div class="link-placeholder">
<UiIcon :icon="icon" class="icon" />
<FontAwesomeIcon :icon="icon" class="icon" />
<div class="loader">&nbsp;</div>
</div>
</div>
@@ -10,7 +10,6 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineProps<{

View File

@@ -1,40 +1,67 @@
<template>
<UiTabBar :disabled="!isReady">
<RouterTab :to="{ name: 'pool.dashboard', params: { uuid: poolUuid } }">
<TabBar>
<TabBarItem
:disabled="!isReady"
:to="{ name: 'pool.dashboard', params: { uuid: poolUuid } }"
>
{{ $t("dashboard") }}
</RouterTab>
<RouterTab :to="{ name: 'pool.alarms', params: { uuid: poolUuid } }">
</TabBarItem>
<TabBarItem
:disabled="!isReady"
:to="{ name: 'pool.alarms', params: { uuid: poolUuid } }"
>
{{ $t("alarms") }}
</RouterTab>
<RouterTab :to="{ name: 'pool.stats', params: { uuid: poolUuid } }">
</TabBarItem>
<TabBarItem
:disabled="!isReady"
:to="{ name: 'pool.stats', params: { uuid: poolUuid } }"
>
{{ $t("stats") }}
</RouterTab>
<RouterTab :to="{ name: 'pool.system', params: { uuid: poolUuid } }">
</TabBarItem>
<TabBarItem
:disabled="!isReady"
:to="{ name: 'pool.system', params: { uuid: poolUuid } }"
>
{{ $t("system") }}
</RouterTab>
<RouterTab :to="{ name: 'pool.network', params: { uuid: poolUuid } }">
</TabBarItem>
<TabBarItem
:disabled="!isReady"
:to="{ name: 'pool.network', params: { uuid: poolUuid } }"
>
{{ $t("network") }}
</RouterTab>
<RouterTab :to="{ name: 'pool.storage', params: { uuid: poolUuid } }">
</TabBarItem>
<TabBarItem
:disabled="!isReady"
:to="{ name: 'pool.storage', params: { uuid: poolUuid } }"
>
{{ $t("storage") }}
</RouterTab>
<RouterTab :to="{ name: 'pool.tasks', params: { uuid: poolUuid } }">
</TabBarItem>
<TabBarItem
:disabled="!isReady"
:to="{ name: 'pool.tasks', params: { uuid: poolUuid } }"
>
{{ $t("tasks") }}
</RouterTab>
<RouterTab :to="{ name: 'pool.hosts', params: { uuid: poolUuid } }">
</TabBarItem>
<TabBarItem
:disabled="!isReady"
:to="{ name: 'pool.hosts', params: { uuid: poolUuid } }"
>
{{ $t("hosts") }}
</RouterTab>
<RouterTab :to="{ name: 'pool.vms', params: { uuid: poolUuid } }">
</TabBarItem>
<TabBarItem
:disabled="!isReady"
:to="{ name: 'pool.vms', params: { uuid: poolUuid } }"
>
{{ $t("vms") }}
</RouterTab>
</UiTabBar>
</TabBarItem>
</TabBar>
</template>
<script lang="ts" setup>
import { storeToRefs } from "pinia";
import { computed } from "vue";
import RouterTab from "@/components/RouterTab.vue";
import UiTabBar from "@/components/ui/UiTabBar.vue";
import TabBar from "@/components/TabBar.vue";
import TabBarItem from "@/components/TabBarItem.vue";
import { usePoolStore } from "@/stores/pool.store";
const poolStore = usePoolStore();

View File

@@ -1,67 +0,0 @@
<template>
<tr class="finished-task-row" :class="{ finished: !isPending }">
<td>{{ task.name_label }}</td>
<td>
<RouterLink
:to="{
name: 'host.dashboard',
params: { uuid: host.uuid },
}"
>
{{ host.name_label }}
</RouterLink>
</td>
<td>
<UiProgressBar v-if="isPending" :max-value="1" :value="task.progress" />
</td>
<td>
<RelativeTime v-if="isPending" :date="createdAt" />
<template v-else>{{ $d(createdAt, "datetime_short") }}</template>
</td>
<td>
<template v-if="finishedAt !== undefined">
{{ $d(finishedAt, "datetime_short") }}
</template>
<RelativeTime
v-else-if="isPending && estimatedEndAt !== Infinity"
:date="estimatedEndAt"
/>
</td>
</tr>
</template>
<script lang="ts" setup>
import RelativeTime from "@/components/RelativeTime.vue";
import UiProgressBar from "@/components/ui/UiProgressBar.vue";
import { parseDateTime } from "@/libs/utils";
import type { XenApiTask } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store";
import { computed } from "vue";
const props = defineProps<{
isPending?: boolean;
task: XenApiTask;
}>();
const { getRecord: getHost } = useHostStore();
const createdAt = computed(() => parseDateTime(props.task.created));
const host = computed(() => getHost(props.task.resident_on));
const estimatedEndAt = computed(
() =>
createdAt.value +
(new Date().getTime() - createdAt.value) / props.task.progress
);
const finishedAt = computed(() =>
props.isPending ? undefined : parseDateTime(props.task.finished)
);
</script>
<style lang="postcss" scoped>
.finished {
opacity: 0.5;
}
</style>

View File

@@ -1,32 +0,0 @@
<template>
<UiTable class="tasks-table">
<template #header>
<th>{{ $t("name") }}</th>
<th>{{ $t("object") }}</th>
<th>{{ $t("task.progress") }}</th>
<th>{{ $t("task.started") }}</th>
<th>{{ $t("task.estimated-end") }}</th>
</template>
<TaskRow
v-for="task in pendingTasks"
:key="task.uuid"
:task="task"
is-pending
/>
<TaskRow v-for="task in finishedTasks" :key="task.uuid" :task="task" />
</UiTable>
</template>
<script lang="ts" setup>
import TaskRow from "@/components/tasks/TaskRow.vue";
import UiTable from "@/components/ui/UiTable.vue";
import type { XenApiTask } from "@/libs/xen-api";
defineProps<{
pendingTasks: XenApiTask[];
finishedTasks: XenApiTask[];
}>();
</script>
<style lang="postcss" scoped></style>

View File

@@ -5,7 +5,7 @@
:type="type || 'button'"
class="ui-button"
>
<UiSpinner v-if="isBusy" />
<span v-if="isBusy" class="loader" />
<template v-else>
<UiIcon :icon="icon" class="icon" />
<slot />
@@ -14,7 +14,6 @@
</template>
<script lang="ts" setup>
import UiSpinner from "@/components/ui/UiSpinner.vue";
import { computed, inject, unref } from "vue";
import type { Color } from "@/types";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";

View File

@@ -1,51 +0,0 @@
<template>
<span :class="color" class="ui-counter">
<span :class="{ overflow: value > 99 }">
{{ value }}
</span>
</span>
</template>
<script lang="ts" setup>
import type { Color } from "@/types";
defineProps<{
value: number;
color?: Color;
}>();
</script>
<style lang="postcss" scoped>
.ui-counter {
font-weight: 700;
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--size);
height: var(--size);
color: var(--color-blue-scale-500);
border-radius: calc(var(--size) / 2);
background-color: var(--color-blue-scale-300);
--size: 1.75em;
.overflow {
font-size: 0.8em;
}
&.info {
background-color: var(--color-extra-blue-base);
}
&.success {
background-color: var(--color-green-infra-base);
}
&.warning {
background-color: var(--color-orange-world-base);
}
&.error {
background-color: var(--color-red-vates-base);
}
}
</style>

View File

@@ -4,13 +4,12 @@
<slot />
</span>
<span class="remove" @click.stop="emit('remove')">
<UiIcon :icon="faRemove" />
<FontAwesomeIcon :icon="faRemove" class="icon" />
</span>
</span>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import { faRemove } from "@fortawesome/free-solid-svg-icons";
const emit = defineEmits<{
@@ -40,7 +39,6 @@ const emit = defineEmits<{
display: flex;
align-items: center;
justify-content: center;
&:hover {
opacity: 0.7;
}
@@ -56,7 +54,6 @@ const emit = defineEmits<{
width: 2.8rem;
margin: 0.2rem;
background-color: var(--color-extra-blue-l40);
&:hover {
background-color: var(--color-red-vates-l20);
}

View File

@@ -1,19 +1,19 @@
<template>
<UiSpinner v-if="busy" class="ui-icon" />
<FontAwesomeIcon
v-else-if="icon !== undefined"
v-if="icon !== undefined"
:icon="icon"
:spin="busy"
class="ui-icon"
:fixed-width="fixedWidth"
/>
</template>
<script lang="ts" setup>
import UiSpinner from "@/components/ui/UiSpinner.vue";
import { computed } from "vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { faSpinner } from "@fortawesome/free-solid-svg-icons";
withDefaults(
const props = withDefaults(
defineProps<{
busy?: boolean;
icon?: IconDefinition;
@@ -21,6 +21,8 @@ withDefaults(
}>(),
{ fixedWidth: true }
);
const icon = computed(() => (props.busy ? faSpinner : props.icon));
</script>
<style lang="postcss" scoped></style>

View File

@@ -8,11 +8,11 @@
>
<div class="container">
<span v-if="onClose" class="close-icon" @click="emit('close')">
<UiIcon :icon="faXmark" />
<FontAwesomeIcon :icon="faXmark" />
</span>
<div v-if="icon || $slots.icon" class="modal-icon">
<slot name="icon">
<UiIcon :icon="icon" />
<FontAwesomeIcon :icon="icon" />
</slot>
</div>
<UiTitle v-if="$slots.title" type="h4">
@@ -34,7 +34,6 @@
<script lang="ts" setup>
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import UiIcon from "@/components/ui/UiIcon.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faXmark } from "@fortawesome/free-solid-svg-icons";

View File

@@ -1,68 +0,0 @@
<template>
<component
:is="tag"
:class="{ active, disabled: disabled || isTabBarDisabled }"
class="ui-tab"
>
<slot />
</component>
</template>
<script lang="ts" setup>
import { type ComputedRef, computed, inject } from "vue";
withDefaults(
defineProps<{
disabled?: boolean;
active?: boolean;
tag?: string;
}>(),
{ tag: "span" }
);
const isTabBarDisabled = inject<ComputedRef<boolean>>(
"isTabBarDisabled",
computed(() => false)
);
</script>
<style lang="postcss" scoped>
.ui-tab {
font-size: 1.8rem;
font-weight: 600;
display: flex;
align-items: center;
padding: 0 1.2em;
text-decoration: none;
text-transform: uppercase;
color: var(--color-blue-scale-100);
border-bottom: 2px solid transparent;
&.disabled {
pointer-events: none;
color: var(--color-blue-scale-400);
}
&:not(.disabled) {
cursor: pointer;
&:hover {
cursor: pointer;
border-bottom-color: var(--color-extra-blue-base);
background-color: var(--background-color-secondary);
}
&:active {
color: var(--color-extra-blue-base);
border-bottom-color: var(--color-extra-blue-base);
background-color: var(--background-color-secondary);
}
&.active {
color: var(--color-extra-blue-base);
border-bottom-color: var(--color-extra-blue-base);
background-color: var(--background-color-primary);
}
}
}
</style>

View File

@@ -18,8 +18,8 @@
border-spacing: 0;
}
:deep(th),
:deep(td) {
:slotted(th),
:slotted(td) {
padding: 1rem;
border-top: 1px solid lightgrey;
border-right: 1px solid lightgrey;
@@ -30,14 +30,14 @@
}
}
:deep(.header-row th) {
:slotted(.header-row th) {
color: var(--color-extra-blue-base);
font-size: 1.4rem;
font-weight: 400;
text-transform: uppercase;
}
:deep(.body td) {
:slotted(.body td) {
font-weight: 400;
font-size: 1.6rem;
line-height: 2.4rem;

View File

@@ -1,18 +1,13 @@
import type { MaybeRef } from "@vueuse/core";
import { differenceBy } from "lodash-es";
import { type Ref, ref, unref, watch } from "vue";
export default function useArrayRemovedItemsHistory<T>(
list: Ref<T[]>,
iteratee: (item: T) => unknown = (item) => item,
options: {
limit?: MaybeRef<number>;
onRemove?: (items: T[]) => any[];
} = {}
limit = Infinity,
iteratee: (item: T) => unknown = (item) => item
) {
const currentList: Ref<T[]> = ref([]);
const history: Ref<T[]> = ref([]);
const { limit = Infinity, onRemove = (items) => items } = options;
watch(
list,
@@ -24,10 +19,10 @@ export default function useArrayRemovedItemsHistory<T>(
watch(currentList, (nextList, previousList) => {
const removedItems = differenceBy(previousList, nextList, iteratee);
history.value.push(...onRemove(removedItems));
history.value.push(...removedItems);
const currentLimit = unref(limit);
if (history.value.length > currentLimit) {
history.value = history.value.slice(-currentLimit);
history.value.slice(-currentLimit);
}
});

View File

@@ -1,64 +0,0 @@
import HLJS from "highlight.js";
import MarkdownIt from "markdown-it";
export const markdown = new MarkdownIt();
markdown.set({
highlight: (str: string, lang: string) => {
const code = highlight(str, lang);
return `<pre class="hljs"><button class="copy-button" type="button">Copy</button><code class="hljs-code">${code}</code></pre>`;
},
});
function highlight(str: string, lang: string) {
switch (lang) {
case "vue-template": {
const indented = str
.trim()
.split("\n")
.map((s) => ` ${s}`)
.join("\n");
return wrap(indented, "template");
}
case "vue-script":
return wrap(str.trim(), "script");
case "vue-style":
return wrap(str.trim(), "style");
default: {
if (HLJS.getLanguage(lang) !== undefined) {
return copyable(HLJS.highlight(str, { language: lang }).value);
}
return copyable(markdown.utils.escapeHtml(str));
}
}
}
function wrap(str: string, tag: "template" | "script" | "style") {
let openTag;
let code;
switch (tag) {
case "template":
openTag = "<template>";
code = HLJS.highlight(str, { language: "xml" }).value;
break;
case "script":
openTag = '<script lang="ts" setup>';
code = HLJS.highlight(str, { language: "typescript" }).value;
break;
case "style":
openTag = '<style lang="postcss" scoped>';
code = HLJS.highlight(str, { language: "scss" }).value;
break;
}
const openTagHtml = HLJS.highlight(openTag, { language: "xml" }).value;
const closeTagHtml = HLJS.highlight(`</${tag}>`, { language: "xml" }).value;
return `${openTagHtml}${copyable(code)}${closeTagHtml}`;
}
function copyable(code: string) {
return `<div class="copyable">${code}</div>`;
}

View File

@@ -56,7 +56,6 @@ export function getFilterIcon(filter: Filter | undefined) {
}
export function parseDateTime(dateTime: string) {
dateTime = dateTime.replace(/(-|\.\d{3})/g, ""); // Allow toISOString() date-time format
const date = utcParse("%Y%m%dT%H:%M:%SZ")(dateTime);
if (date === null) {
throw new RangeError(

View File

@@ -115,15 +115,6 @@ export type XenApiVmMetrics = XenApiRecord;
export type XenApiVmGuestMetrics = XenApiRecord;
export interface XenApiTask extends XenApiRecord {
name_label: string;
resident_on: string;
created: string;
finished: string;
status: string;
progress: number;
}
type WatchCallbackResult = {
id: string;
class: ObjectType;

View File

@@ -36,14 +36,12 @@
"log-out": "Log out",
"login": "Login",
"migrate": "Migrate",
"name": "Name",
"network": "Network",
"network-download": "Download",
"network-throughput": "Network throughput",
"network-upload": "Upload",
"news": "News",
"news-name": "{name} news",
"object": "Object",
"object-not-found": "Object {id} can't be found…",
"or": "Or",
"page-not-found": "This page is not to be found…",
@@ -55,7 +53,6 @@
"property": "Property",
"ram-usage": "RAM usage",
"reboot": "Reboot",
"reformat": "Reformat",
"relative-time": {
"day": "1 day | {n} days",
"future": "In {str}",
@@ -68,7 +65,6 @@
"year": "1 year | {n} years"
},
"resume": "Resume",
"save": "Save",
"send-us-feedback": "Send us feedback",
"settings": "Settings",
"shutdown": "Shutdown",
@@ -85,12 +81,6 @@
"suspend": "Suspend",
"switch-theme": "Switch theme",
"system": "System",
"task": {
"estimated-end": "Estimated end",
"page-title": "Tasks | (1) Tasks | ({n}) Tasks",
"progress": "Progress",
"started": "Started"
},
"tasks": "Tasks",
"theme-auto": "Auto",
"theme-dark": "Dark",

View File

@@ -36,14 +36,12 @@
"log-out": "Se déconnecter",
"login": "Connexion",
"migrate": "Migrer",
"name": "Nom",
"network": "Réseau",
"network-download": "Descendant",
"network-throughput": "Débit du réseau",
"network-upload": "Montant",
"news": "Actualités",
"news-name": "Actualités {name}",
"object": "Objet",
"object-not-found": "L'objet {id} est introuvable…",
"or": "Ou",
"page-not-found": "Cette page est introuvable…",
@@ -55,7 +53,6 @@
"property": "Propriété",
"ram-usage": "Utilisation de la RAM",
"reboot": "Redémarrer",
"reformat": "Reformater",
"relative-time": {
"day": "1 jour | {n} jours",
"future": "Dans {str}",
@@ -68,7 +65,6 @@
"year": "1 an | {n} ans"
},
"resume": "Reprendre",
"save": "Enregistrer",
"send-us-feedback": "Envoyez-nous vos commentaires",
"settings": "Paramètres",
"shutdown": "Arrêter",
@@ -85,12 +81,6 @@
"suspend": "Suspendre",
"switch-theme": "Changer de thème",
"system": "Système",
"task": {
"estimated-end": "Fin estimée",
"page-title": "Tâches | (1) Tâches | ({n}) Tâches",
"progress": "Progression",
"started": "Démarré"
},
"tasks": "Tâches",
"theme-auto": "Auto",
"theme-dark": "Sombre",

View File

@@ -3,11 +3,13 @@ import { createApp } from "vue";
import App from "@/App.vue";
import i18n from "@/i18n";
import router from "@/router";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
const app = createApp(App);
app.use(i18n);
app.use(createPinia());
app.use(router);
app.component("FontAwesomeIcon", FontAwesomeIcon);
app.mount("#root");

View File

@@ -1,7 +0,0 @@
import { defineStore } from "pinia";
import type { XenApiTask } from "@/libs/xen-api";
import { createRecordContext } from "@/stores/index";
export const useTaskStore = defineStore("task", () =>
createRecordContext<XenApiTask>("task")
);

View File

@@ -10,7 +10,6 @@ import { useHostStore } from "@/stores/host.store";
import { usePoolStore } from "@/stores/pool.store";
import { useRecordsStore } from "@/stores/records.store";
import { useSrStore } from "@/stores/storage.store";
import { useTaskStore } from "@/stores/task.store";
import { useVmGuestMetricsStore } from "@/stores/vm-guest-metrics.store";
import { useVmMetricsStore } from "@/stores/vm-metrics.store";
import { useVmStore } from "@/stores/vm.store";
@@ -87,9 +86,6 @@ export const useXenApiStore = defineStore("xen-api", () => {
srStore.init(),
]);
const taskStore = useTaskStore();
taskStore.init();
const consoleStore = useConsoleStore();
consoleStore.init();
}

View File

@@ -126,7 +126,6 @@ onMounted(() => {
.item {
margin: 0;
padding: 0.5rem;
overflow: hidden;
}
@media (min-width: 768px) {

View File

@@ -1,82 +1 @@
<template>
<UiCard>
<UiTitle class="title-with-counter" type="h4">
{{ $t("tasks") }}
<UiCounter :value="pendingTasks.length" color="info" />
</UiTitle>
<TasksTable :pending-tasks="pendingTasks" :finished-tasks="finishedTasks" />
<UiSpinner class="loader" v-if="!isReady" />
</UiCard>
</template>
<script lang="ts" setup>
import TasksTable from "@/components/tasks/TasksTable.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCounter from "@/components/ui/UiCounter.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import useArrayRemovedItemsHistory from "@/composables/array-removed-items-history.composable";
import useCollectionFilter from "@/composables/collection-filter.composable";
import useCollectionSorter from "@/composables/collection-sorter.composable";
import useFilteredCollection from "@/composables/filtered-collection.composable";
import useSortedCollection from "@/composables/sorted-collection.composable";
import type { XenApiTask } from "@/libs/xen-api";
import { useTaskStore } from "@/stores/task.store";
import { useTitle } from "@vueuse/core";
import { storeToRefs } from "pinia";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
const { allRecords, isReady } = storeToRefs(useTaskStore());
const { t } = useI18n();
const { compareFn } = useCollectionSorter<XenApiTask>({
initialSorts: ["-created"],
});
const allTasks = useSortedCollection(allRecords, compareFn);
const { predicate } = useCollectionFilter({
initialFilters: ["!name_label:|(SR.scan host.call_plugin)", "status:pending"],
});
const pendingTasks = useFilteredCollection<XenApiTask>(allTasks, predicate);
const finishedTasks = useArrayRemovedItemsHistory(
allTasks,
(task) => task.uuid,
{
limit: 50,
onRemove: (tasks) =>
tasks.map((task) => ({
...task,
finished: new Date().toISOString(),
})),
}
);
useTitle(
computed(() => t("task.page-title", { n: pendingTasks.value.length }))
);
</script>
<style lang="postcss" scoped>
.title-with-counter {
display: flex;
align-items: center;
margin-bottom: 1rem;
gap: 0.5rem;
.ui-counter {
font-size: 1.4rem;
}
}
.loader {
color: var(--color-extra-blue-base);
display: block;
font-size: 4rem;
margin: 2rem auto 0;
}
</style>
<template>Tasks (coming soon)</template>

View File

@@ -67,44 +67,38 @@ ${pkg.name} v${pkg.version}`
// sequence path of the current call
const callPath = []
let url
let { token } = opts
if (opts.url !== '') {
url = new URL(opts.url)
const { username } = url
if (username !== '') {
token = username
url.username = ''
}
} else {
url = new URL('https://localhost/')
if (opts.host !== '') {
url.host = opts.host
} else {
const { hostname = 'localhost', port } = config?.http?.listen?.https ?? {}
url.hostname = hostname
url.port = port
}
}
url = new URL('/api/v1', url)
const baseRequest = {
headers: {
'content-type': 'application/json',
cookie: `authenticationToken=${token}`,
},
method: 'POST',
pathname: '/api/v1',
rejectUnauthorized: false,
}
let { token } = opts
if (opts.url !== '') {
const { protocol, host, username } = new URL(opts.url)
Object.assign(baseRequest, { protocol, host })
if (username !== '') {
token = username
}
} else {
baseRequest.protocol = 'https:'
if (opts.host !== '') {
baseRequest.host = opts.host
} else {
const { hostname = 'localhost', port } = config?.http?.listen?.https ?? {}
baseRequest.hostname = hostname
baseRequest.port = port
}
}
baseRequest.headers.cookie = `authenticationToken=${token}`
const call = async ({ method, params }) => {
if (callPath.length !== 0) {
process.stderr.write(`\n${colors.bold(`--- call #${callPath.join('.')}`)} ---\n\n`)
}
const response = await hrp(url, {
...baseRequest,
const response = await hrp.post(baseRequest, {
body: format.request(0, method, params),
})

View File

@@ -32,7 +32,7 @@
"content-type": "^1.0.4",
"cson-parser": "^4.0.7",
"getopts": "^2.2.3",
"http-request-plus": "^1.0.0",
"http-request-plus": "^0.14.0",
"json-rpc-protocol": "^0.13.1",
"promise-toolbox": "^0.21.0",
"pumpify": "^2.0.1",

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@xen-orchestra/proxy",
"version": "0.26.13",
"version": "0.26.10",
"license": "AGPL-3.0-or-later",
"description": "XO Proxy used to remotely execute backup jobs",
"keywords": [
@@ -32,7 +32,7 @@
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.4",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.29.6",
"@xen-orchestra/backups": "^0.29.5",
"@xen-orchestra/fs": "^3.3.1",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/mixin": "^0.1.0",
@@ -46,7 +46,7 @@
"get-stream": "^6.0.0",
"getopts": "^2.2.3",
"golike-defer": "^0.5.1",
"http-server-plus": "^1.0.0",
"http-server-plus": "^0.12.0",
"http2-proxy": "^5.0.53",
"json-rpc-protocol": "^0.13.1",
"jsonrpc-websocket-client": "^0.7.2",
@@ -60,7 +60,7 @@
"source-map-support": "^0.5.16",
"stoppable": "^1.0.6",
"xdg-basedir": "^5.1.0",
"xen-api": "^1.2.5",
"xen-api": "^1.2.2",
"xo-common": "^0.8.0"
},
"devDependencies": {

View File

@@ -35,7 +35,7 @@
"form-data": "^4.0.0",
"fs-extra": "^11.1.0",
"get-stream": "^6.0.0",
"http-request-plus": "^1.0.0",
"http-request-plus": "^0.14.0",
"human-format": "^1.0.0",
"lodash": "^4.17.4",
"pretty-ms": "^7.0.0",
@@ -43,7 +43,7 @@
"pw": "^0.0.4",
"xdg-basedir": "^4.0.0",
"xo-lib": "^0.11.1",
"xo-vmdk-to-vhd": "^2.5.3"
"xo-vmdk-to-vhd": "^2.5.2"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -230,15 +230,10 @@ export async function upload(args) {
)
formData.append('file', input, { filename: 'file', knownLength: length })
try {
const response = await hrp(url.toString(), { body: formData, headers: formData.getHeaders(), method: 'POST' })
return await response.text()
return await hrp.post(url.toString(), { body: formData, headers: formData.getHeaders() }).readAll('utf-8')
} catch (e) {
console.log('ERROR', e)
const { response } = e
if (response !== undefined) {
console.log('ERROR content', await response.text())
}
console.log('ERROR content', await e.response.readAll('utf-8'))
throw e
}
}

View File

@@ -4,6 +4,9 @@ import { FOOTER_SIZE } from 'vhd-lib/_constants.js'
import { notEqual, strictEqual } from 'node:assert'
import { unpackFooter, unpackHeader } from 'vhd-lib/Vhd/_utils.js'
import { VhdAbstract } from 'vhd-lib'
import { createLogger } from '@xen-orchestra/log'
const { debug } = createLogger('xen-orchestra:vmware-explorer:vhdesxisesparse')
// from https://github.com/qemu/qemu/commit/98eb9733f4cf2eeab6d12db7e758665d2fd5367b#
@@ -88,6 +91,9 @@ export default class VhdEsxiSeSparse extends VhdAbstract {
async readHeaderAndFooter() {
const buffer = await this.#read(0, 2048)
strictEqual(buffer.readBigInt64LE(0), 0xcafebaben)
for (let i = 0; i < 2048 / 8; i++) {
debug(i, '> ', buffer.readBigInt64LE(8 * i).toString(16), buffer.readBigInt64LE(8 * i))
}
strictEqual(readInt64(buffer, 1), 0x200000001) // version 2.1
@@ -98,6 +104,14 @@ export default class VhdEsxiSeSparse extends VhdAbstract {
const grain_tables_size = readInt64(buffer, 19)
this.#grainOffset = readInt64(buffer, 24)
debug({
capacity,
grain_size,
grain_tables_offset,
grain_tables_size,
grainSize: this.#grainSize,
})
this.#grainSize = grain_size * 512 // 8 sectors / 4KB default
this.#grainTableOffset = grain_tables_offset * 512
this.#grainTableSize = grain_tables_size * 512
@@ -112,10 +126,12 @@ export default class VhdEsxiSeSparse extends VhdAbstract {
}
async readBlockAllocationTable() {
debug('READ BLOCK ALLOCATION', this.#grainTableSize)
const CHUNK_SIZE = 64 * 512
strictEqual(this.#grainTableSize % CHUNK_SIZE, 0)
debug(' will read ', this.#grainTableSize / CHUNK_SIZE, 'table')
for (let chunkIndex = 0, grainIndex = 0; chunkIndex < this.#grainTableSize / CHUNK_SIZE; chunkIndex++) {
process.stdin.write('.')
const start = chunkIndex * CHUNK_SIZE + this.#grainTableOffset
@@ -130,11 +146,15 @@ export default class VhdEsxiSeSparse extends VhdAbstract {
break
}
if (entry > 3n) {
const intIndex = +(((entry & 0x0fff000000000000n) >> 48n) | ((entry & 0x0000ffffffffffffn) << 12n))
const pos = intIndex * this.#grainSize + CHUNK_SIZE * chunkIndex + this.#grainOffset
debug({ indexInChunk, grainIndex, intIndex, pos })
this.#grainMap.set(grainIndex)
grainIndex++
}
}
}
debug('found', this.#grainMap.size)
// read grain directory and the grain tables
const nbBlocks = this.header.maxTableEntries

View File

@@ -0,0 +1,5 @@
import Esxi from './esxi.mjs'
import openDeltaVmdkasVhd from './openDeltaVmdkAsVhd.mjs'
import VhdEsxiRaw from './VhdEsxiRaw.mjs'
export { openDeltaVmdkasVhd, Esxi, VhdEsxiRaw }

View File

@@ -4,8 +4,9 @@
"version": "0.0.3",
"name": "@xen-orchestra/vmware-explorer",
"dependencies": {
"@vates/task": "^0.0.1",
"@vates/read-chunk": "^1.0.1",
"@vates/task": "^0.0.1",
"@xen-orchestra/log": "^0.6.0",
"lodash": "^4.17.21",
"node-fetch": "^3.3.0",
"node-vsphere-soap": "^0.0.2-5",

View File

@@ -15,7 +15,7 @@
"node": ">=14"
},
"peerDependencies": {
"xen-api": "^1.2.5"
"xen-api": "^1.2.2"
},
"scripts": {
"postversion": "npm publish --access public"
@@ -26,7 +26,7 @@
"@xen-orchestra/log": "^0.6.0",
"d3-time-format": "^3.0.0",
"golike-defer": "^0.5.1",
"http-request-plus": "^1.0.0",
"http-request-plus": "^0.14.0",
"json-rpc-protocol": "^0.13.2",
"lodash": "^4.17.15",
"promise-toolbox": "^0.21.0",

View File

@@ -1,44 +1,5 @@
# ChangeLog
## **5.79.2** (2023-02-20)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Bug fixes
- [Disk import] Fixes ` Cannot read properties of null (reading "length")` error
- [Continuous Replication] Work-around _premature close_ error
### Released packages
- xen-api 1.2.5
- @xen-orchestra/proxy 0.26.13
- xo-server 5.109.3
## **5.79.1** (2023-02-17)
### Bug fixes
- [Continuous Replication] Fix `VDI_IO_ERROR` when after a VDI has been resized
- [REST API] Fix VDI import
- Fix failing imports (REST API and web UI) [Forum#58146](https://xcp-ng.org/forum/post/58146)
- [Pool/License] Fix license expiration on license binding modal (PR [#6666](https://github.com/vatesfr/xen-orchestra/pull/6666))
- [NBD Backup] Fix VDI not disconnecting from control domain (PR [#6660](https://github.com/vatesfr/xen-orchestra/pull/6660))
- [NBD Backup] Improve performance by avoid unnecessary VDI transfers
- [Home/Pool] Do not check for support on non `XCP-ng` pool (PR [#6661](https://github.com/vatesfr/xen-orchestra/pull/6661))
- [VMDK/OVA import] Fix error importing a VMDK or an OVA generated from XO (PR [#6669](https://github.com/vatesfr/xen-orchestra/pull/6669))
### Released packages
- xen-api 1.2.4
- @vates/nbd-client 1.0.1
- @xen-orchestra/backups 0.29.6
- @xen-orchestra/proxy 0.26.12
- xo-vmdk-to-vhd 2.5.3
- xo-cli 0.14.4
- xo-server 5.109.2
- xo-server-transport-email 0.6.1
- xo-web 5.111.1
## **5.79.0** (2023-01-31)
### Highlights
@@ -97,7 +58,7 @@
## **5.78.0** (2022-12-20)
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Highlights
@@ -121,6 +82,8 @@
## **5.77.2** (2022-12-12)
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
### Bug fixes
- [Backups] Fixes most of the _unexpected number of entries in backup cache_ errors

View File

@@ -11,6 +11,8 @@
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [Continuous Replication] Fix `VDI_IO_ERROR` when after a VDI has been resized
### Packages to release
> When modifying a package, add it here with its release type.
@@ -27,6 +29,7 @@
<!--packages-start-->
- xo-cli minor
- @xen-orchestra/backups patch
- xen-api patch
<!--packages-end-->

View File

@@ -4,7 +4,7 @@ FROM ubuntu:xenial
# https://qastack.fr/programming/25899912/how-to-install-nvm-in-docker
RUN apt-get update
RUN apt-get install -y curl qemu-utils vmdk-stream-converter git libxml2-utils libfuse2 nbdkit
RUN apt-get install -y curl qemu-utils blktap-utils vmdk-stream-converter git libxml2-utils libfuse2 nbdkit
ENV NVM_DIR /usr/local/nvm
RUN mkdir -p /usr/local/nvm
RUN cd /usr/local/nvm

View File

@@ -242,13 +242,10 @@ class StreamNbdParser extends StreamParser {
async *parse() {
yield* this.headers()
// the VHD stream is no longer necessary, destroy it
//
// - not destroying it would leave other writers stuck
// - resuming it would download the whole stream unnecessarily if not other writers
this._stream.destroy()
// put it in free flowing state
// the stream is a fork from the main stream of xapi
// if one of the fork is stopped, all the stream are stopped
this._stream.resume()
yield* this.blocks()
}
}

View File

@@ -1,40 +0,0 @@
'use strict'
/* eslint-env jest */
const fs = require('fs-extra')
const rimraf = require('rimraf')
const tmp = require('tmp')
const { getSyncedHandler } = require('@xen-orchestra/fs')
const { pFromCallback } = require('promise-toolbox')
const { checkFile, createRandomFile, convertFromRawToVhd } = require('./utils')
let tempDir = null
let disposeHandler
beforeEach(async () => {
tempDir = await pFromCallback(cb => tmp.dir(cb))
const d = await getSyncedHandler({ url: `file://${tempDir}` })
disposeHandler = d.dispose
})
afterEach(async () => {
await rimraf(tempDir)
disposeHandler()
})
test('checkFile fails with unvalid VHD file', async () => {
const initalSizeInMB = 4
const rawFileName = `${tempDir}/randomfile`
await createRandomFile(rawFileName, initalSizeInMB)
const vhdFileName = `${tempDir}/vhdFile.vhd`
await convertFromRawToVhd(rawFileName, vhdFileName)
await checkFile(vhdFileName)
const sizeToTruncateInByte = 250000
await fs.truncate(vhdFileName, sizeToTruncateInByte)
await expect(async () => await checkFile(vhdFileName)).rejects.toThrow()
})

View File

@@ -5,7 +5,6 @@ const { pipeline } = require('readable-stream')
const asyncIteratorToStream = require('async-iterator-to-stream')
const execa = require('execa')
const fs = require('fs-extra')
const fsPromise = require('node:fs/promises')
const { randomBytes } = require('crypto')
const createRandomStream = asyncIteratorToStream(function* (size) {
@@ -22,11 +21,7 @@ async function createRandomFile(name, sizeMB) {
exports.createRandomFile = createRandomFile
async function checkFile(vhdName) {
// Since the qemu-img check command isn't compatible with vhd format, we use
// the convert command to do a check by conversion. Indeed, the conversion will
// fail if the source file isn't a proper vhd format.
await execa('qemu-img', ['convert', '-fvpc', '-Oqcow2', vhdName, 'outputFile.qcow2'])
await fsPromise.unlink('./outputFile.qcow2')
await execa('vhd-util', ['check', '-p', '-b', '-t', '-n', vhdName])
}
exports.checkFile = checkFile

View File

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

View File

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

View File

@@ -5,12 +5,13 @@ import ms from 'ms'
import httpRequest from 'http-request-plus'
import map from 'lodash/map'
import noop from 'lodash/noop'
import omit from 'lodash/omit'
import ProxyAgent from 'proxy-agent'
import { coalesceCalls } from '@vates/coalesce-calls'
import { Collection } from 'xo-collection'
import { EventEmitter } from 'events'
import { Index } from 'xo-collection/index'
import { cancelable, defer, fromCallback, ignoreErrors, pDelay, pRetry, pTimeout } from 'promise-toolbox'
import { cancelable, defer, fromCallback, fromEvents, ignoreErrors, pDelay, pRetry, pTimeout } from 'promise-toolbox'
import { limitConcurrency } from 'limit-concurrency-decorator'
import autoTransport from './transports/auto'
@@ -90,7 +91,6 @@ export class Xapi extends EventEmitter {
this._callTimeout = makeCallSetting(opts.callTimeout, 60 * 60 * 1e3) // 1 hour but will be reduced in the future
this._httpInactivityTimeout = opts.httpInactivityTimeout ?? 5 * 60 * 1e3 // 5 mins
this._eventPollDelay = opts.eventPollDelay ?? 60 * 1e3 // 1 min
this._ignorePrematureClose = opts.ignorePrematureClose ?? true
this._pool = null
this._readOnly = Boolean(opts.readOnly)
this._RecordsByType = { __proto__: null }
@@ -152,14 +152,13 @@ export class Xapi extends EventEmitter {
this._resolveObjectsFetched = resolve
})
this._eventWatchers = { __proto__: null }
this._taskWatchers = undefined // set in _watchEvents
this._taskWatchers = { __proto__: null }
this._watchedTypes = undefined
const { watchEvents } = opts
if (watchEvents !== false) {
if (Array.isArray(watchEvents)) {
this._watchedTypes = watchEvents
}
this.watchEvents()
}
}
@@ -392,7 +391,7 @@ export class Xapi extends EventEmitter {
const response = await this._addSyncStackTrace(
pRetry(
async () =>
httpRequest(url, {
httpRequest($cancelToken, url.href, {
rejectUnauthorized: !this._allowUnauthorized,
// this is an inactivity timeout (unclear in Node doc)
@@ -403,8 +402,6 @@ export class Xapi extends EventEmitter {
// Support XS <= 6.5 with Node => 12
minVersion: 'TLSv1',
agent: this.httpAgent,
signal: $cancelToken,
}),
{
when: { code: 302 },
@@ -413,7 +410,7 @@ export class Xapi extends EventEmitter {
if (response === undefined) {
throw error
}
response.destroy()
response.cancel()
url = await this._replaceHostAddressInUrl(new URL(response.headers.location, url))
},
}
@@ -469,45 +466,40 @@ export class Xapi extends EventEmitter {
url.search = new URLSearchParams(query)
await this._setHostAddressInUrl(url, host)
const doRequest = (url, opts) =>
httpRequest(url, {
agent: this.httpAgent,
const doRequest = httpRequest.put.bind(undefined, $cancelToken, {
agent: this.httpAgent,
body,
headers,
method: 'PUT',
rejectUnauthorized: !this._allowUnauthorized,
signal: $cancelToken,
body,
headers,
rejectUnauthorized: !this._allowUnauthorized,
// this is an inactivity timeout (unclear in Node doc)
timeout: this._httpInactivityTimeout,
// this is an inactivity timeout (unclear in Node doc)
timeout: this._httpInactivityTimeout,
// Support XS <= 6.5 with Node => 12
minVersion: 'TLSv1',
...opts,
})
const dummyUrl = new URL(url)
dummyUrl.searchParams.delete('task_id')
// Support XS <= 6.5 with Node => 12
minVersion: 'TLSv1',
})
// if body is a stream, sends a dummy request to probe for a redirection
// before consuming body
const response = await this._addSyncStackTrace(
isStream
? doRequest(dummyUrl, {
? doRequest(url.href, {
body: '',
// omit task_id because this request will fail on purpose
query: 'task_id' in query ? omit(query, 'task_id') : query,
maxRedirects: 0,
}).then(
response => {
response.destroy()
return doRequest(url)
response.cancel()
return doRequest(url.href)
},
async error => {
let response
if (error != null && (response = error.response) != null) {
response.destroy()
response.cancel()
const {
headers: { location },
@@ -517,14 +509,14 @@ export class Xapi extends EventEmitter {
// ensure the original query is sent
const newUrl = new URL(location, url)
newUrl.searchParams.set('task_id', query.task_id)
return doRequest(await this._replaceHostAddressInUrl(newUrl))
return doRequest((await this._replaceHostAddressInUrl(newUrl)).href)
}
}
throw error
}
)
: doRequest(url)
: doRequest(url.href)
)
if (pTaskResult !== undefined) {
@@ -544,30 +536,16 @@ export class Xapi extends EventEmitter {
}
}
try {
const { req } = response
if (!req.finished) {
await new Promise((resolve, reject) => {
req.on('finish', resolve)
response.on('error', reject)
})
}
const { req } = response
if (!req.finished) {
await fromEvents(req, ['close', 'finish'])
}
if (useHack) {
response.destroy()
} else {
// consume the response
response.resume()
await new Promise((resolve, reject) => {
response.on('end', resolve).on('error', reject)
})
}
} catch (error) {
if (this._ignorePrematureClose && error.code === 'ERR_STREAM_PREMATURE_CLOSE') {
console.warn(this._humanId, 'Xapi#putResource', pathname, error)
} else {
throw error
}
if (useHack) {
response.cancel()
} else {
// consume the response
response.resume()
}
return pTaskResult
@@ -981,7 +959,7 @@ export class Xapi extends EventEmitter {
}
const taskWatchers = this._taskWatchers
const taskWatcher = taskWatchers?.[ref]
const taskWatcher = taskWatchers[ref]
if (taskWatcher !== undefined) {
const result = getTaskResult(object)
if (result !== undefined) {
@@ -1083,7 +1061,7 @@ export class Xapi extends EventEmitter {
}
const taskWatchers = this._taskWatchers
const taskWatcher = taskWatchers?.[ref]
const taskWatcher = taskWatchers[ref]
if (taskWatcher !== undefined) {
const error = new Error('task has been destroyed before completion')
error.task = object
@@ -1101,13 +1079,6 @@ export class Xapi extends EventEmitter {
_watchEvents = coalesceCalls(this._watchEvents)
// eslint-disable-next-line no-dupe-class-members
async _watchEvents() {
{
const watchedTypes = this._watchedTypes
if (this._taskWatchers === undefined && (watchedTypes === undefined || watchedTypes.includes('task'))) {
this._taskWatchers = { __proto__: null }
}
}
// eslint-disable-next-line no-labels
mainLoop: while (true) {
if (this._resolveObjectsFetched === undefined) {

View File

@@ -1,5 +1,6 @@
import httpRequestPlus from 'http-request-plus'
import { format, parse } from 'json-rpc-protocol'
import { join } from 'path'
import XapiError from '../_XapiError'
@@ -7,37 +8,41 @@ import UnsupportedTransport from './_UnsupportedTransport'
// https://github.com/xenserver/xenadmin/blob/0df39a9d83cd82713f32d24704852a0fd57b8a64/XenModel/XenAPI/Session.cs#L403-L433
export default ({ secureOptions, url, agent }) => {
url = new URL('./jsonrpc', Object.assign(new URL('http://localhost'), url))
return (method, args) =>
httpRequestPlus
.post(url, {
...secureOptions,
body: format.request(0, method, args),
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
pathname: join(url.pathname, 'jsonrpc'),
agent,
})
.readAll('utf8')
.then(
text => {
let response
try {
response = parse(text)
} catch (error) {
throw new UnsupportedTransport()
}
return async function (method, args) {
const res = await httpRequestPlus(url, {
...secureOptions,
body: format.request(0, method, args),
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
method: 'POST',
agent,
}).catch(error => {
console.warn('xen-api/transports/json-rpc', error)
if (response.type === 'response') {
return response.result
}
throw new UnsupportedTransport()
})
throw XapiError.wrap(response.error)
},
error => {
if (error.response !== undefined) {
// HTTP error
throw new UnsupportedTransport()
}
const text = await res.text()
let response
try {
response = parse(text)
} catch (error) {
throw new UnsupportedTransport()
}
if (response.type === 'response') {
return response.result
}
throw XapiError.wrap(response.error)
}
throw error
}
)
}

View File

@@ -4,6 +4,7 @@ import { createReadStream, createWriteStream, readFileSync } from 'fs'
import { PassThrough, pipeline } from 'stream'
import { stat } from 'fs/promises'
import chalk from 'chalk'
import execPromise from 'exec-promise'
import forEach from 'lodash/forEach.js'
import fromCallback from 'promise-toolbox/fromCallback'
import getKeys from 'lodash/keys.js'
@@ -23,7 +24,6 @@ import XoLib from 'xo-lib'
// -------------------------------------------------------------------
import * as config from './config.mjs'
import { inspect } from 'util'
const Xo = XoLib.default
@@ -46,47 +46,34 @@ async function connect() {
return xo
}
async function parseRegisterArgs(args, tokenDescription, acceptToken = false) {
async function parseRegisterArgs(args) {
const {
allowUnauthorized,
expiresIn,
token,
_: opts,
} = getopts(args, {
alias: {
allowUnauthorized: 'au',
token: 't',
},
boolean: ['allowUnauthorized'],
stopEarly: true,
string: ['expiresIn', 'token'],
})
const result = {
allowUnauthorized,
expiresIn: expiresIn || undefined,
url: opts[0],
}
if (token !== '') {
if (!acceptToken) {
// eslint-disable-next-line no-throw-literal
throw '`token` option is not accepted by this command'
}
result.token = token
} else {
const [
,
_: [
url,
email,
password = await new Promise(function (resolve) {
process.stdout.write('Password: ')
pw(resolve)
}),
] = opts
result.token = await _createToken({ ...result, description: tokenDescription, email, password })
}
],
} = getopts(args, {
alias: {
allowUnauthorized: 'au',
},
boolean: ['allowUnauthorized'],
stopEarly: true,
string: ['expiresIn'],
})
return result
return {
allowUnauthorized,
email,
expiresIn: expiresIn || undefined,
password,
url,
}
}
async function _createToken({ allowUnauthorized, description, email, expiresIn, password, url }) {
@@ -206,20 +193,16 @@ const help = wrap(
(function (pkg) {
return `Usage:
$name --register [--allowUnauthorized] [--expiresIn <duration>] <XO-Server URL> <username> [<password>]
$name --register [--allowUnauthorized] [--expiresIn <duration>] --token <token> <XO-Server URL>
$name --register [--allowUnauthorized] [--expiresIn duration] <XO-Server URL> <username> [<password>]
Registers the XO instance to use.
--allowUnauthorized, --au
Accept invalid certificate (e.g. self-signed).
--expiresIn <duration>
--expiresIn duration
Can be used to change the validity duration of the
authorization token (default: one month).
--token <token>
An authentication token to use instead of username/password.
$name --createToken <params>…
Create an authentication token for XO API.
@@ -311,8 +294,10 @@ async function main(args) {
COMMANDS.help = help
async function createToken(args) {
const { token } = await parseRegisterArgs(args, 'xo-cli --createToken')
const opts = await parseRegisterArgs(args)
opts.description = 'xo-cli --createToken'
const token = await _createToken(opts)
console.warn('Authentication token created')
console.warn()
console.log(token)
@@ -320,11 +305,13 @@ async function createToken(args) {
COMMANDS.createToken = createToken
async function register(args) {
const opts = await parseRegisterArgs(args, 'xo-cli --register', true)
const opts = await parseRegisterArgs(args)
opts.description = 'xo-cli --register'
await config.set({
allowUnauthorized: opts.allowUnauthorized,
server: opts.url,
token: opts.token,
token: await _createToken(opts),
})
}
COMMANDS.register = register
@@ -404,20 +391,16 @@ async function listObjects(args) {
const filter = args.length ? parseParameters(args) : undefined
const xo = await connect()
try {
const objects = await xo.call('xo.getAllObjects', { filter })
const objects = await xo.call('xo.getAllObjects', { filter })
const stdout = process.stdout
stdout.write('[\n')
const keys = Object.keys(objects)
for (let i = 0, n = keys.length; i < n; ) {
stdout.write(JSON.stringify(filterProperties(objects[keys[i]]), null, 2))
stdout.write(++i < n ? ',\n' : '\n')
}
stdout.write(']\n')
} finally {
await xo.close()
const stdout = process.stdout
stdout.write('[\n')
const keys = Object.keys(objects)
for (let i = 0, n = keys.length; i < n; ) {
stdout.write(JSON.stringify(filterProperties(objects[keys[i]]), null, 2))
stdout.write(++i < n ? ',\n' : '\n')
}
stdout.write(']\n')
}
COMMANDS.listObjects = listObjects
@@ -486,15 +469,14 @@ async function call(args) {
noop
)
const response = await hrp(url, {
...httpOptions,
body: input,
headers: {
'content-length': length,
},
method: 'POST',
})
return response.text()
return hrp
.post(url, httpOptions, {
body: input,
headers: {
'content-length': length,
},
})
.readAll('utf-8')
}
}
@@ -507,25 +489,4 @@ COMMANDS.call = call
// ===================================================================
// don't call process.exit() to avoid truncated output
main(process.argv.slice(2)).then(
result => {
if (result !== undefined) {
if (Number.isInteger(result)) {
process.exitCode = result
} else {
const { stdout } = process
stdout.write(typeof result === 'string' ? result : inspect(result))
stdout.write('\n')
}
}
},
error => {
const { stderr } = process
stderr.write(chalk.bold.red('✖'))
stderr.write(' ')
stderr.write(typeof error === 'string' ? error : inspect(error))
stderr.write('\n')
process.exitCode = 1
}
)
execPromise(main)

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-cli",
"version": "0.14.4",
"version": "0.14.3",
"license": "AGPL-3.0-or-later",
"description": "Basic CLI for Xen-Orchestra",
"keywords": [
@@ -30,9 +30,10 @@
},
"dependencies": {
"chalk": "^5.0.1",
"exec-promise": "^0.7.0",
"fs-extra": "^11.1.0",
"getopts": "^2.3.0",
"http-request-plus": "^1.0.0",
"http-request-plus": "^0.14.0",
"human-format": "^1.0.0",
"lodash": "^4.17.4",
"micromatch": "^4.0.2",

View File

@@ -195,7 +195,7 @@ class BackupReportsXoPlugin {
this._xo = xo
this._eventListener = async (...args) => {
try {
await this._report(...args)
this._report(...args)
} catch (error) {
logger.warn(error)
}

View File

@@ -35,6 +35,11 @@ const indexName = (name, index) => {
return name.slice(0, NAME_MAX_LENGTH - suffix.length) + suffix
}
const onRequest = req => {
req.setTimeout(REQUEST_TIMEOUT)
req.on('timeout', req.abort)
}
class Netbox {
#allowUnauthorized
#endpoint
@@ -103,15 +108,15 @@ class Netbox {
const options = {
headers: { 'Content-Type': 'application/json', Authorization: `Token ${this.#token}` },
method,
onRequest,
rejectUnauthorized: !this.#allowUnauthorized,
timeout: REQUEST_TIMEOUT,
}
const httpRequest = async () => {
try {
const response = await this.#xo.httpRequest(url, options)
this.#netboxApiVersion = response.headers['api-version']
const body = await response.text()
const body = await response.readAll()
if (body.length > 0) {
return JSON.parse(body)
}
@@ -122,7 +127,7 @@ class Netbox {
body: dataDebug,
}
try {
const body = await error.response.text()
const body = await error.response.readAll()
if (body.length > 0) {
error.data.error = JSON.parse(body)
}

View File

@@ -689,7 +689,7 @@ ${entriesWithMissingStats.map(({ listItem }) => listItem).join('\n')}`
payload.vm_uuid = xapiObject.uuid
}
// JSON is not well formed, can't use the default node parser
return JSON5.parse(await (await xapi.getResource('/rrd_updates', payload)).text())
return JSON5.parse(await (await xapi.getResource('/rrd_updates', payload)).readAll())
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-transport-email",
"version": "0.6.1",
"version": "0.6.0",
"license": "AGPL-3.0-or-later",
"description": "xo-server plugin to send emails",
"keywords": [

View File

@@ -121,7 +121,7 @@ class XoServerIcinga2 {
exit_status: icinga2Status,
}),
})
.then(response => response.text())
.readAll()
}
}

View File

@@ -38,7 +38,10 @@ class XoServerHooks {
body: JSON.stringify({ ...data, type }),
headers: { 'Content-Type': 'application/json' },
method: 'POST',
timeout: 1e4,
onRequest: req => {
req.setTimeout(1e4)
req.on('timeout', req.abort)
},
})
}

View File

@@ -132,10 +132,7 @@ port = 80
#
# This breaks a number of XO use cases, for instance uploading a VDI via the
# REST API, therefore it's changed to 1 day.
#
# Completely disabled for now because it appears to be broken:
# https://github.com/nodejs/node/issues/46574
requestTimeout = 0
requestTimeout = 86400000
[http.mounts]
'/' = '../xo-web/dist'

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.109.3",
"version": "5.109.0",
"license": "AGPL-3.0-or-later",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -41,7 +41,7 @@
"@vates/predicates": "^1.1.0",
"@vates/read-chunk": "^1.0.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.29.6",
"@xen-orchestra/backups": "^0.29.5",
"@xen-orchestra/cron": "^1.0.6",
"@xen-orchestra/defined": "^0.0.1",
"@xen-orchestra/emit-async": "^1.0.0",
@@ -82,7 +82,7 @@
"helmet": "^3.9.0",
"highland": "^2.11.1",
"http-proxy": "^1.16.2",
"http-request-plus": "^1.0.0",
"http-request-plus": "^0.14.0",
"http-server-plus": "^1.0.0",
"human-format": "^1.0.0",
"iterable-backoff": "^0.1.0",
@@ -131,12 +131,12 @@
"vhd-lib": "^4.2.1",
"ws": "^8.2.3",
"xdg-basedir": "^5.1.0",
"xen-api": "^1.2.5",
"xen-api": "^1.2.2",
"xo-acl-resolver": "^0.4.1",
"xo-collection": "^0.5.0",
"xo-common": "^0.8.0",
"xo-remote-parser": "^0.9.2",
"xo-vmdk-to-vhd": "^2.5.3"
"xo-vmdk-to-vhd": "^2.5.2"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -11,7 +11,6 @@ import { vmdkToVhd } from 'xo-vmdk-to-vhd'
import { VDI_FORMAT_VHD, VDI_FORMAT_RAW } from '../xapi/index.mjs'
import { parseSize } from '../utils.mjs'
import { readChunk } from '@vates/read-chunk'
const log = createLogger('xo:disk')
@@ -226,14 +225,6 @@ async function handleImport(req, res, { type, name, description, vmdkData, srId,
)
try {
await vdi.$importContent(vhdStream, { format: diskFormat })
let buffer
const CHUNK_SIZE = 1024 * 1024
// drain remaining content ( padding .header)
// didn't succeed to ensure the stream is completly consumed with resume/finished
do {
buffer = await readChunk(part, CHUNK_SIZE)
} while (buffer?.length === CHUNK_SIZE)
res.end(format.response(0, vdi.$id))
} catch (e) {
await vdi.$destroy()

View File

@@ -1133,7 +1133,7 @@ async function handleExport(req, res, { xapi, vmRef, compress, format = 'xva' })
const stream =
format === 'ova' ? await xapi.exportVmOva(vmRef) : await xapi.VM_export(FAIL_ON_QUEUE, vmRef, { compress })
res.on('close', () => stream.destroy())
res.on('close', () => stream.cancel())
// Remove the filename as it is already part of the URL.
stream.headers['content-disposition'] = 'attachment'

View File

@@ -263,7 +263,7 @@ export default class XapiStats {
start: timestamp,
},
})
.then(response => response.text().then(JSON5.parse))
.then(response => response.readAll().then(JSON5.parse))
}
// To avoid multiple requests, we keep a cash for the stats and

View File

@@ -65,7 +65,11 @@ export default {
async _getXenUpdates() {
const response = await this.xo.httpRequest('http://updates.xensource.com/XenServer/updates.xml')
const data = parseXml(await response.buffer()).patchdata
if (response.statusCode !== 200) {
throw new Error('cannot fetch patches list from Citrix')
}
const data = parseXml(await response.readAll()).patchdata
const patches = { __proto__: null }
forEach(data.patches.patch, patch => {

View File

@@ -26,11 +26,13 @@ export default class Http {
})
}
httpRequest(url, opts) {
return hrp(url, {
...opts,
agent: this._agent,
})
httpRequest(...args) {
return hrp(
{
agent: this._agent,
},
...args
)
}
// Inject the proxy into the environnement, it will be automatically used by `_agent` and by most libs (e.g `axios`)

View File

@@ -6,10 +6,8 @@ import { Task } from '@xen-orchestra/mixins/Tasks.mjs'
import { v4 as generateUuid } from 'uuid'
import { VDI_FORMAT_VHD } from '@xen-orchestra/xapi'
import asyncMapSettled from '@xen-orchestra/async-map/legacy.js'
import Esxi from '@xen-orchestra/vmware-explorer/esxi.mjs'
import openDeltaVmdkasVhd from '@xen-orchestra/vmware-explorer/openDeltaVmdkAsVhd.mjs'
import { openDeltaVmdkasVhd, Esxi, VhdEsxiRaw } from '@xen-orchestra/vmware-explorer'
import OTHER_CONFIG_TEMPLATE from '../xapi/other-config-template.mjs'
import VhdEsxiRaw from '@xen-orchestra/vmware-explorer/VhdEsxiRaw.mjs'
export default class MigrateVm {
constructor(app) {

View File

@@ -419,8 +419,6 @@ export default class Proxy {
async callProxyMethod(id, method, params, { assertType = 'scalar' } = {}) {
const proxy = await this._getProxy(id)
const url = new URL('https://localhost/api/v1')
const request = {
body: format.request(0, method, params),
headers: {
@@ -428,12 +426,14 @@ export default class Proxy {
Cookie: cookie.serialize('authenticationToken', proxy.authenticationToken),
},
method: 'POST',
pathname: '/api/v1',
protocol: 'https:',
rejectUnauthorized: false,
timeout: this._app.config.getDuration('xo-proxy.callTimeout'),
}
if (proxy.address !== undefined) {
url.host = proxy.address
request.host = proxy.address
} else {
const vm = this._app.getXapi(proxy.vmUuid).getObjectByUuid(proxy.vmUuid)
@@ -444,10 +444,11 @@ export default class Proxy {
throw error
}
url.hostname = address.includes(':') ? `[${address}]` : address
// use hostname field to avoid issues with IPv6 addresses
request.hostname = address
}
const response = await hrp(url, request)
const response = await hrp(request)
const authenticationToken = parseSetCookie(response, {
map: true,

View File

@@ -150,32 +150,6 @@ export default class RestApi {
)
})
)
// should go before routes /:collection/:object because they will match but
// will not work due to the extension being included in the object identifer
api.get(
'/:collection(vdis|vdi-snapshots)/:object.:format(vhd|raw)',
wrap(async (req, res) => {
const stream = await req.xapiObject.$exportContent({ format: req.params.format })
stream.headers['content-disposition'] = 'attachment'
res.writeHead(stream.statusCode, stream.statusMessage != null ? stream.statusMessage : '', stream.headers)
await fromCallback(pipeline, stream, res)
})
)
api.get(
'/:collection(vms|vm-snapshots|vm-templates)/:object.xva',
wrap(async (req, res) => {
const stream = await req.xapiObject.$export({ compress: req.query.compress })
stream.headers['content-disposition'] = 'attachment'
res.writeHead(stream.statusCode, stream.statusMessage != null ? stream.statusMessage : '', stream.headers)
await fromCallback(pipeline, stream, res)
})
)
api.get('/:collection/:object', (req, res) => {
res.json(req.xoObject)
})
@@ -199,7 +173,7 @@ export default class RestApi {
)
api.post(
'/:collection(srs)/:object/vdis',
'/srs/:object/vdis',
wrap(async (req, res) => {
const sr = req.xapiObject
req.length = +req.headers['content-length']
@@ -222,5 +196,29 @@ export default class RestApi {
res.sendStatus(200)
})
)
api.get(
'/:collection(vdis|vdi-snapshots)/:object.:format(vhd|raw)',
wrap(async (req, res) => {
const stream = await req.xapiObject.$exportContent({ format: req.params.format })
stream.headers['content-disposition'] = 'attachment'
res.writeHead(stream.statusCode, stream.statusMessage != null ? stream.statusMessage : '', stream.headers)
await fromCallback(pipeline, stream, res)
})
)
api.get(
'/:collection(vms|vm-snapshots|vm-templates)/:object.xva',
wrap(async (req, res) => {
const stream = await req.xapiObject.$export({ compress: req.query.compress })
stream.headers['content-disposition'] = 'attachment'
res.writeHead(stream.statusCode, stream.statusMessage != null ? stream.statusMessage : '', stream.headers)
await fromCallback(pipeline, stream, res)
})
)
}
}

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-vmdk-to-vhd",
"version": "2.5.3",
"version": "2.5.2",
"license": "AGPL-3.0-or-later",
"description": "JS lib reading and writing .vmdk and .ova files",
"keywords": [

View File

@@ -186,11 +186,7 @@ export default class VMDKDirectParser {
const grainPosition = this.grainFileOffsetList[tableIndex] * SECTOR_SIZE
const grainSizeBytes = this.header.grainSizeSectors * SECTOR_SIZE
const lba = this.grainLogicalAddressList[tableIndex] * grainSizeBytes
assert.strictEqual(
grainPosition >= position,
true,
`Grain position ${grainPosition} must be after current position ${position}`
)
assert.strictEqual(grainPosition >= position, true)
await this.virtualBuffer.readChunk(grainPosition - position, `blank from ${position} to ${grainPosition}`)
let grain
if (this.header.flags.hasMarkers) {

View File

@@ -12,7 +12,6 @@ import { vmdkToVhd, readVmdkGrainTable } from '.'
import VMDKDirectParser from './vmdk-read'
import { generateVmdkData } from './vmdk-generate'
import asyncIteratorToStream from 'async-iterator-to-stream'
import fs from 'fs'
const initialDir = process.cwd()
jest.setTimeout(100000)
@@ -37,14 +36,6 @@ function bufferToArray(buffer) {
return res
}
async function checkFile(vhdName) {
// Since the qemu-img check command isn't compatible with vhd format, we use
// the convert command to do a check by conversion. Indeed, the conversion will
// fail if the source file isn't a proper vhd format.
await execa('qemu-img', ['convert', '-fvpc', '-Oqcow2', vhdName, 'outputFile.qcow2'])
await fs.promises.unlink('./outputFile.qcow2')
}
function createFileAccessor(file) {
return async (start, end) => {
if (start < 0 || end < 0) {
@@ -80,7 +71,7 @@ test('VMDK to VHD can convert a random data file with VMDKDirectParser', async (
)
).pipe(createWriteStream(vhdFileName))
await fromEvent(pipe, 'finish')
await checkFile(vhdFileName)
await execa('vhd-util', ['check', '-p', '-b', '-t', '-n', vhdFileName])
await execa('qemu-img', ['convert', '-fvmdk', '-Oraw', vmdkFileName, reconvertedFromVmdk])
await execa('qemu-img', ['convert', '-fvpc', '-Oraw', vhdFileName, reconvertedFromVhd])
await execa('qemu-img', ['compare', inputRawFileName, vhdFileName])

View File

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

View File

@@ -2598,8 +2598,8 @@ export default {
// Original text: "To SR:"
vmImportToSr: 'al SR:',
// Original text: "VM{nVms, plural, one {} other {s}} to import"
vmsToImport: 'VM{nVms, plural, one {} other {s}} para importar',
// Original text: "VMs to import"
vmsToImport: 'VMs para importar',
// Original text: "Reset"
importVmsCleanList: 'Reiniciar',

View File

@@ -909,39 +909,6 @@ export default {
// Original text: "Domain"
remoteSmbPlaceHolderDomain: 'Domaine',
// Original text : "Use HTTPS"
remoteS3LabelUseHttps: 'Utiliser HTTPS',
// Original text : "Allow unauthorized"
remoteS3LabelAllowInsecure: 'Autoriser les connexions non-sécurisées',
// Original text : "AWS S3 endpoint (ex: s3.us-east-2.amazonaws.com)"
remoteS3PlaceHolderEndpoint: 'Point de terminaison AWS S3 (ex: s3.us-east-2.amazonaws.com)',
// Original text : "AWS S3 bucket name"
remoteS3PlaceHolderBucket: 'Nom du bucket AWS S3',
// Original text : "Directory"
remoteS3PlaceHolderDirectory: 'Chemin',
// Original text : "Access key ID"
remoteS3PlaceHolderAccessKeyID: "Clé d'accès",
// Original text : "Paste secret here to change it"
remoteS3PlaceHolderSecret: 'Secret',
// Original text : "Enter your encryption key here (32 characters)"
remoteS3PlaceHolderEncryptionKey: 'Clé de chiffrement (32 caractères)',
// Original text : "Region, leave blank for default"
remoteS3Region: 'Région (optionnel)',
// Original text : "Uncheck if you want HTTP instead of HTTPS"
remoteS3TooltipProtocol: 'Décocher pour utiliser HTTP au lieu de HTTPS',
// Original text : "Check if you want to accept self signed certificates"
remoteS3TooltipAcceptInsecure: 'Cochez pour accepter les certificats auto-signés',
// Original text: "<address>\\<share> *"
remoteSmbPlaceHolderAddressShare: '<adresse>\\<partage>',
@@ -2657,8 +2624,8 @@ export default {
// Original text: "To SR:"
vmImportToSr: 'Sur le SR:',
// Original text: "VM{nVms, plural, one {} other {s}} to import"
vmsToImport: 'VM{nVms, plural, one {} other {s}} à importer',
// Original text: "VMs to import"
vmsToImport: 'VMs à importer',
// Original text: "Reset"
importVmsCleanList: 'Réinitialiser',

View File

@@ -2276,7 +2276,7 @@ export default {
// Original text: 'To SR:'
vmImportToSr: undefined,
// Original text: 'VM{nVms, plural, one {} other {s}} to import'
// Original text: 'VMs to import'
vmsToImport: undefined,
// Original text: 'Reset'

View File

@@ -2490,7 +2490,7 @@ export default {
// Original text: "To SR:"
vmImportToSr: 'Adattárolóra:',
// Original text: "VM{nVms, plural, one {} other {s}} to import"
// Original text: "VMs to import"
vmsToImport: 'Importálandó VPS-el',
// Original text: "Reset"

View File

@@ -3746,8 +3746,8 @@ export default {
// Original text: 'To SR:'
vmImportToSr: 'Per SR:',
// Original text: 'VM{nVms, plural, one {} other {s}} to import'
vmsToImport: 'VM{nVms, plural, one {} other {s}} da importare',
// Original text: 'VMs to import'
vmsToImport: 'VMs da importare',
// Original text: 'Reset'
importVmsCleanList: 'Ripristina',

View File

@@ -2280,8 +2280,8 @@ export default {
// Original text: "To SR:"
vmImportToSr: 'To SR:',
// Original text: "VM{nVms, plural, one {} other {s}} to import"
vmsToImport: 'VM{nVms, plural, one {} other {s}} to import',
// Original text: "VMs to import"
vmsToImport: 'VMs to import',
// Original text: "Reset"
importVmsCleanList: 'Reset',

View File

@@ -2279,8 +2279,8 @@ export default {
// Original text: "To SR:"
vmImportToSr: 'Enviar para SR:',
// Original text: "VM{nVms, plural, one {} other {s}} to import"
vmsToImport: 'Importar VM{nVms, plural, one {} other {s}} ',
// Original text: "VMs to import"
vmsToImport: 'Importar VMs',
// Original text: "Reset"
importVmsCleanList: 'Reiniciar',

View File

@@ -2600,7 +2600,7 @@ export default {
// Original text: "To SR:"
vmImportToSr: 'В SR:',
// Original text: "VM{nVms, plural, one {} other {s}} to import"
// Original text: "VMs to import"
vmsToImport: 'ВМ для импорта',
// Original text: "Reset"

Some files were not shown because too many files have changed in this diff Show More