Compare commits

..

1 Commits

Author SHA1 Message Date
Thierry
b8f741cb29 feat(lite): use new defineSlots macro from Vue 3.3 2023-07-12 10:52:11 +02:00
116 changed files with 22186 additions and 49961 deletions

View File

@@ -21,12 +21,12 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
cache: 'yarn'
- name: Install project dependencies
run: npm ci
run: yarn
- name: Build the project
run: npm run build
run: yarn build
- name: Lint tests
run: npm run test-lint
run: yarn test-lint
- name: Integration tests
run: sudo npm run test-integration
run: sudo yarn test-integration

View File

@@ -1,7 +1,9 @@
import LRU from 'lru-cache'
import Fuse from 'fuse-native'
import { VhdSynthetic } from 'vhd-lib'
import { Disposable, fromCallback } from 'promise-toolbox'
'use strict'
const LRU = require('lru-cache')
const Fuse = require('fuse-native')
const { VhdSynthetic } = require('vhd-lib')
const { Disposable, fromCallback } = require('promise-toolbox')
// build a s stat object from https://github.com/fuse-friends/fuse-native/blob/master/test/fixtures/stat.js
const stat = st => ({
@@ -14,7 +16,7 @@ const stat = st => ({
gid: st.gid !== undefined ? st.gid : process.getgid(),
})
export const mount = Disposable.factory(async function* mount(handler, diskPath, mountDir) {
exports.mount = Disposable.factory(async function* mount(handler, diskPath, mountDir) {
const vhd = yield VhdSynthetic.fromVhdChain(handler, diskPath)
const cache = new LRU({

View File

@@ -15,9 +15,8 @@
"url": "https://vates.fr"
},
"engines": {
"node": ">=14"
"node": ">=10.0"
},
"main": "./index.mjs",
"dependencies": {
"fuse-native": "^2.2.6",
"lru-cache": "^7.14.0",

View File

@@ -0,0 +1,42 @@
'use strict'
exports.INIT_PASSWD = Buffer.from('NBDMAGIC') // "NBDMAGIC" ensure we're connected to a nbd server
exports.OPTS_MAGIC = Buffer.from('IHAVEOPT') // "IHAVEOPT" start an option block
exports.NBD_OPT_REPLY_MAGIC = 1100100111001001n // magic received during negociation
exports.NBD_OPT_EXPORT_NAME = 1
exports.NBD_OPT_ABORT = 2
exports.NBD_OPT_LIST = 3
exports.NBD_OPT_STARTTLS = 5
exports.NBD_OPT_INFO = 6
exports.NBD_OPT_GO = 7
exports.NBD_FLAG_HAS_FLAGS = 1 << 0
exports.NBD_FLAG_READ_ONLY = 1 << 1
exports.NBD_FLAG_SEND_FLUSH = 1 << 2
exports.NBD_FLAG_SEND_FUA = 1 << 3
exports.NBD_FLAG_ROTATIONAL = 1 << 4
exports.NBD_FLAG_SEND_TRIM = 1 << 5
exports.NBD_FLAG_FIXED_NEWSTYLE = 1 << 0
exports.NBD_CMD_FLAG_FUA = 1 << 0
exports.NBD_CMD_FLAG_NO_HOLE = 1 << 1
exports.NBD_CMD_FLAG_DF = 1 << 2
exports.NBD_CMD_FLAG_REQ_ONE = 1 << 3
exports.NBD_CMD_FLAG_FAST_ZERO = 1 << 4
exports.NBD_CMD_READ = 0
exports.NBD_CMD_WRITE = 1
exports.NBD_CMD_DISC = 2
exports.NBD_CMD_FLUSH = 3
exports.NBD_CMD_TRIM = 4
exports.NBD_CMD_CACHE = 5
exports.NBD_CMD_WRITE_ZEROES = 6
exports.NBD_CMD_BLOCK_STATUS = 7
exports.NBD_CMD_RESIZE = 8
exports.NBD_REQUEST_MAGIC = 0x25609513 // magic number to create a new NBD request to send to the server
exports.NBD_REPLY_MAGIC = 0x67446698 // magic number received from the server when reading response to a nbd request
exports.NBD_REPLY_ACK = 1
exports.NBD_DEFAULT_PORT = 10809
exports.NBD_DEFAULT_BLOCK_SIZE = 64 * 1024

View File

@@ -1,41 +0,0 @@
export const INIT_PASSWD = Buffer.from('NBDMAGIC') // "NBDMAGIC" ensure we're connected to a nbd server
export const OPTS_MAGIC = Buffer.from('IHAVEOPT') // "IHAVEOPT" start an option block
export const NBD_OPT_REPLY_MAGIC = 1100100111001001n // magic received during negociation
export const NBD_OPT_EXPORT_NAME = 1
export const NBD_OPT_ABORT = 2
export const NBD_OPT_LIST = 3
export const NBD_OPT_STARTTLS = 5
export const NBD_OPT_INFO = 6
export const NBD_OPT_GO = 7
export const NBD_FLAG_HAS_FLAGS = 1 << 0
export const NBD_FLAG_READ_ONLY = 1 << 1
export const NBD_FLAG_SEND_FLUSH = 1 << 2
export const NBD_FLAG_SEND_FUA = 1 << 3
export const NBD_FLAG_ROTATIONAL = 1 << 4
export const NBD_FLAG_SEND_TRIM = 1 << 5
export const NBD_FLAG_FIXED_NEWSTYLE = 1 << 0
export const NBD_CMD_FLAG_FUA = 1 << 0
export const NBD_CMD_FLAG_NO_HOLE = 1 << 1
export const NBD_CMD_FLAG_DF = 1 << 2
export const NBD_CMD_FLAG_REQ_ONE = 1 << 3
export const NBD_CMD_FLAG_FAST_ZERO = 1 << 4
export const NBD_CMD_READ = 0
export const NBD_CMD_WRITE = 1
export const NBD_CMD_DISC = 2
export const NBD_CMD_FLUSH = 3
export const NBD_CMD_TRIM = 4
export const NBD_CMD_CACHE = 5
export const NBD_CMD_WRITE_ZEROES = 6
export const NBD_CMD_BLOCK_STATUS = 7
export const NBD_CMD_RESIZE = 8
export const NBD_REQUEST_MAGIC = 0x25609513 // magic number to create a new NBD request to send to the server
export const NBD_REPLY_MAGIC = 0x67446698 // magic number received from the server when reading response to a nbd request
export const NBD_REPLY_ACK = 1
export const NBD_DEFAULT_PORT = 10809
export const NBD_DEFAULT_BLOCK_SIZE = 64 * 1024

View File

@@ -1,11 +1,8 @@
import assert from 'node:assert'
import { Socket } from 'node:net'
import { connect } from 'node:tls'
import { fromCallback, pRetry, pDelay, pTimeout } from 'promise-toolbox'
import { readChunkStrict } from '@vates/read-chunk'
import { createLogger } from '@xen-orchestra/log'
import {
'use strict'
const assert = require('node:assert')
const { Socket } = require('node:net')
const { connect } = require('node:tls')
const {
INIT_PASSWD,
NBD_CMD_READ,
NBD_DEFAULT_BLOCK_SIZE,
@@ -20,13 +17,16 @@ import {
NBD_REQUEST_MAGIC,
OPTS_MAGIC,
NBD_CMD_DISC,
} from './constants.mjs'
} = require('./constants.js')
const { fromCallback, pRetry, pDelay, pTimeout } = require('promise-toolbox')
const { readChunkStrict } = require('@vates/read-chunk')
const { createLogger } = require('@xen-orchestra/log')
const { warn } = createLogger('vates:nbd-client')
// documentation is here : https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md
export default class NbdClient {
module.exports = class NbdClient {
#serverAddress
#serverCert
#serverPort

View File

@@ -17,7 +17,6 @@
"engines": {
"node": ">=14.0"
},
"main": "./index.mjs",
"dependencies": {
"@vates/async-each": "^1.0.0",
"@vates/read-chunk": "^1.1.1",
@@ -32,6 +31,6 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test-integration": "tap --lines 97 --functions 95 --branches 74 --statements 97 tests/*.integ.mjs"
"test-integration": "tap --lines 97 --functions 95 --branches 74 --statements 97 tests/*.integ.js"
}
}

View File

@@ -1,12 +1,13 @@
import NbdClient from '../index.mjs'
import { spawn, exec } from 'node:child_process'
import fs from 'node:fs/promises'
import { test } from 'tap'
import tmp from 'tmp'
import { pFromCallback } from 'promise-toolbox'
import { Socket } from 'node:net'
import { NBD_DEFAULT_PORT } from '../constants.mjs'
import assert from 'node:assert'
'use strict'
const NbdClient = require('../index.js')
const { spawn, exec } = require('node:child_process')
const fs = require('node:fs/promises')
const { test } = require('tap')
const tmp = require('tmp')
const { pFromCallback } = require('promise-toolbox')
const { Socket } = require('node:net')
const { NBD_DEFAULT_PORT } = require('../constants.js')
const assert = require('node:assert')
const FILE_SIZE = 10 * 1024 * 1024

View File

@@ -1,3 +1,4 @@
'use strict'
/*
node-vsphere-soap
@@ -11,18 +12,17 @@
*/
import { EventEmitter } from 'events'
import axios from 'axios'
import https from 'node:https'
import util from 'util'
import soap from 'soap'
import Cookie from 'soap-cookie' // required for session persistence
const EventEmitter = require('events').EventEmitter
const axios = require('axios')
const https = require('node:https')
const util = require('util')
const soap = require('soap')
const Cookie = require('soap-cookie') // required for session persistence
// Client class
// inherits from EventEmitter
// possible events: connect, error, ready
export function Client(vCenterHostname, username, password, sslVerify) {
function Client(vCenterHostname, username, password, sslVerify) {
this.status = 'disconnected'
this.reconnectCount = 0
@@ -228,3 +228,4 @@ function _soapErrorHandler(self, emitter, command, args, err) {
}
// end
exports.Client = Client

View File

@@ -2,7 +2,7 @@
"name": "@vates/node-vsphere-soap",
"version": "1.0.0",
"description": "interface to vSphere SOAP/WSDL from node for interfacing with vCenter or ESXi, forked from node-vsphere-soap",
"main": "lib/client.mjs",
"main": "lib/client.js",
"author": "reedog117",
"repository": {
"directory": "@vates/node-vsphere-soap",
@@ -30,7 +30,7 @@
"private": false,
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/node-vsphere-soap",
"engines": {
"node": ">=14"
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public"

View File

@@ -1,11 +1,15 @@
'use strict'
// place your own credentials here for a vCenter or ESXi server
// this information will be used for connecting to a vCenter instance
// for module testing
// name the file config-test.js
export const vCenterTestCreds = {
const vCenterTestCreds = {
vCenterIP: 'vcsa',
vCenterUser: 'vcuser',
vCenterPassword: 'vcpw',
vCenter: true,
}
exports.vCenterTestCreds = vCenterTestCreds

View File

@@ -1,16 +1,18 @@
'use strict'
/*
vsphere-soap.test.js
tests for the vCenterConnectionInstance class
*/
import assert from 'assert'
import { describe, it } from 'test'
const assert = require('assert')
const { describe, it } = require('test')
import * as vc from '../lib/client.mjs'
const vc = require('../lib/client')
// eslint-disable-next-line n/no-missing-import
import { vCenterTestCreds as TestCreds } from '../config-test.mjs'
// eslint-disable-next-line n/no-missing-require
const TestCreds = require('../config-test.js').vCenterTestCreds
const VItest = new vc.Client(TestCreds.vCenterIP, TestCreds.vCenterUser, TestCreds.vCenterPassword, false)

View File

@@ -1,7 +1,6 @@
'use strict'
const assert = require('assert')
const isUtf8 = require('isutf8')
/**
* Read a chunk of data from a stream.
@@ -82,13 +81,6 @@ exports.readChunkStrict = async function readChunkStrict(stream, size) {
if (size !== undefined && chunk.length !== size) {
const error = new Error(`stream has ended with not enough data (actual: ${chunk.length}, expected: ${size})`)
// Buffer.isUtf8 is too recent for now
// @todo : replace external package by Buffer.isUtf8 when the supported version of node reach 18
if (chunk.length < 1024 && isUtf8(chunk)) {
error.text = chunk.toString('utf8')
}
Object.defineProperties(error, {
chunk: {
value: chunk,

View File

@@ -102,37 +102,12 @@ describe('readChunkStrict', function () {
assert.strictEqual(error.chunk, undefined)
})
it('throws if stream ends with not enough data, utf8', async () => {
it('throws if stream ends with not enough data', async () => {
const error = await rejectionOf(readChunkStrict(makeStream(['foo', 'bar']), 10))
assert(error instanceof Error)
assert.strictEqual(error.message, 'stream has ended with not enough data (actual: 6, expected: 10)')
assert.strictEqual(error.text, 'foobar')
assert.deepEqual(error.chunk, Buffer.from('foobar'))
})
it('throws if stream ends with not enough data, non utf8 ', async () => {
const source = [Buffer.alloc(10, 128), Buffer.alloc(10, 128)]
const error = await rejectionOf(readChunkStrict(makeStream(source), 30))
assert(error instanceof Error)
assert.strictEqual(error.message, 'stream has ended with not enough data (actual: 20, expected: 30)')
assert.strictEqual(error.text, undefined)
assert.deepEqual(error.chunk, Buffer.concat(source))
})
it('throws if stream ends with not enough data, utf8 , long data', async () => {
const source = Buffer.from('a'.repeat(1500))
const error = await rejectionOf(readChunkStrict(makeStream([source]), 2000))
assert(error instanceof Error)
assert.strictEqual(error.message, `stream has ended with not enough data (actual: 1500, expected: 2000)`)
assert.strictEqual(error.text, undefined)
assert.deepEqual(error.chunk, source)
})
it('succeed', async () => {
const source = Buffer.from('a'.repeat(20))
const chunk = await readChunkStrict(makeStream([source]), 10)
assert.deepEqual(source.subarray(10), chunk)
})
})
describe('skip', function () {
@@ -159,16 +134,6 @@ describe('skip', function () {
it('returns less size if stream ends', async () => {
assert.deepEqual(await skip(makeStream('foo bar'), 10), 7)
})
it('put back if it read too much', async () => {
let source = makeStream(['foo', 'bar'])
await skip(source, 1) // read part of data chunk
const chunk = (await readChunkStrict(source, 2)).toString('utf-8')
assert.strictEqual(chunk, 'oo')
source = makeStream(['foo', 'bar'])
assert.strictEqual(await skip(source, 3), 3) // read aligned with data chunk
})
})
describe('skipStrict', function () {
@@ -179,9 +144,4 @@ describe('skipStrict', function () {
assert.strictEqual(error.message, 'stream has ended with not enough data (actual: 7, expected: 10)')
assert.deepEqual(error.bytesSkipped, 7)
})
it('succeed', async () => {
const source = makeStream(['foo', 'bar', 'baz'])
const res = await skipStrict(source, 4)
assert.strictEqual(res, undefined)
})
})

View File

@@ -33,8 +33,5 @@
},
"devDependencies": {
"test": "^3.2.1"
},
"dependencies": {
"isutf8": "^4.0.0"
}
}

View File

@@ -61,10 +61,10 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "npm run clean",
"predev": "npm run clean",
"prepublishOnly": "npm run build",
"pretest": "npm run build",
"prebuild": "yarn run clean",
"predev": "yarn run clean",
"prepublishOnly": "yarn run build",
"pretest": "yarn run build",
"postversion": "npm publish",
"test": "node--test ./dist/"
},

View File

@@ -209,7 +209,7 @@ describe('encryption', () => {
// encrypt with a non default algorithm
const encryptor = _getEncryptor('aes-256-cbc', '73c1838d7d8a6088ca2317fb5f29cd91')
await fs.writeFile(`${dir}/encryption.json`, `{"algorithm": "aes-256-gcm"}`)
await fs.writeFile(`${dir}/encryption.json`, `{"algorithm": "aes-256-gmc"}`)
await fs.writeFile(`${dir}/metadata.json`, encryptor.encryptData(`{"random": "NOTSORANDOM"}`))
// remote is now non empty : can't modify key anymore

View File

@@ -2,9 +2,6 @@
## **next**
- Ability to export selected VMs as CSV file (PR [#6915](https://github.com/vatesfr/xen-orchestra/pull/6915))
- [Pool/VMs] Ability to export selected VMs as JSON file (PR [#6911](https://github.com/vatesfr/xen-orchestra/pull/6911))
## **0.1.1** (2023-07-03)
- Invalidate sessionId token after logout (PR [#6480](https://github.com/vatesfr/xen-orchestra/pull/6480))

View File

@@ -2,8 +2,8 @@
- Clone
- Copy `.env.dist` to `.env` and set vars
- `npm ci`
- `npm run dev`
- `yarn`
- `yarn dev`
## Conventions

View File

@@ -7,7 +7,7 @@
"preview": "vite preview --port 4173",
"build-only": "GIT_HEAD=$(git rev-parse HEAD) vite build",
"deploy": "./scripts/deploy.sh",
"test": "npm run type-check",
"test": "yarn run type-check",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
@@ -17,7 +17,6 @@
"@fortawesome/vue-fontawesome": "^3.0.1",
"@novnc/novnc": "^1.3.0",
"@types/d3-time-format": "^4.0.0",
"@types/file-saver": "^2.0.5",
"@types/lodash-es": "^4.17.6",
"@types/marked": "^4.0.8",
"@vueuse/core": "^10.1.2",
@@ -26,7 +25,6 @@
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",
"echarts": "^5.3.3",
"file-saver": "^2.0.5",
"highlight.js": "^11.6.0",
"human-format": "^1.1.0",
"iterable-backoff": "^0.1.0",

View File

@@ -14,8 +14,8 @@ SERVER="www-xo.gpn.vates.fr"
echo "Building XO Lite"
(cd ../.. && npm ci)
npm run build-only --base="$BASE"
(cd ../.. && yarn)
yarn build-only --base="$BASE"
echo "Deploying XO Lite from $DIST"

View File

@@ -21,9 +21,14 @@ import AccountButton from "@/components/AccountButton.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useNavigationStore } from "@/stores/navigation.store";
import { useUiStore } from "@/stores/ui.store";
import type { SlotDefinition } from "@/types";
import { faBars } from "@fortawesome/free-solid-svg-icons";
import { storeToRefs } from "pinia";
defineSlots<{
default: SlotDefinition;
}>();
const uiStore = useUiStore();
const { isMobile } = storeToRefs(uiStore);

View File

@@ -43,6 +43,7 @@
</template>
<script lang="ts" setup>
import type { SlotDefinition } from "@/types";
import { computed, toRef, watch } from "vue";
import type { Filters } from "@/types/filter";
import type { Sorts } from "@/types/sort";
@@ -55,6 +56,11 @@ import useFilteredCollection from "@/composables/filtered-collection.composable"
import useMultiSelect from "@/composables/multi-select.composable";
import useSortedCollection from "@/composables/sorted-collection.composable";
defineSlots<{
"head-row": SlotDefinition;
"body-row": SlotDefinition<{ item: any }>;
}>();
const props = defineProps<{
modelValue?: string[];
availableFilters?: Filters;

View File

@@ -11,8 +11,13 @@
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { SlotDefinition } from "@/types";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineSlots<{
default: SlotDefinition;
}>();
defineProps<{
icon?: IconDefinition;
}>();

View File

@@ -13,10 +13,15 @@
>
import UiSpinner from "@/components/ui/UiSpinner.vue";
import type { XenApiRecord } from "@/libs/xen-api";
import type { SlotDefinition } from "@/types";
import ObjectNotFoundView from "@/views/ObjectNotFoundView.vue";
import { computed } from "vue";
import { useRouter } from "vue-router";
defineSlots<{
default: SlotDefinition;
}>();
const props = defineProps<{
isReady: boolean;
uuidChecker: (uuid: I) => boolean;

View File

@@ -11,10 +11,15 @@
</template>
<script lang="ts" setup>
import UiTab from "@/components/ui/UiTab.vue";
import type { SlotDefinition } from "@/types";
import { IK_TAB_BAR_DISABLED } from "@/types/injection-keys";
import { computed, inject } from "vue";
import type { RouteLocationRaw } from "vue-router";
import UiTab from "@/components/ui/UiTab.vue";
defineSlots<{
default: SlotDefinition;
}>();
defineProps<{
to: RouteLocationRaw;

View File

@@ -12,8 +12,14 @@
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { SlotDefinition } from "@/types";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineSlots<{
default: SlotDefinition;
actions: SlotDefinition;
}>();
defineProps<{
icon: IconDefinition;
}>();

View File

@@ -26,8 +26,13 @@
import UiProgressBar from "@/components/ui/progress/UiProgressBar.vue";
import UiProgressLegend from "@/components/ui/progress/UiProgressLegend.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import type { SlotDefinition } from "@/types";
import { computed } from "vue";
defineSlots<{
footer: SlotDefinition<{ totalPercent: number }>;
}>();
interface Data {
id: string;
value: number;

View File

@@ -7,6 +7,7 @@
<script lang="ts" setup>
import UiCard from "@/components/ui/UiCard.vue";
import type { SlotDefinition } from "@/types";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_CHART_VALUE_FORMATTER } from "@/types/injection-keys";
import { utcFormat } from "d3-time-format";
@@ -25,6 +26,10 @@ import VueCharts from "vue-echarts";
const Y_AXIS_MAX_VALUE = 200;
defineSlots<{
summary: SlotDefinition;
}>();
const props = defineProps<{
title?: string;
subtitle?: string;

View File

@@ -118,12 +118,20 @@ import {
ModelParam,
type Param,
} from "@/libs/story/story-param";
import type { SlotDefinition } from "@/types";
import { faSliders } from "@fortawesome/free-solid-svg-icons";
import "highlight.js/styles/github-dark.css";
import { uniqueId, upperFirst } from "lodash-es";
import { computed, reactive, ref, watch, watchEffect } from "vue";
import { useRoute } from "vue-router";
defineSlots<{
default: SlotDefinition<{
properties: Record<string, any>;
settings: Record<string, any>;
}>;
}>();
const tab = (tab: TAB, params: Param[]) =>
reactive({
onClick: () => (selectedTab.value = tab),

View File

@@ -41,8 +41,18 @@
</template>
<script lang="ts" setup>
import type { SlotDefinition } from "@/types";
const moonDistance = 384400;
defineSlots<{
default: SlotDefinition;
"named-slot": SlotDefinition;
"named-scoped-slot": SlotDefinition<{
moonDistance: number;
}>;
}>();
withDefaults(
defineProps<{
imString: string;

View File

@@ -4,7 +4,13 @@
</table>
</template>
<script lang="ts" setup></script>
<script lang="ts" setup>
import type { SlotDefinition } from "@/types";
defineSlots<{
default: SlotDefinition;
}>();
</script>
<style lang="postcss" scoped>
.story-params-table {

View File

@@ -52,7 +52,7 @@
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { Color } from "@/types";
import type { Color, SlotDefinition } from "@/types";
import {
IK_FORM_INPUT_COLOR,
IK_FORM_LABEL_DISABLED,
@@ -73,6 +73,10 @@ import {
defineOptions({ inheritAttrs: false });
defineSlots<{
default: SlotDefinition;
}>();
const props = withDefaults(
defineProps<{
id?: string;

View File

@@ -39,7 +39,6 @@
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { Color } from "@/types";
import {
IK_FORM_HAS_LABEL,
IK_FORM_INPUT_COLOR,
IK_FORM_LABEL_DISABLED,
IK_INPUT_ID,
@@ -47,9 +46,7 @@ import {
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { uniqueId } from "lodash-es";
import { computed, provide, useSlots } from "vue";
const slots = useSlots();
import { computed, provide } from "vue";
const props = defineProps<{
label?: string;
@@ -79,11 +76,6 @@ const color = computed<Color | undefined>(() => {
provide(IK_FORM_INPUT_COLOR, color);
provide(
IK_FORM_HAS_LABEL,
computed(() => slots.label !== undefined)
);
provide(
IK_FORM_LABEL_DISABLED,
computed(() => props.disabled ?? false)

View File

@@ -6,9 +6,14 @@
<script lang="ts" setup>
import FormInput from "@/components/form/FormInput.vue";
import type { SlotDefinition } from "@/types";
import { IK_INPUT_TYPE } from "@/types/injection-keys";
import { provide } from "vue";
defineSlots<{
default: SlotDefinition;
}>();
provide(IK_INPUT_TYPE, "select");
</script>

View File

@@ -8,8 +8,13 @@
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { SlotDefinition } from "@/types";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineSlots<{
default: SlotDefinition;
}>();
defineProps<{
icon?: IconDefinition;
}>();

View File

@@ -24,10 +24,16 @@
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { vTooltip } from "@/directives/tooltip.directive";
import { hasEllipsis } from "@/libs/utils";
import type { SlotDefinition } from "@/types";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { computed, ref } from "vue";
import type { RouteLocationRaw } from "vue-router";
defineSlots<{
default: SlotDefinition;
actions: SlotDefinition;
}>();
defineProps<{
icon: IconDefinition;
route: RouteLocationRaw;

View File

@@ -1,6 +1,6 @@
<template>
<slot :is-open="isOpen" :open="open" name="trigger" />
<Teleport to="body" :disabled="!shouldTeleport">
<Teleport :disabled="!shouldTeleport" to="body">
<ul
v-if="!hasTrigger || isOpen"
ref="menu"
@@ -14,15 +14,24 @@
</template>
<script lang="ts" setup>
import type { SlotDefinition } from "@/types";
import {
IK_CLOSE_MENU,
IK_MENU_DISABLED,
IK_MENU_HORIZONTAL,
IK_MENU_TELEPORTED,
} from "@/types/injection-keys";
import { onClickOutside, unrefElement, whenever } from "@vueuse/core";
import placementJs, { type Options } from "placement.js";
import { computed, inject, nextTick, provide, ref, useSlots } from "vue";
import { onClickOutside, unrefElement, whenever } from "@vueuse/core";
defineSlots<{
default: SlotDefinition;
trigger: SlotDefinition<{
isOpen: boolean;
open: (event: MouseEvent) => void;
}>;
}>();
const props = defineProps<{
horizontal?: boolean;

View File

@@ -36,6 +36,7 @@
import AppMenu from "@/components/menu/AppMenu.vue";
import MenuTrigger from "@/components/menu/MenuTrigger.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { SlotDefinition } from "@/types";
import {
IK_CLOSE_MENU,
IK_MENU_DISABLED,
@@ -45,6 +46,11 @@ import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faAngleDown, faAngleRight } from "@fortawesome/free-solid-svg-icons";
import { computed, inject, ref } from "vue";
defineSlots<{
default: SlotDefinition;
submenu?: SlotDefinition;
}>();
const props = defineProps<{
icon?: IconDefinition;
onClick?: () => any;

View File

@@ -6,9 +6,14 @@
</template>
<script lang="ts" setup>
import type { SlotDefinition } from "@/types";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
defineSlots<{
default: SlotDefinition;
}>();
defineProps<{
active?: boolean;
busy?: boolean;

View File

@@ -1,48 +1,21 @@
<template>
<UiCard :color="hasError ? 'error' : undefined">
<UiCardTitle>
{{ $t("cpu-usage") }}
<template v-if="vmStatsCanBeExpired || hostStatsCanBeExpired" #right>
<UiSpinner v-tooltip="$t('fetching-fresh-data')" />
</template>
</UiCardTitle>
<UiCardTitle>{{ $t("cpu-usage") }}</UiCardTitle>
<HostsCpuUsage />
<VmsCpuUsage />
</UiCard>
</template>
<script lang="ts" setup>
import { vTooltip } from "@/directives/tooltip.directive";
import HostsCpuUsage from "@/components/pool/dashboard/cpuUsage/HostsCpuUsage.vue";
import VmsCpuUsage from "@/components/pool/dashboard/cpuUsage/VmsCpuUsage.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostStore } from "@/stores/host.store";
import { useVmStore } from "@/stores/vm.store";
import { computed, inject, type ComputedRef } from "vue";
import type { Stat } from "@/composables/fetch-stats.composable";
import type { HostStats, VmStats } from "@/libs/xapi-stats";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import { computed } from "vue";
const { hasError: hasVmError } = useVmStore().subscribe();
const { hasError: hasHostError } = useHostStore().subscribe();
const vmStats = inject<ComputedRef<Stat<VmStats>[]>>(
"vmStats",
computed(() => [])
);
const hostStats = inject<ComputedRef<Stat<HostStats>[]>>(
"hostStats",
computed(() => [])
);
const vmStatsCanBeExpired = computed(() =>
vmStats.value.some((stat) => stat.canBeExpired)
);
const hostStatsCanBeExpired = computed(() =>
hostStats.value.some((stat) => stat.canBeExpired)
);
const hasError = computed(() => hasVmError.value || hasHostError.value);
</script>

View File

@@ -1,6 +1,5 @@
<template>
<!-- TODO: add a loader when data is not fully loaded or undefined -->
<!-- TODO: add small loader with tooltips when stats can be expired -->
<!-- TODO: display the NoData component in case of a data recovery error -->
<LinearChart
:data="data"

View File

@@ -1,50 +1,22 @@
<template>
<UiCard :color="hasError ? 'error' : undefined">
<UiCardTitle>
{{ $t("ram-usage") }}
<template v-if="vmStatsCanBeExpired || hostStatsCanBeExpired" #right>
<UiSpinner v-tooltip="$t('fetching-fresh-data')" />
</template>
</UiCardTitle>
<UiCardTitle>{{ $t("ram-usage") }}</UiCardTitle>
<HostsRamUsage />
<VmsRamUsage />
</UiCard>
</template>
<script lang="ts" setup>
import { vTooltip } from "@/directives/tooltip.directive";
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 UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostStore } from "@/stores/host.store";
import { useVmStore } from "@/stores/vm.store";
import { computed, inject } from "vue";
import type { ComputedRef } from "vue";
import type { HostStats, VmStats } from "@/libs/xapi-stats";
import type { Stat } from "@/composables/fetch-stats.composable";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import { computed } from "vue";
const { hasError: hasVmError } = useVmStore().subscribe();
const { hasError: hasHostError } = useHostStore().subscribe();
const vmStats = inject<ComputedRef<Stat<VmStats>[]>>(
"vmStats",
computed(() => [])
);
const hostStats = inject<ComputedRef<Stat<HostStats>[]>>(
"hostStats",
computed(() => [])
);
const vmStatsCanBeExpired = computed(() =>
vmStats.value.some((stat) => stat.canBeExpired)
);
const hostStatsCanBeExpired = computed(() =>
hostStats.value.some((stat) => stat.canBeExpired)
);
const hasError = computed(() => hasVmError.value || hasHostError.value);
</script>

View File

@@ -1,6 +1,5 @@
<template>
<!-- TODO: add a loader when data is not fully loaded or undefined -->
<!-- TODO: add small loader with tooltips when stats can be expired -->
<!-- TODO: Display the NoDataError component in case of a data recovery error -->
<LinearChart
:data="data"

View File

@@ -1,6 +1,5 @@
<template>
<!-- TODO: add a loader when data is not fully loaded or undefined -->
<!-- TODO: add small loader with tooltips when stats can be expired -->
<!-- TODO: display the NoDataError component in case of a data recovery error -->
<LinearChart
:data="data"

View File

@@ -7,8 +7,8 @@
'has-icon': icon !== undefined,
}"
:disabled="isBusy || isDisabled"
type="button"
class="ui-action-button"
type="button"
>
<UiIcon :busy="isBusy" :icon="icon" />
<slot />
@@ -17,6 +17,7 @@
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { SlotDefinition } from "@/types";
import {
IK_BUTTON_GROUP_BUSY,
IK_BUTTON_GROUP_DISABLED,
@@ -24,6 +25,10 @@ import {
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { computed, inject } from "vue";
defineSlots<{
default: SlotDefinition;
}>();
const props = withDefaults(
defineProps<{
busy?: boolean;

View File

@@ -6,9 +6,14 @@
</template>
<script lang="ts" setup>
import type { SlotDefinition } from "@/types";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
defineSlots<{
default: SlotDefinition;
}>();
defineProps<{
icon?: IconDefinition;
}>();

View File

@@ -14,7 +14,9 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import type { Color, SlotDefinition } from "@/types";
import {
IK_BUTTON_GROUP_BUSY,
IK_BUTTON_GROUP_COLOR,
@@ -22,10 +24,12 @@ import {
IK_BUTTON_GROUP_OUTLINED,
IK_BUTTON_GROUP_TRANSPARENT,
} from "@/types/injection-keys";
import { computed, inject } from "vue";
import type { Color } from "@/types";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { computed, inject } from "vue";
defineSlots<{
default: SlotDefinition;
}>();
const props = withDefaults(
defineProps<{

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts" setup>
import type { Color } from "@/types";
import type { Color, SlotDefinition } from "@/types";
import {
IK_BUTTON_GROUP_BUSY,
IK_BUTTON_GROUP_COLOR,
@@ -15,6 +15,9 @@ import {
} from "@/types/injection-keys";
import { computed, provide } from "vue";
defineSlots<{
default: SlotDefinition;
}>();
const props = defineProps<{
busy?: boolean;
disabled?: boolean;

View File

@@ -1,51 +0,0 @@
<template>
<MenuItem :icon="faFileExport">
{{ $t("export") }}
<template #submenu>
<MenuItem
v-tooltip="{ content: $t('coming-soon'), placement: 'left' }"
:icon="faDisplay"
>
{{ $t("export-vms") }}
</MenuItem>
<MenuItem
:icon="faCode"
@click="
exportVmsAsJsonFile(vms, `vms_${new Date().toISOString()}.json`)
"
>
{{ $t("export-table-to", { type: ".json" }) }}
</MenuItem>
<MenuItem
:icon="faFileCsv"
@click="exportVmsAsCsvFile(vms, `vms_${new Date().toISOString()}.csv`)"
>
{{ $t("export-table-to", { type: ".csv" }) }}
</MenuItem>
</template>
</MenuItem>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { exportVmsAsCsvFile, exportVmsAsJsonFile } from "@/libs/vm";
import MenuItem from "@/components/menu/MenuItem.vue";
import {
faCode,
faDisplay,
faFileCsv,
faFileExport,
} from "@fortawesome/free-solid-svg-icons";
import { useVmStore } from "@/stores/vm.store";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api";
const props = defineProps<{
vmRefs: XenApiVm["$ref"][];
}>();
const { getByOpaqueRef: getVm } = useVmStore().subscribe();
const vms = computed(() =>
props.vmRefs.map(getVm).filter((vm): vm is XenApiVm => vm !== undefined)
);
</script>

View File

@@ -25,8 +25,30 @@
<MenuItem v-tooltip="$t('coming-soon')" :icon="faCamera">
{{ $t("snapshot") }}
</MenuItem>
<VmActionExportItem :vm-refs="selectedRefs" />
<VmActionDeleteItem :vm-refs="selectedRefs" />
<MenuItem :icon="faFileExport">
{{ $t("export") }}
<template #submenu>
<MenuItem
v-tooltip="{ content: $t('coming-soon'), placement: 'left' }"
:icon="faDisplay"
>
{{ $t("export-vms") }}
</MenuItem>
<MenuItem
v-tooltip="{ content: $t('coming-soon'), placement: 'left' }"
:icon="faCode"
>
{{ $t("export-table-to", { type: ".json" }) }}
</MenuItem>
<MenuItem
v-tooltip="{ content: $t('coming-soon'), placement: 'left' }"
:icon="faFileCsv"
>
{{ $t("export-table-to", { type: ".csv" }) }}
</MenuItem>
</template>
</MenuItem>
</AppMenu>
</template>
@@ -35,7 +57,6 @@ import AppMenu from "@/components/menu/AppMenu.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import UiButton from "@/components/ui/UiButton.vue";
import VmActionCopyItem from "@/components/vm/VmActionItems/VmActionCopyItem.vue";
import VmActionExportItem from "@/components/vm/VmActionItems/VmActionExportItem.vue";
import VmActionDeleteItem from "@/components/vm/VmActionItems/VmActionDeleteItem.vue";
import VmActionPowerStateItems from "@/components/vm/VmActionItems/VmActionPowerStateItems.vue";
import { vTooltip } from "@/directives/tooltip.directive";
@@ -43,8 +64,12 @@ import type { XenApiVm } from "@/libs/xen-api";
import { useUiStore } from "@/stores/ui.store";
import {
faCamera,
faCode,
faDisplay,
faEdit,
faEllipsis,
faFileCsv,
faFileExport,
faPowerOff,
faRoute,
} from "@fortawesome/free-solid-svg-icons";

View File

@@ -10,7 +10,6 @@ import { type Pausable, promiseTimeout, useTimeoutPoll } from "@vueuse/core";
import { computed, type ComputedRef, onUnmounted, ref } from "vue";
export type Stat<T> = {
canBeExpired: boolean;
id: string;
name: string;
stats: T | undefined;
@@ -22,10 +21,8 @@ type GetStats<
S extends HostStats | VmStats
> = (
uuid: T["uuid"],
granularity: GRANULARITY,
ignoreExpired: boolean,
opts: { abortSignal?: AbortSignal }
) => Promise<XapiStatsResponse<S> | undefined> | undefined;
granularity: GRANULARITY
) => Promise<XapiStatsResponse<S>> | undefined;
export type FetchedStats<
T extends XenApiHost | XenApiVm,
@@ -44,7 +41,6 @@ export default function useFetchStats<
>(getStats: GetStats<T, S>, granularity: GRANULARITY): FetchedStats<T, S> {
const stats = ref<Map<string, Stat<S>>>(new Map());
const timestamp = ref<number[]>([0, 0]);
const abortController = new AbortController();
const register = (object: T) => {
const mapKey = `${object.uuid}-${granularity}`;
@@ -53,18 +49,13 @@ export default function useFetchStats<
return;
}
const ignoreExpired = computed(() => !stats.value.has(mapKey));
const pausable = useTimeoutPoll(
async () => {
const newStats = (await getStats(
object.uuid,
granularity,
ignoreExpired.value,
{
abortSignal: abortController.signal,
}
)) as XapiStatsResponse<S>;
if (!stats.value.has(mapKey)) {
return;
}
const newStats = await getStats(object.uuid, granularity);
if (newStats === undefined) {
return;
@@ -78,7 +69,6 @@ export default function useFetchStats<
];
stats.value.get(mapKey)!.stats = newStats.stats;
stats.value.get(mapKey)!.canBeExpired = newStats.canBeExpired;
await promiseTimeout(newStats.interval * 1000);
},
0,
@@ -86,7 +76,6 @@ export default function useFetchStats<
);
stats.value.set(mapKey, {
canBeExpired: false,
id: object.uuid,
name: object.name_label,
stats: undefined,
@@ -101,7 +90,6 @@ export default function useFetchStats<
};
onUnmounted(() => {
abortController.abort();
stats.value.forEach((stat) => stat.pausable.pause());
});

View File

@@ -1,41 +0,0 @@
import { saveAs } from "file-saver";
import type { XenApiVm } from "@/libs/xen-api";
function stringifyCsvValue(value: any) {
let res = "";
if (Array.isArray(value)) {
res = value.join(";");
} else if (typeof value === "object") {
res = JSON.stringify(value);
} else {
res = String(value);
}
return `"${res.replace(/"/g, '""')}"`;
}
export function exportVmsAsCsvFile(vms: XenApiVm[], fileName: string) {
const csvHeaders = Object.keys(vms[0]);
const csvRows = vms.map((vm) =>
csvHeaders.map((header) => stringifyCsvValue(vm[header as keyof XenApiVm]))
);
saveAs(
new Blob(
[[csvHeaders, ...csvRows].map((row) => row.join(",")).join("\n")],
{
type: "text/csv;charset=utf-8",
}
),
fileName
);
}
export function exportVmsAsJsonFile(vms: XenApiVm[], fileName: string) {
saveAs(
new Blob([JSON.stringify(vms, null, 2)], {
type: "application/json",
}),
fileName
);
}

View File

@@ -295,22 +295,18 @@ export type HostStats = {
};
export type XapiStatsResponse<T> = {
canBeExpired: boolean;
endTimestamp: number;
interval: number;
stats: T;
};
type StatsByObject = {
[uuid: string]: {
[step: string]: XapiStatsResponse<HostStats | VmStats>;
};
};
export default class XapiStats {
#xapi;
#statsByObject: StatsByObject = {};
#cachedStatsByObject: StatsByObject = {};
#statsByObject: {
[uuid: string]: {
[step: string]: XapiStatsResponse<HostStats | any>;
};
} = {};
constructor(xapi: XenApi) {
this.#xapi = xapi;
}
@@ -318,12 +314,7 @@ export default class XapiStats {
// Execute one http request on a XenServer for get stats
// Return stats (Json format) or throws got exception
@limitConcurrency(3)
async _getJson(
host: XenApiHost,
timestamp: number,
step: RRD_STEP,
{ abortSignal }: { abortSignal?: AbortSignal } = {}
) {
async _getJson(host: XenApiHost, timestamp: any, step: any) {
const resp = await this.#xapi.getResource("/rrd_updates", {
host,
query: {
@@ -333,23 +324,13 @@ export default class XapiStats {
json: "true",
start: timestamp,
},
abortSignal,
});
return JSON5.parse(await resp.text());
}
// To avoid multiple requests, we keep a cache for the stats and
// only return it if we not exceed a step
#getCachedStats(
uuid: string,
step: RRD_STEP,
currentTimeStamp: number,
ignoreExpired = false
) {
if (ignoreExpired) {
return this.#cachedStatsByObject[uuid]?.[step];
}
#getCachedStats(uuid: any, step: any, currentTimeStamp: any) {
const statsByObject = this.#statsByObject;
const stats = statsByObject[uuid]?.[step];
@@ -366,16 +347,12 @@ export default class XapiStats {
}
@synchronized.withKey(({ host }: { host: XenApiHost }) => host.uuid)
async _getAndUpdateStats<T extends VmStats | HostStats>({
abortSignal,
async _getAndUpdateStats({
host,
ignoreExpired = false,
uuid,
granularity,
}: {
abortSignal?: AbortSignal;
host: XenApiHost;
ignoreExpired?: boolean;
uuid: any;
granularity: GRANULARITY;
}) {
@@ -390,13 +367,7 @@ export default class XapiStats {
}
const currentTimeStamp = Math.floor(new Date().getTime() / 1000);
const stats = this.#getCachedStats(
uuid,
step,
currentTimeStamp,
ignoreExpired
) as XapiStatsResponse<T>;
const stats = this.#getCachedStats(uuid, step, currentTimeStamp);
if (stats !== undefined) {
return stats;
}
@@ -405,113 +376,75 @@ export default class XapiStats {
// To avoid crossing over the boundary, we ask for one less step
const optimumTimestamp = currentTimeStamp - maxDuration + step;
const json = await this._getJson(host, optimumTimestamp, step);
try {
const json = await this._getJson(host, optimumTimestamp, step, {
abortSignal,
});
const actualStep = json.meta.step as number;
const actualStep = json.meta.step as number;
if (json.data.length > 0) {
// fetched data is organized from the newest to the oldest
// but this implementation requires it in the other direction
json.data.reverse();
json.meta.legend.forEach((legend: any, index: number) => {
const [, type, uuid, metricType] = /^AVERAGE:([^:]+):(.+):(.+)$/.exec(
legend
) as any;
if (json.data.length > 0) {
// fetched data is organized from the newest to the oldest
// but this implementation requires it in the other direction
json.data.reverse();
json.meta.legend.forEach((legend: any, index: number) => {
const [, type, uuid, metricType] = /^AVERAGE:([^:]+):(.+):(.+)$/.exec(
legend
) as any;
const metrics = STATS[type] as any;
if (metrics === undefined) {
return;
}
const metrics = STATS[type] as any;
if (metrics === undefined) {
return;
}
const { metric, testResult } = findMetric(metrics, metricType) as any;
if (metric === undefined) {
return;
}
const { metric, testResult } = findMetric(metrics, metricType) as any;
if (metric === undefined) {
return;
}
const xoObjectStats = createGetProperty(this.#statsByObject, uuid, {});
let stepStats = xoObjectStats[actualStep];
if (
stepStats === undefined ||
stepStats.endTimestamp !== json.meta.end
) {
stepStats = xoObjectStats[actualStep] = {
endTimestamp: json.meta.end,
interval: actualStep,
};
}
const xoObjectStats = createGetProperty(
this.#statsByObject,
uuid,
{}
);
const cacheXoObjectStats = createGetProperty(
this.#cachedStatsByObject,
uuid,
{}
);
const path =
metric.getPath !== undefined
? metric.getPath(testResult)
: [findKey(metrics, metric)];
let stepStats = xoObjectStats[actualStep];
let cacheStepStats = cacheXoObjectStats[actualStep];
if (
stepStats === undefined ||
stepStats.endTimestamp !== json.meta.end
) {
stepStats = xoObjectStats[actualStep] = {
endTimestamp: json.meta.end,
interval: actualStep,
canBeExpired: false,
};
cacheStepStats = cacheXoObjectStats[actualStep] = {
endTimestamp: json.meta.end,
interval: actualStep,
canBeExpired: true,
};
}
const path =
metric.getPath !== undefined
? metric.getPath(testResult)
: [findKey(metrics, metric)];
const lastKey = path.length - 1;
let metricStats = createGetProperty(stepStats, "stats", {});
let cacheMetricStats = createGetProperty(cacheStepStats, "stats", {});
path.forEach((property: any, key: number) => {
if (key === lastKey) {
metricStats[property] = computeValues(
json.data,
index,
metric.transformValue
);
cacheMetricStats[property] = computeValues(
json.data,
index,
metric.transformValue
);
return;
}
metricStats = createGetProperty(metricStats, property, {});
cacheMetricStats = createGetProperty(
cacheMetricStats,
property,
{}
const lastKey = path.length - 1;
let metricStats = createGetProperty(stepStats, "stats", {});
path.forEach((property: any, key: number) => {
if (key === lastKey) {
metricStats[property] = computeValues(
json.data,
index,
metric.transformValue
);
});
return;
}
metricStats = createGetProperty(metricStats, property, {});
});
}
if (actualStep !== step) {
throw new FaultyGranularity(
`Unable to get the true granularity: ${actualStep}`
);
}
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
return;
}
throw error;
});
}
return (this.#statsByObject[uuid]?.[step] ?? {
endTimestamp: currentTimeStamp,
interval: step,
stats: {},
}) as XapiStatsResponse<T>;
if (actualStep !== step) {
throw new FaultyGranularity(
`Unable to get the true granularity: ${actualStep}`
);
}
return (
this.#statsByObject[uuid]?.[step] ?? {
endTimestamp: currentTimeStamp,
interval: step,
stats: {},
}
);
}
}

View File

@@ -266,11 +266,7 @@ export default class XenApi {
async getResource(
pathname: string,
{
abortSignal,
host,
query,
}: { abortSignal?: AbortSignal; host: XenApiHost; query: any }
{ host, query }: { host: XenApiHost; query: any }
) {
const url = new URL("http://localhost");
url.protocol = window.location.protocol;
@@ -281,7 +277,7 @@ export default class XenApi {
session_id: this.#sessionId,
}).toString();
return fetch(url, { signal: abortSignal });
return fetch(url);
}
async loadRecords<T extends XenApiRecord<string>>(

View File

@@ -36,7 +36,6 @@
"export": "Export",
"export-table-to": "Export table to {type}",
"export-vms": "Export VMs",
"fetching-fresh-data": "Fetching fresh data",
"filter": {
"comparison": {
"contains": "Contains",

View File

@@ -36,7 +36,6 @@
"export": "Exporter",
"export-table-to": "Exporter le tableau en {type}",
"export-vms": "Exporter les VMs",
"fetching-fresh-data": "Récupération de données à jour",
"filter": {
"comparison": {
"contains": "Contient",

View File

@@ -1,9 +1,5 @@
import { isHostRunning, sortRecordsByNameLabel } from "@/libs/utils";
import type {
GRANULARITY,
HostStats,
XapiStatsResponse,
} from "@/libs/xapi-stats";
import type { GRANULARITY, XapiStatsResponse } from "@/libs/xapi-stats";
import type { XenApiHost, XenApiHostMetrics } from "@/libs/xen-api";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { useXenApiStore } from "@/stores/xen-api.store";
@@ -12,15 +8,11 @@ import { createSubscribe } from "@/types/xapi-collection";
import { defineStore } from "pinia";
import { computed, type ComputedRef } from "vue";
type GetStats = (
hostUuid: XenApiHost["uuid"],
granularity: GRANULARITY,
ignoreExpired: boolean,
opts: { abortSignal?: AbortSignal }
) => Promise<XapiStatsResponse<HostStats> | undefined> | undefined;
type GetStatsExtension = {
getStats: GetStats;
getStats: (
hostUuid: XenApiHost["uuid"],
granularity: GRANULARITY
) => Promise<XapiStatsResponse<any>> | undefined;
};
type RunningHostsExtension = [
@@ -39,11 +31,9 @@ export const useHostStore = defineStore("host", () => {
const subscribe = createSubscribe<XenApiHost, Extensions>((options) => {
const originalSubscription = hostCollection.subscribe(options);
const getStats: GetStats = (
hostUuid,
granularity,
ignoreExpired = false,
{ abortSignal }
const getStats = (
hostUuid: XenApiHost["uuid"],
granularity: GRANULARITY
) => {
const host = originalSubscription.getByUuid(hostUuid);
@@ -55,10 +45,8 @@ export const useHostStore = defineStore("host", () => {
? xenApiStore.getXapiStats()
: undefined;
return xapiStats?._getAndUpdateStats<HostStats>({
abortSignal,
return xapiStats?._getAndUpdateStats({
host,
ignoreExpired,
uuid: host.uuid,
granularity,
});

View File

@@ -1,9 +1,5 @@
import { sortRecordsByNameLabel } from "@/libs/utils";
import type {
GRANULARITY,
VmStats,
XapiStatsResponse,
} from "@/libs/xapi-stats";
import type { GRANULARITY, XapiStatsResponse } from "@/libs/xapi-stats";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api";
import { POWER_STATE } from "@/libs/xen-api";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
@@ -12,12 +8,6 @@ import { createSubscribe, type Subscription } from "@/types/xapi-collection";
import { defineStore } from "pinia";
import { computed, type ComputedRef } from "vue";
type GetStats = (
id: XenApiVm["uuid"],
granularity: GRANULARITY,
ignoreExpired: boolean,
opts: { abortSignal?: AbortSignal }
) => Promise<XapiStatsResponse<VmStats> | undefined> | undefined;
type DefaultExtension = {
recordsByHostRef: ComputedRef<Map<XenApiHost["$ref"], XenApiVm[]>>;
runningVms: ComputedRef<XenApiVm[]>;
@@ -25,7 +15,10 @@ type DefaultExtension = {
type GetStatsExtension = [
{
getStats: GetStats;
getStats: (
id: XenApiVm["uuid"],
granularity: GRANULARITY
) => Promise<XapiStatsResponse<any>>;
},
{ hostSubscription: Subscription<XenApiHost, object> }
];
@@ -67,49 +60,33 @@ export const useVmStore = defineStore("vm", () => {
const hostSubscription = options?.hostSubscription;
const getStatsSubscription:
| {
getStats: GetStats;
const getStatsSubscription = hostSubscription !== undefined && {
getStats: (vmUuid: XenApiVm["uuid"], granularity: GRANULARITY) => {
const xenApiStore = useXenApiStore();
if (!xenApiStore.isConnected) {
return undefined;
}
| undefined =
hostSubscription !== undefined
? {
getStats: (
id,
granularity,
ignoreExpired = false,
{ abortSignal }
) => {
const xenApiStore = useXenApiStore();
if (!xenApiStore.isConnected) {
return undefined;
}
const vm = originalSubscription.getByUuid(vmUuid);
const vm = originalSubscription.getByUuid(id);
if (vm === undefined) {
throw new Error(`VM ${vmUuid} could not be found.`);
}
if (vm === undefined) {
throw new Error(`VM ${id} could not be found.`);
}
const host = hostSubscription.getByOpaqueRef(vm.resident_on);
const host = hostSubscription.getByOpaqueRef(vm.resident_on);
if (host === undefined) {
throw new Error(`VM ${vmUuid} is halted or host could not be found.`);
}
if (host === undefined) {
throw new Error(
`VM ${id} is halted or host could not be found.`
);
}
return xenApiStore.getXapiStats()._getAndUpdateStats<VmStats>({
abortSignal,
host,
ignoreExpired,
uuid: vm.uuid,
granularity,
});
},
}
: undefined;
return xenApiStore.getXapiStats()._getAndUpdateStats({
host,
uuid: vm.uuid,
granularity,
});
},
};
return {
...originalSubscription,

View File

@@ -1 +1,6 @@
export type Color = "info" | "error" | "warning" | "success";
export type SlotDefinition<
T extends Record<string, unknown> = Record<string, never>,
R = any
> = (props: T) => R;

View File

@@ -101,7 +101,7 @@ configure(transportConsole())
Optional dependency:
```
> npm add nodemailer pretty-format
> yarn add nodemailer pretty-format
```
Configuration:
@@ -127,7 +127,7 @@ configure(
Optional dependency:
```
> npm add split-host syslog-client
> yarn add split-host syslog-client
```
Configuration:

View File

@@ -119,7 +119,7 @@ configure(transportConsole())
Optional dependency:
```
> npm add nodemailer pretty-format
> yarn add nodemailer pretty-format
```
Configuration:
@@ -145,7 +145,7 @@ configure(
Optional dependency:
```
> npm add split-host syslog-client
> yarn add split-host syslog-client
```
Configuration:

View File

@@ -19,8 +19,8 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"postversion": "npm publish --access public",
"prebuild": "rimraf dist/",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build"
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"devDependencies": {
"@babel/cli": "^7.7.4",

View File

@@ -56,8 +56,8 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build"
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true,
"author": {

View File

@@ -48,7 +48,7 @@ export default class VhdEsxiCowd extends VhdAbstract {
// depending on the paramters we also look into the parent data
return (
this.#grainDirectory.readUInt32LE(blockId * 4) !== 0 ||
this.#grainDirectory.readInt32LE(blockId * 4) !== 0 ||
(this.#lookMissingBlockInParent && this.#parentVhd.containsBlock(blockId))
)
}
@@ -61,14 +61,14 @@ export default class VhdEsxiCowd extends VhdAbstract {
const buffer = await this.#read(0, 2048)
strictEqual(buffer.slice(0, 4).toString('ascii'), 'COWD')
strictEqual(buffer.readUInt32LE(4), 1) // version
strictEqual(buffer.readUInt32LE(8), 3) // flags
const numSectors = buffer.readUInt32LE(12)
const grainSize = buffer.readUInt32LE(16)
strictEqual(buffer.readInt32LE(4), 1) // version
strictEqual(buffer.readInt32LE(8), 3) // flags
const numSectors = buffer.readInt32LE(12)
const grainSize = buffer.readInt32LE(16)
strictEqual(grainSize, 1) // 1 grain should be 1 sector long
strictEqual(buffer.readUInt32LE(20), 4) // grain directory position in sectors
strictEqual(buffer.readInt32LE(20), 4) // grain directory position in sectors
const nbGrainDirectoryEntries = buffer.readUInt32LE(24)
const nbGrainDirectoryEntries = buffer.readInt32LE(24)
strictEqual(nbGrainDirectoryEntries, Math.ceil(numSectors / 4096))
const size = numSectors * 512
// a grain directory entry contains the address of a grain table
@@ -90,7 +90,7 @@ export default class VhdEsxiCowd extends VhdAbstract {
// we're lucky : a grain address can address exacty a full block
async readBlock(blockId) {
notEqual(this.#grainDirectory, undefined, 'grainDirectory is not loaded')
const sectorOffset = this.#grainDirectory.readUInt32LE(blockId * 4)
const sectorOffset = this.#grainDirectory.readInt32LE(blockId * 4)
const buffer = (await this.#parentVhd.readBlock(blockId)).buffer
@@ -137,7 +137,7 @@ export default class VhdEsxiCowd extends VhdAbstract {
}
for (let i = 0; i < graintable.length / 4; i++) {
const grainOffset = graintable.readUInt32LE(i * 4)
const grainOffset = graintable.readInt32LE(i * 4)
if (grainOffset === 0) {
// the content from parent : it is already in buffer
await changeRange()

View File

@@ -1,7 +1,7 @@
import _computeGeometryForSize from 'vhd-lib/_computeGeometryForSize.js'
import { createFooter, createHeader } from 'vhd-lib/_createFooterHeader.js'
import { DISK_TYPES, FOOTER_SIZE } from 'vhd-lib/_constants.js'
import { readChunkStrict, skipStrict } from '@vates/read-chunk'
import { readChunk } from '@vates/read-chunk'
import { Task } from '@vates/task'
import { unpackFooter, unpackHeader } from 'vhd-lib/Vhd/_utils.js'
import { VhdAbstract } from 'vhd-lib'
@@ -21,10 +21,6 @@ export default class VhdEsxiRaw extends VhdAbstract {
#header
#footer
#streamOffset = 0
#stream
#reading = false
static async open(esxi, datastore, path, opts) {
const vhd = new VhdEsxiRaw(esxi, datastore, path, opts)
await vhd.readHeaderAndFooter()
@@ -53,10 +49,10 @@ export default class VhdEsxiRaw extends VhdAbstract {
this.#header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
const geometry = _computeGeometryForSize(length)
const actualSize = geometry.actualSize
this.#footer = unpackFooter(
// length can be smaller than disk capacity due to alignment to head/cylinder/sector
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DYNAMIC)
createFooter(actualSize, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DYNAMIC)
)
}
@@ -68,65 +64,12 @@ export default class VhdEsxiRaw extends VhdAbstract {
return this.#bat.has(blockId)
}
async #readChunk(start, length) {
if (this.#reading) {
throw new Error('reading must be done sequentially')
}
try {
this.#reading = true
if (this.#stream !== undefined) {
// stream is too far ahead or to far behind
if (this.#streamOffset > start || this.#streamOffset + VHD_BLOCK_LENGTH < start) {
this.#stream.destroy()
this.#stream = undefined
this.#streamOffset = 0
}
}
// no stream
if (this.#stream === undefined) {
const end = this.footer.currentSize - 1
const res = await this.#esxi.download(this.#datastore, this.#path, `${start}-${end}`)
this.#stream = res.body
this.#streamOffset = start
}
// stream a little behind
if (this.#streamOffset < start) {
await skipStrict(this.#stream, start - this.#streamOffset)
this.#streamOffset = start
}
// really read data
this.#streamOffset += length
const data = await readChunkStrict(this.#stream, length)
return data
} catch (error) {
error.start = start
error.length = length
error.streamLength = this.footer.currentSize
this.#stream?.destroy()
this.#stream = undefined
this.#streamOffset = 0
throw error
} finally {
this.#reading = false
}
}
async #readBlock(blockId) {
async readBlock(blockId) {
const start = blockId * VHD_BLOCK_LENGTH
let length = VHD_BLOCK_LENGTH
let partial = false
if (start + length > this.footer.currentSize) {
length = this.footer.currentSize - start
partial = true
}
const end = (blockId + 1) * VHD_BLOCK_LENGTH - 1
let data = await this.#readChunk(start, length)
const data = await (await this.#esxi.download(this.#datastore, this.#path, `${start}-${end}`)).buffer()
if (partial) {
data = Buffer.concat([data, Buffer.alloc(VHD_BLOCK_LENGTH - data.length)])
}
const bitmap = Buffer.alloc(512, 255)
return {
id: blockId,
@@ -136,44 +79,28 @@ export default class VhdEsxiRaw extends VhdAbstract {
}
}
async readBlock(blockId) {
let tries = 5
let lastError
while (tries > 0) {
try {
const res = await this.#readBlock(blockId)
return res
} catch (error) {
lastError = error
lastError.blockId = blockId
console.warn('got error , will retry in 2seconds', lastError)
}
await new Promise(resolve => setTimeout(() => resolve(), 2000))
tries--
}
throw lastError
}
// this will read all the disk once to check which block contains data, it can take a long time to execute depending on the network speed
async readBlockAllocationTable() {
if (!this.#thin) {
// fast path : is we do not use thin mode, the BAT is full
return
}
const res = await this.#esxi.download(this.#datastore, this.#path)
const length = res.headers.get('content-length')
const stream = res.body
const empty = Buffer.alloc(VHD_BLOCK_LENGTH, 0)
let pos = 0
this.#bat = new Set()
let nextChunkLength = Math.min(VHD_BLOCK_LENGTH, this.footer.currentSize)
Task.set('total', this.footer.currentSize / VHD_BLOCK_LENGTH)
let nextChunkLength = Math.min(VHD_BLOCK_LENGTH, length)
Task.set('total', length / VHD_BLOCK_LENGTH)
const progress = setInterval(() => {
Task.set('progress', Math.round((pos * 100) / this.footer.currentSize))
console.log('reading blocks', pos / VHD_BLOCK_LENGTH, '/', this.footer.currentSize / VHD_BLOCK_LENGTH)
Task.set('progress', Math.round((pos * 100) / length))
console.log('reading blocks', pos / VHD_BLOCK_LENGTH, '/', length / VHD_BLOCK_LENGTH)
}, 30 * 1000)
while (nextChunkLength > 0) {
try {
const chunk = await this.#readChunk(pos, nextChunkLength)
const chunk = await readChunk(stream, nextChunkLength)
let isEmpty
if (nextChunkLength === VHD_BLOCK_LENGTH) {
isEmpty = empty.equals(chunk)
@@ -185,28 +112,15 @@ export default class VhdEsxiRaw extends VhdAbstract {
this.#bat.add(pos / VHD_BLOCK_LENGTH)
}
pos += VHD_BLOCK_LENGTH
nextChunkLength = Math.min(VHD_BLOCK_LENGTH, this.footer.currentSize - pos)
nextChunkLength = Math.min(VHD_BLOCK_LENGTH, length - pos)
} catch (error) {
clearInterval(progress)
throw error
}
}
console.log(
'BAT reading done, remaining ',
this.#bat.size,
'/',
Math.ceil(this.footer.currentSize / VHD_BLOCK_LENGTH)
)
console.log('BAT reading done, remaining ', this.#bat.size, '/', Math.ceil(length / VHD_BLOCK_LENGTH))
clearInterval(progress)
}
rawContent() {
return this.#esxi.download(this.#datastore, this.#path).then(res => {
const stream = res.body
stream.length = this.footer.currentSize
return stream
})
}
}
/* eslint-enable no-console */

View File

@@ -1,5 +1,4 @@
import { Client } from '@vates/node-vsphere-soap'
import { createLogger } from '@xen-orchestra/log'
import { dirname } from 'node:path'
import { EventEmitter } from 'node:events'
import { strictEqual, notStrictEqual } from 'node:assert'
@@ -10,8 +9,6 @@ import parseVmdk from './parsers/vmdk.mjs'
import parseVmsd from './parsers/vmsd.mjs'
import parseVmx from './parsers/vmx.mjs'
const { warn } = createLogger('xo:vmware-explorer:esxi')
export default class Esxi extends EventEmitter {
#client
#cookies
@@ -67,7 +64,7 @@ export default class Esxi extends EventEmitter {
})
}
async #download(dataStore, path, range) {
async download(dataStore, path, range) {
strictEqual(this.#ready, true)
notStrictEqual(this.#dcPath, undefined)
const url = new URL('https://localhost')
@@ -105,24 +102,6 @@ export default class Esxi extends EventEmitter {
return res
}
async download(dataStore, path, range) {
let tries = 5
let lastError
while (tries > 0) {
try {
const res = await this.#download(dataStore, path, range)
return res
} catch (error) {
warn('got error , will retry in 2 seconds', { error })
lastError = error
}
await new Promise(resolve => setTimeout(() => resolve(), 2000))
tries--
}
throw lastError
}
// inspired from https://github.com/reedog117/node-vsphere-soap/blob/master/test/vsphere-soap.test.js#L95
async search(type, properties) {
// get property collector

View File

@@ -4,12 +4,11 @@
"version": "0.2.3",
"name": "@xen-orchestra/vmware-explorer",
"dependencies": {
"@vates/node-vsphere-soap": "^1.0.0",
"@vates/read-chunk": "^1.1.1",
"@vates/task": "^0.2.0",
"@xen-orchestra/log": "^0.6.0",
"@vates/read-chunk": "^1.1.1",
"lodash": "^4.17.21",
"node-fetch": "^3.3.0",
"@vates/node-vsphere-soap": "^1.0.0",
"vhd-lib": "^4.5.0"
},
"engines": {

View File

@@ -15,10 +15,6 @@
- [Incremental Backup & Replication] Attempt to work around HVM multiplier issues when creating VMs on older XAPIs (PR [#6866](https://github.com/vatesfr/xen-orchestra/pull/6866))
- [REST API] Fix VDI export when NBD is enabled
- [XO Config Cloud Backup] Improve wording about passphrase (PR [#6938](https://github.com/vatesfr/xen-orchestra/pull/6938))
- [Pool] Fix IPv6 handling when adding hosts
- [New SR] Send provided NFS version to XAPI when probing a share
- [Backup/exports] Show more information on error ` stream has ended with not enough data (actual: xxx, expected: 512)` (PR [#6940](https://github.com/vatesfr/xen-orchestra/pull/6940))
### Packages to release
@@ -36,18 +32,10 @@
<!--packages-start-->
- @vates/fuse-vhd major
- @vates/nbd-client major
- @vates/node-vsphere-soap major
- @xen-orchestra/backups minor
- @xen-orchestra/vmware-explorer minor
- @xen-orchestra/xapi major
- @vates/read-chunk minor
- complex-matcher patch
- xen-api patch
- xo-server patch
- xo-server-transport-xmpp patch
- xo-server-audit patch
- xo-web minor
<!--packages-end-->

View File

@@ -93,6 +93,12 @@ v16.14.0
If not, see [this page](https://nodejs.org/en/download/package-manager/) for instructions on how to install Node.
#### Yarn
Yarn is a package manager that offers more guarantees than npm.
See [this page](https://yarnpkg.com/en/docs/install#debian-stable) for instructions on how to install Yarn.
#### Packages
XO needs the following packages to be installed. Redis is used as a database by XO.
@@ -123,12 +129,12 @@ git clone -b master https://github.com/vatesfr/xen-orchestra
### Installing dependencies
Now that you have the code, you can enter the `xen-orchestra` directory and use `npm ci` to install other dependencies. Then finally build it using `npm run build`. Be sure to run `npm` commands as the same user you will be using to run Xen Orchestra:
Now that you have the code, you can enter the `xen-orchestra` directory and use `yarn` to install other dependencies. Then finally build it using `yarn build`. Be sure to run `yarn` commands as the same user you will be using to run Xen Orchestra:
```sh
cd xen-orchestra
npm ci
npm run build
yarn
yarn build
```
Now you have to create a config file for `xo-server`:
@@ -146,7 +152,7 @@ In this config file, you can change default ports (80 and 443) for xo-server. If
You can try to start xo-server to see if it works. You should have something like this:
```console
$ npm run start
$ yarn start
WebServer listening on localhost:80
[INFO] Default user: "admin@admin.net" with password "admin"
```
@@ -156,7 +162,7 @@ WebServer listening on localhost:80
The only part you need to launch is xo-server, which is quite easy to do. From the `xen-orchestra/packages/xo-server` directory, run the following:
```sh
npm run start
yarn start
```
That's it! Use your browser to visit the xo-server IP address, and it works! :)
@@ -170,8 +176,8 @@ If you would like to update your current version, enter your `xen-orchestra` dir
git checkout .
git pull --ff-only
npm ci
npm run build
yarn
yarn build
```
Then restart Xen Orchestra if it was running.
@@ -181,7 +187,7 @@ Then restart Xen Orchestra if it was running.
- You can use [forever](https://github.com/nodejitsu/forever) to have the process always running:
```sh
npm i -g forever
yarn global add forever
# Run the below as the user owning XO
forever start dist/cli.mjs
@@ -190,8 +196,8 @@ forever start dist/cli.mjs
- Or you can use [forever-service](https://github.com/zapty/forever-service) to install XO as a system service, so it starts automatically at boot. Run the following as root:
```sh
npm i -g forever
npm i -g forever-service
yarn global add forever
yarn global add forever-service
# Be sure to edit the path below to where your install is located!
cd /home/username/xen-orchestra/packages/xo-server/
@@ -232,8 +238,8 @@ Exceptional individual contributers are awarded with a free XOA Premium subscrip
If you have problems during the building phase, follow these steps in your `xen-orchestra` directory:
1. `rm -rf node_modules`
1. `npm ci`
1. `npm run build`
1. `yarn`
1. `yarn build`
### FreeBSD
@@ -278,7 +284,7 @@ service redis start
If you are using OpenBSD, you need to install these packages:
```sh
pkg_add gmake redis python--%2.7 git node autoconf
pkg_add gmake redis python--%2.7 git node autoconf yarn
```
A few of the npm packages look for system binaries as part of their installation, and if missing will try to build it themselves. Installing these will save some time and allow for easier upgrades later:
@@ -301,10 +307,10 @@ ulimit -n 10240
ln -s /usr/local/bin/node /tmp/node
```
If `npm` cannot find Python, give it an hand :
If `yarn` cannot find Python, give it an hand :
```sh
PYTHON=/usr/local/bin/python2 npm
PYTHON=/usr/local/bin/python2 yarn
```
Enable redis on boot with:

49047
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -88,7 +88,7 @@
"scripts": {
"build": "turbo run build --scope xo-server --scope xo-server-'*' --scope xo-web",
"build:xo-lite": "turbo run build --scope @xen-orchestra/lite",
"ci": "npm ci && npm run build && npm run test-lint && npm run test-integration",
"ci": "yarn && yarn build && yarn test-lint && yarn test-integration",
"clean": "scripts/run-script.js --parallel clean",
"dev": "scripts/run-script.js --parallel --concurrency 0 --verbose dev",
"dev-test": "jest --bail --watch \"^(?!.*\\.integ\\.spec\\.js$)\"",
@@ -104,5 +104,6 @@
"workspaces": [
"@*/*",
"packages/*"
]
],
"packageManager": "yarn@1.22.19"
}

View File

@@ -53,8 +53,8 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -64,8 +64,8 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"plot": "gnuplot -p memory-test.gnu",
"prebuild": "rimraf dist/",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build",
"postversion": "npm publish",
"test": "tap"
}

View File

@@ -954,8 +954,6 @@ export class Xapi extends EventEmitter {
url,
agent: this.httpAgent,
})
const { hostname } = url
url.hostnameRaw = hostname[0] === '[' ? hostname.slice(1, -1) : hostname
this._url = url
}

View File

@@ -30,12 +30,14 @@ const parseResult = result => {
return result.Value
}
export default ({ secureOptions, url: { hostnameRaw, pathname, port, protocol }, agent }) => {
const removeBrackets = hostname => (hostname[0] === '[' ? hostname.slice(1, -1) : hostname)
export default ({ secureOptions, url: { hostname, pathname, port, protocol }, agent }) => {
const secure = protocol === 'https:'
const client = (secure ? createSecureClient : createClient)({
...(secure ? secureOptions : undefined),
agent,
host: hostnameRaw,
host: removeBrackets(hostname),
pathname,
port,
})

View File

@@ -37,8 +37,8 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -47,7 +47,7 @@
"scripts": {
"build": "tsc",
"dev": "tsc -w",
"prepublishOnly": "npm run build",
"prepublishOnly": "yarn run build",
"start": "node dist/index.js",
"postversion": "npm publish"
}

View File

@@ -44,8 +44,8 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -37,8 +37,8 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build",
"postversion": "npm publish",
"test": "node--test"
}

View File

@@ -40,8 +40,8 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build"
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"dependencies": {
"@xen-orchestra/audit-core": "^0.2.3",

View File

@@ -31,7 +31,6 @@ const DEFAULT_BLOCKED_LIST = {
'job.getAll': true,
'log.get': true,
'metadataBackup.getAllJobs': true,
'mirrorBackup.getAllJobs': true,
'network.getBondModes': true,
'pif.getIpv4ConfigurationModes': true,
'plugin.get': true,

View File

@@ -35,7 +35,7 @@
"scripts": {
"build": "NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "NODE_DEV=development babel --watch --source-maps --out-dir=dist/ src/",
"prepublishOnly": "npm run build"
"prepublishOnly": "yarn run build"
},
"private": true
}

View File

@@ -42,8 +42,8 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build"
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}

View File

@@ -49,8 +49,8 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build"
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}

View File

@@ -42,9 +42,9 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "npm run clean",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build"
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}

View File

@@ -49,9 +49,9 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "npm run clean",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build"
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}

View File

@@ -39,7 +39,7 @@
"scripts": {
"build": "NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "NODE_DEV=development babel --watch --source-maps --out-dir=dist/ src/",
"prepublishOnly": "npm run build"
"prepublishOnly": "yarn run build"
},
"private": true
}

View File

@@ -44,8 +44,8 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build"
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}

View File

@@ -33,9 +33,9 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "npm run clean",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build"
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true,
"author": {

View File

@@ -13,8 +13,8 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build"
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"version": "1.0.8",
"engines": {

View File

@@ -118,13 +118,14 @@ describe('issue', () => {
```
> npm ci
> npm run start
> yarn start
```
You get all the test suites passed (`PASS`) or failed (`FAIL`).
```
> npm run test
> yarn test
yarn run v1.9.4
$ jest
PASS src/user/user.spec.js
PASS src/job/job.spec.js
@@ -138,11 +139,11 @@ describe('issue', () => {
Done in 7.92s.
```
- You can run only tests related to changed files, and review the failed output by using: `> npm run start -- --watch`
- You can run only tests related to changed files, and review the failed output by using: `> yarn start --watch`
- ⚠ Warning: snapshots ⚠
After each run of the tests, check that snapshots are not inadvertently modified.
- ⚠ Jest known issue ⚠
If a test timeout is triggered the next async tests can fail, it's due to an inadvertently modified snapshots.
As a workaround, you can clean your git working tree and re-run jest using a large timeout: `> npm run start -- --testTimeout=100000`
As a workaround, you can clean your git working tree and re-run jest using a large timeout: `> yarn start --testTimeout=100000`

View File

@@ -126,13 +126,14 @@ describe('issue', () => {
```
> npm ci
> npm run start
> yarn start
```
You get all the test suites passed (`PASS`) or failed (`FAIL`).
```
> npm run test
> yarn test
yarn run v1.9.4
$ jest
PASS src/user/user.spec.js
PASS src/job/job.spec.js
@@ -146,14 +147,14 @@ describe('issue', () => {
Done in 7.92s.
```
- You can run only tests related to changed files, and review the failed output by using: `> npm run start -- --watch`
- You can run only tests related to changed files, and review the failed output by using: `> yarn start --watch`
- ⚠ Warning: snapshots ⚠
After each run of the tests, check that snapshots are not inadvertently modified.
- ⚠ Jest known issue ⚠
If a test timeout is triggered the next async tests can fail, it's due to an inadvertently modified snapshots.
As a workaround, you can clean your git working tree and re-run jest using a large timeout: `> npm run start -- --testTimeout=100000`
As a workaround, you can clean your git working tree and re-run jest using a large timeout: `> yarn start --testTimeout=100000`
## Contributions

View File

@@ -43,8 +43,8 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build"
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}

View File

@@ -13,8 +13,8 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build"
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"version": "0.1.2",
"engines": {

View File

@@ -43,9 +43,9 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "npm run clean",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build"
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}

View File

@@ -43,9 +43,9 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "npm run clean",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build"
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}

View File

@@ -26,10 +26,10 @@
"preferGlobal": false,
"main": "dist/",
"engines": {
"node": ">=10"
"node": ">=6"
},
"dependencies": {
"@xmpp/client": "^0.13.1",
"node-xmpp-client": "^3.0.0",
"promise-toolbox": "^0.21.0"
},
"devDependencies": {
@@ -43,9 +43,9 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "npm run clean",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build"
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}

View File

@@ -1,5 +1,5 @@
import fromEvent from 'promise-toolbox/fromEvent'
import { client, xml } from '@xmpp/client'
import XmppClient from 'node-xmpp-client'
// ===================================================================
@@ -46,16 +46,13 @@ class TransportXmppPlugin {
this._client = null
}
configure({ host, jid, port, password }) {
this._conf = {
password,
service: Object.assign(new URL('xmpp://localhost'), { hostname: host, port }).href,
username: jid,
}
configure(conf) {
this._conf = conf
this._conf.reconnect = true
}
async load() {
this._client = client(this._conf)
this._client = new XmppClient(this._conf)
this._client.on('error', () => {})
await fromEvent(this._client.connection.socket, 'data')
@@ -74,14 +71,12 @@ class TransportXmppPlugin {
_sendToXmppClient({ to, message }) {
for (const receiver of to) {
this._client.send(
xml(
'message',
{
to: receiver,
type: 'chat',
},
xml('body', {}, message)
)
new XmppClient.Stanza('message', {
to: receiver,
type: 'chat',
})
.c('body')
.t(message)
)
}
}

View File

@@ -50,9 +50,9 @@
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"clean": "rimraf dist/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "npm run clean",
"predev": "npm run prebuild",
"prepublishOnly": "npm run build"
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}

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