Compare commits

...

62 Commits

Author SHA1 Message Date
Julien Fontanet
c249c91186 feat(backups): use zip CLI for file restore
Much faster and memory efficient than the previous implementation.

Downside is that it requires `zip` from being installed on the system.
2023-07-27 17:32:11 +02:00
Julien Fontanet
90c3319880 feat(xo-web/backup/file-restore): add export format selection 2023-07-27 17:22:58 +02:00
Julien Fontanet
348db876d2 feat(xo-server/backupNg.fetchFiles): add format param 2023-07-27 17:22:58 +02:00
Julien Fontanet
408fd7ec03 feat(proxy/backup.fetchPartitionFiles): add format param 2023-07-27 17:22:58 +02:00
Julien Fontanet
1fd84836b1 feat(backups/fetchPartitionFiles): add tgz (tar+gzip) support
Around 6 times faster than ZIP export.
2023-07-27 17:22:58 +02:00
Julien Fontanet
522204795f fix(backups/fetchPartitionFiles): rewrite ZIP creation
It's now sequential which leads to better performance and less memory consumption.

Empty directories are now included and all entries have correct mode and modification time.
2023-07-27 17:22:58 +02:00
Julien Fontanet
e29c422ac9 fix(xo-server/_handleHttpRequest): use pipeline between result and response
Properly closes one stream if the other is destroyed.
2023-07-27 17:22:58 +02:00
Florent BEAUCHAMP
152cf09b7e feat(vmware-explorer): handle sesparse files (#6909) 2023-07-27 17:15:29 +02:00
Pierre Donias
ff728099dc docs(netbox): update screenshot (#6955) 2023-07-27 17:13:57 +02:00
Mathieu
706d94221d feat(xo-server/pool/rpu): avoid unnecessary VMs migration (#6943) 2023-07-27 17:12:31 +02:00
Gabriel Gunullu
340e9af7f4 fix(backups): handle incremental replication to multiple SRs (#6811)
Fix matching previous replications when multiple SRs.

Fixes #6582
2023-07-27 17:09:15 +02:00
Pierre Donias
40e536ba61 feat(xo-server-netbox): synchronize VM platform (#6954)
See Zammad#12478
See https://xcp-ng.org/forum/topic/6902
2023-07-27 16:59:50 +02:00
Thierry Goettelmann
fd4c56c8c2 feat(lite/pool): add tasks to Pool Dashboard (#6713)
Other updates:
- Move pending/finished tasks logic to store subscription
- Add `count` prop to `UiCardTitle`
- Add "No tasks" message on Task table if empty
- Make the `finishedTasks` prop optional
- Add ability to have full width dashboard cards
2023-07-27 16:23:52 +02:00
Thierry Goettelmann
20d04ba956 feat(lite): dynamic page title (#6853)
See #6793

ℹ️ This PR adds a `pageTitleStore` which allows defining the current page title
according to 3 parts: an object, a string, and a count. Each part is optional.

 The page title is **reactive** when function argument is a `Ref`, a `Computed`
or a getter. For example, when updating a VM name, the page title will be
updated in every tabs.

🪄 Each title part is automatically unset when the component that set it is
unmounted.
2023-07-27 11:41:33 +02:00
Pierre Donias
3b1bcc67ae feat(xo-server-netbox): rewrite (#6950)
Fixes #6038, Fixes #6135, Fixes #6024, Fixes #6036
See https://xcp-ng.org/forum/topic/6070
See zammad#5695
See https://xcp-ng.org/forum/topic/6149
See https://xcp-ng.org/forum/topic/6332

Complete rewrite of the plugin. Main functional changes:
- Synchronize VM description
- Fix duplicated VMs in Netbox after disconnecting one pool
- Migrating a VM from one pool to another keeps VM data added manually
- Fix largest IP prefix being picked instead of smallest
- Fix synchronization not working if some pools are unavailable
- Better error messages
2023-07-27 10:07:26 +02:00
Julien Fontanet
1add3fbf9d fix(yarn.lock): refresh
Introduced by 1c23bd5ff
2023-07-26 13:36:28 +02:00
Julien Fontanet
97f0759de0 feat(mixins/Hooks): warning every 5s if listener still running
This helps diagnosticate issues when a hook is stuck.'
2023-07-25 16:41:29 +02:00
Julien Fontanet
005ab47d9b fix(xo-web): clear token on authentication failure (#6937)
This prevents infinite refreshes when the token is deemed valid by the server
but the authentication failed for any reasons.
2023-07-25 09:49:11 +02:00
Julien Fontanet
14a0caa4c6 fix(xo-web/xoa/licenses): fix message *go TO* 2023-07-25 09:43:11 +02:00
Florent BEAUCHAMP
1c23bd5ff7 feat(read-chunk/readChunkStrict): attach read chunk to error if small text (#6940) 2023-07-20 17:01:26 +02:00
Julien Fontanet
49c161b17a fix(xo-server,xo-web): send version when probing NFS SR
Reported by @benjamreis
2023-07-20 16:46:18 +02:00
Gabriel Gunullu
18dce3fce6 test(fs): fix wrong encryption (#6945) 2023-07-20 16:32:09 +02:00
Julien Fontanet
d6fc86b6bc chore(xo-server-transport-xmpp): remove old dep node-xmpp-client
Fix possibly #6942
2023-07-20 10:54:52 +02:00
Florent BEAUCHAMP
61d960d4b1 fix(vmware-explorer): handle snapshot of 1TB+ disks 2023-07-20 10:25:28 +02:00
Florent BEAUCHAMP
02d3465832 feat(vmware-explorer): don't transform stream for raw import in thick mode 2023-07-20 10:25:28 +02:00
Florent BEAUCHAMP
4bbadc9515 feat(vmware-explorer): improve import
- use one stream instead of per block queries if possible
- retry block reading if failing
- handle unaligned end block
2023-07-20 10:25:28 +02:00
Florent BEAUCHAMP
78586291ca fix(vmware-explorer): better disk size computation 2023-07-20 10:25:28 +02:00
Florent BEAUCHAMP
945dec94bf feat(vmware-explorer): retry connection to ESXi 2023-07-20 10:25:28 +02:00
Julien Fontanet
003140d96b test(nbd-client): fix issues introduced by conversion to ESM
Introduced by 7c80d0c1e
2023-07-19 23:09:48 +02:00
Julien Fontanet
363d7cf0d0 fix(node-vsphere-soap): add missing files
Introduced by f0c94496b
2023-07-19 23:02:34 +02:00
Julien Fontanet
f0c94496bf chore(node-vsphere-soap): convert to ESM
BREAKING CHANGE
2023-07-19 11:03:56 +02:00
Julien Fontanet
de217eabd9 test(nbd-client): fix issues introduced by conversion to ESM
Introduced by 7c80d0c1e
2023-07-19 11:02:12 +02:00
Julien Fontanet
7c80d0c1e1 chore(nbd-client): convert to ESM
BREAKING CHANGE
2023-07-19 10:46:05 +02:00
Julien Fontanet
9fb749b1db chore(fuse-vhd): convert to ESM
BREAKING CHANGE
2023-07-19 10:13:35 +02:00
Julien Fontanet
ad9c59669a chore: update dev deps 2023-07-19 10:11:30 +02:00
Julien Fontanet
76a038e403 fix(xo-web): fix doc link to incremental/key backup interval 2023-07-19 09:48:23 +02:00
Julien Fontanet
0e12072922 fix(xo-server/pool.mergeInto): fix IPv6 handling 2023-07-18 17:28:39 +02:00
Julien Fontanet
158a8e14a2 chore(xen-api): expose bracketless IPv6 as hostnameRaw 2023-07-18 17:28:38 +02:00
Julien Fontanet
0c97910349 chore(xo-server): remove unused _mounts property
Introduced by 5c9a47b6b
2023-07-18 11:14:24 +02:00
Florent BEAUCHAMP
8347ac6ed8 fix(xo-server/xapi-stats): simplify caching (#6920)
Following #6903

- change cache system per object => per host
- update cache at the beginning of the query to handle race conditions leading to duplicate requests
- remove concurrency limit (was leading to a huge backlog of queries, and response handling is quite fast)
2023-07-18 09:47:39 +02:00
Mathieu
996abd6e7e fix(xo-web/settings/config): wording fix for XO Config Cloud Backup (#6938)
See zammad#15904
2023-07-13 10:29:36 +02:00
rbarhtaoui
de8abd5b63 feat(lite/pool/vms): ability to export selected VMs as JSON file (#6911) 2023-07-13 09:30:10 +02:00
Julien Fontanet
3de928c488 fix(xo-server-audit): ignore mirrorBackup.getAllJobs 2023-07-12 21:52:51 +02:00
Mathieu
a2a514e483 feat(lite/stats): cache stats from rrd_update (#6781) 2023-07-12 15:30:05 +02:00
rbarhtaoui
ff432e04b0 feat(lite/pool/vms): export selected VMs as CSV file (#6915) 2023-07-12 14:34:16 +02:00
Julien Fontanet
4502590bb0 fix(xapi/VM_create): work-around HVM multiplier issues (#6935)
Fixes zammad#15189
2023-07-12 10:29:14 +02:00
Thierry Goettelmann
6d440a5af5 feat(lite/components): rewrite FormInputWrapper (#6918)
Rewrite `FormInputWrapper` and update `FormInput` to match Figma design.

Slotted input will change color according to passed `warning` and/or `error` message.
2023-07-12 10:20:33 +02:00
Julien Fontanet
0840b4c359 fix(xo-server/rest-api): VDI export via NBD 2023-07-12 10:18:45 +02:00
Julien Fontanet
696ee7dbe5 fix(CHANGELOG): add 5.84.0 highlights
Introduced by f32742225
2023-07-12 10:06:15 +02:00
Thierry Goettelmann
5e23e356ce chore(lite): bundling, dynamic import, optimizations (#6910)
Added dynamic imports for views and components.

Extracted to their own bundle:
- Vue related libs (vue, vue-router, pinia etc.)
- Lodash
- Charts

Removed `vite-plugin-pages` package.

Optimize highlight/markdown loading.
2023-07-12 10:05:09 +02:00
Thierry Goettelmann
c705051a89 chore(lite): use injection keys (#6898)
Using injection keys for `provide`/`inject` to prevent errors and code
repetition.
2023-07-11 14:56:03 +02:00
Julien Fontanet
ce2b918a29 fix(xo-server): xoData ESModule import
Introduced by c3e0308ad
2023-07-10 18:15:05 +02:00
Julien Fontanet
df740b1e8e test(backups): fix issues introduced by conversion to ESM
Introduced by 1005e295b
2023-07-10 16:58:12 +02:00
Julien Fontanet
c3e0308ad0 chore(xapi): convert to ESM
BREAKING CHANGE
2023-07-10 16:45:31 +02:00
Julien Fontanet
1005e295b2 chore(backups): convert to ESM
BREAKING CHANGE
2023-07-10 16:45:13 +02:00
Julien Fontanet
b3cf58b8c0 fix(complex-matcher): import specific lodash functions (#6904)
This reduces the bundled size when the library is used in a bundled app.
2023-07-10 16:01:54 +02:00
Julien Fontanet
2652c87917 feat(xo-web/backup/restore): can open raw log (#6936) 2023-07-07 11:20:49 +02:00
Thierry Goettelmann
9e0b5575a4 feat(lite/component): new component FormSection (#6926)
Can take a `collapsible` prop in conjunction of a `collapsed` prop.
If `collapsible` is set to `true`, the style is changed and clicking the section
header will toggle the content visibility.
Collapse status updates are sent via the `update:collapsed` event.
This allows to use `collapsed` as a model, e.g.:
`<FormSection collapsible v-model:collapsed="collapsed">`
2023-07-06 15:55:05 +02:00
Thierry Goettelmann
56c089dc01 feat(lite/stories): identify models with an icon and tooltip (#6927)
When a prop is a model, add an indicator (icon + tooltip) to identify it as such.
2023-07-06 15:49:15 +02:00
Julien Fontanet
3b94da1790 docs(supported_hosts): clearer symbol to indicate EOL 2023-07-06 10:41:52 +02:00
Julien Fontanet
ec39a8e9fe docs(supported_hosts): issues on XenServer 7.2 2023-07-06 10:39:16 +02:00
Julien Fontanet
6339f971ca docs(supported_hosts): Citrix Hypervisor → XenServer
The name has been reverted back to XenServer.
2023-07-06 10:33:21 +02:00
244 changed files with 5639 additions and 4203 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,12 @@
'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')
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'
const FILE_SIZE = 10 * 1024 * 1024

View File

@@ -1,4 +1,3 @@
'use strict'
/*
node-vsphere-soap
@@ -12,17 +11,18 @@
*/
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
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
// Client class
// inherits from EventEmitter
// possible events: connect, error, ready
function Client(vCenterHostname, username, password, sslVerify) {
export function Client(vCenterHostname, username, password, sslVerify) {
this.status = 'disconnected'
this.reconnectCount = 0
@@ -228,4 +228,3 @@ function _soapErrorHandler(self, emitter, command, args, err) {
}
// end
exports.Client = Client

View File

@@ -2,7 +2,7 @@
"name": "@vates/node-vsphere-soap",
"version": "1.0.0",
"description": "interface to vSphere SOAP/WSDL from node for interfacing with vCenter or ESXi, forked from node-vsphere-soap",
"main": "lib/client.js",
"main": "lib/client.mjs",
"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": ">=8.10"
"node": ">=14"
},
"scripts": {
"postversion": "npm publish --access public"

View File

@@ -1,15 +1,11 @@
'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
const vCenterTestCreds = {
export const vCenterTestCreds = {
vCenterIP: 'vcsa',
vCenterUser: 'vcuser',
vCenterPassword: 'vcpw',
vCenter: true,
}
exports.vCenterTestCreds = vCenterTestCreds

View File

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

View File

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

View File

@@ -102,12 +102,37 @@ describe('readChunkStrict', function () {
assert.strictEqual(error.chunk, undefined)
})
it('throws if stream ends with not enough data', async () => {
it('throws if stream ends with not enough data, utf8', 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 () {
@@ -134,6 +159,16 @@ 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 () {
@@ -144,4 +179,9 @@ describe('skipStrict', function () {
assert.strictEqual(error.message, 'stream has ended with not enough data (actual: 7, expected: 10)')
assert.deepEqual(error.bytesSkipped, 7)
})
it('succeed', async () => {
const source = makeStream(['foo', 'bar', 'baz'])
const res = await skipStrict(source, 4)
assert.strictEqual(res, undefined)
})
})

View File

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

View File

@@ -1,5 +1,5 @@
import { asyncMap } from '@xen-orchestra/async-map'
import { RemoteAdapter } from '@xen-orchestra/backups/RemoteAdapter.js'
import { RemoteAdapter } from '@xen-orchestra/backups/RemoteAdapter.mjs'
import { getSyncedHandler } from '@xen-orchestra/fs'
import getopts from 'getopts'
import { basename, dirname } from 'path'

View File

@@ -1,10 +1,8 @@
'use strict'
import { Metadata } from './_runners/Metadata.mjs'
import { VmsRemote } from './_runners/VmsRemote.mjs'
import { VmsXapi } from './_runners/VmsXapi.mjs'
const { Metadata } = require('./_runners/Metadata.js')
const { VmsRemote } = require('./_runners/VmsRemote.js')
const { VmsXapi } = require('./_runners/VmsXapi.js')
exports.createRunner = function createRunner(opts) {
export function createRunner(opts) {
const { type } = opts.job
switch (type) {
case 'backup':

View File

@@ -1,8 +1,6 @@
'use strict'
import { asyncMap } from '@xen-orchestra/async-map'
const { asyncMap } = require('@xen-orchestra/async-map')
exports.DurablePartition = class DurablePartition {
export class DurablePartition {
// private resource API is used exceptionally to be able to separate resource creation and release
#partitionDisposers = {}

View File

@@ -1,8 +1,6 @@
'use strict'
import { Task } from './Task.mjs'
const { Task } = require('./Task')
exports.HealthCheckVmBackup = class HealthCheckVmBackup {
export class HealthCheckVmBackup {
#restoredVm
#timeout
#xapi

View File

@@ -1,13 +1,11 @@
'use strict'
import assert from 'node:assert'
const assert = require('assert')
import { formatFilenameDate } from './_filenameDate.mjs'
import { importIncrementalVm } from './_incrementalVm.mjs'
import { Task } from './Task.mjs'
import { watchStreamSize } from './_watchStreamSize.mjs'
const { formatFilenameDate } = require('./_filenameDate.js')
const { importIncrementalVm } = require('./_incrementalVm.js')
const { Task } = require('./Task.js')
const { watchStreamSize } = require('./_watchStreamSize.js')
exports.ImportVmBackup = class ImportVmBackup {
export class ImportVmBackup {
constructor({ adapter, metadata, srUuid, xapi, settings: { newMacAddresses, mapVdisSrs = {} } = {} }) {
this._adapter = adapter
this._importIncrementalVmSettings = { newMacAddresses, mapVdisSrs }

View File

@@ -1,43 +1,39 @@
'use strict'
import { asyncEach } from '@vates/async-each'
import { asyncMap, asyncMapSettled } from '@xen-orchestra/async-map'
import { compose } from '@vates/compose'
import { createLogger } from '@xen-orchestra/log'
import { createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic } from 'vhd-lib'
import { decorateMethodsWith } from '@vates/decorate-with'
import { deduped } from '@vates/disposable/deduped.js'
import { dirname, join, resolve } from 'node:path'
import { execFile } from 'child_process'
import { mount } from '@vates/fuse-vhd'
import { readdir, lstat } from 'node:fs/promises'
import { synchronized } from 'decorator-synchronized'
import { v4 as uuidv4 } from 'uuid'
import Disposable from 'promise-toolbox/Disposable'
import fromCallback from 'promise-toolbox/fromCallback'
import fromEvent from 'promise-toolbox/fromEvent'
import groupBy from 'lodash/groupBy.js'
import pDefer from 'promise-toolbox/defer'
import pickBy from 'lodash/pickBy.js'
import tar from 'tar'
import zlib from 'zlib'
const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
const { synchronized } = require('decorator-synchronized')
const Disposable = require('promise-toolbox/Disposable')
const fromCallback = require('promise-toolbox/fromCallback')
const fromEvent = require('promise-toolbox/fromEvent')
const pDefer = require('promise-toolbox/defer')
const groupBy = require('lodash/groupBy.js')
const pickBy = require('lodash/pickBy.js')
const { dirname, join, normalize, resolve } = require('path')
const { createLogger } = require('@xen-orchestra/log')
const { createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic } = require('vhd-lib')
const { deduped } = require('@vates/disposable/deduped.js')
const { decorateMethodsWith } = require('@vates/decorate-with')
const { compose } = require('@vates/compose')
const { execFile } = require('child_process')
const { readdir, lstat } = require('fs-extra')
const { v4: uuidv4 } = require('uuid')
const { ZipFile } = require('yazl')
const zlib = require('zlib')
import { BACKUP_DIR } from './_getVmBackupDir.mjs'
import { cleanVm } from './_cleanVm.mjs'
import { formatFilenameDate } from './_filenameDate.mjs'
import { getTmpDir } from './_getTmpDir.mjs'
import { isMetadataFile } from './_backupType.mjs'
import { isValidXva } from './_isValidXva.mjs'
import { listPartitions, LVM_PARTITION_TYPE } from './_listPartitions.mjs'
import { lvs, pvs } from './_lvm.mjs'
import { watchStreamSize } from './_watchStreamSize.mjs'
import { spawn } from 'node:child_process'
const { BACKUP_DIR } = require('./_getVmBackupDir.js')
const { cleanVm } = require('./_cleanVm.js')
const { formatFilenameDate } = require('./_filenameDate.js')
const { getTmpDir } = require('./_getTmpDir.js')
const { isMetadataFile } = require('./_backupType.js')
const { isValidXva } = require('./_isValidXva.js')
const { listPartitions, LVM_PARTITION_TYPE } = require('./_listPartitions.js')
const { lvs, pvs } = require('./_lvm.js')
const { watchStreamSize } = require('./_watchStreamSize')
// @todo : this import is marked extraneous , sould be fixed when lib is published
const { mount } = require('@vates/fuse-vhd')
const { asyncEach } = require('@vates/async-each')
export const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
exports.DIR_XO_CONFIG_BACKUPS = DIR_XO_CONFIG_BACKUPS
const DIR_XO_POOL_METADATA_BACKUPS = 'xo-pool-metadata-backups'
exports.DIR_XO_POOL_METADATA_BACKUPS = DIR_XO_POOL_METADATA_BACKUPS
export const DIR_XO_POOL_METADATA_BACKUPS = 'xo-pool-metadata-backups'
const { debug, warn } = createLogger('xo:backups:RemoteAdapter')
@@ -46,22 +42,8 @@ const compareTimestamp = (a, b) => a.timestamp - b.timestamp
const noop = Function.prototype
const resolveRelativeFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)
const resolveSubpath = (root, path) => resolve(root, `.${resolve('/', path)}`)
async function addDirectory(files, realPath, metadataPath) {
const stats = await lstat(realPath)
if (stats.isDirectory()) {
await asyncMap(await readdir(realPath), file =>
addDirectory(files, realPath + '/' + file, metadataPath + '/' + file)
)
} else if (stats.isFile()) {
files.push({
realPath,
metadataPath,
})
}
}
const makeRelative = path => resolve('/', path).slice(1)
const resolveSubpath = (root, path) => resolve(root, makeRelative(path))
const createSafeReaddir = (handler, methodName) => (path, options) =>
handler.list(path, options).catch(error => {
@@ -76,7 +58,7 @@ const debounceResourceFactory = factory =>
return this._debounceResource(factory.apply(this, arguments))
}
class RemoteAdapter {
export class RemoteAdapter {
constructor(
handler,
{ debounceResource = res => res, dirMode, vhdDirectoryCompression, useGetDiskLegacy = false } = {}
@@ -187,17 +169,6 @@ class RemoteAdapter {
})
}
async *_usePartitionFiles(diskId, partitionId, paths) {
const path = yield this.getPartition(diskId, partitionId)
const files = []
await asyncMap(paths, file =>
addDirectory(files, resolveSubpath(path, file), normalize('./' + file).replace(/\/+$/, ''))
)
return files
}
// check if we will be allowed to merge a a vhd created in this adapter
// with the vhd at path `path`
async isMergeableParent(packedParentUid, path) {
@@ -214,15 +185,30 @@ class RemoteAdapter {
})
}
fetchPartitionFiles(diskId, partitionId, paths) {
fetchPartitionFiles(diskId, partitionId, paths, format) {
const { promise, reject, resolve } = pDefer()
Disposable.use(
async function* () {
const files = yield this._usePartitionFiles(diskId, partitionId, paths)
const zip = new ZipFile()
files.forEach(({ realPath, metadataPath }) => zip.addFile(realPath, metadataPath))
zip.end()
const { outputStream } = zip
const path = yield this.getPartition(diskId, partitionId)
let outputStream
if (format === 'tgz') {
outputStream = tar.c({ cwd: path, gzip: true }, paths.map(makeRelative))
} else if (format === 'zip') {
// don't use --symlinks due to bug
//
// see https://bugs.launchpad.net/ubuntu/+source/zip/+bug/1892338
const cp = spawn('zip', ['--quiet', '--recurse-paths', '-', ...paths.map(makeRelative)], { cwd: path })
await new Promise((resolve, reject) => {
cp.on('error', reject).on('spawn', resolve)
})
outputStream = cp.stdout
} else {
throw new Error('unsupported format ' + format)
}
resolve(outputStream)
await fromEvent(outputStream, 'end')
}.bind(this)
@@ -829,11 +815,7 @@ decorateMethodsWith(RemoteAdapter, {
debounceResourceFactory,
]),
_usePartitionFiles: Disposable.factory,
getDisk: compose([Disposable.factory, [deduped, diskId => [diskId]], debounceResourceFactory]),
getPartition: Disposable.factory,
})
exports.RemoteAdapter = RemoteAdapter

View File

@@ -1,11 +1,9 @@
'use strict'
import { join, resolve } from 'node:path/posix'
const { join, resolve } = require('node:path/posix')
import { DIR_XO_POOL_METADATA_BACKUPS } from './RemoteAdapter.mjs'
import { PATH_DB_DUMP } from './_runners/_PoolMetadataBackup.mjs'
const { DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter.js')
const { PATH_DB_DUMP } = require('./_runners/_PoolMetadataBackup.js')
exports.RestoreMetadataBackup = class RestoreMetadataBackup {
export class RestoreMetadataBackup {
constructor({ backupId, handler, xapi }) {
this._backupId = backupId
this._handler = handler

View File

@@ -1,7 +1,5 @@
'use strict'
const CancelToken = require('promise-toolbox/CancelToken')
const Zone = require('node-zone')
import CancelToken from 'promise-toolbox/CancelToken'
import Zone from 'node-zone'
const logAfterEnd = log => {
const error = new Error('task has already ended')
@@ -30,7 +28,7 @@ const serializeError = error =>
const $$task = Symbol('@xen-orchestra/backups/Task')
class Task {
export class Task {
static get cancelToken() {
const task = Zone.current.data[$$task]
return task !== undefined ? task.#cancelToken : CancelToken.none
@@ -151,7 +149,6 @@ class Task {
})
}
}
exports.Task = Task
for (const method of ['info', 'warning']) {
Task[method] = (...args) => Zone.current.data[$$task]?.[method](...args)

View File

@@ -1,6 +0,0 @@
'use strict'
exports.isMetadataFile = filename => filename.endsWith('.json')
exports.isVhdFile = filename => filename.endsWith('.vhd')
exports.isXvaFile = filename => filename.endsWith('.xva')
exports.isXvaSumFile = filename => filename.endsWith('.xva.checksum')

View File

@@ -0,0 +1,4 @@
export const isMetadataFile = filename => filename.endsWith('.json')
export const isVhdFile = filename => filename.endsWith('.vhd')
export const isXvaFile = filename => filename.endsWith('.xva')
export const isXvaSumFile = filename => filename.endsWith('.xva.checksum')

View File

@@ -1,25 +1,25 @@
'use strict'
import { createLogger } from '@xen-orchestra/log'
import { catchGlobalErrors } from '@xen-orchestra/log/configure'
const logger = require('@xen-orchestra/log').createLogger('xo:backups:worker')
import Disposable from 'promise-toolbox/Disposable'
import ignoreErrors from 'promise-toolbox/ignoreErrors'
import { compose } from '@vates/compose'
import { createCachedLookup } from '@vates/cached-dns.lookup'
import { createDebounceResource } from '@vates/disposable/debounceResource.js'
import { createRunner } from './Backup.mjs'
import { decorateMethodsWith } from '@vates/decorate-with'
import { deduped } from '@vates/disposable/deduped.js'
import { getHandler } from '@xen-orchestra/fs'
import { parseDuration } from '@vates/parse-duration'
import { Xapi } from '@xen-orchestra/xapi'
require('@xen-orchestra/log/configure').catchGlobalErrors(logger)
import { RemoteAdapter } from './RemoteAdapter.mjs'
import { Task } from './Task.mjs'
require('@vates/cached-dns.lookup').createCachedLookup().patchGlobal()
const Disposable = require('promise-toolbox/Disposable')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const { compose } = require('@vates/compose')
const { createDebounceResource } = require('@vates/disposable/debounceResource.js')
const { decorateMethodsWith } = require('@vates/decorate-with')
const { deduped } = require('@vates/disposable/deduped.js')
const { getHandler } = require('@xen-orchestra/fs')
const { createRunner } = require('./Backup.js')
const { parseDuration } = require('@vates/parse-duration')
const { Xapi } = require('@xen-orchestra/xapi')
const { RemoteAdapter } = require('./RemoteAdapter.js')
const { Task } = require('./Task.js')
createCachedLookup().patchGlobal()
const logger = createLogger('xo:backups:worker')
catchGlobalErrors(logger)
const { debug } = logger
class BackupWorker {

View File

@@ -1,13 +1,11 @@
'use strict'
const cancelable = require('promise-toolbox/cancelable')
const CancelToken = require('promise-toolbox/CancelToken')
import cancelable from 'promise-toolbox/cancelable'
import CancelToken from 'promise-toolbox/CancelToken'
// Similar to `Promise.all` + `map` but pass a cancel token to the callback
//
// If any of the executions fails, the cancel token will be triggered and the
// first reason will be rejected.
exports.cancelableMap = cancelable(async function cancelableMap($cancelToken, iterable, callback) {
export const cancelableMap = cancelable(async function cancelableMap($cancelToken, iterable, callback) {
const { cancel, token } = CancelToken.source([$cancelToken])
try {
return await Promise.all(

View File

@@ -1,19 +1,19 @@
'use strict'
import test from 'test'
import { strict as assert } from 'node:assert'
const { beforeEach, afterEach, test, describe } = require('test')
const assert = require('assert').strict
import tmp from 'tmp'
import fs from 'fs-extra'
import * as uuid from 'uuid'
import { getHandler } from '@xen-orchestra/fs'
import { pFromCallback } from 'promise-toolbox'
import { RemoteAdapter } from './RemoteAdapter.mjs'
import { VHDFOOTER, VHDHEADER } from './tests.fixtures.mjs'
import { VhdFile, Constants, VhdDirectory, VhdAbstract } from 'vhd-lib'
import { checkAliases } from './_cleanVm.mjs'
import { dirname, basename } from 'node:path'
import { rimraf } from 'rimraf'
const tmp = require('tmp')
const fs = require('fs-extra')
const uuid = require('uuid')
const { getHandler } = require('@xen-orchestra/fs')
const { pFromCallback } = require('promise-toolbox')
const { RemoteAdapter } = require('./RemoteAdapter')
const { VHDFOOTER, VHDHEADER } = require('./tests.fixtures.js')
const { VhdFile, Constants, VhdDirectory, VhdAbstract } = require('vhd-lib')
const { checkAliases } = require('./_cleanVm')
const { dirname, basename } = require('path')
const { rimraf } = require('rimraf')
const { beforeEach, afterEach, describe } = test
let tempDir, adapter, handler, jobId, vdiId, basePath, relativePath
const rootPath = 'xo-vm-backups/VMUUID/'

View File

@@ -1,19 +1,18 @@
'use strict'
import * as UUID from 'uuid'
import sum from 'lodash/sum.js'
import { asyncMap } from '@xen-orchestra/async-map'
import { Constants, openVhd, VhdAbstract, VhdFile } from 'vhd-lib'
import { isVhdAlias, resolveVhdAlias } from 'vhd-lib/aliases.js'
import { dirname, resolve } from 'node:path'
import { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } from './_backupType.mjs'
import { limitConcurrency } from 'limit-concurrency-decorator'
import { mergeVhdChain } from 'vhd-lib/merge.js'
import { Task } from './Task.mjs'
import { Disposable } from 'promise-toolbox'
import handlerPath from '@xen-orchestra/fs/path'
const sum = require('lodash/sum')
const UUID = require('uuid')
const { asyncMap } = require('@xen-orchestra/async-map')
const { Constants, openVhd, VhdAbstract, VhdFile } = require('vhd-lib')
const { isVhdAlias, resolveVhdAlias } = require('vhd-lib/aliases')
const { dirname, resolve } = require('path')
const { DISK_TYPES } = Constants
const { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } = require('./_backupType.js')
const { limitConcurrency } = require('limit-concurrency-decorator')
const { mergeVhdChain } = require('vhd-lib/merge')
const { Task } = require('./Task.js')
const { Disposable } = require('promise-toolbox')
const handlerPath = require('@xen-orchestra/fs/path')
// checking the size of a vhd directory is costly
// 1 Http Query per 1000 blocks
@@ -117,7 +116,7 @@ const listVhds = async (handler, vmDir, logWarn) => {
return { vhds, interruptedVhds, aliases }
}
async function checkAliases(
export async function checkAliases(
aliasPaths,
targetDataRepository,
{ handler, logInfo = noop, logWarn = console.warn, remove = false }
@@ -176,11 +175,9 @@ async function checkAliases(
})
}
exports.checkAliases = checkAliases
const defaultMergeLimiter = limitConcurrency(1)
exports.cleanVm = async function cleanVm(
export async function cleanVm(
vmDir,
{
fixMetadata,

View File

@@ -1,8 +0,0 @@
'use strict'
const { utcFormat, utcParse } = require('d3-time-format')
// Format a date in ISO 8601 in a safe way to be used in filenames
// (even on Windows).
exports.formatFilenameDate = utcFormat('%Y%m%dT%H%M%SZ')
exports.parseFilenameDate = utcParse('%Y%m%dT%H%M%SZ')

View File

@@ -0,0 +1,6 @@
import { utcFormat, utcParse } from 'd3-time-format'
// Format a date in ISO 8601 in a safe way to be used in filenames
// (even on Windows).
export const formatFilenameDate = utcFormat('%Y%m%dT%H%M%SZ')
export const parseFilenameDate = utcParse('%Y%m%dT%H%M%SZ')

View File

@@ -1,6 +1,4 @@
'use strict'
// returns all entries but the last retention-th
exports.getOldEntries = function getOldEntries(retention, entries) {
export function getOldEntries(retention, entries) {
return entries === undefined ? [] : retention > 0 ? entries.slice(0, -retention) : entries
}

View File

@@ -1,13 +1,11 @@
'use strict'
const Disposable = require('promise-toolbox/Disposable')
const { join } = require('path')
const { mkdir, rmdir } = require('fs-extra')
const { tmpdir } = require('os')
import Disposable from 'promise-toolbox/Disposable'
import { join } from 'node:path'
import { mkdir, rmdir } from 'node:fs/promises'
import { tmpdir } from 'os'
const MAX_ATTEMPTS = 3
exports.getTmpDir = async function getTmpDir() {
export async function getTmpDir() {
for (let i = 0; true; ++i) {
const path = join(tmpdir(), Math.random().toString(36).slice(2))
try {

View File

@@ -1,8 +0,0 @@
'use strict'
const BACKUP_DIR = 'xo-vm-backups'
exports.BACKUP_DIR = BACKUP_DIR
exports.getVmBackupDir = function getVmBackupDir(uuid) {
return `${BACKUP_DIR}/${uuid}`
}

View File

@@ -0,0 +1,5 @@
export const BACKUP_DIR = 'xo-vm-backups'
export function getVmBackupDir(uuid) {
return `${BACKUP_DIR}/${uuid}`
}

View File

@@ -1,24 +1,22 @@
'use strict'
import find from 'lodash/find.js'
import groupBy from 'lodash/groupBy.js'
import ignoreErrors from 'promise-toolbox/ignoreErrors'
import omit from 'lodash/omit.js'
import { asyncMap } from '@xen-orchestra/async-map'
import { CancelToken } from 'promise-toolbox'
import { compareVersions } from 'compare-versions'
import { createVhdStreamWithLength } from 'vhd-lib'
import { defer } from 'golike-defer'
const find = require('lodash/find.js')
const groupBy = require('lodash/groupBy.js')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const omit = require('lodash/omit.js')
const { asyncMap } = require('@xen-orchestra/async-map')
const { CancelToken } = require('promise-toolbox')
const { compareVersions } = require('compare-versions')
const { createVhdStreamWithLength } = require('vhd-lib')
const { defer } = require('golike-defer')
import { cancelableMap } from './_cancelableMap.mjs'
import { Task } from './Task.mjs'
import pick from 'lodash/pick.js'
const { cancelableMap } = require('./_cancelableMap.js')
const { Task } = require('./Task.js')
const pick = require('lodash/pick.js')
export const TAG_BASE_DELTA = 'xo:base_delta'
const TAG_BASE_DELTA = 'xo:base_delta'
exports.TAG_BASE_DELTA = TAG_BASE_DELTA
export const TAG_COPY_SRC = 'xo:copy_of'
const TAG_COPY_SRC = 'xo:copy_of'
exports.TAG_COPY_SRC = TAG_COPY_SRC
const TAG_BACKUP_SR = 'xo:backup:sr'
const ensureArray = value => (value === undefined ? [] : Array.isArray(value) ? value : [value])
const resolveUuid = async (xapi, cache, uuid, type) => {
@@ -33,7 +31,7 @@ const resolveUuid = async (xapi, cache, uuid, type) => {
return ref
}
exports.exportIncrementalVm = async function exportIncrementalVm(
export async function exportIncrementalVm(
vm,
baseVm,
{
@@ -143,7 +141,7 @@ exports.exportIncrementalVm = async function exportIncrementalVm(
)
}
exports.importIncrementalVm = defer(async function importIncrementalVm(
export const importIncrementalVm = defer(async function importIncrementalVm(
$defer,
incrementalVm,
sr,
@@ -161,7 +159,10 @@ exports.importIncrementalVm = defer(async function importIncrementalVm(
if (detectBase) {
const remoteBaseVmUuid = vmRecord.other_config[TAG_BASE_DELTA]
if (remoteBaseVmUuid) {
baseVm = find(xapi.objects.all, obj => (obj = obj.other_config) && obj[TAG_COPY_SRC] === remoteBaseVmUuid)
baseVm = find(
xapi.objects.all,
obj => (obj = obj.other_config) && obj[TAG_COPY_SRC] === remoteBaseVmUuid && obj[TAG_BACKUP_SR] === sr.$id
)
if (!baseVm) {
throw new Error(`could not find the base VM (copy of ${remoteBaseVmUuid})`)

View File

@@ -1,6 +1,4 @@
'use strict'
const assert = require('assert')
import assert from 'node:assert'
const COMPRESSED_MAGIC_NUMBERS = [
// https://tools.ietf.org/html/rfc1952.html#page-5
@@ -47,7 +45,7 @@ const isValidTar = async (handler, size, fd) => {
}
// TODO: find an heuristic for compressed files
async function isValidXva(path) {
export async function isValidXva(path) {
const handler = this._handler
// size is longer when encrypted + reading part of an encrypted file is not implemented
@@ -74,6 +72,5 @@ async function isValidXva(path) {
return true
}
}
exports.isValidXva = isValidXva
const noop = Function.prototype

View File

@@ -1,9 +1,7 @@
'use strict'
const fromCallback = require('promise-toolbox/fromCallback')
const { createLogger } = require('@xen-orchestra/log')
const { createParser } = require('parse-pairs')
const { execFile } = require('child_process')
import fromCallback from 'promise-toolbox/fromCallback'
import { createLogger } from '@xen-orchestra/log'
import { createParser } from 'parse-pairs'
import { execFile } from 'child_process'
const { debug } = createLogger('xo:backups:listPartitions')
@@ -24,8 +22,7 @@ const IGNORED_PARTITION_TYPES = {
0x82: true, // swap
}
const LVM_PARTITION_TYPE = 0x8e
exports.LVM_PARTITION_TYPE = LVM_PARTITION_TYPE
export const LVM_PARTITION_TYPE = 0x8e
const parsePartxLine = createParser({
keyTransform: key => (key === 'UUID' ? 'id' : key.toLowerCase()),
@@ -33,7 +30,7 @@ const parsePartxLine = createParser({
})
// returns an empty array in case of a non-partitioned disk
exports.listPartitions = async function listPartitions(devicePath) {
export async function listPartitions(devicePath) {
const parts = await fromCallback(execFile, 'partx', [
'--bytes',
'--output=NR,START,SIZE,NAME,UUID,TYPE',

View File

@@ -1,8 +1,6 @@
'use strict'
const fromCallback = require('promise-toolbox/fromCallback')
const { createParser } = require('parse-pairs')
const { execFile } = require('child_process')
import fromCallback from 'promise-toolbox/fromCallback'
import { createParser } from 'parse-pairs'
import { execFile } from 'child_process'
// ===================================================================
@@ -29,5 +27,5 @@ const makeFunction =
.map(Array.isArray(fields) ? parse : line => parse(line)[fields])
}
exports.lvs = makeFunction('lvs')
exports.pvs = makeFunction('pvs')
export const lvs = makeFunction('lvs')
export const pvs = makeFunction('pvs')

View File

@@ -1,22 +1,20 @@
'use strict'
import { asyncMap } from '@xen-orchestra/async-map'
import Disposable from 'promise-toolbox/Disposable'
import ignoreErrors from 'promise-toolbox/ignoreErrors'
const { asyncMap } = require('@xen-orchestra/async-map')
const Disposable = require('promise-toolbox/Disposable')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const { extractIdsFromSimplePattern } = require('../extractIdsFromSimplePattern.js')
const { PoolMetadataBackup } = require('./_PoolMetadataBackup.js')
const { XoMetadataBackup } = require('./_XoMetadataBackup.js')
const { DEFAULT_SETTINGS, Abstract } = require('./_Abstract.js')
const { runTask } = require('./_runTask.js')
const { getAdaptersByRemote } = require('./_getAdaptersByRemote.js')
import { extractIdsFromSimplePattern } from '../extractIdsFromSimplePattern.mjs'
import { PoolMetadataBackup } from './_PoolMetadataBackup.mjs'
import { XoMetadataBackup } from './_XoMetadataBackup.mjs'
import { DEFAULT_SETTINGS, Abstract } from './_Abstract.mjs'
import { runTask } from './_runTask.mjs'
import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
const DEFAULT_METADATA_SETTINGS = {
retentionPoolMetadata: 0,
retentionXoMetadata: 0,
}
exports.Metadata = class MetadataBackupRunner extends Abstract {
export const Metadata = class MetadataBackupRunner extends Abstract {
_computeBaseSettings(config, job) {
const baseSettings = { ...DEFAULT_SETTINGS }
Object.assign(baseSettings, DEFAULT_METADATA_SETTINGS, config.defaultSettings, config.metadata?.defaultSettings)

View File

@@ -1,17 +1,15 @@
'use strict'
import { asyncMapSettled } from '@xen-orchestra/async-map'
import Disposable from 'promise-toolbox/Disposable'
import { limitConcurrency } from 'limit-concurrency-decorator'
const { asyncMapSettled } = require('@xen-orchestra/async-map')
const Disposable = require('promise-toolbox/Disposable')
const { limitConcurrency } = require('limit-concurrency-decorator')
const { extractIdsFromSimplePattern } = require('../extractIdsFromSimplePattern.js')
const { Task } = require('../Task.js')
const createStreamThrottle = require('./_createStreamThrottle.js')
const { DEFAULT_SETTINGS, Abstract } = require('./_Abstract.js')
const { runTask } = require('./_runTask.js')
const { getAdaptersByRemote } = require('./_getAdaptersByRemote.js')
const { FullRemote } = require('./_vmRunners/FullRemote.js')
const { IncrementalRemote } = require('./_vmRunners/IncrementalRemote.js')
import { extractIdsFromSimplePattern } from '../extractIdsFromSimplePattern.mjs'
import { Task } from '../Task.mjs'
import createStreamThrottle from './_createStreamThrottle.mjs'
import { DEFAULT_SETTINGS, Abstract } from './_Abstract.mjs'
import { runTask } from './_runTask.mjs'
import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
import { FullRemote } from './_vmRunners/FullRemote.mjs'
import { IncrementalRemote } from './_vmRunners/IncrementalRemote.mjs'
const DEFAULT_REMOTE_VM_SETTINGS = {
concurrency: 2,
@@ -27,7 +25,7 @@ const DEFAULT_REMOTE_VM_SETTINGS = {
vmTimeout: 0,
}
exports.VmsRemote = class RemoteVmsBackupRunner extends Abstract {
export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
_computeBaseSettings(config, job) {
const baseSettings = { ...DEFAULT_SETTINGS }
Object.assign(baseSettings, DEFAULT_REMOTE_VM_SETTINGS, config.defaultSettings, config.vm?.defaultSettings)

View File

@@ -1,17 +1,15 @@
'use strict'
import { asyncMapSettled } from '@xen-orchestra/async-map'
import Disposable from 'promise-toolbox/Disposable'
import { limitConcurrency } from 'limit-concurrency-decorator'
const { asyncMapSettled } = require('@xen-orchestra/async-map')
const Disposable = require('promise-toolbox/Disposable')
const { limitConcurrency } = require('limit-concurrency-decorator')
const { extractIdsFromSimplePattern } = require('../extractIdsFromSimplePattern.js')
const { Task } = require('../Task.js')
const createStreamThrottle = require('./_createStreamThrottle.js')
const { DEFAULT_SETTINGS, Abstract } = require('./_Abstract.js')
const { runTask } = require('./_runTask.js')
const { getAdaptersByRemote } = require('./_getAdaptersByRemote.js')
const { IncrementalXapi } = require('./_vmRunners/IncrementalXapi.js')
const { FullXapi } = require('./_vmRunners/FullXapi.js')
import { extractIdsFromSimplePattern } from '../extractIdsFromSimplePattern.mjs'
import { Task } from '../Task.mjs'
import createStreamThrottle from './_createStreamThrottle.mjs'
import { DEFAULT_SETTINGS, Abstract } from './_Abstract.mjs'
import { runTask } from './_runTask.mjs'
import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
import { IncrementalXapi } from './_vmRunners/IncrementalXapi.mjs'
import { FullXapi } from './_vmRunners/FullXapi.mjs'
const DEFAULT_XAPI_VM_SETTINGS = {
bypassVdiChainsCheck: false,
@@ -36,7 +34,7 @@ const DEFAULT_XAPI_VM_SETTINGS = {
vmTimeout: 0,
}
exports.VmsXapi = class VmsXapiBackupRunner extends Abstract {
export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
_computeBaseSettings(config, job) {
const baseSettings = { ...DEFAULT_SETTINGS }
Object.assign(baseSettings, DEFAULT_XAPI_VM_SETTINGS, config.defaultSettings, config.vm?.defaultSettings)

View File

@@ -1,17 +1,15 @@
'use strict'
import Disposable from 'promise-toolbox/Disposable'
import pTimeout from 'promise-toolbox/timeout'
import { compileTemplate } from '@xen-orchestra/template'
import { runTask } from './_runTask.mjs'
import { RemoteTimeoutError } from './_RemoteTimeoutError.mjs'
const Disposable = require('promise-toolbox/Disposable')
const pTimeout = require('promise-toolbox/timeout')
const { compileTemplate } = require('@xen-orchestra/template')
const { runTask } = require('./_runTask.js')
const { RemoteTimeoutError } = require('./_RemoteTimeoutError.js')
exports.DEFAULT_SETTINGS = {
export const DEFAULT_SETTINGS = {
getRemoteTimeout: 300e3,
reportWhen: 'failure',
}
exports.Abstract = class AbstractRunner {
export const Abstract = class AbstractRunner {
constructor({ config, getAdapter, getConnectedRecord, job, schedule }) {
this._config = config
this._getRecord = getConnectedRecord

View File

@@ -1,16 +1,13 @@
'use strict'
import { asyncMap } from '@xen-orchestra/async-map'
const { asyncMap } = require('@xen-orchestra/async-map')
import { DIR_XO_POOL_METADATA_BACKUPS } from '../RemoteAdapter.mjs'
import { forkStreamUnpipe } from './_forkStreamUnpipe.mjs'
import { formatFilenameDate } from '../_filenameDate.mjs'
import { Task } from '../Task.mjs'
const { DIR_XO_POOL_METADATA_BACKUPS } = require('../RemoteAdapter.js')
const { forkStreamUnpipe } = require('./_forkStreamUnpipe.js')
const { formatFilenameDate } = require('../_filenameDate.js')
const { Task } = require('../Task.js')
export const PATH_DB_DUMP = '/pool/xmldbdump'
const PATH_DB_DUMP = '/pool/xmldbdump'
exports.PATH_DB_DUMP = PATH_DB_DUMP
exports.PoolMetadataBackup = class PoolMetadataBackup {
export class PoolMetadataBackup {
constructor({ config, job, pool, remoteAdapters, schedule, settings }) {
this._config = config
this._job = job

View File

@@ -1,8 +1,6 @@
'use strict'
class RemoteTimeoutError extends Error {
export class RemoteTimeoutError extends Error {
constructor(remoteId) {
super('timeout while getting the remote ' + remoteId)
this.remoteId = remoteId
}
}
exports.RemoteTimeoutError = RemoteTimeoutError

View File

@@ -1,13 +1,11 @@
'use strict'
import { asyncMap } from '@xen-orchestra/async-map'
import { join } from '@xen-orchestra/fs/path'
const { asyncMap } = require('@xen-orchestra/async-map')
const { join } = require('@xen-orchestra/fs/path')
import { DIR_XO_CONFIG_BACKUPS } from '../RemoteAdapter.mjs'
import { formatFilenameDate } from '../_filenameDate.mjs'
import { Task } from '../Task.mjs'
const { DIR_XO_CONFIG_BACKUPS } = require('../RemoteAdapter.js')
const { formatFilenameDate } = require('../_filenameDate.js')
const { Task } = require('../Task.js')
exports.XoMetadataBackup = class XoMetadataBackup {
export class XoMetadataBackup {
constructor({ config, job, remoteAdapters, schedule, settings }) {
this._config = config
this._job = job

View File

@@ -1,12 +1,10 @@
'use strict'
const { pipeline } = require('node:stream')
const { ThrottleGroup } = require('@kldzj/stream-throttle')
const identity = require('lodash/identity.js')
import { pipeline } from 'node:stream'
import { ThrottleGroup } from '@kldzj/stream-throttle'
import identity from 'lodash/identity.js'
const noop = Function.prototype
module.exports = function createStreamThrottle(rate) {
export default function createStreamThrottle(rate) {
if (rate === 0) {
return identity
}

View File

@@ -1,14 +1,13 @@
'use strict'
import { createLogger } from '@xen-orchestra/log'
import { finished, PassThrough } from 'node:stream'
const { finished, PassThrough } = require('node:stream')
const { debug } = require('@xen-orchestra/log').createLogger('xo:backups:forkStreamUnpipe')
const { debug } = createLogger('xo:backups:forkStreamUnpipe')
// create a new readable stream from an existing one which may be piped later
//
// in case of error in the new readable stream, it will simply be unpiped
// from the original one
exports.forkStreamUnpipe = function forkStreamUnpipe(source) {
export function forkStreamUnpipe(source) {
const { forks = 0 } = source
source.forks = forks + 1

View File

@@ -1,9 +1,7 @@
'use strict'
const getAdaptersByRemote = adapters => {
export function getAdaptersByRemote(adapters) {
const adaptersByRemote = {}
adapters.forEach(({ adapter, remoteId }) => {
adaptersByRemote[remoteId] = adapter
})
return adaptersByRemote
}
exports.getAdaptersByRemote = getAdaptersByRemote

View File

@@ -1,6 +0,0 @@
'use strict'
const { Task } = require('../Task.js')
const noop = Function.prototype
const runTask = (...args) => Task.run(...args).catch(noop) // errors are handled by logs
exports.runTask = runTask

View File

@@ -0,0 +1,5 @@
import { Task } from '../Task.mjs'
const noop = Function.prototype
export const runTask = (...args) => Task.run(...args).catch(noop) // errors are handled by logs

View File

@@ -1,14 +1,12 @@
'use strict'
import { decorateMethodsWith } from '@vates/decorate-with'
import { defer } from 'golike-defer'
import { AbstractRemote } from './_AbstractRemote.mjs'
import { FullRemoteWriter } from '../_writers/FullRemoteWriter.mjs'
import { forkStreamUnpipe } from '../_forkStreamUnpipe.mjs'
import { watchStreamSize } from '../../_watchStreamSize.mjs'
import { Task } from '../../Task.mjs'
const { decorateMethodsWith } = require('@vates/decorate-with')
const { defer } = require('golike-defer')
const { AbstractRemote } = require('./_AbstractRemote')
const { FullRemoteWriter } = require('../_writers/FullRemoteWriter')
const { forkStreamUnpipe } = require('../_forkStreamUnpipe')
const { watchStreamSize } = require('../../_watchStreamSize')
const { Task } = require('../../Task')
class FullRemoteVmBackupRunner extends AbstractRemote {
export const FullRemote = class FullRemoteVmBackupRunner extends AbstractRemote {
_getRemoteWriter() {
return FullRemoteWriter
}
@@ -47,7 +45,6 @@ class FullRemoteVmBackupRunner extends AbstractRemote {
}
}
exports.FullRemote = FullRemoteVmBackupRunner
decorateMethodsWith(FullRemoteVmBackupRunner, {
decorateMethodsWith(FullRemote, {
_run: defer,
})

View File

@@ -1,16 +1,14 @@
'use strict'
import { createLogger } from '@xen-orchestra/log'
const { createLogger } = require('@xen-orchestra/log')
const { forkStreamUnpipe } = require('../_forkStreamUnpipe.js')
const { FullRemoteWriter } = require('../_writers/FullRemoteWriter.js')
const { FullXapiWriter } = require('../_writers/FullXapiWriter.js')
const { watchStreamSize } = require('../../_watchStreamSize.js')
const { AbstractXapi } = require('./_AbstractXapi.js')
import { forkStreamUnpipe } from '../_forkStreamUnpipe.mjs'
import { FullRemoteWriter } from '../_writers/FullRemoteWriter.mjs'
import { FullXapiWriter } from '../_writers/FullXapiWriter.mjs'
import { watchStreamSize } from '../../_watchStreamSize.mjs'
import { AbstractXapi } from './_AbstractXapi.mjs'
const { debug } = createLogger('xo:backups:FullXapiVmBackup')
exports.FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
export const FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
_getWriters() {
return [FullRemoteWriter, FullXapiWriter]
}

View File

@@ -1,15 +1,14 @@
'use strict'
const assert = require('node:assert')
import { asyncEach } from '@vates/async-each'
import { decorateMethodsWith } from '@vates/decorate-with'
import { defer } from 'golike-defer'
import assert from 'node:assert'
import isVhdDifferencingDisk from 'vhd-lib/isVhdDifferencingDisk.js'
import mapValues from 'lodash/mapValues.js'
const { decorateMethodsWith } = require('@vates/decorate-with')
const { defer } = require('golike-defer')
const { mapValues } = require('lodash')
const { Task } = require('../../Task')
const { AbstractRemote } = require('./_AbstractRemote')
const { IncrementalRemoteWriter } = require('../_writers/IncrementalRemoteWriter')
const { forkDeltaExport } = require('./_forkDeltaExport')
const isVhdDifferencingDisk = require('vhd-lib/isVhdDifferencingDisk')
const { asyncEach } = require('@vates/async-each')
import { AbstractRemote } from './_AbstractRemote.mjs'
import { forkDeltaExport } from './_forkDeltaExport.mjs'
import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs'
import { Task } from '../../Task.mjs'
class IncrementalRemoteVmBackupRunner extends AbstractRemote {
_getRemoteWriter() {
@@ -61,7 +60,7 @@ class IncrementalRemoteVmBackupRunner extends AbstractRemote {
}
}
exports.IncrementalRemote = IncrementalRemoteVmBackupRunner
export const IncrementalRemote = IncrementalRemoteVmBackupRunner
decorateMethodsWith(IncrementalRemoteVmBackupRunner, {
_run: defer,
})

View File

@@ -1,28 +1,26 @@
'use strict'
import { asyncEach } from '@vates/async-each'
import { asyncMap } from '@xen-orchestra/async-map'
import { createLogger } from '@xen-orchestra/log'
import { pipeline } from 'node:stream'
import findLast from 'lodash/findLast.js'
import isVhdDifferencingDisk from 'vhd-lib/isVhdDifferencingDisk.js'
import keyBy from 'lodash/keyBy.js'
import mapValues from 'lodash/mapValues.js'
import vhdStreamValidator from 'vhd-lib/vhdStreamValidator.js'
const findLast = require('lodash/findLast.js')
const keyBy = require('lodash/keyBy.js')
const mapValues = require('lodash/mapValues.js')
const vhdStreamValidator = require('vhd-lib/vhdStreamValidator.js')
const { asyncMap } = require('@xen-orchestra/async-map')
const { createLogger } = require('@xen-orchestra/log')
const { pipeline } = require('node:stream')
const { IncrementalRemoteWriter } = require('../_writers/IncrementalRemoteWriter.js')
const { IncrementalXapiWriter } = require('../_writers/IncrementalXapiWriter.js')
const { exportIncrementalVm } = require('../../_incrementalVm.js')
const { Task } = require('../../Task.js')
const { watchStreamSize } = require('../../_watchStreamSize.js')
const { AbstractXapi } = require('./_AbstractXapi.js')
const { forkDeltaExport } = require('./_forkDeltaExport.js')
const isVhdDifferencingDisk = require('vhd-lib/isVhdDifferencingDisk')
const { asyncEach } = require('@vates/async-each')
import { AbstractXapi } from './_AbstractXapi.mjs'
import { exportIncrementalVm } from '../../_incrementalVm.mjs'
import { forkDeltaExport } from './_forkDeltaExport.mjs'
import { IncrementalRemoteWriter } from '../_writers/IncrementalRemoteWriter.mjs'
import { IncrementalXapiWriter } from '../_writers/IncrementalXapiWriter.mjs'
import { Task } from '../../Task.mjs'
import { watchStreamSize } from '../../_watchStreamSize.mjs'
const { debug } = createLogger('xo:backups:IncrementalXapiVmBackup')
const noop = Function.prototype
exports.IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXapi {
export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends AbstractXapi {
_getWriters() {
return [IncrementalRemoteWriter, IncrementalXapiWriter]
}

View File

@@ -1,8 +1,6 @@
'use strict'
const { asyncMap } = require('@xen-orchestra/async-map')
const { createLogger } = require('@xen-orchestra/log')
const { Task } = require('../../Task.js')
import { asyncMap } from '@xen-orchestra/async-map'
import { createLogger } from '@xen-orchestra/log'
import { Task } from '../../Task.mjs'
const { debug, warn } = createLogger('xo:backups:AbstractVmRunner')
@@ -19,7 +17,7 @@ const asyncEach = async (iterable, fn, thisArg = iterable) => {
}
}
exports.Abstract = class AbstractVmBackupRunner {
export const Abstract = class AbstractVmBackupRunner {
// calls fn for each function, warns of any errors, and throws only if there are no writers left
async _callWriters(fn, step, parallel = true) {
const writers = this._writers

View File

@@ -1,11 +1,11 @@
'use strict'
const { Abstract } = require('./_Abstract')
import { asyncEach } from '@vates/async-each'
import { Disposable } from 'promise-toolbox'
const { getVmBackupDir } = require('../../_getVmBackupDir')
const { asyncEach } = require('@vates/async-each')
const { Disposable } = require('promise-toolbox')
import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
exports.AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstract {
import { Abstract } from './_Abstract.mjs'
export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstract {
constructor({
config,
job,

View File

@@ -1,18 +1,16 @@
'use strict'
import assert from 'node:assert'
import groupBy from 'lodash/groupBy.js'
import ignoreErrors from 'promise-toolbox/ignoreErrors'
import { asyncMap } from '@xen-orchestra/async-map'
import { decorateMethodsWith } from '@vates/decorate-with'
import { defer } from 'golike-defer'
import { formatDateTime } from '@xen-orchestra/xapi'
const assert = require('assert')
const groupBy = require('lodash/groupBy.js')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const { asyncMap } = require('@xen-orchestra/async-map')
const { decorateMethodsWith } = require('@vates/decorate-with')
const { defer } = require('golike-defer')
const { formatDateTime } = require('@xen-orchestra/xapi')
import { getOldEntries } from '../../_getOldEntries.mjs'
import { Task } from '../../Task.mjs'
import { Abstract } from './_Abstract.mjs'
const { getOldEntries } = require('../../_getOldEntries.js')
const { Task } = require('../../Task.js')
const { Abstract } = require('./_Abstract.js')
class AbstractXapiVmBackupRunner extends Abstract {
export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
constructor({
config,
getSnapshotNameLabel,
@@ -271,8 +269,7 @@ class AbstractXapiVmBackupRunner extends Abstract {
await this._healthCheck()
}
}
exports.AbstractXapi = AbstractXapiVmBackupRunner
decorateMethodsWith(AbstractXapiVmBackupRunner, {
decorateMethodsWith(AbstractXapi, {
run: defer,
})

View File

@@ -1,12 +0,0 @@
'use strict'
const { mapValues } = require('lodash')
const { forkStreamUnpipe } = require('../_forkStreamUnpipe')
exports.forkDeltaExport = function forkDeltaExport(deltaExport) {
return Object.create(deltaExport, {
streams: {
value: mapValues(deltaExport.streams, forkStreamUnpipe),
},
})
}

View File

@@ -0,0 +1,11 @@
import mapValues from 'lodash/mapValues.js'
import { forkStreamUnpipe } from '../_forkStreamUnpipe.mjs'
export function forkDeltaExport(deltaExport) {
return Object.create(deltaExport, {
streams: {
value: mapValues(deltaExport.streams, forkStreamUnpipe),
},
})
}

View File

@@ -1,13 +1,11 @@
'use strict'
import { formatFilenameDate } from '../../_filenameDate.mjs'
import { getOldEntries } from '../../_getOldEntries.mjs'
import { Task } from '../../Task.mjs'
const { formatFilenameDate } = require('../../_filenameDate.js')
const { getOldEntries } = require('../../_getOldEntries.js')
const { Task } = require('../../Task.js')
import { MixinRemoteWriter } from './_MixinRemoteWriter.mjs'
import { AbstractFullWriter } from './_AbstractFullWriter.mjs'
const { MixinRemoteWriter } = require('./_MixinRemoteWriter.js')
const { AbstractFullWriter } = require('./_AbstractFullWriter.js')
exports.FullRemoteWriter = class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
constructor(props) {
super(props)

View File

@@ -1,18 +1,16 @@
'use strict'
import ignoreErrors from 'promise-toolbox/ignoreErrors'
import { asyncMap, asyncMapSettled } from '@xen-orchestra/async-map'
import { formatDateTime } from '@xen-orchestra/xapi'
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
const { formatDateTime } = require('@xen-orchestra/xapi')
import { formatFilenameDate } from '../../_filenameDate.mjs'
import { getOldEntries } from '../../_getOldEntries.mjs'
import { Task } from '../../Task.mjs'
const { formatFilenameDate } = require('../../_filenameDate.js')
const { getOldEntries } = require('../../_getOldEntries.js')
const { Task } = require('../../Task.js')
import { AbstractFullWriter } from './_AbstractFullWriter.mjs'
import { MixinXapiWriter } from './_MixinXapiWriter.mjs'
import { listReplicatedVms } from './_listReplicatedVms.mjs'
const { AbstractFullWriter } = require('./_AbstractFullWriter.js')
const { MixinXapiWriter } = require('./_MixinXapiWriter.js')
const { listReplicatedVms } = require('./_listReplicatedVms.js')
exports.FullXapiWriter = class FullXapiWriter extends MixinXapiWriter(AbstractFullWriter) {
export class FullXapiWriter extends MixinXapiWriter(AbstractFullWriter) {
constructor(props) {
super(props)

View File

@@ -1,29 +1,27 @@
'use strict'
import assert from 'node:assert'
import mapValues from 'lodash/mapValues.js'
import ignoreErrors from 'promise-toolbox/ignoreErrors'
import { asyncEach } from '@vates/async-each'
import { asyncMap } from '@xen-orchestra/async-map'
import { chainVhd, checkVhdChain, openVhd, VhdAbstract } from 'vhd-lib'
import { createLogger } from '@xen-orchestra/log'
import { decorateClass } from '@vates/decorate-with'
import { defer } from 'golike-defer'
import { dirname } from 'node:path'
const assert = require('assert')
const mapValues = require('lodash/mapValues.js')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const { asyncEach } = require('@vates/async-each')
const { asyncMap } = require('@xen-orchestra/async-map')
const { chainVhd, checkVhdChain, openVhd, VhdAbstract } = require('vhd-lib')
const { createLogger } = require('@xen-orchestra/log')
const { decorateClass } = require('@vates/decorate-with')
const { defer } = require('golike-defer')
const { dirname } = require('path')
import { formatFilenameDate } from '../../_filenameDate.mjs'
import { getOldEntries } from '../../_getOldEntries.mjs'
import { Task } from '../../Task.mjs'
const { formatFilenameDate } = require('../../_filenameDate.js')
const { getOldEntries } = require('../../_getOldEntries.js')
const { Task } = require('../../Task.js')
const { MixinRemoteWriter } = require('./_MixinRemoteWriter.js')
const { AbstractIncrementalWriter } = require('./_AbstractIncrementalWriter.js')
const { checkVhd } = require('./_checkVhd.js')
const { packUuid } = require('./_packUuid.js')
const { Disposable } = require('promise-toolbox')
import { MixinRemoteWriter } from './_MixinRemoteWriter.mjs'
import { AbstractIncrementalWriter } from './_AbstractIncrementalWriter.mjs'
import { checkVhd } from './_checkVhd.mjs'
import { packUuid } from './_packUuid.mjs'
import { Disposable } from 'promise-toolbox'
const { warn } = createLogger('xo:backups:DeltaBackupWriter')
class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWriter) {
export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWriter) {
async checkBaseVdis(baseUuidToSrcVdi) {
const { handler } = this._adapter
const adapter = this._adapter
@@ -238,6 +236,6 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
// TODO: run cleanup?
}
}
exports.IncrementalRemoteWriter = decorateClass(IncrementalRemoteWriter, {
decorateClass(IncrementalRemoteWriter, {
_transfer: defer,
})

View File

@@ -1,19 +1,17 @@
'use strict'
import { asyncMap, asyncMapSettled } from '@xen-orchestra/async-map'
import ignoreErrors from 'promise-toolbox/ignoreErrors'
import { formatDateTime } from '@xen-orchestra/xapi'
const { asyncMap, asyncMapSettled } = require('@xen-orchestra/async-map')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const { formatDateTime } = require('@xen-orchestra/xapi')
import { formatFilenameDate } from '../../_filenameDate.mjs'
import { getOldEntries } from '../../_getOldEntries.mjs'
import { importIncrementalVm, TAG_COPY_SRC } from '../../_incrementalVm.mjs'
import { Task } from '../../Task.mjs'
const { formatFilenameDate } = require('../../_filenameDate.js')
const { getOldEntries } = require('../../_getOldEntries.js')
const { importIncrementalVm, TAG_COPY_SRC } = require('../../_incrementalVm.js')
const { Task } = require('../../Task.js')
import { AbstractIncrementalWriter } from './_AbstractIncrementalWriter.mjs'
import { MixinXapiWriter } from './_MixinXapiWriter.mjs'
import { listReplicatedVms } from './_listReplicatedVms.mjs'
const { AbstractIncrementalWriter } = require('./_AbstractIncrementalWriter.js')
const { MixinXapiWriter } = require('./_MixinXapiWriter.js')
const { listReplicatedVms } = require('./_listReplicatedVms.js')
exports.IncrementalXapiWriter = class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWriter) {
async checkBaseVdis(baseUuidToSrcVdi, baseVm) {
const sr = this._sr
const replicatedVm = listReplicatedVms(sr.$xapi, this._job.id, sr.uuid, this._vmUuid).find(

View File

@@ -1,8 +1,6 @@
'use strict'
import { AbstractWriter } from './_AbstractWriter.mjs'
const { AbstractWriter } = require('./_AbstractWriter.js')
exports.AbstractFullWriter = class AbstractFullWriter extends AbstractWriter {
export class AbstractFullWriter extends AbstractWriter {
async run({ timestamp, sizeContainer, stream, vm, vmSnapshot }) {
try {
return await this._run({ timestamp, sizeContainer, stream, vm, vmSnapshot })

View File

@@ -1,8 +1,6 @@
'use strict'
import { AbstractWriter } from './_AbstractWriter.mjs'
const { AbstractWriter } = require('./_AbstractWriter.js')
exports.AbstractIncrementalWriter = class AbstractIncrementalWriter extends AbstractWriter {
export class AbstractIncrementalWriter extends AbstractWriter {
checkBaseVdis(baseUuidToSrcVdi, baseVm) {
throw new Error('Not implemented')
}

View File

@@ -1,9 +1,7 @@
'use strict'
import { formatFilenameDate } from '../../_filenameDate.mjs'
import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
const { formatFilenameDate } = require('../../_filenameDate')
const { getVmBackupDir } = require('../../_getVmBackupDir')
exports.AbstractWriter = class AbstractWriter {
export class AbstractWriter {
constructor({ config, healthCheckSr, job, vmUuid, scheduleId, settings }) {
this._config = config
this._healthCheckSr = healthCheckSr

View File

@@ -1,19 +1,17 @@
'use strict'
import { createLogger } from '@xen-orchestra/log'
import { join } from 'node:path'
import assert from 'node:assert'
const { createLogger } = require('@xen-orchestra/log')
const { join } = require('path')
const assert = require('assert')
const { formatFilenameDate } = require('../../_filenameDate.js')
const { getVmBackupDir } = require('../../_getVmBackupDir.js')
const { HealthCheckVmBackup } = require('../../HealthCheckVmBackup.js')
const { ImportVmBackup } = require('../../ImportVmBackup.js')
const { Task } = require('../../Task.js')
const MergeWorker = require('../../merge-worker/index.js')
import { formatFilenameDate } from '../../_filenameDate.mjs'
import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
import { HealthCheckVmBackup } from '../../HealthCheckVmBackup.mjs'
import { ImportVmBackup } from '../../ImportVmBackup.mjs'
import { Task } from '../../Task.mjs'
import * as MergeWorker from '../../merge-worker/index.mjs'
const { info, warn } = createLogger('xo:backups:MixinBackupWriter')
exports.MixinRemoteWriter = (BaseClass = Object) =>
export const MixinRemoteWriter = (BaseClass = Object) =>
class MixinRemoteWriter extends BaseClass {
#lock

View File

@@ -1,12 +1,10 @@
'use strict'
import { extractOpaqueRef } from '@xen-orchestra/xapi'
import assert from 'node:assert/strict'
const { extractOpaqueRef } = require('@xen-orchestra/xapi')
import { HealthCheckVmBackup } from '../../HealthCheckVmBackup.mjs'
import { Task } from '../../Task.mjs'
const { Task } = require('../../Task')
const assert = require('node:assert/strict')
const { HealthCheckVmBackup } = require('../../HealthCheckVmBackup')
exports.MixinXapiWriter = (BaseClass = Object) =>
export const MixinXapiWriter = (BaseClass = Object) =>
class MixinXapiWriter extends BaseClass {
constructor({ sr, ...rest }) {
super(rest)

View File

@@ -1,8 +0,0 @@
'use strict'
const openVhd = require('vhd-lib').openVhd
const Disposable = require('promise-toolbox/Disposable')
exports.checkVhd = async function checkVhd(handler, path) {
await Disposable.use(openVhd(handler, path), () => {})
}

View File

@@ -0,0 +1,6 @@
import { openVhd } from 'vhd-lib'
import Disposable from 'promise-toolbox/Disposable'
export async function checkVhd(handler, path) {
await Disposable.use(openVhd(handler, path), () => {})
}

View File

@@ -1,5 +1,3 @@
'use strict'
const getReplicatedVmDatetime = vm => {
const { 'xo:backup:datetime': datetime = vm.name_label.slice(-17, -1) } = vm.other_config
return datetime
@@ -7,7 +5,7 @@ const getReplicatedVmDatetime = vm => {
const compareReplicatedVmDatetime = (a, b) => (getReplicatedVmDatetime(a) < getReplicatedVmDatetime(b) ? -1 : 1)
exports.listReplicatedVms = function listReplicatedVms(xapi, scheduleOrJobId, srUuid, vmUuid) {
export function listReplicatedVms(xapi, scheduleOrJobId, srUuid, vmUuid) {
const { all } = xapi.objects
const vms = {}
for (const key in all) {

View File

@@ -1,7 +1,5 @@
'use strict'
const PARSE_UUID_RE = /-/g
exports.packUuid = function packUuid(uuid) {
export function packUuid(uuid) {
return Buffer.from(uuid.replace(PARSE_UUID_RE, ''), 'hex')
}

View File

@@ -1,6 +1,4 @@
'use strict'
exports.watchStreamSize = function watchStreamSize(stream, container = { size: 0 }) {
export function watchStreamSize(stream, container = { size: 0 }) {
stream.on('data', data => {
container.size += data.length
})

View File

@@ -1,6 +1,4 @@
'use strict'
exports.extractIdsFromSimplePattern = function extractIdsFromSimplePattern(pattern) {
export function extractIdsFromSimplePattern(pattern) {
if (pattern === undefined) {
return []
}

View File

@@ -1,7 +1,5 @@
'use strict'
const mapValues = require('lodash/mapValues.js')
const { dirname } = require('path')
import mapValues from 'lodash/mapValues.js'
import { dirname } from 'node:path'
function formatVmBackup(backup) {
return {
@@ -31,6 +29,6 @@ function formatVmBackup(backup) {
}
// format all backups as returned by RemoteAdapter#listAllVmBackups()
exports.formatVmBackups = function formatVmBackups(backupsByVM) {
export function formatVmBackups(backupsByVM) {
return mapValues(backupsByVM, backups => backups.map(formatVmBackup))
}

View File

@@ -2,19 +2,17 @@
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable n/shebang */
'use strict'
import { catchGlobalErrors } from '@xen-orchestra/log/configure'
import { createLogger } from '@xen-orchestra/log'
import { getSyncedHandler } from '@xen-orchestra/fs'
import { join } from 'node:path'
import Disposable from 'promise-toolbox/Disposable'
import min from 'lodash/min.js'
const { catchGlobalErrors } = require('@xen-orchestra/log/configure')
const { createLogger } = require('@xen-orchestra/log')
const { getSyncedHandler } = require('@xen-orchestra/fs')
const { join } = require('path')
const Disposable = require('promise-toolbox/Disposable')
const min = require('lodash/min')
import { getVmBackupDir } from '../_getVmBackupDir.mjs'
import { RemoteAdapter } from '../RemoteAdapter.mjs'
const { getVmBackupDir } = require('../_getVmBackupDir.js')
const { RemoteAdapter } = require('../RemoteAdapter.js')
const { CLEAN_VM_QUEUE } = require('./index.js')
import { CLEAN_VM_QUEUE } from './index.mjs'
// -------------------------------------------------------------------

View File

@@ -1,13 +1,12 @@
'use strict'
import { join } from 'node:path'
import { spawn } from 'child_process'
import { check } from 'proper-lockfile'
const { join, resolve } = require('path')
const { spawn } = require('child_process')
const { check } = require('proper-lockfile')
export const CLEAN_VM_QUEUE = '/xo-vm-backups/.queue/clean-vm/'
const CLEAN_VM_QUEUE = (exports.CLEAN_VM_QUEUE = '/xo-vm-backups/.queue/clean-vm/')
const CLI_PATH = new URL('cli.mjs', import.meta.url).pathname
const CLI_PATH = resolve(__dirname, 'cli.js')
exports.run = async function runMergeWorker(remotePath) {
export const run = async function runMergeWorker(remotePath) {
try {
// TODO: find a way to pass the acquire the lock and then pass it down the worker
if (await check(join(remotePath, CLEAN_VM_QUEUE))) {

View File

@@ -14,7 +14,7 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test-integration": "node--test *.integ.js"
"test-integration": "node--test *.integ.mjs"
},
"dependencies": {
"@kldzj/stream-throttle": "^1.1.1",
@@ -33,7 +33,6 @@
"compare-versions": "^5.0.1",
"d3-time-format": "^3.0.0",
"decorator-synchronized": "^0.6.0",
"fs-extra": "^11.1.0",
"golike-defer": "^0.5.1",
"limit-concurrency-decorator": "^0.5.0",
"lodash": "^4.17.20",
@@ -41,12 +40,13 @@
"parse-pairs": "^2.0.0",
"promise-toolbox": "^0.21.0",
"proper-lockfile": "^4.1.2",
"tar": "^6.1.15",
"uuid": "^9.0.0",
"vhd-lib": "^4.5.0",
"xen-api": "^1.3.3",
"yazl": "^2.5.1"
"xen-api": "^1.3.3"
},
"devDependencies": {
"fs-extra": "^11.1.0",
"rimraf": "^5.0.1",
"sinon": "^15.0.1",
"test": "^3.2.1",

View File

@@ -1,8 +1,6 @@
'use strict'
import { DIR_XO_CONFIG_BACKUPS, DIR_XO_POOL_METADATA_BACKUPS } from './RemoteAdapter.mjs'
const { DIR_XO_CONFIG_BACKUPS, DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter.js')
exports.parseMetadataBackupId = function parseMetadataBackupId(backupId) {
export function parseMetadataBackupId(backupId) {
const [dir, ...rest] = backupId.split('/')
if (dir === DIR_XO_CONFIG_BACKUPS) {
const [scheduleId, timestamp] = rest

View File

@@ -1,14 +1,11 @@
'use strict'
const path = require('path')
const { createLogger } = require('@xen-orchestra/log')
const { fork } = require('child_process')
import { createLogger } from '@xen-orchestra/log'
import { fork } from 'child_process'
const { warn } = createLogger('xo:backups:backupWorker')
const PATH = path.resolve(__dirname, '_backupWorker.js')
const PATH = new URL('_backupWorker.mjs', import.meta.url).pathname
exports.runBackupWorker = function runBackupWorker(params, onLog) {
export function runBackupWorker(params, onLog) {
return new Promise((resolve, reject) => {
const worker = fork(PATH)

View File

@@ -1,7 +1,5 @@
'use strict'
// a valid footer of a 2
exports.VHDFOOTER = {
export const VHDFOOTER = {
cookie: 'conectix',
features: 2,
fileFormatVersion: 65536,
@@ -20,7 +18,7 @@ exports.VHDFOOTER = {
hidden: '',
reserved: '',
}
exports.VHDHEADER = {
export const VHDHEADER = {
cookie: 'cxsparse',
dataOffset: undefined,
tableOffset: 2048,

View File

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

View File

@@ -2,6 +2,10 @@
## **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))
- Add Tasks to Pool Dashboard (PR [#6713](https://github.com/vatesfr/xen-orchestra/pull/6713))
## **0.1.1** (2023-07-03)
- Invalidate sessionId token after logout (PR [#6480](https://github.com/vatesfr/xen-orchestra/pull/6480))

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<title>XO Lite</title>
</head>
<body>
<div id="root"></div>

View File

@@ -17,6 +17,7 @@
"@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",
@@ -25,6 +26,7 @@
"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",
@@ -57,7 +59,6 @@
"postcss-nested": "^6.0.0",
"typescript": "^4.9.3",
"vite": "^4.3.8",
"vite-plugin-pages": "^0.29.1",
"vue-tsc": "^1.6.5"
},
"private": true,

View File

@@ -41,8 +41,6 @@ if (link == null) {
}
link.href = favicon;
document.title = "XO Lite";
const xenApiStore = useXenApiStore();
const { pool } = usePoolStore().subscribe();
useChartTheme();

View File

@@ -24,6 +24,7 @@
</template>
<script lang="ts" setup>
import { usePageTitleStore } from "@/stores/page-title.store";
import { storeToRefs } from "pinia";
import { onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
@@ -33,6 +34,7 @@ import UiButton from "@/components/ui/UiButton.vue";
import { useXenApiStore } from "@/stores/xen-api.store";
const { t } = useI18n();
usePageTitleStore().setTitle(t("login"));
const xenApiStore = useXenApiStore();
const { isConnecting } = storeToRefs(xenApiStore);
const login = ref("root");

View File

@@ -5,7 +5,6 @@
<script lang="ts" setup>
import markdown from "@/libs/markdown";
import { useEventListener } from "@vueuse/core";
import "highlight.js/styles/github-dark.css";
import { computed, type Ref, ref } from "vue";
const rootElement = ref() as Ref<HTMLElement>;

View File

@@ -3,14 +3,13 @@
</template>
<script lang="ts" setup>
import HLJS from "highlight.js";
import { type AcceptedLanguage, highlight } from "@/libs/highlight";
import { computed } from "vue";
import "highlight.js/styles/github-dark.css";
const props = withDefaults(
defineProps<{
code?: any;
lang?: string;
lang?: AcceptedLanguage;
}>(),
{ lang: "typescript" }
);
@@ -27,7 +26,7 @@ const codeAsText = computed(() => {
});
const codeAsHtml = computed(
() => HLJS.highlight(codeAsText.value, { language: props.lang }).value
() => highlight(codeAsText.value, { language: props.lang }).value
);
</script>

View File

@@ -11,7 +11,8 @@
</template>
<script lang="ts" setup>
import { type ComputedRef, computed, inject } from "vue";
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";
@@ -20,8 +21,8 @@ defineProps<{
disabled?: boolean;
}>();
const isTabBarDisabled = inject<ComputedRef<boolean>>(
"isTabBarDisabled",
const isTabBarDisabled = inject(
IK_TAB_BAR_DISABLED,
computed(() => false)
);
</script>

View File

@@ -31,7 +31,7 @@ import { faServer } from "@fortawesome/free-solid-svg-icons";
import UiModal from "@/components/ui/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { computed, ref, watch } from "vue";
import { difference } from "lodash";
import { difference } from "lodash-es";
import { useHostStore } from "@/stores/host.store";
const { records: hosts } = useHostStore().subscribe();

View File

@@ -1,61 +0,0 @@
<template>
<div class="chart-summary">
<div>
<div class="label">{{ $t("total-used") }}</div>
<div>
{{ usedPercent }}%
<br />
{{ valueFormatter(used) }}
</div>
</div>
<div>
<div class="label">{{ $t("total-free") }}</div>
<div>
{{ freePercent }}%
<br />
{{ valueFormatter(total - used) }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { percent } from "@/libs/utils";
import { computed, type ComputedRef, inject } from "vue";
const props = defineProps<{
total: number;
used: number;
}>();
const usedPercent = computed(() => percent(props.used, props.total));
const freePercent = computed(() =>
percent(props.total - props.used, props.total)
);
const valueFormatter = inject("valueFormatter") as ComputedRef<
(value: number) => string
>;
</script>
<style lang="postcss" scoped>
.chart-summary {
font-size: 1.4rem;
font-weight: 700;
display: flex;
margin-top: 2rem;
color: var(--color-blue-scale-200);
gap: 4rem;
& > div {
display: flex;
flex: 1;
justify-content: space-between;
}
}
.label {
text-transform: uppercase;
}
</style>

View File

@@ -6,11 +6,11 @@
</template>
<script lang="ts" setup>
import UiCard from "@/components/ui/UiCard.vue";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_CHART_VALUE_FORMATTER } from "@/types/injection-keys";
import { utcFormat } from "d3-time-format";
import type { EChartsOption } from "echarts";
import { computed, provide } from "vue";
import VueCharts from "vue-echarts";
import type { LinearChartData } from "@/types/chart";
import { LineChart } from "echarts/charts";
import {
GridComponent,
@@ -20,8 +20,8 @@ import {
} from "echarts/components";
import { use } from "echarts/core";
import { CanvasRenderer } from "echarts/renderers";
import type { OptionDataValue } from "echarts/types/src/util/types";
import UiCard from "@/components/ui/UiCard.vue";
import { computed, provide } from "vue";
import VueCharts from "vue-echarts";
const Y_AXIS_MAX_VALUE = 200;
@@ -29,23 +29,23 @@ const props = defineProps<{
title?: string;
subtitle?: string;
data: LinearChartData;
valueFormatter?: (value: number) => string;
valueFormatter?: ValueFormatter;
maxValue?: number;
}>();
const valueFormatter = computed(() => {
const valueFormatter = computed<ValueFormatter>(() => {
const formatter = props.valueFormatter;
return (value: OptionDataValue | OptionDataValue[]) => {
if (formatter) {
return formatter(value as number);
return (value) => {
if (formatter === undefined) {
return value.toString();
}
return value.toString();
return formatter(value);
};
});
provide("valueFormatter", valueFormatter);
provide(IK_CHART_VALUE_FORMATTER, valueFormatter);
use([
CanvasRenderer,
@@ -65,7 +65,7 @@ const option = computed<EChartsOption>(() => ({
data: props.data.map((series) => series.label),
},
tooltip: {
valueFormatter: valueFormatter.value,
valueFormatter: (v) => valueFormatter.value(v as number),
},
xAxis: {
type: "time",

View File

@@ -273,13 +273,9 @@ const documentation = ref();
const route = useRoute();
const mdPaths = import.meta.glob("../../stories/*.md", { as: "raw" });
if (route.meta.storyMdPath !== undefined && route.meta.storyMdPath in mdPaths) {
mdPaths[route.meta.storyMdPath]().then((md) => {
documentation.value = md;
});
}
route.meta.storyMdLoader?.().then((md) => {
documentation.value = md;
});
const applyPreset = (preset: {
props?: Record<string, any>;

View File

@@ -29,6 +29,13 @@
>
<th class="name">
{{ param.getFullName() }}
<sup
v-if="param.isVModel()"
v-tooltip="`[Model] Can be used with ${param.getVModelDirective()}`"
class="v-model-indicator"
>
<UiIcon :icon="faRepeat" />
</sup>
</th>
<td>
<CodeHighlight :code="param.getTypeLabel()" />
@@ -86,8 +93,9 @@ import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import useSortedCollection from "@/composables/sorted-collection.composable";
import { vTooltip } from "@/directives/tooltip.directive";
import type { PropParam } from "@/libs/story/story-param";
import { faClose } from "@fortawesome/free-solid-svg-icons";
import { faClose, faRepeat } from "@fortawesome/free-solid-svg-icons";
import { useVModel } from "@vueuse/core";
import { toRef } from "vue";
@@ -173,4 +181,8 @@ const {
color: var(--color-green-infra-base);
}
}
.v-model-indicator {
color: var(--color-green-infra-base);
}
</style>

View File

@@ -81,4 +81,10 @@ const model = useVModel(props, "modelValue", emit);
display: flex;
gap: 1rem;
}
.form-select,
.form-input,
.form-json {
font-size: 1.4rem;
}
</style>

View File

@@ -19,7 +19,12 @@
</template>
<script lang="ts" setup>
import { type HTMLAttributes, computed, inject, ref } from "vue";
import {
IK_FORM_HAS_LABEL,
IK_FORM_LABEL_DISABLED,
IK_CHECKBOX_TYPE,
} from "@/types/injection-keys";
import { type HTMLAttributes, computed, inject } from "vue";
import { faCheck, faCircle, faMinus } from "@fortawesome/free-solid-svg-icons";
import { useVModel } from "@vueuse/core";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
@@ -37,9 +42,15 @@ const emit = defineEmits<{
}>();
const value = useVModel(props, "modelValue", emit);
const type = inject<"checkbox" | "radio" | "toggle">("inputType", "checkbox");
const hasLabel = inject("hasLabel", false);
const isLabelDisabled = inject("isLabelDisabled", ref(false));
const type = inject(IK_CHECKBOX_TYPE, "checkbox");
const hasLabel = inject(
IK_FORM_HAS_LABEL,
computed(() => false)
);
const isLabelDisabled = inject(
IK_FORM_LABEL_DISABLED,
computed(() => false)
);
const icon = computed(() => {
if (type !== "checkbox") {
return faCircle;

View File

@@ -1,13 +1,14 @@
<template>
<span :class="wrapperClass" v-bind="wrapperAttrs">
<span :class="wrapperClass" class="container" v-bind="wrapperAttrs">
<template v-if="inputType === 'select'">
<select
:id="id"
ref="inputElement"
v-model="value"
:class="inputClass"
:disabled="disabled || isLabelDisabled"
:required="required"
class="select"
ref="inputElement"
v-bind="$attrs"
>
<slot />
@@ -18,6 +19,7 @@
</template>
<textarea
v-else-if="inputType === 'textarea'"
:id="id"
ref="textarea"
v-model="value"
:class="inputClass"
@@ -28,12 +30,13 @@
/>
<input
v-else
:id="id"
ref="inputElement"
v-model="value"
:class="inputClass"
:disabled="disabled || isLabelDisabled"
:required="required"
class="input"
ref="inputElement"
v-bind="$attrs"
/>
<span v-if="before !== undefined" class="before">
@@ -48,24 +51,31 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { Color } from "@/types";
import {
IK_FORM_INPUT_COLOR,
IK_FORM_LABEL_DISABLED,
IK_INPUT_ID,
IK_INPUT_TYPE,
} from "@/types/injection-keys";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
import { useTextareaAutosize, useVModel } from "@vueuse/core";
import {
type HTMLAttributes,
computed,
type HTMLAttributes,
inject,
nextTick,
ref,
watch,
} from "vue";
import type { Color } from "@/types";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
import { useTextareaAutosize, useVModel } from "@vueuse/core";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
defineOptions({ inheritAttrs: false });
const props = withDefaults(
defineProps<{
id?: string;
modelValue?: any;
color?: Color;
before?: IconDefinition | string;
@@ -90,17 +100,20 @@ const value = useVModel(props, "modelValue", emit);
const isEmpty = computed(
() => props.modelValue == null || String(props.modelValue).trim() === ""
);
const inputType = inject("inputType", "input");
const isLabelDisabled = inject("isLabelDisabled", ref(false));
const inputType = inject(IK_INPUT_TYPE, "input");
const isLabelDisabled = inject(
IK_FORM_LABEL_DISABLED,
computed(() => false)
);
const parentColor = inject(
"color",
IK_FORM_INPUT_COLOR,
computed(() => undefined)
);
const wrapperClass = computed(() => [
`form-${inputType}`,
{
disabled: props.disabled || isLabelDisabled.value,
disabled: props.disabled === true || isLabelDisabled.value,
empty: isEmpty.value,
},
]);
@@ -114,6 +127,10 @@ const inputClass = computed(() => [
},
]);
const parentId = inject(IK_INPUT_ID, undefined);
const id = computed(() => props.id ?? parentId?.value);
const { textarea, triggerResize } = useTextareaAutosize();
watch(value, () => nextTick(() => triggerResize()), {
@@ -128,11 +145,16 @@ defineExpose({
</script>
<style lang="postcss" scoped>
.container {
font-size: 2rem;
}
.form-input,
.form-select,
.form-textarea {
display: inline-grid;
display: grid;
align-items: stretch;
max-width: 30em;
--before-width: v-bind('beforeWidth || "1.75em"');
--after-width: v-bind('afterWidth || "1.625em"');
@@ -167,11 +189,11 @@ defineExpose({
.select {
font-size: 1em;
width: 100%;
height: 2em;
height: 3em;
margin: 0;
color: var(--text-color);
border: 0.0625em solid var(--border-color);
border-radius: 0.5em;
border: 0.05em solid var(--border-color);
border-radius: 0.4em;
outline: none;
background-color: var(--background-color);
box-shadow: var(--shadow-100);

View File

@@ -1,96 +1,148 @@
<template>
<div class="wrapper">
<label
v-if="$slots.label"
class="form-label"
:class="{ disabled, ...formInputWrapperClass }"
<div class="form-input-wrapper">
<div
v-if="label !== undefined || learnMoreUrl !== undefined"
class="label-container"
>
<label :for="id" class="label">
<UiIcon :icon="icon" />
{{ label }}
</label>
<a
v-if="learnMoreUrl !== undefined"
:href="learnMoreUrl"
class="learn-more-url"
target="_blank"
>
<UiIcon :icon="faInfoCircle" />
<span>{{ $t("learn-more") }}</span>
</a>
</div>
<div class="input-container">
<slot />
</label>
<slot />
<p v-if="hasError || hasWarning" :class="formInputWrapperClass">
<UiIcon :icon="faCircleExclamation" v-if="hasError" />{{
error ?? warning
}}
</p>
</div>
<div class="messages-container">
<div v-if="warning !== undefined" class="warning">
{{ warning }}
</div>
<div v-if="error !== undefined" class="error">
{{ error }}
</div>
<div v-if="help !== undefined" class="help">
{{ help }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, provide, useSlots } from "vue";
import { faCircleExclamation } from "@fortawesome/free-solid-svg-icons";
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,
} from "@/types/injection-keys";
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();
const props = defineProps<{
disabled?: boolean;
error?: string;
label?: string;
id?: string;
icon?: IconDefinition;
learnMoreUrl?: string;
warning?: string;
error?: string;
help?: string;
disabled?: boolean;
}>();
provide("hasLabel", slots.label !== undefined);
provide(
"isLabelDisabled",
computed(() => props.disabled)
);
const id = computed(() => props.id ?? uniqueId("form-input-"));
provide(IK_INPUT_ID, id);
const hasError = computed(
() => props.error !== undefined && props.error.trim() !== ""
);
const hasWarning = computed(
() => props.warning !== undefined && props.warning.trim() !== ""
const color = computed<Color | undefined>(() => {
if (props.error !== undefined && props.error.trim() !== "") {
return "error";
}
if (props.warning !== undefined && props.warning.trim() !== "") {
return "warning";
}
return undefined;
});
provide(IK_FORM_INPUT_COLOR, color);
provide(
IK_FORM_HAS_LABEL,
computed(() => slots.label !== undefined)
);
provide(
"color",
computed(() =>
hasError.value ? "error" : hasWarning.value ? "warning" : undefined
)
IK_FORM_LABEL_DISABLED,
computed(() => props.disabled ?? false)
);
const formInputWrapperClass = computed(() => ({
error: hasError.value,
warning: !hasError.value && hasWarning.value,
}));
</script>
<style lang="postcss" scoped>
.wrapper {
.form-input-wrapper {
max-width: 60rem;
}
.label-container {
display: flex;
flex-direction: column;
}
.wrapper :deep(.input) {
margin-bottom: 1rem;
}
.form-label {
font-size: 1.6rem;
display: inline-flex;
justify-content: space-between;
align-items: center;
gap: 0.625em;
}
&.disabled {
cursor: not-allowed;
color: var(--color-blue-scale-300);
.label {
text-transform: uppercase;
font-weight: 700;
color: var(--color-blue-scale-100);
font-size: 1.4rem;
padding: 1rem 0;
}
.messages-container {
margin-top: 1rem;
}
.warning,
.error,
.help,
.learn-more-url {
font-size: 1.3rem;
line-height: 150%;
margin: 0.5rem 0;
}
.learn-more-url {
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
color: var(--color-extra-blue-base);
& > span {
text-decoration: underline;
}
}
p.error,
p.warning {
font-size: 0.65em;
margin-bottom: 1rem;
}
.error {
color: var(--color-red-vates-base);
}
.warning {
color: var(--color-orange-world-base);
}
p svg {
margin-right: 0.4em;
.error {
color: var(--color-red-vates-base);
}
.help {
color: var(--color-blue-scale-300);
}
</style>

View File

@@ -3,8 +3,9 @@
</template>
<script lang="ts" setup>
import { provide } from "vue";
import FormCheckbox from "@/components/form/FormCheckbox.vue";
import { IK_CHECKBOX_TYPE } from "@/types/injection-keys";
import { provide } from "vue";
provide("inputType", "radio");
provide(IK_CHECKBOX_TYPE, "radio");
</script>

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