Compare commits

..

2 Commits

Author SHA1 Message Date
Florent Beauchamp
0957b5b6b1 doc 2022-11-22 09:23:00 +01:00
Florent Beauchamp
33b758d0b2 feat(vhd-cli): implement deeper checks for vhd 2022-11-22 09:09:47 +01:00
66 changed files with 479 additions and 532 deletions

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

View File

@@ -30,7 +30,6 @@ if (args.length === 0) {
${name} v${version}
`)
// eslint-disable-next-line n/no-process-exit
process.exit()
}

View File

@@ -22,6 +22,7 @@ export default async function cleanVms(args) {
await asyncMap(_, vmDir =>
Disposable.use(getSyncedHandler({ url: pathToFileURL(dirname(vmDir)).href }), async handler => {
console.log(handler, basename(vmDir))
try {
await new RemoteAdapter(handler).cleanVm(basename(vmDir), {
fixMetadata: fix,

View File

@@ -537,6 +537,10 @@ class RemoteAdapter {
}
}
async invalidateVmBackupListCache(vmUuid) {
await this.handler.unlink(this.#getVmBackupsCache(vmUuid))
}
async #getCachabledDataListVmBackups(dir) {
debug('generating cache', { path: dir })

View File

@@ -49,6 +49,7 @@ exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(Abst
const dataBasename = basename + '.xva'
const dataFilename = backupDir + '/' + dataBasename
const metadataFilename = `${backupDir}/${basename}.json`
const metadata = {
jobId: job.id,
mode: job.mode,

View File

@@ -91,6 +91,7 @@ export default class RemoteHandlerAbstract {
const sharedLimit = limitConcurrency(options.maxParallelOperations ?? DEFAULT_MAX_PARALLEL_OPERATIONS)
this.closeFile = sharedLimit(this.closeFile)
this.copy = sharedLimit(this.copy)
this.exists = sharedLimit(this.exists)
this.getInfo = sharedLimit(this.getInfo)
this.getSize = sharedLimit(this.getSize)
this.list = sharedLimit(this.list)
@@ -284,24 +285,15 @@ export default class RemoteHandlerAbstract {
return this._encryptor.decryptData(data)
}
async #rename(oldPath, newPath, { checksum }, createTree = true) {
try {
let p = timeout.call(this._rename(oldPath, newPath), this._timeout)
if (checksum) {
p = Promise.all([p, this._rename(checksumFile(oldPath), checksumFile(newPath))])
}
await p
} catch (error) {
// ENOENT can be a missing target directory OR a missing source
if (error.code === 'ENOENT' && createTree) {
await this._mktree(dirname(newPath))
return this.#rename(oldPath, newPath, { checksum }, false)
}
}
}
async rename(oldPath, newPath, { checksum = false } = {}) {
oldPath = normalizePath(oldPath)
newPath = normalizePath(newPath)
rename(oldPath, newPath, { checksum = false } = {}) {
return this.#rename(normalizePath(oldPath), normalizePath(newPath), { checksum })
let p = timeout.call(this._rename(oldPath, newPath), this._timeout)
if (checksum) {
p = Promise.all([p, this._rename(checksumFile(oldPath), checksumFile(newPath))])
}
return p
}
async copy(oldPath, newPath, { checksum = false } = {}) {
@@ -323,6 +315,14 @@ export default class RemoteHandlerAbstract {
await this._rmtree(normalizePath(dir))
}
async _exists(file){
throw new Error('not implemented')
}
async exists(file){
return this._exists(normalizePath(file))
}
// Asks the handler to sync the state of the effective remote with its'
// metadata
//

View File

@@ -228,17 +228,6 @@ handlers.forEach(url => {
expect(await handler.list('.')).toEqual(['file2'])
expect(await handler.readFile(`file2`)).toEqual(TEST_DATA)
})
it(`should rename the file and create dest directory`, async () => {
await handler.outputFile('file', TEST_DATA)
await handler.rename('file', `sub/file2`)
expect(await handler.list('sub')).toEqual(['file2'])
expect(await handler.readFile(`sub/file2`)).toEqual(TEST_DATA)
})
it(`should fail with enoent if source file is missing`, async () => {
const error = await rejectionOf(handler.rename('file', `sub/file2`))
expect(error.code).toBe('ENOENT')
})
})
describe('#rmdir()', () => {

View File

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

View File

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

View File

@@ -5,7 +5,6 @@
- Invalidate sessionId token after logout (PR [#6480](https://github.com/vatesfr/xen-orchestra/pull/6480))
- Settings page (PR [#6418](https://github.com/vatesfr/xen-orchestra/pull/6418))
- Uncollapse hosts in the tree by default (PR [#6428](https://github.com/vatesfr/xen-orchestra/pull/6428))
- Display RAM usage in pool dashboard (PR [#6419](https://github.com/vatesfr/xen-orchestra/pull/6419))
## **0.1.0**

View File

@@ -105,7 +105,7 @@ Use the `busy` prop to display a loader icon.
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue";
import UiIcon from "@/components/ui/UiIcon.vue"
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
</script>
```

View File

@@ -19,7 +19,6 @@
"@types/d3-time-format": "^4.0.0",
"@types/lodash-es": "^4.17.6",
"@vueuse/core": "^9.5.0",
"@vueuse/math": "^9.5.0",
"complex-matcher": "^0.7.0",
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",

View File

@@ -32,9 +32,6 @@
</template>
<script lang="ts" setup>
import { useUiStore } from "@/stores/ui.store";
import { useActiveElement, useMagicKeys, whenever } from "@vueuse/core";
import { logicAnd } from "@vueuse/math";
import { difference } from "lodash";
import { computed, ref, watch, watchEffect } from "vue";
import favicon from "@/assets/favicon.svg";
@@ -61,28 +58,13 @@ link.href = favicon;
document.title = "XO Lite";
if (window.localStorage?.getItem("colorMode") !== "light") {
document.documentElement.classList.add("dark");
}
const xenApiStore = useXenApiStore();
const hostStore = useHostStore();
useChartTheme();
const uiStore = useUiStore();
if (import.meta.env.DEV) {
const activeElement = useActiveElement();
const { D } = useMagicKeys();
const canToggleDarkMode = computed(() => {
if (activeElement.value == null) {
return true;
}
return !["INPUT", "TEXTAREA"].includes(activeElement.value.tagName);
});
whenever(
logicAnd(D, canToggleDarkMode),
() => (uiStore.colorMode = uiStore.colorMode === "dark" ? "light" : "dark")
);
}
watchEffect(() => {
if (xenApiStore.isConnected) {

View File

@@ -11,7 +11,7 @@
</template>
<script lang="ts" setup>
import AccountButton from "@/components/AccountButton.vue";
import AccountButton from '@/components/AccountButton.vue'
</script>
<style lang="postcss" scoped>

View File

@@ -43,14 +43,14 @@
<template #buttons>
<UiButton transparent @click="addNewFilter">
{{ $t("add-or") }}
</UiButton>
{{ $t("add-or") }}
</UiButton>
<UiButton :disabled="!isFilterValid" type="submit">
{{ $t(editedFilter ? "update" : "add") }}
</UiButton>
<UiButton outlined @click="handleCancel">
{{ $t("cancel") }}
</UiButton>
{{ $t("cancel") }}
</UiButton>
</template>
</UiModal>
</template>

View File

@@ -41,8 +41,8 @@
<template #buttons>
<UiButton type="submit">{{ $t("add") }}</UiButton>
<UiButton outlined @click="handleCancel">
{{ $t("cancel") }}
</UiButton>
{{ $t("cancel") }}
</UiButton>
</template>
</UiModal>
</template>

View File

@@ -3,10 +3,10 @@
<div class="progress-bar">
<div class="progress-bar-fill" />
</div>
<div class="legend" v-if="label !== undefined">
<div class="badge" v-if="label !== undefined">
<span class="circle" />
{{ label }}
<UiBadge class="badge">{{ badgeLabel ?? progressWithUnit }}</UiBadge>
<UiBadge>{{ badgeLabel ?? progressWithUnit }}</UiBadge>
</div>
</div>
</template>
@@ -33,14 +33,9 @@ const progressWithUnit = computed(() => {
</script>
<style lang="postcss" scoped>
.legend {
text-align: right;
margin: 1.6em 0;
}
.badge {
font-size: 0.9em;
font-weight: 700;
text-align: right;
margin: 1rem 0;
}
.circle {

View File

@@ -12,7 +12,7 @@
:icon="faServer"
:route="{ name: 'host.dashboard', params: { uuid: host.uuid } }"
>
{{ host.name_label || "(Host)" }}
{{ host.name_label || '(Host)' }}
<template #actions>
<InfraAction
:icon="isExpanded ? faAngleDown : faAngleUp"

View File

@@ -12,7 +12,7 @@
:icon="faDisplay"
:route="{ name: 'vm.console', params: { uuid: vm.uuid } }"
>
{{ vm.name_label || "(VM)" }}
{{ vm.name_label || '(VM)' }}
<template #actions>
<InfraAction>
<PowerStateIcon :state="vm?.power_state" />

View File

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

View File

@@ -1,10 +1,10 @@
<template>
<UiCard>
<UiTitle type="h4">{{ $t("storage-usage") }}</UiTitle>
<UsageBar :data="srStore.isReady ? data.result : undefined" :nItems="N_ITEMS">
<UsageBar :data="srStore.isReady ? data.result : undefined" :nItems="5">
<template #header>
<span>{{ $t("storage") }}</span>
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
<span>{{ $t("top-#", { n: 5 }) }}</span>
</template>
<template #footer v-if="showFooter">
<div class="footer-card">
@@ -37,7 +37,6 @@ import UiCard from "@/components/ui/UiCard.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import { formatSize, percent } from "@/libs/utils";
import { useSrStore } from "@/stores/storage.store";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
const srStore = useSrStore();

View File

@@ -1,8 +1,8 @@
<template>
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
<UsageBar :data="statFetched ? data : undefined" :n-items="5">
<template #header>
<span>{{ $t("hosts") }}</span>
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
<span>{{ $t("top-#", { n: 5 }) }}</span>
</template>
</UsageBar>
</template>
@@ -13,7 +13,6 @@ import UsageBar from "@/components/UsageBar.vue";
import type { Stat } from "@/composables/fetch-stats.composable";
import { getAvgCpuUsage } from "@/libs/utils";
import type { HostStats } from "@/libs/xapi-stats";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
const stats = inject<ComputedRef<Stat<HostStats>[]>>(
"hostStats",

View File

@@ -1,8 +1,8 @@
<template>
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
<UsageBar :data="statFetched ? data : undefined" :n-items="5">
<template #header>
<span>{{ $t("vms") }}</span>
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
<span>{{ $t("top-#", { n: 5 }) }}</span>
</template>
</UsageBar>
</template>
@@ -13,7 +13,6 @@ import UsageBar from "@/components/UsageBar.vue";
import type { Stat } from "@/composables/fetch-stats.composable";
import { getAvgCpuUsage } from "@/libs/utils";
import type { VmStats } from "@/libs/xapi-stats";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
const stats = inject<ComputedRef<Stat<VmStats>[]>>(
"vmStats",

View File

@@ -1,52 +0,0 @@
<template>
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
<template #header>
<span>{{ $t("hosts") }}</span>
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
</template>
</UsageBar>
</template>
<script lang="ts" setup>
import { type ComputedRef, computed, inject } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import type { Stat } from "@/composables/fetch-stats.composable";
import { formatSize, parseRamUsage } from "@/libs/utils";
import type { HostStats } from "@/libs/xapi-stats";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
const stats = inject<ComputedRef<Stat<HostStats>[]>>(
"hostStats",
computed(() => [])
);
const data = computed(() => {
const result: {
id: string;
label: string;
value: number;
badgeLabel: string;
}[] = [];
stats.value.forEach((stat) => {
if (stat.stats === undefined) {
return;
}
const { percentUsed, total, used } = parseRamUsage(stat.stats);
result.push({
id: stat.id,
label: stat.name,
value: percentUsed,
badgeLabel: `${formatSize(used)}/${formatSize(total)}`,
});
});
return result;
});
const statFetched: ComputedRef<boolean> = computed(
() =>
statFetched.value ||
(stats.value.length > 0 && stats.value.length === data.value.length)
);
</script>

View File

@@ -1,52 +0,0 @@
<template>
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
<template #header>
<span>{{ $t("vms") }}</span>
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
</template>
</UsageBar>
</template>
<script lang="ts" setup>
import { type ComputedRef, computed, inject } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import type { Stat } from "@/composables/fetch-stats.composable";
import { formatSize, parseRamUsage } from "@/libs/utils";
import type { VmStats } from "@/libs/xapi-stats";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
const stats = inject<ComputedRef<Stat<VmStats>[]>>(
"vmStats",
computed(() => [])
);
const data = computed(() => {
const result: {
id: string;
label: string;
value: number;
badgeLabel: string;
}[] = [];
stats.value.forEach((stat) => {
if (stat.stats === undefined) {
return;
}
const { percentUsed, total, used } = parseRamUsage(stat.stats);
result.push({
id: stat.id,
label: stat.name,
value: percentUsed,
badgeLabel: `${formatSize(used)}/${formatSize(total)}`,
});
});
return result;
});
const statFetched: ComputedRef<boolean> = computed(
() =>
statFetched.value ||
(stats.value.length > 0 && stats.value.length === data.value.length)
);
</script>

View File

@@ -22,7 +22,7 @@ defineProps<{
font-size: 1.4rem;
font-weight: 500;
padding: 0 0.8rem;
height: 1.8em;
height: 2.4rem;
color: var(--color-blue-scale-500);
border-radius: 9.6rem;
background-color: var(--color-blue-scale-300);

View File

@@ -1,22 +1,23 @@
# useBusy composable
```vue
<template>
<span class="error" v-if="error">{{ error }}</span>
<button @click="run" :disabled="isBusy">Do something</button>
</template>
<script lang="ts" setup>
import useBusy from "@/composables/busy.composable";
import useBusy from '@/composables/busy.composable';
async function doSomething() {
try {
// Doing some async work
} catch (e) {
throw "Something bad happened";
async function doSomething() {
try {
// Doing some async work
} catch (e) {
throw "Something bad happened";
}
}
}
const { isBusy, error, run } = useBusy(doSomething);
const { isBusy, error, run } = useBusy(doSomething)
</script>
```

View File

@@ -13,23 +13,19 @@ const filteredCollection = myCollection.filter(predicate);
By default, when adding/removing filters, the URL will update automatically.
```typescript
addFilter("name:/^foo/i"); // Will update the URL with ?filter=name:/^foo/i
addFilter('name:/^foo/i'); // Will update the URL with ?filter=name:/^foo/i
```
### Change the URL query string parameter name
```typescript
const {
/* ... */
} = useCollectionFilter({ queryStringParam: "f" }); // ?f=name:/^foo/i
const { /* ... */ } = useCollectionFilter({ queryStringParam: 'f' }); // ?f=name:/^foo/i
```
### Disable the usage of URL query string
```typescript
const {
/* ... */
} = useCollectionFilter({ queryStringParam: undefined });
const { /* ... */ } = useCollectionFilter({ queryStringParam: undefined });
```
## Example of using the composable with the `CollectionFilter` component
@@ -42,32 +38,32 @@ const {
@add-filter="addFilter"
@remove-filter="removeFilter"
/>
<div v-for="item in filteredCollection">...</div>
</template>
<script lang="ts" setup>
import CollectionFilter from "@/components/CollectionFilter.vue";
import useCollectionFilter from "@/composables/collection-filter.composable";
import { computed } from "vue";
import CollectionFilter from "@/components/CollectionFilter.vue";
import useCollectionFilter from "@/composables/collection-filter.composable";
import { computed } from "vue";
const collection = [
{ name: "Foo", age: 5, registered: true },
{ name: "Bar", age: 12, registered: false },
{ name: "Foo Bar", age: 2, registered: true },
{ name: "Bar Baz", age: 45, registered: false },
{ name: "Foo Baz", age: 32, registered: false },
{ name: "Foo Bar Baz", age: 32, registered: true },
];
const collection = [
{ name: "Foo", age: 5, registered: true },
{ name: "Bar", age: 12, registered: false },
{ name: "Foo Bar", age: 2, registered: true },
{ name: "Bar Baz", age: 45, registered: false },
{ name: "Foo Baz", age: 32, registered: false },
{ name: "Foo Bar Baz", age: 32, registered: true },
];
const availableFilters: AvailableFilter[] = [
{ property: "name", label: "Name", type: "string" },
{ property: "age", label: "Age", type: "number" },
{ property: "registered", label: "Registered", type: "boolean", icon: faKey },
];
const availableFilters: AvailableFilter[] = [
{ property: "name", label: "Name", type: "string" },
{ property: "age", label: "Age", type: "number" },
{ property: "registered", label: "Registered", type: "boolean", icon: faKey },
];
const { filters, addFilter, removeFilter, predicate } = useCollectionFilter();
const filteredCollection = computed(() => collection.filter(predicate));
const { filters, addFilter, removeFilter, predicate } = useCollectionFilter();
const filteredCollection = computed(() => collection.filter(predicate));
</script>
```

View File

@@ -2,17 +2,14 @@
```vue
<script lang="ts" setup>
import useFilteredCollection from "./filtered-collection.composable";
import useFilteredCollection from './filtered-collection.composable';
const players = [
{ name: "Foo", team: "Blue" },
{ name: "Bar", team: "Red" },
{ name: "Baz", team: "Blue" },
];
const bluePlayers = useFilteredCollection(
players,
(player) => player.team === "Blue"
);
const players = [
{ name: "Foo", team: "Blue" },
{ name: "Bar", team: "Red" },
{ name: "Baz", team: "Blue" },
]
const bluePlayers = useFilteredCollection(players, (player) => player.team === "Blue");
</script>
```

View File

@@ -5,28 +5,27 @@
<div v-for="item in items">
{{ item.name }} <button @click="openRemoveModal(item)">Delete</button>
</div>
<UiModal v-if="isRemoveModalOpen">
Are you sure you want to delete {{ removeModalPayload.name }}
<button @click="handleRemove">Yes</button>
<button @click="closeRemoveModal">No</button>
<button @click="handleRemove">Yes</button> <button @click="closeRemoveModal">No</button>
</UiModal>
</template>
<script lang="ts" setup>
import useModal from "@/composables/modal.composable";
import useModal from '@/composables/modal.composable';
const {
payload: removeModalPayload,
isOpen: isRemoveModalOpen,
open: openRemoveModal,
close: closeRemoveModal,
} = useModal();
async function handleRemove() {
await removeItem(removeModalPayload.id);
closeRemoveModal();
}
const {
payload: removeModalPayload,
isOpen: isRemoveModalOpen,
open: openRemoveModal,
close: closeRemoveModal,
} = useModal()
async function handleRemove() {
await removeItem(removeModalPayload.id);
closeRemoveModal()
}
</script>
```

View File

@@ -4,30 +4,34 @@
<template>
<table>
<thead>
<tr>
<th>
<input type="checkbox" v-model="areAllSelected" />
</th>
<th>Name</th>
</tr>
<tr>
<th>
<input type="checkbox" v-model="areAllSelected">
</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items">
<td>
<input type="checkbox" :value="item.id" v-model="selected" />
</td>
<td>{{ item.name }}</td>
</tr>
<tr v-for="item in items">
<td>
<input type="checkbox" :value="item.id" v-model="selected" />
</td>
<td>{{ item.name }}</td>
</tr>
</tbody>
</table>
<!-- You can use something else than a "Select All" checkbox -->
<button @click="areAllSelected = !areAllSelected">Toggle all selected</button>
</template>
<script lang="ts" setup>
import useMultiSelect from "./multi-select.composable";
const { selected, areAllSelected } = useMultiSelect();
<script lang="ts" setup>
import useMultiSelect from './multi-select.composable';
const {
selected,
areAllSelected,
} = useMultiSelect()
</script>
```

View File

@@ -123,37 +123,3 @@ export const buildXoObject = (
...record,
$ref: params.opaqueRef,
});
export function parseRamUsage(
{
memory,
memoryFree,
}: {
memory: number[];
memoryFree?: number[];
},
{ nSequence = 4 } = {}
) {
const _nSequence = Math.min(memory.length, nSequence);
let total = 0;
let used = 0;
memory = memory.slice(memory.length - _nSequence);
memoryFree = memoryFree?.slice(memoryFree.length - _nSequence);
memory.forEach((ram, key) => {
total += ram;
used += ram - (memoryFree?.[key] ?? 0);
});
const percentUsed = percent(used, total);
return {
// In case `memoryFree` is not given by the xapi,
// we won't be able to calculate the percentage of used memory properly.
percentUsed:
memoryFree === undefined || isNaN(percentUsed) ? 0 : percentUsed,
total: total / _nSequence,
used: memoryFree === undefined ? 0 : used / _nSequence,
};
}

View File

@@ -259,7 +259,7 @@ export type VmStats = {
w: Record<string, number[]>;
};
memory: number[];
memoryFree?: number[];
memoryFree: number[];
vifs: {
rx: Record<string, number[]>;
tx: Record<string, number[]>;

View File

@@ -15,9 +15,7 @@
"community-name": "{name} community",
"copy": "Copy",
"cpu-usage":"CPU usage",
"theme-dark": "Dark",
"theme-light": "Light",
"theme-auto": "Auto",
"dark-mode": "Dark mode",
"dashboard": "Dashboard",
"delete": "Delete",
"descending": "descending",
@@ -39,7 +37,6 @@
"or": "Or",
"password": "Password",
"property": "Property",
"ram-usage":"RAM usage",
"send-us-feedback": "Send us feedback",
"settings": "Settings",
"snapshot": "Snapshot",

View File

@@ -15,9 +15,7 @@
"community-name": "Communauté {name}",
"copy": "Copier",
"cpu-usage":"Utilisation CPU",
"theme-dark": "Sombre",
"theme-light": "Clair",
"theme-auto": "Auto",
"dark-mode": "Mode sombre",
"dashboard": "Tableau de bord",
"delete": "Supprimer",
"descending": "descendant",
@@ -39,7 +37,6 @@
"or": "Ou",
"password": "Mot de passe",
"property": "Propriété",
"ram-usage":"Utilisation de la RAM",
"send-us-feedback": "Envoyez-nous vos commentaires",
"settings": "Paramètres",
"snapshot": "Instantané",

View File

@@ -1,14 +1,10 @@
import { useColorMode } from "@vueuse/core";
import { defineStore } from "pinia";
import { ref } from "vue";
export const useUiStore = defineStore("ui", () => {
const currentHostOpaqueRef = ref();
const colorMode = useColorMode({ emitAuto: true, initialValue: "dark" });
return {
colorMode,
currentHostOpaqueRef,
};
});

View File

@@ -3,18 +3,13 @@
<PoolDashboardStatus class="item" />
<PoolDashboardStorageUsage class="item" />
<PoolDashboardCpuUsage class="item" />
<PoolDashboardRamUsage class="item" />
</div>
</template>
<script lang="ts">
export const N_ITEMS = 5;
</script>
<script lang="ts" setup>
import { differenceBy } from "lodash-es";
import { computed, onMounted, provide, watch } from "vue";
import PoolDashboardCpuUsage from "@/components/pool/dashboard/PoolDashboardCpuUsage.vue";
import PoolDashboardRamUsage from "@/components/pool/dashboard/PoolDashboardRamUsage.vue";
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
import PoolDashboardStorageUsage from "@/components/pool/dashboard/PoolDashboardStorageUsage.vue";
import useFetchStats from "@/composables/fetch-stats.composable";

View File

@@ -19,9 +19,7 @@
rel="noopener noreferrer"
href="https://xcp-ng.org/blog/"
>{{ $t("news-name", { name: "XCP-ng" }) }}</a
>
-
<a
> - <a
target="_blank"
rel="noopener noreferrer"
href="https://xen-orchestra.com/blog/"
@@ -37,9 +35,7 @@
rel="noopener noreferrer"
href="https://xcp-ng.org/forum"
>{{ $t("community-name", { name: "XCP-ng" }) }}</a
>
-
<a
> - <a
target="_blank"
rel="noopener noreferrer"
href="https://xcp-ng.org/forum/category/12/xen-orchestra"
@@ -54,15 +50,14 @@
<UiKeyValueList>
<UiKeyValueRow>
<template #key>{{ $t("appearance") }}</template>
<template #value>
<FormLabel>
<FormSelect v-model="colorMode">
<option value="auto">{{ $t("theme-auto") }}</option>
<option value="dark">{{ $t("theme-dark") }}</option>
<option value="light">{{ $t("theme-light") }}</option>
</FormSelect>
</FormLabel>
</template>
<template #value
><FormLabel>
<FormToggle
:modelValue="darkMode"
@update:modelValue="setDarkMode"
/>{{ $t("dark-mode") }}</FormLabel
></template
>
</UiKeyValueRow>
</UiKeyValueList>
</UiCard>
@@ -90,16 +85,15 @@
</template>
<script lang="ts" setup>
import FormSelect from "@/components/form/FormSelect.vue";
import { useUiStore } from "@/stores/ui.store";
import { storeToRefs } from "pinia";
import { watch } from "vue";
import { computed, watch } from "vue";
import { useI18n } from "vue-i18n";
import { locales } from "@/i18n";
import { faEarthAmericas, faGear } from "@fortawesome/free-solid-svg-icons";
import { useLocalStorage } from "@vueuse/core";
import FormWidget from "@/components/FormWidget.vue";
import TitleBar from "@/components/TitleBar.vue";
import FormLabel from "@/components/form/FormLabel.vue";
import FormToggle from "@/components/form/FormToggle.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiKeyValueList from "@/components/ui/UiKeyValueList.vue";
import UiKeyValueRow from "@/components/ui/UiKeyValueRow.vue";
@@ -111,7 +105,12 @@ const { locale } = useI18n();
watch(locale, (newLocale) => localStorage.setItem("lang", newLocale));
const { colorMode } = storeToRefs(useUiStore());
const colorMode = useLocalStorage<string>("colorMode", "dark");
const darkMode = computed(() => colorMode.value !== "light");
const setDarkMode = (enabled: boolean) => {
colorMode.value = enabled ? "dark" : "light";
document.documentElement.classList[enabled ? "add" : "remove"]("dark");
};
</script>
<style lang="postcss" scoped>

View File

@@ -1,9 +1,9 @@
'use strict'
const fromCallback = require('promise-toolbox/fromCallback')
// eslint-disable-next-line n/no-extraneous-require
// eslint-disable-next-line n/no-missing-require
const splitHost = require('split-host')
// eslint-disable-next-line n/no-extraneous-require
// eslint-disable-next-line n/no-missing-require
const { createClient, Facility, Severity, Transport } = require('syslog-client')
const LEVELS = require('../levels')

View File

@@ -8,15 +8,12 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [Remotes] Prevent remote path from ending with `xo-vm-backups` as it's usually a mistake
- [OVA export] Speed up OVA generation by 2. Generated file will be bigger (as big as uncompressed XVA) (PR [#6487](https://github.com/vatesfr/xen-orchestra/pull/6487))
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [Dashboard/Health] Fix `Unknown SR` and `Unknown VDI` in Unhealthy VDIs (PR [#6519](https://github.com/vatesfr/xen-orchestra/pull/6519))
- [Delta Backup] Can now recover VHD merge when failed at the begining
- [Delta Backup] Fix `ENOENT` errors when merging a VHD directory on non-S3 remote
### Packages to release
@@ -33,14 +30,12 @@
> Keep this list alphabetically ordered to avoid merge conflicts
<!--packages-start-->
- @xen-orchestra/backups-cli major
- @xen-orchestra/fs minor
- @xen-orchestra/log minor
- vhd-lib minor
- vhd-cli major
- @xen-orchestra/backups-cli major
- @xen-orchestra/log minor
- xo-cli patch
- xo-server minor
- xo-vmdk-to-vhd minor
- xo-web minor
<!--packages-end-->

View File

@@ -20,7 +20,7 @@
"getopts": "^2.3.0",
"globby": "^13.1.1",
"handlebars": "^4.7.6",
"husky": "^8.0.2",
"husky": "^4.2.5",
"jest": "^29.0.3",
"lint-staged": "^13.0.3",
"lodash": "^4.17.4",
@@ -34,6 +34,11 @@
"node": ">=14",
"yarn": "^1.7.0"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged && scripts/lint-staged.js"
}
},
"jest": {
"moduleNameMapper": {
"^(@vates/[^/]+)$": [
@@ -70,29 +75,21 @@
"testRegex": "\\.spec\\.js$"
},
"lint-staged": {
"*": [
"scripts/run-changed-pkgs.js test",
"prettier --ignore-unknown --write"
],
"*.{{,c,m}j,t}s{,x}": [
"eslint --ignore-pattern '!*'",
"jest --testRegex='^(?!.*.integ.spec.js$).*.spec.js$' --findRelatedTests --passWithNoTests"
]
"*.{md,ts,ts}": "prettier --write"
},
"private": true,
"scripts": {
"build": "scripts/run-script.js --parallel --concurrency 2 build",
"ci": "yarn && scripts/run-script.js --parallel prepare && yarn test-lint && yarn test-integration",
"ci": "yarn && yarn build && yarn test-integration",
"clean": "scripts/run-script.js --parallel clean",
"dev": "scripts/run-script.js --parallel dev",
"dev-test": "jest --bail --watch \"^(?!.*\\.integ\\.spec\\.js$)\"",
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs",
"prepare": "husky install",
"prettify": "prettier --ignore-path .gitignore --write '**/*.{cjs,js,jsx,md,mjs,ts,tsx}'",
"test": "npm run test-lint && npm run test-unit",
"test-integration": "jest \".integ\\.spec\\.js$\"",
"test-lint": "eslint --ignore-path .gitignore --ignore-pattern packages/xo-web .",
"test-lint": "eslint --ignore-path .gitignore .",
"test-unit": "jest \"^(?!.*\\.integ\\.spec\\.js$)\" && scripts/run-script.js test",
"travis-tests": "scripts/travis-tests.js"
},

View File

@@ -94,5 +94,5 @@ describe('setPropertyClause', () => {
})
it('toString', () => {
assert.equal(ast.toString(), pattern)
assert.equal(ast.toString(), pattern)
})

View File

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

View File

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

View File

@@ -2,9 +2,7 @@
// This file has been generated by [index-modules](https://npmjs.com/index-modules)
//
'use strict'
const d = Object.defineProperty
var d = Object.defineProperty
function de(o, n, v) {
d(o, n, { enumerable: true, value: v })
return v
@@ -19,7 +17,7 @@ function dl(o, n, g, a) {
})
}
function r(p) {
const v = require(p)
var v = require(p)
return v && v.__esModule
? v
: typeof v === 'object' || typeof v === 'function'
@@ -34,7 +32,7 @@ function e(p, i) {
}
d(exports, '__esModule', { value: true })
const defaults = de(exports, 'default', {})
var defaults = de(exports, 'default', {})
e('./check.js', 'check')
e('./compare.js', 'compare')
e('./copy.js', 'copy')

View File

@@ -95,9 +95,15 @@ test('It rename and unlink a VHDFile', async () => {
await convertFromRawToVhd(rawFileName, vhdFileName)
await Disposable.use(async function* () {
const handler = yield getSyncedHandler({ url: 'file:///' })
const { size } = await fs.stat(vhdFileName)
const targetFileName = `${tempDir}/renamed.vhd`
await VhdAbstract.unlink(handler, vhdFileName)
await VhdAbstract.rename(handler, vhdFileName, targetFileName)
expect(await fs.exists(vhdFileName)).toEqual(false)
const { size: renamedSize } = await fs.stat(targetFileName)
expect(size).toEqual(renamedSize)
await VhdAbstract.unlink(handler, targetFileName)
expect(await fs.exists(targetFileName)).toEqual(false)
})
})
@@ -116,8 +122,12 @@ test('It rename and unlink a VhdDirectory', async () => {
// it should clean an existing directory
await fs.mkdir(targetFileName)
await fs.writeFile(`${targetFileName}/dummy`, 'I exists')
await VhdAbstract.unlink(handler, `${targetFileName}/dummy`)
await VhdAbstract.rename(handler, vhdDirectory, targetFileName)
expect(await fs.exists(vhdDirectory)).toEqual(false)
expect(await fs.exists(targetFileName)).toEqual(true)
expect(await fs.exists(`${targetFileName}/dummy`)).toEqual(false)
await VhdAbstract.unlink(handler, targetFileName)
expect(await fs.exists(targetFileName)).toEqual(false)
})
})
@@ -128,6 +138,7 @@ test('It create , rename and unlink alias', async () => {
const vhdFileName = `${tempDir}/randomfile.vhd`
await convertFromRawToVhd(rawFileName, vhdFileName)
const aliasFileName = `${tempDir}/aliasFileName.alias.vhd`
const aliasFileNameRenamed = `${tempDir}/aliasFileNameRenamed.alias.vhd`
await Disposable.use(async function* () {
const handler = yield getSyncedHandler({ url: 'file:///' })
@@ -135,9 +146,15 @@ test('It create , rename and unlink alias', async () => {
expect(await fs.exists(aliasFileName)).toEqual(true)
expect(await fs.exists(vhdFileName)).toEqual(true)
await VhdAbstract.unlink(handler, aliasFileName)
await VhdAbstract.rename(handler, aliasFileName, aliasFileNameRenamed)
expect(await fs.exists(aliasFileName)).toEqual(false)
expect(await fs.exists(vhdFileName)).toEqual(true)
expect(await fs.exists(aliasFileNameRenamed)).toEqual(true)
await VhdAbstract.unlink(handler, aliasFileNameRenamed)
expect(await fs.exists(aliasFileName)).toEqual(false)
expect(await fs.exists(vhdFileName)).toEqual(false)
expect(await fs.exists(aliasFileNameRenamed)).toEqual(false)
})
})

View File

@@ -200,6 +200,14 @@ exports.VhdAbstract = class VhdAbstract {
}
}
static async rename(handler, sourcePath, targetPath) {
try {
// delete target if it already exists
await VhdAbstract.unlink(handler, targetPath)
} catch (e) {}
await handler.rename(sourcePath, targetPath)
}
static async unlink(handler, path) {
const resolved = await resolveVhdAlias(handler, path)
try {
@@ -386,4 +394,14 @@ exports.VhdAbstract = class VhdAbstract {
assert.strictEqual(copied, length, 'invalid length')
return copied
}
_checkBlock() {
throw new Error('not implemented')
}
// check if a block is ok without reading it
// there still can be error when reading the block later (if it's deleted, if right are incorrects,...)
async checkBlock(blockId) {
return this._checkBlock(blockId)
}
}

View File

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

View File

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

View File

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

View File

@@ -43,7 +43,7 @@ const { warn } = createLogger('vhd-lib:merge')
// write the merge progress file at most every `delay` seconds
function makeThrottledWriter(handler, path, delay) {
let lastWrite = 0
let lastWrite = Date.now()
return async json => {
const now = Date.now()
if (now - lastWrite > delay) {
@@ -61,7 +61,7 @@ async function cleanupVhds(handler, chain, { logInfo = noop, removeUnused = fals
const children = chain.slice(1, -1)
const mergeTargetChild = chain[chain.length - 1]
await handler.rename(parent, mergeTargetChild)
await VhdAbstract.rename(handler, parent, mergeTargetChild)
return asyncMap(children, child => {
logInfo(`the VHD child is already merged`, { child })
@@ -175,7 +175,6 @@ module.exports.mergeVhdChain = limitConcurrency(2)(async function mergeVhdChain(
let counter = 0
const mergeStateWriter = makeThrottledWriter(handler, mergeStatePath, 10e3)
await mergeStateWriter(mergeState)
await asyncEach(
toMerge,
async blockId => {

View File

@@ -1,5 +0,0 @@
'use strict'
module.exports = {
ignorePatterns: ['*'],
}

View File

@@ -55,4 +55,3 @@ setTimeout(function () {
name: 'Steve',
})
}, 10)
/* eslint-enable no-console */

View File

@@ -1,5 +1,3 @@
/* eslint-disable no-console */
'use strict'
process.on('unhandledRejection', function (error) {
@@ -61,5 +59,3 @@ xo.open()
.then(function () {
return xo.close()
})
/* eslint-enable no-console */

View File

@@ -1,5 +1,3 @@
/* eslint-disable no-console */
'use strict'
// This is one of the simplest xo-server's plugin than can be created.
@@ -80,5 +78,3 @@ exports.default = function (opts) {
},
}
}
/* eslint-enable no-console */

View File

@@ -6,7 +6,6 @@ function handleHook(type, data) {
const hooks = this._hooks[data.method]?.[type]
if (hooks !== undefined) {
return Promise.all(
// eslint-disable-next-line array-callback-return
hooks.map(({ url, waitForResponse = false }) => {
const promise = this._makeRequest(url, type, data).catch(error => {
log.error('web hook failed', {

View File

@@ -57,10 +57,8 @@ export async function copyVm({ vm, sr }) {
// full
{
// eslint-disable-next-line no-console
console.log('export full VM...')
const input = await srcXapi.VM_export(vm._xapiRef)
// eslint-disable-next-line no-console
console.log('import full VM...')
await tgtXapi.VM_destroy((await tgtXapi.importVm(input, { srId: sr })).$ref)
}

View File

@@ -29,8 +29,7 @@ async function vmdkToVhd(vmdkReadStream, grainLogicalAddressList, grainFileOffse
export async function computeVmdkLength(diskName, vhdReadStream) {
let length = 0
const { iterator } = await vhdToVMDKIterator(diskName, vhdReadStream)
for await (const b of iterator) {
for await (const b of await vhdToVMDKIterator(diskName, vhdReadStream)) {
length += b.length
}
return length
@@ -44,15 +43,13 @@ export async function computeVmdkLength(diskName, vhdReadStream) {
* @returns a readable stream representing a VMDK file
*/
export async function vhdToVMDK(diskName, vhdReadStreamGetter, withLength = false) {
const { iterator, size } = await vhdToVMDKIterator(diskName, await vhdReadStreamGetter())
let length
const stream = await asyncIteratorToStream(iterator)
if (withLength) {
if (size === undefined) {
length = await computeVmdkLength(diskName, await vhdReadStreamGetter())
} else {
length = size
}
length = await computeVmdkLength(diskName, await vhdReadStreamGetter())
}
const iterable = await vhdToVMDKIterator(diskName, await vhdReadStreamGetter())
const stream = await asyncIteratorToStream(iterable)
if (withLength) {
stream.length = length
}
return stream
@@ -65,15 +62,8 @@ export async function vhdToVMDK(diskName, vhdReadStreamGetter, withLength = fals
* @returns a readable stream representing a VMDK file
*/
export async function vhdToVMDKIterator(diskName, vhdReadStream) {
const { blockSize, blockCount, blocks, diskSize, geometry } = await parseVhdToBlocks(vhdReadStream)
const vmdkTargetSize = blockSize * blockCount + 3 * 1024 * 1024 // header/footer/descriptor
const iterator = await generateVmdkData(diskName, diskSize, blockSize, blocks, geometry, vmdkTargetSize)
return {
iterator,
size: vmdkTargetSize,
}
const { blockSize, blocks, diskSize, geometry } = await parseVhdToBlocks(vhdReadStream)
return generateVmdkData(diskName, diskSize, blockSize, blocks, geometry)
}
export { ParsableFile, parseOVAFile, vmdkToVhd, writeOvaOn }

View File

@@ -32,18 +32,17 @@ export async function writeOvaOn(
// https://github.com/mafintosh/tar-stream/issues/24#issuecomment-558358268
async function pushDisk(disk) {
let { iterator, size } = await vhdToVMDKIterator(disk.name, await disk.getStream())
if (size === undefined) {
size = await computeVmdkLength(disk.name, await disk.getStream())
}
const size = await computeVmdkLength(disk.name, await disk.getStream())
disk.fileSize = size
const blockIterator = await vhdToVMDKIterator(disk.name, await disk.getStream())
return new Promise((resolve, reject) => {
const entry = pack.entry({ name: `${disk.name}.vmdk`, size }, err => {
const entry = pack.entry({ name: `${disk.name}.vmdk`, size: size }, err => {
if (err == null) {
return resolve()
} else return reject(err)
})
return writeDisk(entry, iterator).then(
return writeDisk(entry, blockIterator).then(
() => entry.end(),
e => reject(e)
)

View File

@@ -33,8 +33,7 @@ export async function generateVmdkData(
sectorsPerTrackCylinder: 63,
heads: 16,
cylinders: 10402,
},
targetSize
}
) {
const cid = Math.floor(Math.random() * Math.pow(2, 32))
const diskCapacitySectors = Math.ceil(diskCapacityBytes / SECTOR_SIZE)
@@ -151,39 +150,10 @@ ddb.geometry.cylinders = "${geometry.cylinders}"
}
}
function* padding() {
if (targetSize === undefined) {
return
}
let remaining = targetSize - streamPosition
remaining -= SECTOR_SIZE // MARKER_GT
remaining -= tableBuffer.length
remaining -= SECTOR_SIZE // MARKER_GD
remaining -= roundToSector(headerData.grainDirectoryEntries * 4)
remaining -= SECTOR_SIZE // MARKER_GT
remaining -= tableBuffer.length
remaining -= SECTOR_SIZE // MARKER_GD
remaining -= roundToSector(headerData.grainDirectoryEntries * 4)
remaining -= SECTOR_SIZE // MARKER_FOOTER
remaining -= SECTOR_SIZE // stream optimizedheader
remaining -= SECTOR_SIZE // MARKER_EOS
if (remaining < 0) {
throw new Error('vmdk is bigger than precalculed size ')
}
const size = 1024 * 1024
while (remaining > 0) {
const yieldSize = Math.min(size, remaining)
remaining -= yieldSize
yield track(Buffer.alloc(yieldSize))
}
}
async function* iterator() {
yield track(headerData.buffer)
yield track(descriptorBuffer)
yield* emitBlocks(grainSizeBytes, blockGenerator)
yield* padding()
yield track(createEmptyMarker(MARKER_GT))
let tableOffset = streamPosition
// grain tables
@@ -211,5 +181,6 @@ ddb.geometry.cylinders = "${geometry.cylinders}"
yield track(footer.buffer)
yield track(createEmptyMarker(MARKER_EOS))
}
return iterator()
}

View File

@@ -84,7 +84,7 @@ export default {
homeTemplatePage: 'Шаблоны',
// Original text: 'Storages'
homeSrPage: 'Хранилища',
homeSrPage: "Хранилища",
// Original text: "Dashboard"
dashboardPage: 'Контрольные панели',
@@ -144,7 +144,7 @@ export default {
aboutPage: 'О программе',
// Original text: 'About XO {xoaPlan}'
aboutXoaPlan: 'О Xen Orchestra {xoaPlan}',
aboutXoaPlan: "О Xen Orchestra {xoaPlan}",
// Original text: "New"
newMenu: 'Добавить',
@@ -399,10 +399,10 @@ export default {
highAvailability: 'Высокая доступность',
// Original text: 'Shared {type}'
srSharedType: 'Совместное использование {type}',
srSharedType: "Совместное использование {type}",
// Original text: 'Not shared {type}'
srNotSharedType: 'Без совместного использования {type}',
srNotSharedType: "Без совместного использования {type}",
// Original text: "Add"
add: 'Добавить',
@@ -561,10 +561,10 @@ export default {
unknownSchedule: 'Неизвестно',
// Original text: 'Web browser timezone'
timezonePickerUseLocalTime: 'Часовой пояс WEB-браузера',
timezonePickerUseLocalTime: "Часовой пояс WEB-браузера",
// Original text: 'Server timezone ({value})'
serverTimezoneOption: 'Часовой пояс сервера ({value})',
serverTimezoneOption: "Часовой пояс сервера ({value})",
// Original text: 'Cron Pattern:'
cronPattern: 'Cron-шаблон: ',
@@ -726,8 +726,7 @@ export default {
localRemoteWarningTitle: undefined,
// Original text: 'Warning: local remotes will use limited XOA disk space. Only for advanced users.'
localRemoteWarningMessage:
'Предупреждение: локальные удаленные устройства будут использовать ограниченное дисковое пространство XOA. Только для продвинутых пользователей.',
localRemoteWarningMessage: 'Предупреждение: локальные удаленные устройства будут использовать ограниченное дисковое пространство XOA. Только для продвинутых пользователей.',
// Original text: 'Warning: this feature works only with XenServer 6.5 or newer.'
backupVersionWarning: undefined,
@@ -2554,8 +2553,7 @@ export default {
noHostsAvailable: 'Нет доступных хостов',
// Original text: "VMs created from this resource set shall run on the following hosts."
availableHostsDescription:
'Виртуальные машины, созданные из этого набора ресурсов, должны работать на следующих хостах.',
availableHostsDescription: 'Виртуальные машины, созданные из этого набора ресурсов, должны работать на следующих хостах.',
// Original text: "Maximum CPUs"
maxCpus: 'Максимум CPUs',
@@ -2884,8 +2882,7 @@ export default {
deleteVmModalTitle: 'Удалить ВМ',
// Original text: "Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED"
deleteVmModalMessage:
'Вы уверены, что хотите удалить эту виртуальную машину? ВСЕ ДИСКИ ВИРТУАЛЬНОЙ МАШИНЫ БУДУТ УДАЛЕНЫ!',
deleteVmModalMessage: 'Вы уверены, что хотите удалить эту виртуальную машину? ВСЕ ДИСКИ ВИРТУАЛЬНОЙ МАШИНЫ БУДУТ УДАЛЕНЫ!',
// Original text: "Migrate VM"
migrateVmModalTitle: 'Переместить ВМ',

View File

@@ -2418,7 +2418,6 @@ const messages = {
licensesBinding: 'Licenses binding',
notEnoughXcpngLicenses: 'Not enough XCP-ng licenses',
notBoundSelectLicense: 'Not bound (Plan (ID), expiration date)',
xcpngLicensesBindingAvancedView: "To bind an XCP-ng license, go the pool's Advanced tab.",
xosanUnregisteredDisclaimer:
'You are not registered and therefore will not be able to create or manage your XOSAN SRs. {link}',
xosanSourcesDisclaimer:

View File

@@ -323,9 +323,6 @@ export default class Licenses extends Component {
return (
<Container>
<Row className='text-info mb-1'>
<Icon icon='info' /> <i>{_('xcpngLicensesBindingAvancedView')}</i>
</Row>
<Row className='mb-1'>
<Col>
<a

68
scripts/lint-staged.js Executable file
View File

@@ -0,0 +1,68 @@
#!/usr/bin/env node
'use strict'
const formatFiles = files => {
run('./node_modules/.bin/prettier', ['--write'].concat(files))
}
const testFiles = files => {
run('./node_modules/.bin/eslint', ['--ignore-pattern', '!*'].concat(files))
run(
'./node_modules/.bin/jest',
['--testRegex=^(?!.*.integ.spec.js$).*.spec.js$', '--findRelatedTests', '--passWithNoTests'].concat(files)
)
}
// -----------------------------------------------------------------------------
const { execFileSync, spawnSync } = require('child_process')
const { readFileSync, writeFileSync } = require('fs')
const run = (command, args) => {
const { status } = spawnSync(command, args, { stdio: 'inherit' })
if (status !== 0) {
process.exit(status)
}
}
const gitDiff = (what, args = []) =>
execFileSync('git', ['diff-' + what, '--diff-filter=AM', '--ignore-submodules', '--name-only'].concat(args), {
encoding: 'utf8',
})
.split('\n')
.filter(_ => _ !== '')
const gitDiffFiles = (files = []) => gitDiff('files', files)
const gitDiffIndex = () => gitDiff('index', ['--cached', 'HEAD'])
// -----------------------------------------------------------------------------
const files = gitDiffIndex().filter(_ => _.endsWith('.cjs') || _.endsWith('.js') || _.endsWith('.mjs'))
if (files.length === 0) {
return
}
// save the list of files with unstaged changes
let unstaged = gitDiffFiles(files)
// format all files
formatFiles(files)
if (unstaged.length !== 0) {
// refresh the list of files with unstaged changes, maybe the
// changes have been reverted by the formatting
run('git', ['update-index', '-q', '--refresh'])
unstaged = gitDiffFiles(unstaged)
if (unstaged.length !== 0) {
const contents = unstaged.map(name => readFileSync(name))
process.on('exit', () => unstaged.map((name, i) => writeFileSync(name, contents[i])))
run('git', ['checkout'].concat(unstaged))
formatFiles(unstaged)
}
}
// add formatting changes so that even if the test fails, there won't be
// stylistic diffs between files and index
run('git', ['add'].concat(files))
testFiles(files)

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env node
'use strict'
const { join, relative, sep } = require('path')
const [, , script, ...files] = process.argv
const pkgs = new Set()
const root = join(__dirname, '..')
for (const file of files) {
const parts = relative(root, file).split(sep)
if ((parts.length > 2 && parts[0] === 'packages') || parts[0][0] === '@') {
pkgs.add(parts.slice(0, 2).join(sep))
}
}
if (pkgs.size !== 0) {
const args = ['run', '--if-present', script]
for (const pkg of pkgs) {
args.push('-w', pkg)
}
const { status } = require('child_process').spawnSync('npm', args, { stdio: 'inherit' })
if (status !== 0) {
process.exit(status)
}
}

View File

@@ -2952,6 +2952,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.3.tgz#d7f7ba828ad9e540270f01ce00d391c54e6e0abc"
integrity sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg==
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
"@types/prettier@^2.1.5":
version "2.7.1"
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e"
@@ -3652,14 +3657,6 @@
"@vueuse/shared" "9.5.0"
vue-demi "*"
"@vueuse/math@^9.5.0":
version "9.5.0"
resolved "https://registry.yarnpkg.com/@vueuse/math/-/math-9.5.0.tgz#df20ce74031727a4eaef3cdbaa443bfda80fb3e1"
integrity sha512-dPr5CkxE4Oo+OEvTqPfAZ8Lv1AVbnLH2N5gJSm5EWykxGPLbSaimUIckqXXR8DDyvaWIV545tELekpFUHLoFmw==
dependencies:
"@vueuse/shared" "9.5.0"
vue-demi "*"
"@vueuse/metadata@9.5.0":
version "9.5.0"
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-9.5.0.tgz#b01c84230261ddee4d439ae5d9c21343dc5ae565"
@@ -6170,6 +6167,11 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
compare-versions@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62"
integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==
compare-versions@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-5.0.1.tgz#14c6008436d994c3787aba38d4087fabe858555e"
@@ -6447,6 +6449,17 @@ cosmiconfig@^5.0.0:
js-yaml "^3.13.1"
parse-json "^4.0.0"
cosmiconfig@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6"
integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==
dependencies:
"@types/parse-json" "^4.0.0"
import-fresh "^3.2.1"
parse-json "^5.0.0"
path-type "^4.0.0"
yaml "^1.10.0"
create-ecdh@^4.0.0:
version "4.0.4"
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
@@ -9274,6 +9287,13 @@ find-up@^5.0.0:
locate-path "^6.0.0"
path-exists "^4.0.0"
find-versions@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-4.0.0.tgz#3c57e573bf97769b8cb8df16934b627915da4965"
integrity sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==
dependencies:
semver-regex "^3.1.2"
findit@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/findit/-/findit-2.0.0.tgz#6509f0126af4c178551cfa99394e032e13a4d56e"
@@ -10718,10 +10738,21 @@ human-signals@^3.0.1:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-3.0.1.tgz#c740920859dafa50e5a3222da9d3bf4bb0e5eef5"
integrity sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==
husky@^8.0.2:
version "8.0.2"
resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.2.tgz#5816a60db02650f1f22c8b69b928fd6bcd77a236"
integrity sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg==
husky@^4.2.5:
version "4.3.8"
resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.8.tgz#31144060be963fd6850e5cc8f019a1dfe194296d"
integrity sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==
dependencies:
chalk "^4.0.0"
ci-info "^2.0.0"
compare-versions "^3.6.0"
cosmiconfig "^7.0.0"
find-versions "^4.0.0"
opencollective-postinstall "^2.0.2"
pkg-dir "^5.0.0"
please-upgrade-node "^3.2.0"
slash "^3.0.0"
which-pm-runs "^1.0.0"
iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24:
version "0.4.24"
@@ -14766,7 +14797,7 @@ parse-json@^4.0.0:
error-ex "^1.3.1"
json-parse-better-errors "^1.0.1"
parse-json@^5.2.0:
parse-json@^5.0.0, parse-json@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
@@ -15117,6 +15148,13 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0:
dependencies:
find-up "^4.0.0"
pkg-dir@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760"
integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==
dependencies:
find-up "^5.0.0"
placement.js@^1.0.0-beta.5:
version "1.0.0-beta.5"
resolved "https://registry.yarnpkg.com/placement.js/-/placement.js-1.0.0-beta.5.tgz#2aac6bd8e670729bbf26ad47f2f9656b19e037d5"
@@ -15127,6 +15165,13 @@ platform@^1.3.0, platform@^1.3.3:
resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7"
integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==
please-upgrade-node@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"
integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==
dependencies:
semver-compare "^1.0.0"
plugin-error@1.0.1, plugin-error@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c"
@@ -17252,6 +17297,11 @@ selfsigned@^1.10.8:
dependencies:
node-forge "^0.10.0"
semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==
semver-diff@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b"
@@ -17266,6 +17316,11 @@ semver-greatest-satisfied-range@^1.1.0:
dependencies:
sver-compat "^1.5.0"
semver-regex@^3.1.2:
version "3.1.4"
resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.4.tgz#13053c0d4aa11d070a2f2872b6b1e3ae1e1971b4"
integrity sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==
"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
@@ -19968,6 +20023,11 @@ which-module@^2.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==
which-pm-runs@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.1.0.tgz#35ccf7b1a0fce87bd8b92a478c9d045785d3bf35"
integrity sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==
which@^1.2.14, which@^1.2.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"