Compare commits

..

1 Commits

Author SHA1 Message Date
Julien Fontanet
42cc6d4dd6 WiP 2024-02-12 13:14:26 +01:00
31 changed files with 1348 additions and 1522 deletions

View File

@@ -65,11 +65,10 @@ module.exports = {
typescript: true,
'eslint-import-resolver-custom-alias': {
alias: {
'@core': '../web-core/lib',
'@': './src',
},
extensions: ['.ts'],
packages: ['@xen-orchestra/lite', '@xen-orchestra/web'],
packages: ['@xen-orchestra/lite'],
},
},
},

View File

@@ -160,10 +160,10 @@ export class ImportVmBackup {
// update the stream with the negative vhd stream
stream = await negativeVhd.stream()
vdis[vdiRef].baseVdi = snapshotCandidate
} catch (error) {
} catch (err) {
// can be a broken VHD chain, a vhd chain with a key backup, ....
// not an irrecuperable error, don't dispose parentVhd, and fallback to full restore
warn(`can't use differential restore`, { error })
warn(`can't use differential restore`, err)
disposableDescendants?.dispose()
}
}

View File

@@ -143,10 +143,8 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
let metadataContent = await this._isAlreadyTransferred(timestamp)
if (metadataContent !== undefined) {
// skip backup while being vigilant to not stuck the forked stream
// @todo : should skip backup while being vigilant to not stuck the forked stream
Task.info('This backup has already been transfered')
Object.values(deltaExport.streams).forEach(stream => stream.destroy())
return { size: 0 }
}
const basename = formatFilenameDate(timestamp)

View File

@@ -113,13 +113,13 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
)
}
async _isAlreadyTransferred(timestamp) {
_isAlreadyTransferred(timestamp) {
const vmUuid = this._vmUuid
const adapter = this._adapter
const backupDir = getVmBackupDir(vmUuid)
try {
const actualMetadata = JSON.parse(
await adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
)
return actualMetadata
} catch (error) {}

View File

@@ -20,7 +20,5 @@ export function split(path) {
return parts
}
// paths are made absolute otherwise fs.relative() would resolve them against working directory
export const relativeFromFile = (file, path) => relative(dirname(normalize(file)), normalize(path))
export const relativeFromFile = (file, path) => relative(dirname(file), path)
export const resolveFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)

View File

@@ -1,17 +0,0 @@
import { describe, it } from 'test'
import { strict as assert } from 'assert'
import { relativeFromFile } from './path.js'
describe('relativeFromFile()', function () {
for (const [title, args] of Object.entries({
'file absolute and path absolute': ['/foo/bar/file.vhd', '/foo/baz/path.vhd'],
'file relative and path absolute': ['foo/bar/file.vhd', '/foo/baz/path.vhd'],
'file absolute and path relative': ['/foo/bar/file.vhd', 'foo/baz/path.vhd'],
'file relative and path relative': ['foo/bar/file.vhd', 'foo/baz/path.vhd'],
})) {
it('works with ' + title, function () {
assert.equal(relativeFromFile(...args), '../baz/path.vhd')
})
}
})

View File

@@ -54,10 +54,10 @@ async function handleExistingFile(root, indexPath, path) {
await indexFile(fullPath, indexPath)
}
}
} catch (error) {
if (error.code !== 'EEXIST') {
} catch (err) {
if (err.code !== 'EEXIST') {
// there can be a symbolic link in the tree
warn('handleExistingFile', { error })
warn('handleExistingFile', err)
}
}
}
@@ -106,7 +106,7 @@ export async function watchRemote(remoteId, { root, immutabilityDuration, rebuil
await File.liftImmutability(settingPath)
} catch (error) {
// file may not exists, and it's not really a problem
info('lifting immutability on current settings', { error })
info('lifting immutability on current settings', error)
}
await fs.writeFile(
settingPath,

View File

@@ -1,15 +1,13 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "../web-core/lib/**/*", "../web-core/lib/**/*.vue"],
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"noEmit": true,
"baseUrl": ".",
"rootDir": "..",
"paths": {
"@/*": ["./src/*"],
"@core/*": ["../web-core/lib/*"]
"@/*": ["./src/*"]
}
}
}

View File

@@ -23,7 +23,6 @@ export default defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@core': fileURLToPath(new URL('../web-core/lib', import.meta.url)),
},
},

View File

@@ -27,16 +27,6 @@ log.error('could not join server', {
})
```
A logging method has the following signature:
```ts
interface LoggingMethod {
(error): void
(message: string, data?: { error?: Error; [property: string]: any }): void
}
```
### Consumer
Then, at application level, configure the logs are handled:

View File

@@ -45,16 +45,6 @@ log.error('could not join server', {
})
```
A logging method has the following signature:
```ts
interface LoggingMethod {
(error): void
(message: string, data?: { error?: Error; [property: string]: any }): void
}
```
### Consumer
Then, at application level, configure the logs are handled:

View File

@@ -10,8 +10,7 @@
}
},
"devDependencies": {
"vue": "^3.4.13",
"@vue/tsconfig": "^0.5.1"
"vue": "^3.4.13"
},
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/web-core",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
@@ -26,6 +25,6 @@
},
"license": "AGPL-3.0-or-later",
"engines": {
"node": ">=18"
"node": ">=8.10"
}
}

View File

@@ -1,12 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "lib/**/*", "lib/**/*.vue"],
"exclude": ["lib/**/__tests__/*"],
"compilerOptions": {
"noEmit": true,
"baseUrl": ".",
"paths": {
"@core/*": ["./lib/*"]
}
}
}

View File

@@ -1,22 +1,13 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [
"env.d.ts",
"typed-router.d.ts",
"src/**/*",
"src/**/*.vue",
"../web-core/lib/**/*",
"../web-core/lib/**/*.vue"
],
"include": ["env.d.ts", "typed-router.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"noEmit": true,
"baseUrl": ".",
"rootDir": "..",
"paths": {
"@/*": ["./src/*"],
"@core/*": ["../web-core/lib/*"]
"@/*": ["./src/*"]
}
}
}

View File

@@ -11,7 +11,6 @@ export default defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@core': fileURLToPath(new URL('../web-core/lib', import.meta.url)),
},
},
})

View File

@@ -21,23 +21,12 @@ export default class Vif {
MAC = '',
} = {}
) {
if (device === undefined) {
const allowedDevices = await this.call('VM.get_allowed_VIF_devices', VM)
if (allowedDevices.length === 0) {
const error = new Error('could not find an allowed VIF device')
error.poolUuid = this.pool.uuid
error.vmRef = VM
throw error
}
device = allowedDevices[0]
}
const [powerState, ...rest] = await Promise.all([
this.getField('VM', VM, 'power_state'),
MTU ?? this.getField('network', network, 'MTU'),
device ?? (await this.call('VM.get_allowed_VIF_devices', VM))[0],
MTU ?? (await this.getField('network', network, 'MTU')),
])
;[MTU] = rest
;[device, MTU] = rest
const vifRef = await this.call('VIF.create', {
currently_attached: powerState === 'Suspended' ? currently_attached : undefined,

View File

@@ -399,6 +399,16 @@ class Vm {
return ref
}
async createFull($defer, { clone = true, boot = false, name_label, name_description, template: templateRef }) {
// Clones the template.
const vmRef = await (
clone
? this.callAsync('VM.clone', templateRef, name_label)
: this.callAsync('VM.copy', templateRef, name_label, '')
).then(extractOpaqueRef)
$defer.onFailure(() => this.VM_destroy(vmRef))
}
async destroy(
vmRef,
{ deleteDisks = true, force = false, bypassBlockedOperation = force, forceDeleteDefaultTemplate = force } = {}
@@ -699,6 +709,7 @@ export default Vm
decorateClass(Vm, {
checkpoint: defer,
create: defer,
createFull: defer,
export: defer,
snapshot: defer,
})

View File

@@ -8,7 +8,6 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- Disable search engine indexing via a `robots.txt`
- [Stats] Support format used by XAPI 23.31
### Bug fixes
@@ -16,7 +15,6 @@
- [Settings/XO Config] Sort backups from newest to oldest
- [Plugins/audit] Don't log `tag.getAllConfigured` calls
- [Remotes] Correctly clear error when the remote is tested with success
### Packages to release
@@ -34,11 +32,7 @@
<!--packages-start-->
- @xen-orchestra/backups patch
- @xen-orchestra/fs patch
- @xen-orchestra/xapi patch
- vhd-lib patch
- xo-server minor
- xo-server patch
- xo-server-audit patch
- xo-web patch

View File

@@ -1,6 +1,4 @@
{
"name": "xen-orchestra",
"version": "0.0.0",
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/eslint-parser": "^7.13.8",
@@ -96,7 +94,7 @@
},
"private": true,
"scripts": {
"build": "TURBO_TELEMETRY_DISABLED=1 turbo run build --scope xo-server --scope xo-server-'*' --scope xo-web",
"build": "turbo run build --scope xo-server --scope xo-server-'*' --scope xo-web",
"build:xo-lite": "turbo run build --scope @xen-orchestra/lite",
"clean": "scripts/run-script.js --parallel clean",
"dev": "scripts/run-script.js --parallel --concurrency 0 --verbose dev",

View File

@@ -1,6 +1,6 @@
'use strict'
const { relativeFromFile } = require('@xen-orchestra/fs/path')
const { dirname, relative } = require('path')
const { openVhd } = require('./openVhd')
const { DISK_TYPES } = require('./_constants')
@@ -21,7 +21,7 @@ module.exports = async function chain(parentHandler, parentPath, childHandler, c
}
await childVhd.readBlockAllocationTable()
const parentName = relativeFromFile(childPath, parentPath)
const parentName = relative(dirname(childPath), parentPath)
header.parentUuid = parentVhd.footer.uuid
header.parentUnicodeName = parentName
await childVhd.setUniqueParentLocator(parentName)

View File

@@ -27,7 +27,7 @@ async function sendToNagios(app, jobName, vmBackupInfo) {
jobName
)
} catch (error) {
warn('sendToNagios:', { error })
warn('sendToNagios:', error)
}
}

View File

@@ -45,17 +45,7 @@ const RRD_POINTS_PER_STEP = {
// Utils
// -------------------------------------------------------------------
function parseNumber(value) {
// Starting from XAPI 23.31, numbers in the JSON payload are encoded as
// strings to support NaN, Infinity and -Infinity
if (typeof value === 'string') {
const asNumber = +value
if (isNaN(asNumber) && value !== 'NaN') {
throw new Error('cannot parse number: ' + value)
}
value = asNumber
}
function convertNanToNull(value) {
return isNaN(value) ? null : value
}
@@ -68,7 +58,7 @@ async function getServerTimestamp(xapi, hostRef) {
// -------------------------------------------------------------------
const computeValues = (dataRow, legendIndex, transformValue = identity) =>
map(dataRow, ({ values }) => transformValue(parseNumber(values[legendIndex])))
map(dataRow, ({ values }) => transformValue(convertNanToNull(values[legendIndex])))
const combineStats = (stats, path, combineValues) => zipWith(...map(stats, path), (...values) => combineValues(values))
@@ -255,15 +245,7 @@ export default class XapiStats {
start: timestamp,
},
})
.then(response => response.text())
.then(data => {
try {
// starting from XAPI 23.31, the response is valid JSON
return JSON.parse(data)
} catch (_) {
return JSON5.parse(data)
}
})
.then(response => response.text().then(JSON5.parse))
.catch(err => {
delete this.#hostCache[hostUuid][step]
throw err
@@ -317,7 +299,7 @@ export default class XapiStats {
// To avoid crossing over the boundary, we ask for one less step
const optimumTimestamp = currentTimeStamp - maxDuration + step
const json = await this._getJson(xapi, host, optimumTimestamp, step)
const actualStep = parseNumber(json.meta.step)
const actualStep = json.meta.step
if (actualStep !== step) {
throw new FaultyGranularity(`Unable to get the true granularity: ${actualStep}`)
@@ -344,10 +326,9 @@ export default class XapiStats {
return
}
const endTimestamp = parseNumber(json.meta.end)
if (stepStats === undefined || stepStats.endTimestamp !== endTimestamp) {
if (stepStats === undefined || stepStats.endTimestamp !== json.meta.end) {
stepStats = {
endTimestamp,
endTimestamp: json.meta.end,
interval: actualStep,
stats: {},
}

View File

@@ -32,6 +32,9 @@ const methods = {
name_label, // eslint-disable-line camelcase
nameLabel = name_label, // eslint-disable-line camelcase
cloudConfig,
networkConfig,
clone = true,
installRepository = undefined,
vdis = undefined,
@@ -217,6 +220,35 @@ const methods = {
await this.createVgpu(vm, gpuGroup, vgpuType)
}
// create cloud config drive
let cloudConfigVdiUuid
if (params.cloudConfig != null) {
// Find the SR of the first VDI.
let srId
forEach(vm.$VBDs, vbdId => {
const vbd = this.getObject(vbdId)
const vdiId = vbd.VDI
if (!vbd.is_cd_drive && vdiId !== undefined) {
srId = this.getObject(vdiId).$SR
return false
}
})
try {
cloudConfigVdiUuid = params.coreOs
? await xapi.createCoreOsCloudInitConfigDrive(vm.id, srId, params.cloudConfig)
: await xapi.createCloudInitConfigDrive(vm.id, srId, params.cloudConfig, params.networkConfig)
} catch (error) {
log.warn('vm.create', { vmId: vm.id, srId, error })
throw error
}
}
if (params.createVtpm) {
const vtpmRef = await xapi.VTPM_create({ VM: xapiVm.$ref })
$defer.onFailure(() => xapi.call('VTPM.destroy', vtpmRef))
}
// wait for the record with all the VBDs and VIFs
return this.barrier(vm.$ref)
},

View File

@@ -297,6 +297,15 @@ export default class RestApi {
auto_poweron: { type: 'boolean', optional: true },
boot: { type: 'boolean', default: false },
clone: { type: 'boolean', default: true },
cloud_init: {
type: 'object',
default: {},
properties: {
cloud_config: { type: 'string', optional: true },
destroy_after_boot: { type: 'boolean', default: false },
network_config: { type: 'string', optional: true },
},
},
install: {
type: 'object',
optional: true,

View File

@@ -75,7 +75,7 @@ export const reportOnSupportPanel = async ({ files = [], formatMessage = identit
ADDITIONAL_FILES.map(({ fetch, name }) =>
timeout.call(fetch(), ADDITIONAL_FILES_FETCH_TIMEOUT).then(
file => formData.append('attachments', createBlobFromString(file), name),
error => logger.warn(`cannot get ${name}`, { error })
error => logger.warn(`cannot get ${name}`, error)
)
)
)

View File

@@ -3621,9 +3621,6 @@ export const unlockXosan = (licenseId, srId) => _call('xosan.unlock', { licenseI
export const bindLicense = (licenseId, boundObjectId) => _call('xoa.licenses.bind', { licenseId, boundObjectId })
export const rebindObjectLicense = (boundObjectId, licenseId, productId) =>
_call('xoa.licenses.rebindObject', { boundObjectId, licenseId, productId })
export const bindXcpngLicense = (licenseId, boundObjectId) =>
bindLicense(licenseId, boundObjectId)::tap(subscribeXcpngLicenses.forceRefresh)

View File

@@ -33,7 +33,7 @@ const formatError = error => (typeof error === 'string' ? error : JSON.stringify
const _changeUrlElement = (value, { remote, element }) =>
editRemote(remote, {
url: format({ ...parse(remote.url), [element]: value === null ? undefined : value }),
url: format({ ...remote, [element]: value === null ? undefined : value }),
})
const _showError = remote => alert(_('remoteConnectionFailed'), <pre>{formatError(remote.error)}</pre>)
const _editRemoteName = (name, { remote }) => editRemote(remote, { name })

View File

@@ -1,53 +0,0 @@
import _ from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import React from 'react'
import SelectLicense from 'select-license'
import { bindLicense, rebindObjectLicense } from 'xo'
import BulkIcons from '../../../common/bulk-icons'
export default class LicenseForm extends Component {
state = {
licenseId: 'none',
}
bind = async () => {
const { userData, item, itemUuidPath = 'uuid', license } = this.props
if (license !== undefined) {
await rebindObjectLicense(item[itemUuidPath], this.state.licenseId, license.productId)
} else {
await bindLicense(this.state.licenseId, item[itemUuidPath])
}
userData.updateLicenses()
this.setState({ licenseId: 'none' })
}
render() {
const { license } = this.props
return (
<div className='d-flex'>
<div>
{license !== undefined && license.id.slice(-4)}
<BulkIcons alerts={this.props.alerts} />
</div>
<form className='form-inline ml-1'>
<SelectLicense
onChange={this.linkState('licenseId')}
productType={this.props.productType}
value={this.state.licenseId}
/>
<ActionButton
btnStyle='primary'
className='ml-1'
disabled={this.state.licenseId === 'none'}
handler={this.bind}
icon='connect'
>
{_(license === undefined ? 'bindLicense' : 'update')}
</ActionButton>
</form>
</div>
)
}
}

View File

@@ -1,68 +1,74 @@
import _ from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import decorate from 'apply-decorators'
import Icon from 'icon'
import React from 'react'
import SelectLicense from 'select-license'
import SortedTable from 'sorted-table'
import Tooltip from 'tooltip'
import { addSubscriptions } from 'utils'
import groupBy from 'lodash/groupBy.js'
import { createSelector } from 'selectors'
import { injectState, provideState } from 'reaclette'
import { Proxy, Vm } from 'render-xo-item'
import { subscribeProxies } from 'xo'
import LicenseForm from './license-form'
import { subscribeProxies, bindLicense } from 'xo'
class ProxyLicensesForm extends Component {
getAlerts = createSelector(
() => this.props.item,
() => this.props.userData,
(proxy, userData) => {
const alerts = []
const licenses = userData.licensesByVmUuid[proxy.vmUuid]
state = {
licenseId: 'none',
}
if (proxy.vmUuid === undefined) {
alerts.push({
level: 'danger',
render: (
<p>
{_('proxyUnknownVm')} <a href='https://xen-orchestra.com/'>{_('contactUs')}</a>
</p>
),
})
}
onChangeLicense = event => {
this.setState({ licenseId: event.target.value })
}
// Proxy bound to multiple licenses
if (licenses?.length > 1) {
alerts.push({
level: 'danger',
render: (
<p>
{_('proxyMultipleLicenses')}
<br />
{licenses.map(license => license.id.slice(-4)).join(',')}
</p>
),
})
}
return alerts
}
)
bind = () => {
const { item, userData } = this.props
return bindLicense(this.state.licenseId, item.vmUuid).then(userData.updateLicenses)
}
render() {
const alerts = this.getAlerts()
const { item, userData } = this.props
const { licenseId } = this.state
const licenses = userData.licensesByVmUuid[item.vmUuid]
if (item.vmUuid === undefined) {
return (
<span className='text-danger'>
{_('proxyUnknownVm')} <a href='https://xen-orchestra.com/'>{_('contactUs')}</a>
</span>
)
}
// Proxy bound to multiple licenses
if (licenses?.length > 1) {
return (
<div>
<span>{licenses.map(license => license.id.slice(-4)).join(',')}</span>{' '}
<Tooltip content={_('proxyMultipleLicenses')}>
<Icon color='text-danger' icon='alarm' />
</Tooltip>
</div>
)
}
const license = licenses?.[0]
return (
<LicenseForm
alerts={alerts}
item={item}
itemUuidPath='vmUuid'
license={license}
productType='xoproxy'
userData={userData}
/>
return license !== undefined ? (
<span>{license.id.slice(-4)}</span>
) : (
<form className='form-inline'>
<SelectLicense onChange={this.onChangeLicense} productType='xoproxy' />
<ActionButton
btnStyle='primary'
className='ml-1'
disabled={licenseId === 'none'}
handler={this.bind}
handlerParam={licenseId}
icon='connect'
>
{_('bindLicense')}
</ActionButton>
</form>
)
}
}

View File

@@ -1,17 +1,30 @@
import _ from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import decorate from 'apply-decorators'
import Icon from 'icon'
import React from 'react'
import SelectLicense from 'select-license'
import SortedTable from 'sorted-table'
import { bindLicense } from 'xo'
import { connectStore } from 'utils'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { groupBy } from 'lodash'
import { injectState, provideState } from 'reaclette'
import { Pool, Sr } from 'render-xo-item'
import LicenseForm from './license-form'
import BulkIcons from '../../../common/bulk-icons'
class XostorLicensesForm extends Component {
state = {
licenseId: 'none',
}
bind = () => {
const { item, userData } = this.props
return bindLicense(this.state.licenseId, item.uuid).then(userData.updateLicenses)
}
getAlerts = createSelector(
() => this.props.item,
() => this.props.userData,
@@ -46,12 +59,39 @@ class XostorLicensesForm extends Component {
render() {
const alerts = this.getAlerts()
if (alerts.length > 0) {
return <BulkIcons alerts={alerts} />
}
const { item, userData } = this.props
const { licenseId } = this.state
const licenses = userData.licensesByXostorUuid[item.id]
const license = licenses?.[0]
return <LicenseForm alerts={alerts} item={item} license={license} productType='xostor' userData={userData} />
return license !== undefined ? (
<span>{license?.id.slice(-4)}</span>
) : (
<div>
{license !== undefined && (
<div className='text-danger mb-1'>
<Icon icon='alarm' /> {_('licenseHasExpired')}
</div>
)}
<form className='form-inline'>
<SelectLicense onChange={this.linkState('licenseId')} productType='xostor' />
<ActionButton
btnStyle='primary'
className='ml-1'
disabled={licenseId === 'none'}
handler={this.bind}
handlerParam={licenseId}
icon='connect'
>
{_('bindLicense')}
</ActionButton>
</form>
</div>
)
}
}

2448
yarn.lock

File diff suppressed because it is too large Load Diff