Compare commits
37 Commits
putResourc
...
cleanup_vm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a15428ac88 | ||
|
|
85a23c68f2 | ||
|
|
c16c1f8eb9 | ||
|
|
8af95b41fd | ||
|
|
d0e3603663 | ||
|
|
2e755ec083 | ||
|
|
724195d66d | ||
|
|
b132ff4fd0 | ||
|
|
6f1054e2d1 | ||
|
|
60c59a0529 | ||
|
|
d382f262fd | ||
|
|
f6baef3bd6 | ||
|
|
4a27fd35bf | ||
|
|
edd37be295 | ||
|
|
e38f00c18b | ||
|
|
24b08037f9 | ||
|
|
1d9bc390bb | ||
|
|
44ba19990e | ||
|
|
5571a1c262 | ||
|
|
9617241b6d | ||
|
|
4b5eadcf88 | ||
|
|
c76295e5c9 | ||
|
|
b61ab4c79a | ||
|
|
2d01192204 | ||
|
|
eb6763b0bb | ||
|
|
2bb935e9ca | ||
|
|
1e72e9d749 | ||
|
|
59700834cc | ||
|
|
95d6ed0376 | ||
|
|
5dfc8b2e0a | ||
|
|
6961361cf8 | ||
|
|
c105057b91 | ||
|
|
29b20753e9 | ||
|
|
f0b93dc7fe | ||
|
|
dd2b054b35 | ||
|
|
bc09387f5e | ||
|
|
6e8e725a94 |
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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.3"
|
||||
"xen-api": "^1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap": "^16.3.0",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"preferGlobal": true,
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.5.1",
|
||||
"xen-api": "^1.2.3"
|
||||
"xen-api": "^1.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"> </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<{
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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…",
|
||||
@@ -83,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",
|
||||
|
||||
@@ -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…",
|
||||
@@ -83,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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -126,7 +126,6 @@ onMounted(() => {
|
||||
.item {
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"content-type": "^1.0.4",
|
||||
"cson-parser": "^4.0.7",
|
||||
"getopts": "^2.2.3",
|
||||
"http-request-plus": "github:JsCommunity/http-request-plus#v1",
|
||||
"http-request-plus": "^0.14.0",
|
||||
"json-rpc-protocol": "^0.13.1",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"pumpify": "^2.0.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@xen-orchestra/proxy",
|
||||
"version": "0.26.11",
|
||||
"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",
|
||||
@@ -60,7 +60,7 @@
|
||||
"source-map-support": "^0.5.16",
|
||||
"stoppable": "^1.0.6",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
"xen-api": "^1.2.3",
|
||||
"xen-api": "^1.2.2",
|
||||
"xo-common": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"get-stream": "^6.0.0",
|
||||
"http-request-plus": "github:JsCommunity/http-request-plus#v1",
|
||||
"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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
5
@xen-orchestra/vmware-explorer/index.mjs
Normal file
5
@xen-orchestra/vmware-explorer/index.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
import Esxi from './esxi.mjs'
|
||||
import openDeltaVmdkasVhd from './openDeltaVmdkAsVhd.mjs'
|
||||
import VhdEsxiRaw from './VhdEsxiRaw.mjs'
|
||||
|
||||
export { openDeltaVmdkasVhd, Esxi, VhdEsxiRaw }
|
||||
@@ -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",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xen-api": "^1.2.3"
|
||||
"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": "github:JsCommunity/http-request-plus#v1",
|
||||
"http-request-plus": "^0.14.0",
|
||||
"json-rpc-protocol": "^0.13.2",
|
||||
"lodash": "^4.17.15",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,30 +1,5 @@
|
||||
# ChangeLog
|
||||
|
||||
## **next**
|
||||
|
||||
### 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.3
|
||||
- @vates/nbd-client 1.0.1
|
||||
- @xen-orchestra/backups 0.29.6
|
||||
- @xen-orchestra/proxy 0.26.11
|
||||
- xo-vmdk-to-vhd 2.5.3
|
||||
- xo-cli 0.14.4
|
||||
- xo-server 5.109.1
|
||||
- xo-server-transport-email 0.6.1
|
||||
- xo-web 5.111.1
|
||||
|
||||
## **5.79.0** (2023-01-31)
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -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-->
|
||||
|
||||
- @xen-orchestra/backups patch
|
||||
- xen-api patch
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"human-format": "^1.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"pw": "^0.0.4",
|
||||
"xen-api": "^1.2.3"
|
||||
"xen-api": "^1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.1.5",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xen-api",
|
||||
"version": "1.2.3",
|
||||
"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": "github:JsCommunity/http-request-plus#v1",
|
||||
"http-request-plus": "^0.14.0",
|
||||
"jest-diff": "^29.0.3",
|
||||
"json-rpc-protocol": "^0.13.1",
|
||||
"kindof": "^2.0.0",
|
||||
|
||||
@@ -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 ?? false
|
||||
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,28 +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')) {
|
||||
throw error
|
||||
}
|
||||
if (useHack) {
|
||||
response.cancel()
|
||||
} else {
|
||||
// consume the response
|
||||
response.resume()
|
||||
}
|
||||
|
||||
return pTaskResult
|
||||
@@ -979,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) {
|
||||
@@ -1081,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
|
||||
@@ -1099,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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -391,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
|
||||
|
||||
@@ -473,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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,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)
|
||||
|
||||
@@ -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": "github:JsCommunity/http-request-plus#v1",
|
||||
"http-request-plus": "^0.14.0",
|
||||
"human-format": "^1.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"micromatch": "^4.0.2",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -121,7 +121,7 @@ class XoServerIcinga2 {
|
||||
exit_status: icinga2Status,
|
||||
}),
|
||||
})
|
||||
.then(response => response.text())
|
||||
.readAll()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.109.1",
|
||||
"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": "github:JsCommunity/http-request-plus#v1",
|
||||
"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.3",
|
||||
"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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>',
|
||||
|
||||
|
||||
@@ -598,12 +598,8 @@ const messages = {
|
||||
remoteSmbPlaceHolderOptions: 'Custom mount options',
|
||||
remoteS3LabelUseHttps: 'Use HTTPS',
|
||||
remoteS3LabelAllowInsecure: 'Allow unauthorized',
|
||||
remoteS3PlaceHolderEndpoint: 'AWS S3 endpoint (ex: s3.us-east-2.amazonaws.com)',
|
||||
remoteS3PlaceHolderBucket: 'AWS S3 bucket name',
|
||||
remoteS3PlaceHolderDirectory: 'Directory',
|
||||
remoteS3PlaceHolderAccessKeyID: 'Access key ID',
|
||||
remoteS3PlaceHolderSecret: 'Paste secret here to change it',
|
||||
remoteS3PlaceHolderEncryptionKey: 'Enter your encryption key here (32 characters)',
|
||||
remoteS3Region: 'Region, leave blank for default',
|
||||
remoteS3TooltipProtocol: 'Uncheck if you want HTTP instead of HTTPS',
|
||||
remoteS3TooltipAcceptInsecure: 'Check if you want to accept self signed certificates',
|
||||
|
||||
@@ -1780,13 +1780,7 @@ const importDisk = async ({ description, file, name, type, vmdkData }, sr) => {
|
||||
})
|
||||
formData.append('file', file)
|
||||
const result = await post(res.$sendTo, formData)
|
||||
const text = await result.text()
|
||||
let body
|
||||
try {
|
||||
body = JSON.parse(text)
|
||||
} catch (error) {
|
||||
throw new Error(`Body is not a JSON, original message is : ${text}`)
|
||||
}
|
||||
const body = await result.json()
|
||||
if (result.status !== 200) {
|
||||
throw new Error(body.error.message)
|
||||
}
|
||||
|
||||
@@ -87,10 +87,6 @@ export default class PoolItem extends Component {
|
||||
return icon(tooltip)
|
||||
}
|
||||
|
||||
_isXcpngPool() {
|
||||
return Object.values(this.props.poolHosts)[0].productBrand === 'XCP-ng'
|
||||
}
|
||||
|
||||
render() {
|
||||
const { item: pool, expandAll, isAdmin, selected, hostMetrics, poolHosts, nSrs, nVms } = this.props
|
||||
const { missingPatchCount } = this.state
|
||||
@@ -106,7 +102,7 @@ export default class PoolItem extends Component {
|
||||
<Ellipsis>
|
||||
<Text value={pool.name_label} onChange={this._setNameLabel} useLongClick />
|
||||
</Ellipsis>
|
||||
{isAdmin && this._isXcpngPool() && <span className='ml-1'>{this._getPoolLicenseIcon()}</span>}
|
||||
{isAdmin && <span className='ml-1'>{this._getPoolLicenseIcon()}</span>}
|
||||
|
||||
{missingPatchCount > 0 && (
|
||||
<span>
|
||||
|
||||
@@ -63,10 +63,9 @@ const BindLicensesButton = decorate([
|
||||
return error(_('licensesBinding'), _('notEnoughXcpngLicenses'))
|
||||
}
|
||||
|
||||
const hostsWithoutLicense = poolHosts.filter(host => {
|
||||
const license = this.state.xcpngLicenseByBoundObjectId[host.id]
|
||||
return license === undefined || license.expires < Date.now()
|
||||
})
|
||||
const hostsWithoutLicense = poolHosts.filter(
|
||||
host => this.state.xcpngLicenseByBoundObjectId[host.id] === undefined
|
||||
)
|
||||
const licenseIdByHost = await confirm({
|
||||
body: <PoolBindLicenseModal hosts={hostsWithoutLicense} />,
|
||||
icon: 'connect',
|
||||
|
||||
@@ -414,7 +414,7 @@ export default decorate([
|
||||
name='host'
|
||||
onChange={effects.linkState}
|
||||
// pattern='^[^\\/]+\\[^\\/]+$'
|
||||
placeholder={formatMessage(messages.remoteS3PlaceHolderEndpoint)}
|
||||
placeholder='AWS S3 endpoint (ex: s3.us-east-2.amazonaws.com)'
|
||||
required
|
||||
type='text'
|
||||
value={host}
|
||||
@@ -461,7 +461,7 @@ export default decorate([
|
||||
className='form-control'
|
||||
name='username'
|
||||
onChange={effects.linkState}
|
||||
placeholder={formatMessage(messages.remoteS3PlaceHolderAccessKeyID)}
|
||||
placeholder='Access key ID'
|
||||
required
|
||||
type='text'
|
||||
value={username}
|
||||
@@ -472,7 +472,7 @@ export default decorate([
|
||||
className='form-control'
|
||||
name='password'
|
||||
onChange={effects.setSecretKey}
|
||||
placeholder={formatMessage(messages.remoteS3PlaceHolderSecret)}
|
||||
placeholder='Paste secret here to change it'
|
||||
autoComplete='off'
|
||||
type='text'
|
||||
/>
|
||||
@@ -498,7 +498,6 @@ export default decorate([
|
||||
autoComplete='new-password'
|
||||
className='form-control'
|
||||
name='encryptionKey'
|
||||
placeholder={formatMessage(messages.remoteS3PlaceHolderEncryptionKey)}
|
||||
onChange={effects.linkState}
|
||||
pattern='^.{32}$'
|
||||
type='password'
|
||||
|
||||
18
yarn.lock
18
yarn.lock
@@ -7890,7 +7890,7 @@ encoding@^0.1.11:
|
||||
dependencies:
|
||||
iconv-lite "^0.6.2"
|
||||
|
||||
end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1:
|
||||
end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
|
||||
@@ -10776,11 +10776,14 @@ http-proxy@^1.16.2, http-proxy@^1.17.0, http-proxy@^1.18.1:
|
||||
follow-redirects "^1.0.0"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
"http-request-plus@github:JsCommunity/http-request-plus#v1":
|
||||
http-request-plus@^0.14.0:
|
||||
version "0.14.0"
|
||||
resolved "https://codeload.github.com/JsCommunity/http-request-plus/tar.gz/af641418c9a91e0285f9662e2794822045062b0c"
|
||||
resolved "https://registry.yarnpkg.com/http-request-plus/-/http-request-plus-0.14.0.tgz#a190ece4b5b67d66ab442d2983a1a1bcd363b866"
|
||||
integrity sha512-EX/OoPA2vWO7k9Whq+vOSN/nH/P1Ae9smCSzhBIIEU8WLDJyXoH9S8pY4Ieh4yQartrMB1vc2o6T4KV4oIQsDQ==
|
||||
dependencies:
|
||||
"@xen-orchestra/log" "^0.6.0"
|
||||
is-redirect "^1.0.0"
|
||||
promise-toolbox "^0.19.2"
|
||||
pump "^3.0.0"
|
||||
|
||||
http-server-plus@^0.12.0:
|
||||
version "0.12.0"
|
||||
@@ -11608,6 +11611,11 @@ is-promise@^2.0.0, is-promise@^2.2.2:
|
||||
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
|
||||
integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
|
||||
|
||||
is-redirect@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
|
||||
integrity sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==
|
||||
|
||||
is-regex@^1.0.3, is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
|
||||
@@ -15843,7 +15851,7 @@ promise-polyfill@^6.0.1:
|
||||
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.1.0.tgz#dfa96943ea9c121fca4de9b5868cb39d3472e057"
|
||||
integrity sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ==
|
||||
|
||||
promise-toolbox@^0.19.0:
|
||||
promise-toolbox@^0.19.0, promise-toolbox@^0.19.2:
|
||||
version "0.19.2"
|
||||
resolved "https://registry.yarnpkg.com/promise-toolbox/-/promise-toolbox-0.19.2.tgz#7453d117313a9afba6add0c67c46210f7b5833f8"
|
||||
integrity sha512-3956j2kaS4nJG1ANd4SZBQj8GrxLSlvfpVzMT4I7k7K8BkhhpAChXOI3B1VMlU7TQstShBh4D4uKt9zFjahKNg==
|
||||
|
||||
Reference in New Issue
Block a user