Compare commits
1 Commits
npm-turbo
...
lite/defin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8f741cb29 |
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -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({
|
||||
@@ -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",
|
||||
|
||||
42
@vates/nbd-client/constants.js
Normal file
42
@vates/nbd-client/constants.js
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -33,8 +33,5 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"test": "^3.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"isutf8": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
- Clone
|
||||
- Copy `.env.dist` to `.env` and set vars
|
||||
- `npm ci`
|
||||
- `npm run dev`
|
||||
- `yarn`
|
||||
- `yarn dev`
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}>();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}>();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}>();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}>();
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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: {},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>>(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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-->
|
||||
|
||||
@@ -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
49047
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user