Compare commits

..

47 Commits

Author SHA1 Message Date
Florent BEAUCHAMP
3e1227c710 feat(nbd): implement server export from/to stream 2023-08-08 14:34:03 +02:00
Florent BEAUCHAMP
ddc73fb836 feat(nbd): server implementation working, no encryption 2023-08-08 11:21:39 +02:00
Florent BEAUCHAMP
a13fda5fe9 fix(backups/_MixinXapiWriter): typo _heathCheckSr → _healthCheckSr (#6969)
Fix `TypeError: Cannot read properties of undefined (reading 'uuid') at #isAlreadyOnHealthCheckSr`
2023-08-08 09:48:53 +02:00
Florent BEAUCHAMP
66bee59774 fix(xen-api/getResource): don't fail silently when HTTP request fails without response (#6970)
Seen while investigating zammad#16309
2023-08-08 09:39:18 +02:00
Julien Fontanet
685400bbf8 fix(xo-server): fix get-stream@3 usage
Fixes #6971

Introduced by 3dca7f2a7
2023-08-08 08:05:38 +02:00
Julien Fontanet
5bef8fc411 fix(lite): disable linting because it's broken
Introduced by 3dca7f2a7
2023-08-05 17:05:02 +02:00
Julien Fontanet
aa7ff1449a fix(lite): adapt ESLint config to prettier@3
Introduced by 3dca7f2a7
2023-08-04 22:09:55 +02:00
Julien Fontanet
3dca7f2a71 chore: update deps 2023-08-03 17:56:24 +02:00
Julien Fontanet
3dc2f649f6 chore: format with Prettier 2023-08-03 17:56:24 +02:00
Julien Fontanet
9eb537c2f9 chore: update dev deps 2023-08-03 17:56:24 +02:00
Thierry Goettelmann
dfd5f6882f feat(lite): enhance typings for improved type safety (#6949) 2023-08-03 11:33:29 +02:00
Julien Fontanet
7214016338 fix(xo-server/_authenticateUser): don't use registerUser()
Introduced by 99605bf18
2023-08-03 10:25:15 +02:00
Julien Fontanet
606e3c4ce5 docs(xo-server-test-plugin): explain configurationPresets 2023-08-03 10:21:15 +02:00
Julien Fontanet
fb04d3d25d docs(xo-server-test-plugin): show title/description for settings 2023-08-03 10:20:51 +02:00
Julien Fontanet
db8c042131 fix(xo-web/plugins): merge preset with existing config
Instead of replacing it.
2023-08-03 10:14:07 +02:00
Julien Fontanet
fd9005fba8 fix(xo-web/plugins): don't disable presets when config not edited 2023-08-03 10:12:52 +02:00
Julien Fontanet
2d25413b8d fix(xo-server-auth-ldap): mark userIdAttribute as required
It can no longer be ommited since 99605bf18
2023-08-03 09:56:33 +02:00
Julien Fontanet
035679800a chore(xo-server-auth-ldap): defaults are merged automatically by xo-server
Related to 8c7d25424
2023-08-03 09:53:01 +02:00
Thierry Goettelmann
abd0a3035a feat(lite/component): created UiResources + UiResource (#6932) 2023-08-01 11:18:10 +02:00
Julien Fontanet
d307730c68 feat: release 5.85.0 2023-07-31 17:06:25 +02:00
Julien Fontanet
1b44de4958 feat(xo-server): 5.120.2 2023-07-31 16:52:03 +02:00
Julien Fontanet
ec78a1ce8b feat(xo-web): 5.122.2 2023-07-31 16:32:42 +02:00
Julien Fontanet
19c82ab30d feat(xo-server): 5.120.1 2023-07-31 16:32:41 +02:00
Julien Fontanet
9986f3fb18 fix(xo-web/removeUserAuthProvider): notify on error
Introduced by 52cf2d151
2023-07-31 16:29:09 +02:00
Julien Fontanet
d24e9c093d fix(xo-server/updaterUser): fix current user auth protection
Introduced by 2d52aee95
2023-07-31 16:28:16 +02:00
Julien Fontanet
70c8b24fac feat(xo-web): 5.122.1 2023-07-31 15:58:15 +02:00
Julien Fontanet
9c9c11104b feat(xo-server-auth-google): 0.3.0 2023-07-31 15:58:05 +02:00
Julien Fontanet
cba90b27f4 feat(xo-server-auth-github): 0.3.0 2023-07-31 15:57:44 +02:00
Julien Fontanet
46cbced570 feat(xo-server): 5.120.0 2023-07-31 15:56:48 +02:00
Julien Fontanet
52cf2d1514 feat(xo-web/settings/users): auth providers can be removed 2023-07-31 15:48:49 +02:00
Julien Fontanet
e51351be8d feat(xo-server/api): user.removeAuthProvider 2023-07-31 15:48:49 +02:00
Julien Fontanet
2a42e0ff94 feat(xo-web/users): display users auth providers
Related to zammad#16318
2023-07-31 15:48:49 +02:00
Julien Fontanet
3a824a2bfc fix(xo-server/updateUser): check password xor auth providers 2023-07-31 15:48:49 +02:00
Julien Fontanet
fc1c809a18 fix(xo-server): remove password when sign in with provider
Password can no longer be used/edited when an auth provider is registered.

For security concerns, this useless password should be removed from the database.
2023-07-31 15:48:49 +02:00
Julien Fontanet
221cd40199 fix(xo-server/updateUser): can remove password 2023-07-31 15:48:49 +02:00
Julien Fontanet
aca19d9a81 fix(xo-server): user pass disabled when associated auth providers 2023-07-31 15:48:49 +02:00
Julien Fontanet
0601bbe18d fix(xo-server/recover-account): remove all auth providers 2023-07-31 15:48:49 +02:00
Julien Fontanet
2d52aee952 fix(xo-server/updateUser): can remove all auth providers with null 2023-07-31 15:48:49 +02:00
Julien Fontanet
99605bf185 feat(xo-server/registerUser): completely disable 2023-07-31 15:48:49 +02:00
Julien Fontanet
91b19d9bc4 feat(xo-server-auth-google): use registerUser2 2023-07-31 15:48:49 +02:00
Julien Fontanet
562401ebe4 feat(xo-server-auth-github): use registerUser2 2023-07-31 15:48:49 +02:00
Julien Fontanet
6fd2f2610d fix(xo-web/new-vm): don't send device in VIFs
Introduced by 6ae19b064

Fixes #6960
2023-07-31 09:34:30 +02:00
Gabriel Gunullu
6ae19b0640 fix(xo-web/new-vm): list VIFs ordered by device (#6944)
Fixes zammad#15920
2023-07-28 18:51:48 +02:00
Pierre Donias
6b936d8a8c feat(lite): 0.1.2 (#6958) 2023-07-28 17:37:07 +02:00
Thierry Goettelmann
8f2cfaae00 feat(lite): open console in new window (#6868)
Add a link to open the console in a new window.
2023-07-28 14:04:06 +02:00
Thierry Goettelmann
5c215e1a8a feat(lite/console): rework VM console page (#6863)
Rework the VM Console page to be better aligned with Figma mockup.
- Spinner while loading the console
- Added the "monitor" image with correct message when VM is powered off
- Better screen space usage
2023-07-28 11:39:33 +02:00
Pierre Donias
e3cb98124f feat: technical release (#6956) 2023-07-28 10:05:26 +02:00
88 changed files with 2862 additions and 1870 deletions

View File

@@ -1,8 +1,11 @@
'use strict'
module.exports = {
arrowParens: 'avoid',
jsxSingleQuote: true,
semi: false,
singleQuote: true,
trailingComma: 'es5',
// 2020-11-24: Requested by nraynaud and approved by the rest of the team
//

View File

@@ -1,6 +1,6 @@
{
"name": "@vates/fuse-vhd",
"version": "1.0.0",
"version": "2.0.0",
"license": "ISC",
"private": false,
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/fuse-vhd",

View File

@@ -0,0 +1,32 @@
import NbdClient from "./client.mjs";
async function bench(){
const client = new NbdClient({
address:'localhost',
port: 9000,
exportname: 'bench_export'
})
await client.connect()
console.log('connected', client.exportSize)
for(let chunk_size=16*1024; chunk_size < 16*1024*1024; chunk_size *=2){
let i=0
const start = + new Date()
for await(const block of client.readBlocks(chunk_size) ){
i++
if((i*chunk_size) % (16*1024*1024) ===0){
process.stdout.write('.')
}
if(i*chunk_size > 1024*1024*1024) break
}
console.log(chunk_size,Math.round( (i*chunk_size/1024/1024*1000)/ (new Date() - start)))
}
await client.disconnect()
}
bench()

View File

@@ -74,7 +74,7 @@ export default class NbdClient {
this.#serverSocket = connect({
socket: this.#serverSocket,
rejectUnauthorized: false,
cert: this.#serverCert,
cert: this.#serverCert
})
this.#serverSocket.once('error', reject)
this.#serverSocket.once('secureConnect', () => {
@@ -88,7 +88,11 @@ export default class NbdClient {
async #unsecureConnect() {
this.#serverSocket = new Socket()
return new Promise((resolve, reject) => {
this.#serverSocket.connect(this.#serverPort, this.#serverAddress)
this.#serverSocket.connect({
port:this.#serverPort,
host: this.#serverAddress,
// @todo should test the onRead to limit buffer copy
})
this.#serverSocket.once('error', reject)
this.#serverSocket.once('connect', () => {
this.#serverSocket.removeListener('error', reject)
@@ -232,19 +236,20 @@ export default class NbdClient {
}
try {
this.#waitingForResponse = true
const magic = await this.#readInt32()
const buffer = await this.#read(4+4+8)
const magic = buffer.readUInt32BE()
if (magic !== NBD_REPLY_MAGIC) {
throw new Error(`magic number for block answer is wrong : ${magic} ${NBD_REPLY_MAGIC}`)
}
const error = await this.#readInt32()
const error = buffer.readUInt32BE(4)
if (error !== 0) {
// @todo use error code from constants.mjs
throw new Error(`GOT ERROR CODE : ${error}`)
}
const blockQueryId = await this.#readInt64()
const blockQueryId = buffer.readBigUInt64BE(8)
const query = this.#commandQueryBacklog.get(blockQueryId)
if (!query) {
throw new Error(` no query associated with id ${blockQueryId}`)
@@ -307,11 +312,11 @@ export default class NbdClient {
})
}
async *readBlocks(indexGenerator) {
async *readBlocks(indexGenerator = 2*1024*1024) {
// default : read all blocks
if (indexGenerator === undefined) {
if (typeof indexGenerator === 'number') {
const exportSize = this.#exportSize
const chunkSize = 2 * 1024 * 1024
const chunkSize = indexGenerator
indexGenerator = function* () {
const nbBlocks = Math.ceil(Number(exportSize / BigInt(chunkSize)))
for (let index = 0; BigInt(index) < nbBlocks; index++) {
@@ -319,12 +324,14 @@ export default class NbdClient {
}
}
}
const readAhead = []
const readAheadMaxLength = this.#readAhead
const makeReadBlockPromise = (index, size) => {
const promise = pRetry(() => this.readBlock(index, size), {
tries: this.#readBlockRetries,
onRetry: async err => {
console.error(err)
warn('will retry reading block ', index, err)
await this.reconnect()
},
@@ -336,6 +343,7 @@ export default class NbdClient {
// read all blocks, but try to keep readAheadMaxLength promise waiting ahead
for (const { index, size } of indexGenerator()) {
// stack readAheadMaxLength promises before starting to handle the results
if (readAhead.length === readAheadMaxLength) {
// any error will stop reading blocks
@@ -348,4 +356,4 @@ export default class NbdClient {
yield readAhead.shift()
}
}
}
}

View File

@@ -1,12 +1,40 @@
// https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md
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_OPT_STRUCTURED_REPLY = 8
export const NBD_OPT_LIST_META_CONTEXT = 9
export const NBD_OPT_SET_META_CONTEXT = 10
export const NBD_OPT_EXTENDED_HEADERS = 11
export const NBD_REP_ACK =1
export const NBD_REP_SERVER = 2
export const NBD_REP_INFO = 3
export const NBD_REP_META_CONTEXT = 4
export const NBD_REP_ERR_UNSUP = 0x80000001 // 2^32+1
export const NBD_REP_ERR_POLICY = 0x80000002
export const NBD_REP_ERR_INVALID = 0x80000003
export const NBD_REP_ERR_PLATFORM = 0x80000004
export const NBD_REP_ERR_TLS_REQD = 0x80000005
export const NBD_REP_ERR_UNKNOWN = 0x80000006
export const NBD_REP_ERR_SHUTDOWN = 0x80000007
export const NBD_REP_ERR_BLOCK_SIZE_REQD = 0x80000008
export const NBD_REP_ERR_TOO_BIG = 0x80000009
export const NBD_REP_ERR_EXT_HEADER_REQD = 0x8000000a
export const NBD_INFO_EXPORT = 0
export const NBD_INFO_NAME = 1
export const NBD_INFO_DESCRIPTION = 2
export const NBD_INFO_BLOCK_SIZE = 3
export const NBD_FLAG_HAS_FLAGS = 1 << 0
export const NBD_FLAG_READ_ONLY = 1 << 1
@@ -14,6 +42,9 @@ 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_SEND_WRITE_ZEROES = 1 << 6
export const NBD_FLAG_SEND_DF = 1 << 7
export const NBD_FLAG_CAN_MULTI_CONN = 1 << 8
export const NBD_FLAG_FIXED_NEWSTYLE = 1 << 0
@@ -36,6 +67,15 @@ 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_SIMPLE_REPLY_MAGIC = 0x67446698
export const NBD_STRUCTURED_REPLY_MAGIC = 0x668e33ef
export const NBD_REPLY_TYPE_NONE = 0
export const NBD_REPLY_TYPE_OFFSET_DATA = 1
export const NBD_REPLY_TYPE_OFFSET_HOLE = 2
export const NBD_REPLY_TYPE_BLOCK_STATUS = 5
export const NBD_REPLY_TYPE_ERROR = 1 << 15 +1
export const NBD_REPLY_TYPE_ERROR_OFFSET = 1 << 15 +2
export const NBD_DEFAULT_PORT = 10809
export const NBD_DEFAULT_BLOCK_SIZE = 64 * 1024

View File

@@ -13,18 +13,18 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "1.2.1",
"version": "2.0.0",
"engines": {
"node": ">=14.0"
},
"main": "./index.mjs",
"dependencies": {
"@vates/async-each": "^1.0.0",
"@vates/read-chunk": "^1.1.1",
"@vates/read-chunk": "^1.2.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"promise-toolbox": "^0.21.0",
"xen-api": "^1.3.3"
"xen-api": "^1.3.4"
},
"devDependencies": {
"tap": "^16.3.0",

View File

@@ -0,0 +1,292 @@
import assert, { deepEqual, strictEqual, notStrictEqual } from 'node:assert'
import { createServer } from 'node:net'
import { fromCallback } from 'promise-toolbox'
import { readChunkStrict } from '@vates/read-chunk'
import {
INIT_PASSWD,
NBD_CMD_READ,
NBD_DEFAULT_PORT,
NBD_FLAG_FIXED_NEWSTYLE,
NBD_FLAG_HAS_FLAGS,
NBD_OPT_EXPORT_NAME,
NBD_OPT_REPLY_MAGIC,
NBD_REPLY_ACK,
NBD_REQUEST_MAGIC,
OPTS_MAGIC,
NBD_CMD_DISC,
NBD_REP_ERR_UNSUP,
NBD_CMD_WRITE,
NBD_OPT_GO,
NBD_OPT_INFO,
NBD_INFO_EXPORT,
NBD_REP_INFO,
NBD_SIMPLE_REPLY_MAGIC,
NBD_REP_ERR_UNKNOWN,
} from './constants.mjs'
import { PassThrough } from 'node:stream'
export default class NbdServer {
#server
#clients = new Map()
constructor(port = NBD_DEFAULT_PORT) {
this.#server = createServer()
this.#server.listen(port)
this.#server.on('connection', client => this.#handleNewConnection(client))
}
// will wait for a client to connect and upload the file to this server
downloadStream(key) {
strictEqual(this.#clients.has(key), false)
const stream = new PassThrough()
const offset = BigInt(0)
this.#clients.set(key, { length: BigInt(2 * 1024 * 1024 * 1024 * 1024), stream, offset, key })
return stream
}
// will wait for a client to connect and downlaod this stream
uploadStream(key, source, length) {
strictEqual(this.#clients.has(key), false)
notStrictEqual(length, undefined)
const offset = BigInt(0)
this.#clients.set(key, { length: BigInt(length), stream: source, offset, key })
}
#read(socket, length) {
return readChunkStrict(socket, length)
}
async #readInt32(socket) {
const buffer = await this.#read(socket, 4)
return buffer.readUInt32BE()
}
#write(socket, buffer) {
return fromCallback.call(socket, 'write', buffer)
}
async #writeInt16(socket, int16) {
const buffer = Buffer.alloc(2)
buffer.writeUInt16BE(int16)
return this.#write(socket, buffer)
}
async #writeInt32(socket, int32) {
const buffer = Buffer.alloc(4)
buffer.writeUInt32BE(int32)
return this.#write(socket, buffer)
}
async #writeInt64(socket, int64) {
const buffer = Buffer.alloc(8)
buffer.writeBigUInt64BE(int64)
return this.#write(socket, buffer)
}
async #openExport(key) {
if (!this.#clients.has(key)) {
// export does not exists
const err = new Error('Export not found ')
err.code = 'ENOTFOUND'
throw err
}
const { length } = this.#clients.get(key)
return length
}
async #sendOptionResponse(socket, option, response, data = Buffer.alloc(0)) {
await this.#writeInt64(socket, NBD_OPT_REPLY_MAGIC)
await this.#writeInt32(socket, option)
await this.#writeInt32(socket, response)
await this.#writeInt32(socket, data.length)
await this.#write(socket, data)
}
/**
*
* @param {Socket} socket
* @returns true if server is waiting for more options
*/
async #readOption(socket) {
console.log('wait for option')
const magic = await this.#read(socket, 8)
console.log(magic.toString('ascii'), magic.length, OPTS_MAGIC.toString('ascii'))
deepEqual(magic, OPTS_MAGIC)
const option = await this.#readInt32(socket)
const length = await this.#readInt32(socket)
console.log({ option, length })
const data = length > 0 ? await this.#read(socket, length) : undefined
switch (option) {
case NBD_OPT_EXPORT_NAME: {
const exportNameLength = data.readInt32BE()
const key = data.slice(4, exportNameLength + 4).toString()
let exportSize
try {
exportSize = await this.#openExport(key)
} catch (err) {
if (err.code === 'ENOTFOUND') {
this.#sendOptionResponse(socket, option, NBD_REP_ERR_UNKNOWN)
return false
}
throw err
}
socket.key = key
await this.#writeInt64(socket, exportSize)
await this.#writeInt16(socket, NBD_FLAG_HAS_FLAGS /* transmission flag */)
await this.#write(socket, Buffer.alloc(124) /* padding */)
return false
}
/*
case NBD_OPT_STARTTLS:
console.log('starttls')
// @todo not working
return true
*/
case NBD_OPT_GO:
case NBD_OPT_INFO: {
const exportNameLength = data.readInt32BE()
const key = data.slice(4, exportNameLength + 4).toString()
let exportSize
try {
exportSize = await this.#openExport(key)
} catch (err) {
if (err.code === 'ENOTFOUND') {
this.#sendOptionResponse(socket, option, NBD_REP_ERR_UNKNOWN)
// @todo should disconnect
return false
}
throw err
}
socket.key = key
await this.#writeInt64(socket, NBD_OPT_REPLY_MAGIC)
await this.#writeInt32(socket, option)
await this.#writeInt32(socket, NBD_REP_INFO)
await this.#writeInt32(socket, 12)
// the export info
await this.#writeInt16(socket, NBD_INFO_EXPORT)
await this.#writeInt64(socket, exportSize)
await this.#writeInt16(socket, NBD_FLAG_HAS_FLAGS /* transmission flag */)
// an ACK at the end of the infos
await this.#sendOptionResponse(socket, option, NBD_REPLY_ACK) // no additionnal data
return option === NBD_OPT_INFO // we stays in option phase is option is INFO
}
default:
// not supported
console.log('not supported', option, length, data?.toString())
await this.#sendOptionResponse(socket, option, NBD_REP_ERR_UNSUP) // no additionnal data
// wait for next option
return true
}
}
async #readCommand(socket) {
const key = socket.key
// this socket has an export key
notStrictEqual(key, undefined)
// this export key is still valid
strictEqual(this.#clients.has(key), true)
const client = this.#clients.get(key)
const buffer = await this.#read(socket, 28)
const magic = buffer.readInt32BE(0)
strictEqual(magic, NBD_REQUEST_MAGIC)
/* const commandFlags = */ buffer.readInt16BE(4)
const command = buffer.readInt16BE(6)
const cookie = buffer.readBigUInt64BE(8)
const offset = buffer.readBigUInt64BE(16)
const length = buffer.readInt32BE(24)
switch (command) {
case NBD_CMD_DISC:
console.log('gotdisconnect', client.offset)
await client.stream?.destroy()
// @todo : disconnect
return false
case NBD_CMD_READ: {
/** simple replies */
// read length byte from offset in export
// the client is writing in contiguous mode
assert.strictEqual(offset, client.offset)
client.offset += BigInt(length)
const data = await readChunkStrict(client.stream, length)
const reply = Buffer.alloc(16)
reply.writeInt32BE(NBD_SIMPLE_REPLY_MAGIC)
reply.writeInt32BE(0, 4) // no error
reply.writeBigInt64BE(cookie, 8)
await this.#write(socket, reply)
await this.#write(socket, data)
/* if we implement non stream read, we can handle read in parallel
const reply = Buffer.alloc(16+length)
reply.writeInt32BE(NBD_SIMPLE_REPLY_MAGIC)
reply.writeInt32BE(0,4)// no error
reply.writeBigInt64BE(cookie,8)
// read length byte from offset in export directly in the given buffer
// may do multiple read in parallel on the same export
size += length
socket.fd.read(reply, 16, length, Number(offset))
.then(()=>{
return this.#write(socket, reply)
})
.catch(err => console.error('NBD_CMD_READ',err)) */
return true
}
case NBD_CMD_WRITE: {
// the client is writing in contiguous mode
assert.strictEqual(offset, client.offset)
const data = await this.#read(socket, length)
client.offset += BigInt(length)
await new Promise((resolve, reject) => {
if (!client.stream.write(data, 0, length, Number(offset))) {
client.stream.once('drain', err => (err ? reject(err) : resolve()))
} else {
process.nextTick(resolve)
}
})
const reply = Buffer.alloc(16)
reply.writeInt32BE(NBD_SIMPLE_REPLY_MAGIC)
reply.writeInt32BE(0, 4) // no error
reply.writeBigInt64BE(cookie, 8)
await this.#write(socket, reply)
return true
}
default:
console.log('GOT unsupported command ', command)
// fail to handle
return true
}
}
async #handleNewConnection(socket) {
const remoteAddress = socket.remoteAddress + ':' + socket.remotePort
console.log('new client connection from %s', remoteAddress)
socket.on('close', () => {
console.log('client ', remoteAddress, 'is done')
})
socket.on('error', error => {
throw error
})
// handshake
await this.#write(socket, INIT_PASSWD)
await this.#write(socket, OPTS_MAGIC)
// send flags , the bare minimum
await this.#writeInt16(socket, NBD_FLAG_FIXED_NEWSTYLE)
const clientFlag = await this.#readInt32(socket)
assert.strictEqual(clientFlag & NBD_FLAG_FIXED_NEWSTYLE, NBD_FLAG_FIXED_NEWSTYLE) // only FIXED_NEWSTYLE one is supported from the server options
// read client response flags
let waitingForOptions = true
while (waitingForOptions) {
waitingForOptions = await this.#readOption(socket)
}
let waitingForCommand = true
while (waitingForCommand) {
waitingForCommand = await this.#readCommand(socket)
}
}
#handleClientData(client, data) {}
}

View File

@@ -1,4 +1,4 @@
import NbdClient from '../index.mjs'
import NbdClient from '../client.mjs'
import { spawn, exec } from 'node:child_process'
import fs from 'node:fs/promises'
import { test } from 'tap'

View File

@@ -1,6 +1,6 @@
{
"name": "@vates/node-vsphere-soap",
"version": "1.0.0",
"version": "2.0.0",
"description": "interface to vSphere SOAP/WSDL from node for interfacing with vCenter or ESXi, forked from node-vsphere-soap",
"main": "lib/client.mjs",
"author": "reedog117",

View File

@@ -19,7 +19,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "1.1.1",
"version": "1.2.0",
"engines": {
"node": ">=8.10"
},

View File

@@ -7,9 +7,9 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.39.0",
"@xen-orchestra/backups": "^0.40.0",
"@xen-orchestra/fs": "^4.0.1",
"filenamify": "^4.1.0",
"filenamify": "^6.0.0",
"getopts": "^2.2.5",
"lodash": "^4.17.15",
"promise-toolbox": "^0.21.0"
@@ -27,7 +27,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "1.0.9",
"version": "1.0.10",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -11,6 +11,7 @@ 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 { ZipFile } from 'yazl'
import Disposable from 'promise-toolbox/Disposable'
import fromCallback from 'promise-toolbox/fromCallback'
import fromEvent from 'promise-toolbox/fromEvent'
@@ -29,7 +30,6 @@ 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'
export const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
@@ -45,6 +45,23 @@ const resolveRelativeFromFile = (file, path) => resolve('/', dirname(file), path
const makeRelative = path => resolve('/', path).slice(1)
const resolveSubpath = (root, path) => resolve(root, makeRelative(path))
async function addZipEntries(zip, realBasePath, virtualBasePath, relativePaths) {
for (const relativePath of relativePaths) {
const realPath = join(realBasePath, relativePath)
const virtualPath = join(virtualBasePath, relativePath)
const stats = await lstat(realPath)
const { mode, mtime } = stats
const opts = { mode, mtime }
if (stats.isDirectory()) {
zip.addEmptyDirectory(virtualPath, opts)
await addZipEntries(zip, realPath, virtualPath, await readdir(realPath))
} else if (stats.isFile()) {
zip.addFile(realPath, virtualPath, opts)
}
}
}
const createSafeReaddir = (handler, methodName) => (path, options) =>
handler.list(path, options).catch(error => {
if (error?.code !== 'ENOENT') {
@@ -195,16 +212,10 @@ export class RemoteAdapter {
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
const zip = new ZipFile()
await addZipEntries(zip, path, '', paths.map(makeRelative))
zip.end()
;({ outputStream } = zip)
} else {
throw new Error('unsupported format ' + format)
}

View File

@@ -18,7 +18,7 @@ export const MixinXapiWriter = (BaseClass = Object) =>
const vdiRefs = await xapi.VM_getDisks(baseVm.$ref)
for (const vdiRef of vdiRefs) {
const vdi = xapi.getObject(vdiRef)
if (vdi.$SR.uuid !== this._heathCheckSr.uuid) {
if (vdi.$SR.uuid !== this._healthCheckSr.uuid) {
return false
}
}

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.39.0",
"version": "0.40.0",
"engines": {
"node": ">=14.18"
},
@@ -23,15 +23,15 @@
"@vates/compose": "^2.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.4",
"@vates/fuse-vhd": "^1.0.0",
"@vates/nbd-client": "^1.2.1",
"@vates/fuse-vhd": "^2.0.0",
"@vates/nbd-client": "^2.0.0",
"@vates/parse-duration": "^0.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^4.0.1",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/template": "^0.1.0",
"compare-versions": "^5.0.1",
"d3-time-format": "^3.0.0",
"compare-versions": "^6.0.0",
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",
"golike-defer": "^0.5.1",
"limit-concurrency-decorator": "^0.5.0",
@@ -43,7 +43,8 @@
"tar": "^6.1.15",
"uuid": "^9.0.0",
"vhd-lib": "^4.5.0",
"xen-api": "^1.3.3"
"xen-api": "^1.3.4",
"yazl": "^2.5.1"
},
"devDependencies": {
"fs-extra": "^11.1.0",
@@ -53,7 +54,7 @@
"tmp": "^0.2.1"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^2.2.1"
"@xen-orchestra/xapi": "^3.0.0"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -18,7 +18,7 @@
"preferGlobal": true,
"dependencies": {
"golike-defer": "^0.5.1",
"xen-api": "^1.3.3"
"xen-api": "^1.3.4"
},
"scripts": {
"postversion": "npm publish"

View File

@@ -29,7 +29,7 @@
"@vates/async-each": "^1.0.0",
"@vates/coalesce-calls": "^0.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/read-chunk": "^1.1.1",
"@vates/read-chunk": "^1.2.0",
"@xen-orchestra/log": "^0.6.0",
"bind-property-descriptor": "^2.0.0",
"decorator-synchronized": "^0.6.0",

View File

@@ -1,2 +1,4 @@
// Keeping this file to prevent applying the global monorepo config for now
module.exports = {};
module.exports = {
trailingComma: "es5",
};

View File

@@ -2,6 +2,8 @@
## **next**
## **0.1.2** (2023-07-28)
- 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))

View File

@@ -40,7 +40,7 @@ export const useConsoleStore = defineStore("console", () =>
To extend the base Subscription, you'll need to override the `subscribe` method.
For that, you can use the `createSubscribe<XenApiRecord, Extensions>((options) => { /* ... */})` helper.
For that, you can use the `createSubscribe<RawObjectType, Extensions>((options) => { /* ... */})` helper.
### Define the extensions
@@ -82,7 +82,7 @@ type Extensions = [
export const useConsoleStore = defineStore("console", () => {
const consoleCollection = useXapiCollectionStore().get("console");
const subscribe = createSubscribe<XenApiConsole, Extensions>((options) => {
const subscribe = createSubscribe<"console", Extensions>((options) => {
const originalSubscription = consoleCollection.subscribe(options);
const extendedSubscription = {

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/lite",
"version": "0.1.1",
"version": "0.1.2",
"scripts": {
"dev": "GIT_HEAD=$(git rev-parse HEAD) vite",
"build": "run-p type-check build-only",
@@ -22,7 +22,7 @@
"@types/marked": "^4.0.8",
"@vueuse/core": "^10.1.2",
"@vueuse/math": "^10.1.2",
"complex-matcher": "^0.7.0",
"complex-matcher": "^0.7.1",
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",
"echarts": "^5.3.3",
@@ -49,7 +49,7 @@
"@rushstack/eslint-patch": "^1.1.0",
"@types/node": "^16.11.41",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.1.3",
"eslint-plugin-vue": "^9.0.0",

View File

@@ -4,10 +4,10 @@
<AppLogin />
</div>
<div v-else>
<AppHeader />
<AppHeader v-if="uiStore.hasUi" />
<div style="display: flex">
<AppNavigation />
<main class="main">
<AppNavigation v-if="uiStore.hasUi" />
<main class="main" :class="{ 'no-ui': !uiStore.hasUi }">
<RouterView />
</main>
</div>
@@ -90,5 +90,9 @@ whenever(
flex: 1;
height: calc(100vh - 8rem);
background-color: var(--background-color-secondary);
&.no-ui {
height: 100vh;
}
}
</style>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -64,7 +64,7 @@ async function handleSubmit() {
isInvalidPassword.value = true;
error.value = t("password-invalid");
} else {
error.value = t("error-occured");
error.value = t("error-occurred");
console.error(err);
}
}

View File

@@ -7,12 +7,12 @@
</template>
<script
generic="T extends XenApiRecord<string>, I extends T['uuid']"
generic="T extends XenApiRecord<RawObjectType>, I extends T['uuid']"
lang="ts"
setup
>
import UiSpinner from "@/components/ui/UiSpinner.vue";
import type { XenApiRecord } from "@/libs/xen-api";
import type { RawObjectType, XenApiRecord } from "@/libs/xen-api";
import ObjectNotFoundView from "@/views/ObjectNotFoundView.vue";
import { computed } from "vue";
import { useRouter } from "vue-router";

View File

@@ -3,11 +3,11 @@
</template>
<script lang="ts" setup>
import { useXenApiStore } from "@/stores/xen-api.store";
import VncClient from "@novnc/novnc/core/rfb";
import { promiseTimeout } from "@vueuse/shared";
import { fibonacci } from "iterable-backoff";
import { computed, onBeforeUnmount, ref, watchEffect } from "vue";
import VncClient from "@novnc/novnc/core/rfb";
import { useXenApiStore } from "@/stores/xen-api.store";
import { promiseTimeout } from "@vueuse/shared";
const N_TOTAL_TRIES = 8;
const FIBONACCI_MS_ARRAY: number[] = Array.from(

View File

@@ -33,7 +33,7 @@
</AppMenu>
</UiTabBar>
<div class="tabs">
<div :class="{ 'full-width': fullWidthComponent }" class="tabs">
<UiCard v-if="selectedTab === TAB.NONE" class="tab-content">
<i>No configuration defined</i>
</UiCard>
@@ -102,11 +102,11 @@ import StorySettingParams from "@/components/component-story/StorySettingParams.
import StorySlotParams from "@/components/component-story/StorySlotParams.vue";
import AppMenu from "@/components/menu/AppMenu.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiCounter from "@/components/ui/UiCounter.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiTab from "@/components/ui/UiTab.vue";
import UiTabBar from "@/components/ui/UiTabBar.vue";
import {
@@ -140,6 +140,7 @@ const props = defineProps<{
settings?: Record<string, any>;
}
>;
fullWidthComponent?: boolean;
}>();
enum TAB {
@@ -329,6 +330,10 @@ const applyPreset = (preset: {
padding: 1rem;
gap: 1rem;
&.full-width {
flex-direction: column;
}
.tab-content {
flex: 1;
height: auto;

View File

@@ -0,0 +1,50 @@
<template>
<li class="ui-resource">
<UiIcon :icon="icon" class="icon" />
<div class="separator" />
<div class="label">{{ label }}</div>
<div class="count">{{ count }}</div>
</li>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
defineProps<{
icon: IconDefinition;
label: string;
count: string | number;
}>();
</script>
<style lang="postcss" scoped>
.ui-resource {
display: flex;
align-items: center;
}
.icon {
color: var(--color-extra-blue-base);
font-size: 3.2rem;
}
.separator {
height: 4.5rem;
width: 0;
border-left: 0.1rem solid var(--color-extra-blue-base);
background-color: var(--color-extra-blue-base);
margin: 0 1.5rem;
}
.label {
font-size: 1.6rem;
font-weight: 700;
}
.count {
font-size: 1.4rem;
font-weight: 400;
margin-left: 2rem;
}
</style>

View File

@@ -0,0 +1,13 @@
<template>
<ul class="ui-resources">
<slot />
</ul>
</template>
<style lang="postcss" scoped>
.ui-resources {
display: flex;
gap: 1rem 5.4rem;
flex-wrap: wrap;
}
</style>

View File

@@ -1,10 +1,10 @@
import type {
RawXenApiRecord,
XenApiHost,
XenApiHostMetrics,
XenApiRecord,
XenApiVm,
VM_OPERATION,
RawObjectType,
} from "@/libs/xen-api";
import type { Filter } from "@/types/filter";
import type { Subscription } from "@/types/xapi-collection";
@@ -116,14 +116,14 @@ export function getStatsLength(stats?: object | any[]) {
export function isHostRunning(
host: XenApiHost,
hostMetricsSubscription: Subscription<XenApiHostMetrics, object>
hostMetricsSubscription: Subscription<"host_metrics", object>
) {
return hostMetricsSubscription.getByOpaqueRef(host.metrics)?.live === true;
}
export function getHostMemory(
host: XenApiHost,
hostMetricsSubscription: Subscription<XenApiHostMetrics, object>
hostMetricsSubscription: Subscription<"host_metrics", object>
) {
const hostMetrics = hostMetricsSubscription.getByOpaqueRef(host.metrics);
@@ -136,7 +136,7 @@ export function getHostMemory(
}
}
export const buildXoObject = <T extends XenApiRecord<string>>(
export const buildXoObject = <T extends XenApiRecord<RawObjectType>>(
record: RawXenApiRecord<T>,
params: { opaqueRef: T["$ref"] }
) => {

View File

@@ -1,4 +1,5 @@
import { buildXoObject, parseDateTime } from "@/libs/utils";
import type { RawTypeToRecord } from "@/types/xapi-collection";
import { JSONRPCClient } from "json-rpc-2.0";
import { castArray } from "lodash-es";
@@ -90,14 +91,17 @@ export enum VM_OPERATION {
declare const __brand: unique symbol;
export interface XenApiRecord<Name extends string> {
export interface XenApiRecord<Name extends RawObjectType> {
$ref: string & { [__brand]: `${Name}Ref` };
uuid: string & { [__brand]: `${Name}Uuid` };
}
export type RawXenApiRecord<T extends XenApiRecord<string>> = Omit<T, "$ref">;
export type RawXenApiRecord<T extends XenApiRecord<RawObjectType>> = Omit<
T,
"$ref"
>;
export interface XenApiPool extends XenApiRecord<"Pool"> {
export interface XenApiPool extends XenApiRecord<"pool"> {
cpu_info: {
cpu_count: string;
};
@@ -105,7 +109,7 @@ export interface XenApiPool extends XenApiRecord<"Pool"> {
name_label: string;
}
export interface XenApiHost extends XenApiRecord<"Host"> {
export interface XenApiHost extends XenApiRecord<"host"> {
address: string;
name_label: string;
metrics: XenApiHostMetrics["$ref"];
@@ -114,13 +118,13 @@ export interface XenApiHost extends XenApiRecord<"Host"> {
software_version: { product_version: string };
}
export interface XenApiSr extends XenApiRecord<"Sr"> {
export interface XenApiSr extends XenApiRecord<"SR"> {
name_label: string;
physical_size: number;
physical_utilisation: number;
}
export interface XenApiVm extends XenApiRecord<"Vm"> {
export interface XenApiVm extends XenApiRecord<"VM"> {
current_operations: Record<string, VM_OPERATION>;
guest_metrics: string;
metrics: XenApiVmMetrics["$ref"];
@@ -135,24 +139,24 @@ export interface XenApiVm extends XenApiRecord<"Vm"> {
VCPUs_at_startup: number;
}
export interface XenApiConsole extends XenApiRecord<"Console"> {
export interface XenApiConsole extends XenApiRecord<"console"> {
protocol: string;
location: string;
}
export interface XenApiHostMetrics extends XenApiRecord<"HostMetrics"> {
export interface XenApiHostMetrics extends XenApiRecord<"host_metrics"> {
live: boolean;
memory_free: number;
memory_total: number;
}
export interface XenApiVmMetrics extends XenApiRecord<"VmMetrics"> {
export interface XenApiVmMetrics extends XenApiRecord<"VM_metrics"> {
VCPUs_number: number;
}
export type XenApiVmGuestMetrics = XenApiRecord<"VmGuestMetrics">;
export type XenApiVmGuestMetrics = XenApiRecord<"VM_guest_metrics">;
export interface XenApiTask extends XenApiRecord<"Task"> {
export interface XenApiTask extends XenApiRecord<"task"> {
name_label: string;
resident_on: XenApiHost["$ref"];
created: string;
@@ -161,17 +165,22 @@ export interface XenApiTask extends XenApiRecord<"Task"> {
progress: number;
}
export interface XenApiMessage extends XenApiRecord<"Message"> {
export interface XenApiMessage<T extends RawObjectType = RawObjectType>
extends XenApiRecord<"message"> {
body: string;
cls: T;
name: string;
cls: RawObjectType;
obj_uuid: RawTypeToRecord<T>["uuid"];
priority: number;
timestamp: string;
}
type WatchCallbackResult = {
id: string;
class: ObjectType;
operation: "add" | "mod" | "del";
ref: XenApiRecord<string>["$ref"];
snapshot: RawXenApiRecord<XenApiRecord<string>>;
ref: XenApiRecord<RawObjectType>["$ref"];
snapshot: RawXenApiRecord<XenApiRecord<RawObjectType>>;
};
type WatchCallback = (results: WatchCallbackResult[]) => void;
@@ -284,16 +293,17 @@ export default class XenApi {
return fetch(url, { signal: abortSignal });
}
async loadRecords<T extends XenApiRecord<string>>(
type: RawObjectType
): Promise<T[]> {
const result = await this.#call<{ [key: string]: RawXenApiRecord<T> }>(
async loadRecords<
T extends RawObjectType,
R extends RawTypeToRecord<T> = RawTypeToRecord<T>
>(type: T): Promise<R[]> {
const result = await this.#call<{ [key: string]: R }>(
`${type}.get_all_records`,
[this.sessionId]
);
return Object.entries(result).map(([opaqueRef, record]) =>
buildXoObject(record, { opaqueRef: opaqueRef as T["$ref"] })
buildXoObject(record, { opaqueRef: opaqueRef as R["$ref"] })
);
}

View File

@@ -18,6 +18,7 @@
"community": "Community",
"community-name": "{name} community",
"console": "Console",
"console-unavailable": "Console unavailable",
"copy": "Copy",
"cpu-provisioning": "CPU provisioning",
"cpu-usage": "CPU usage",
@@ -32,7 +33,7 @@
"do-you-have-needs": "You have needs and/or expectations? Let us know",
"edit-config": "Edit config",
"error-no-data": "Error, can't collect data.",
"error-occured": "An error has occurred",
"error-occurred": "An error has occurred",
"export": "Export",
"export-table-to": "Export table to {type}",
"export-vms": "Export VMs",
@@ -81,6 +82,7 @@
"not-found": "Not found",
"object": "Object",
"object-not-found": "Object {id} can't be found…",
"open-in-new-window": "Open in new window",
"or": "Or",
"page-not-found": "This page is not to be found…",
"password": "Password",
@@ -89,6 +91,7 @@
"please-confirm": "Please confirm",
"pool-cpu-usage": "Pool CPU Usage",
"pool-ram-usage": "Pool RAM Usage",
"power-on-for-console": "Power on your VM to access its console",
"power-state": "Power state",
"property": "Property",
"ram-usage": "RAM usage",
@@ -112,7 +115,6 @@
"settings": "Settings",
"shutdown": "Shutdown",
"snapshot": "Snapshot",
"selected-vms-in-execution": "Some selected VMs are running",
"sort-by": "Sort by",
"stacked-cpu-usage": "Stacked CPU usage",
"stacked-ram-usage": "Stacked RAM usage",

View File

@@ -18,6 +18,7 @@
"community": "Communauté",
"community-name": "Communauté {name}",
"console": "Console",
"console-unavailable": "Console indisponible",
"copy": "Copier",
"cpu-provisioning": "Provisionnement CPU",
"cpu-usage": "Utilisation CPU",
@@ -32,7 +33,7 @@
"do-you-have-needs": "Vous avez des besoins et/ou des attentes ? Faites le nous savoir",
"edit-config": "Modifier config",
"error-no-data": "Erreur, impossible de collecter les données.",
"error-occured": "Une erreur est survenue",
"error-occurred": "Une erreur est survenue",
"export": "Exporter",
"export-table-to": "Exporter le tableau en {type}",
"export-vms": "Exporter les VMs",
@@ -81,6 +82,7 @@
"not-found": "Non trouvé",
"object": "Objet",
"object-not-found": "L'objet {id} est introuvable…",
"open-in-new-window": "Ouvrir dans une nouvelle fenêtre",
"or": "Ou",
"page-not-found": "Cette page est introuvable…",
"password": "Mot de passe",
@@ -89,6 +91,7 @@
"please-confirm": "Veuillez confirmer",
"pool-cpu-usage": "Utilisation CPU du Pool",
"pool-ram-usage": "Utilisation RAM du Pool",
"power-on-for-console": "Allumez votre VM pour accéder à sa console",
"power-state": "État d'alimentation",
"property": "Propriété",
"ram-usage": "Utilisation de la RAM",
@@ -112,7 +115,6 @@
"settings": "Paramètres",
"shutdown": "Arrêter",
"snapshot": "Instantané",
"selected-vms-in-execution": "Certaines VMs sélectionnées sont en cours d'exécution",
"sort-by": "Trier par",
"stacked-cpu-usage": "Utilisation CPU empilée",
"stacked-ram-usage": "Utilisation RAM empilée",

View File

@@ -1,4 +1,3 @@
import type { XenApiMessage } from "@/libs/xen-api";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { createSubscribe } from "@/types/xapi-collection";
import { defineStore } from "pinia";
@@ -7,7 +6,7 @@ import { computed } from "vue";
export const useAlarmStore = defineStore("alarm", () => {
const messageCollection = useXapiCollectionStore().get("message");
const subscribe = createSubscribe<XenApiMessage, []>((options) => {
const subscribe = createSubscribe<"message", []>((options) => {
const originalSubscription = messageCollection.subscribe(options);
const extendedSubscription = {

View File

@@ -4,7 +4,7 @@ import type {
HostStats,
XapiStatsResponse,
} from "@/libs/xapi-stats";
import type { XenApiHost, XenApiHostMetrics } from "@/libs/xen-api";
import type { XenApiHost } from "@/libs/xen-api";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import type { Subscription } from "@/types/xapi-collection";
@@ -25,7 +25,7 @@ type GetStatsExtension = {
type RunningHostsExtension = [
{ runningHosts: ComputedRef<XenApiHost[]> },
{ hostMetricsSubscription: Subscription<XenApiHostMetrics, any> }
{ hostMetricsSubscription: Subscription<"host_metrics", any> }
];
type Extensions = [GetStatsExtension, RunningHostsExtension];
@@ -36,7 +36,7 @@ export const useHostStore = defineStore("host", () => {
hostCollection.setSort(sortRecordsByNameLabel);
const subscribe = createSubscribe<XenApiHost, Extensions>((options) => {
const subscribe = createSubscribe<"host", Extensions>((options) => {
const originalSubscription = hostCollection.subscribe(options);
const getStats: GetStats = (

View File

@@ -14,7 +14,7 @@ type Extensions = [PoolExtension];
export const usePoolStore = defineStore("pool", () => {
const poolCollection = useXapiCollectionStore().get("pool");
const subscribe = createSubscribe<XenApiPool, Extensions>((options) => {
const subscribe = createSubscribe<"pool", Extensions>((options) => {
const originalSubscription = poolCollection.subscribe(options);
const extendedSubscription = {

View File

@@ -22,7 +22,7 @@ type Extensions = [PendingTasksExtension, FinishedTasksExtension];
export const useTaskStore = defineStore("task", () => {
const tasksCollection = useXapiCollectionStore().get("task");
const subscribe = createSubscribe<XenApiTask, Extensions>(() => {
const subscribe = createSubscribe<"task", Extensions>(() => {
const subscription = tasksCollection.subscribe();
const { compareFn } = useCollectionSorter<XenApiTask>({

View File

@@ -1,6 +1,7 @@
import { useBreakpoints, useColorMode } from "@vueuse/core";
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import { useRoute } from "vue-router";
export const useUiStore = defineStore("ui", () => {
const currentHostOpaqueRef = ref();
@@ -13,10 +14,14 @@ export const useUiStore = defineStore("ui", () => {
const isMobile = computed(() => !isDesktop.value);
const route = useRoute();
const hasUi = computed(() => route.query.ui !== "0");
return {
colorMode,
currentHostOpaqueRef,
isDesktop,
isMobile,
hasUi,
};
});

View File

@@ -27,7 +27,7 @@ type GetStatsExtension = [
{
getStats: GetStats;
},
{ hostSubscription: Subscription<XenApiHost, object> }
{ hostSubscription: Subscription<"host", object> }
];
type Extensions = [DefaultExtension, GetStatsExtension];
@@ -41,7 +41,7 @@ export const useVmStore = defineStore("vm", () => {
vmCollection.setSort(sortRecordsByNameLabel);
const subscribe = createSubscribe<XenApiVm, Extensions>((options) => {
const subscribe = createSubscribe<"VM", Extensions>((options) => {
const originalSubscription = vmCollection.subscribe(options);
const extendedSubscription = {

View File

@@ -1,7 +1,7 @@
import type { RawObjectType, XenApiRecord } from "@/libs/xen-api";
import type { RawObjectType } from "@/libs/xen-api";
import { useXenApiStore } from "@/stores/xen-api.store";
import type {
RawTypeToObject,
RawTypeToRecord,
SubscribeOptions,
Subscription,
} from "@/types/xapi-collection";
@@ -10,16 +10,13 @@ import { defineStore } from "pinia";
import { computed, readonly, ref } from "vue";
export const useXapiCollectionStore = defineStore("xapiCollection", () => {
const collections = ref(
new Map<RawObjectType, ReturnType<typeof createXapiCollection<any>>>()
);
const collections = ref(new Map());
function get<
T extends RawObjectType,
S extends XenApiRecord<string> = RawTypeToObject[T]
>(type: T): ReturnType<typeof createXapiCollection<S>> {
function get<T extends RawObjectType>(
type: T
): ReturnType<typeof createXapiCollection<T, RawTypeToRecord<T>>> {
if (!collections.value.has(type)) {
collections.value.set(type, createXapiCollection<S>(type));
collections.value.set(type, createXapiCollection(type));
}
return collections.value.get(type)!;
@@ -28,8 +25,11 @@ export const useXapiCollectionStore = defineStore("xapiCollection", () => {
return { get };
});
const createXapiCollection = <T extends XenApiRecord<string>>(
type: RawObjectType
const createXapiCollection = <
T extends RawObjectType,
R extends RawTypeToRecord<T>
>(
type: T
) => {
const isReady = ref(false);
const isFetching = ref(false);
@@ -37,31 +37,31 @@ const createXapiCollection = <T extends XenApiRecord<string>>(
const lastError = ref<string>();
const hasError = computed(() => lastError.value !== undefined);
const subscriptions = ref(new Set<symbol>());
const recordsByOpaqueRef = ref(new Map<T["$ref"], T>());
const recordsByUuid = ref(new Map<T["uuid"], T>());
const filter = ref<(record: T) => boolean>();
const sort = ref<(record1: T, record2: T) => 1 | 0 | -1>();
const recordsByOpaqueRef = ref(new Map<R["$ref"], R>());
const recordsByUuid = ref(new Map<R["uuid"], R>());
const filter = ref<(record: R) => boolean>();
const sort = ref<(record1: R, record2: R) => 1 | 0 | -1>();
const xenApiStore = useXenApiStore();
const setFilter = (newFilter: (record: T) => boolean) =>
const setFilter = (newFilter: (record: R) => boolean) =>
(filter.value = newFilter);
const setSort = (newSort: (record1: T, record2: T) => 1 | 0 | -1) =>
const setSort = (newSort: (record1: R, record2: R) => 1 | 0 | -1) =>
(sort.value = newSort);
const records = computed<T[]>(() => {
const records = computed<R[]>(() => {
const records = Array.from(recordsByOpaqueRef.value.values()).sort(
sort.value
);
return filter.value !== undefined ? records.filter(filter.value) : records;
});
const getByOpaqueRef = (opaqueRef: T["$ref"]) =>
const getByOpaqueRef = (opaqueRef: R["$ref"]) =>
recordsByOpaqueRef.value.get(opaqueRef);
const getByUuid = (uuid: T["uuid"]) => recordsByUuid.value.get(uuid);
const getByUuid = (uuid: R["uuid"]) => recordsByUuid.value.get(uuid);
const hasUuid = (uuid: T["uuid"]) => recordsByUuid.value.has(uuid);
const hasUuid = (uuid: R["uuid"]) => recordsByUuid.value.has(uuid);
const hasSubscriptions = computed(() => subscriptions.value.size > 0);
@@ -69,7 +69,7 @@ const createXapiCollection = <T extends XenApiRecord<string>>(
try {
isFetching.value = true;
lastError.value = undefined;
const records = await xenApiStore.getXapi().loadRecords<T>(type);
const records = await xenApiStore.getXapi().loadRecords<T, R>(type);
recordsByOpaqueRef.value.clear();
recordsByUuid.value.clear();
records.forEach(add);
@@ -81,17 +81,17 @@ const createXapiCollection = <T extends XenApiRecord<string>>(
}
};
const add = (record: T) => {
const add = (record: R) => {
recordsByOpaqueRef.value.set(record.$ref, record);
recordsByUuid.value.set(record.uuid, record);
};
const update = (record: T) => {
const update = (record: R) => {
recordsByOpaqueRef.value.set(record.$ref, record);
recordsByUuid.value.set(record.uuid, record);
};
const remove = (opaqueRef: T["$ref"]) => {
const remove = (opaqueRef: R["$ref"]) => {
if (!recordsByOpaqueRef.value.has(opaqueRef)) {
return;
}

View File

@@ -0,0 +1,70 @@
<template>
<ComponentStory
v-slot="{ properties }"
:params="[
iconProp().preset(faRocket),
prop('label').required().str().widget().preset('Rockets'),
prop('count')
.required()
.type('string | number')
.widget(text())
.preset('175'),
]"
:presets="presets"
>
<UiResource v-bind="properties" />
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import UiResource from "@/components/ui/resources/UiResource.vue";
import { iconProp, prop } from "@/libs/story/story-param";
import { text } from "@/libs/story/story-widget";
import {
faDatabase,
faDisplay,
faMemory,
faMicrochip,
faNetworkWired,
faRocket,
} from "@fortawesome/free-solid-svg-icons";
const presets = {
VMs: {
props: {
icon: faDisplay,
count: 1,
label: "VMs",
},
},
vCPUs: {
props: {
icon: faMicrochip,
count: 4,
label: "vCPUs",
},
},
RAM: {
props: {
icon: faMemory,
count: 2,
label: "RAM",
},
},
SR: {
props: {
icon: faDatabase,
count: 1,
label: "SR",
},
},
Interfaces: {
props: {
icon: faNetworkWired,
count: 2,
label: "Interfaces",
},
},
};
</script>

View File

@@ -0,0 +1,11 @@
# Example
```vue-template
<UiResources>
<UiResource :icon="faDisplay" count="1" label="VMs" />
<UiResource :icon="faMicrochip" count="4" label="vCPUs" />
<UiResource :icon="faMemory" count="2" label="RAM" />
<UiResource :icon="faDatabase" count="1" label="SR" />
<UiResource :icon="faNetworkWired" count="2" label="Interfaces" />
</UiResources>
```

View File

@@ -0,0 +1,28 @@
<template>
<ComponentStory
:params="[slot().help('One or multiple `UiResource`')]"
full-width-component
>
<UiResources>
<UiResource :icon="faDisplay" count="1" label="VMs" />
<UiResource :icon="faMicrochip" count="4" label="vCPUs" />
<UiResource :icon="faMemory" count="2" label="RAM" />
<UiResource :icon="faDatabase" count="1" label="SR" />
<UiResource :icon="faNetworkWired" count="2" label="Interfaces" />
</UiResources>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import UiResource from "@/components/ui/resources/UiResource.vue";
import UiResources from "@/components/ui/resources/UiResources.vue";
import { slot } from "@/libs/story/story-param";
import {
faDatabase,
faDisplay,
faMemory,
faMicrochip,
faNetworkWired,
} from "@fortawesome/free-solid-svg-icons";
</script>

View File

@@ -1,10 +1,10 @@
import type {
RawObjectType,
XenApiConsole,
XenApiHost,
XenApiHostMetrics,
XenApiMessage,
XenApiPool,
XenApiRecord,
XenApiSr,
XenApiTask,
XenApiVm,
@@ -13,11 +13,14 @@ import type {
} from "@/libs/xen-api";
import type { ComputedRef, Ref } from "vue";
type DefaultExtension<T extends XenApiRecord<string>> = {
records: ComputedRef<T[]>;
getByOpaqueRef: (opaqueRef: T["$ref"]) => T | undefined;
getByUuid: (uuid: T["uuid"]) => T | undefined;
hasUuid: (uuid: T["uuid"]) => boolean;
type DefaultExtension<
T extends RawObjectType,
R extends RawTypeToRecord<T> = RawTypeToRecord<T>
> = {
records: ComputedRef<R[]>;
getByOpaqueRef: (opaqueRef: R["$ref"]) => R | undefined;
getByUuid: (uuid: R["uuid"]) => R | undefined;
hasUuid: (uuid: R["uuid"]) => boolean;
isReady: Readonly<Ref<boolean>>;
isFetching: Readonly<Ref<boolean>>;
isReloading: ComputedRef<boolean>;
@@ -33,7 +36,7 @@ type DeferExtension = [
{ immediate: false }
];
type DefaultExtensions<T extends XenApiRecord<string>> = [
type DefaultExtensions<T extends RawObjectType> = [
DefaultExtension<T>,
DeferExtension
];
@@ -64,14 +67,14 @@ type GenerateSubscription<
: object;
export type Subscription<
T extends XenApiRecord<string>,
T extends RawObjectType,
Options extends object,
Extensions extends any[] = []
> = GenerateSubscription<Options, Extensions> &
GenerateSubscription<Options, DefaultExtensions<T>>;
export function createSubscribe<
T extends XenApiRecord<string>,
T extends RawObjectType,
Extensions extends any[],
Options extends object = SubscribeOptions<Extensions>
>(builder: (options?: Options) => Subscription<T, Options, Extensions>) {
@@ -82,59 +85,24 @@ export function createSubscribe<
};
}
export type RawTypeToObject = {
Bond: never;
Certificate: never;
Cluster: never;
Cluster_host: never;
DR_task: never;
Feature: never;
GPU_group: never;
PBD: never;
PCI: never;
PGPU: never;
PIF: never;
PIF_metrics: never;
PUSB: never;
PVS_cache_storage: never;
PVS_proxy: never;
PVS_server: never;
PVS_site: never;
SDN_controller: never;
SM: never;
SR: XenApiSr;
USB_group: never;
VBD: never;
VBD_metrics: never;
VDI: never;
VGPU: never;
VGPU_type: never;
VIF: never;
VIF_metrics: never;
VLAN: never;
VM: XenApiVm;
VMPP: never;
VMSS: never;
VM_guest_metrics: XenApiVmGuestMetrics;
VM_metrics: XenApiVmMetrics;
VUSB: never;
blob: never;
console: XenApiConsole;
crashdump: never;
host: XenApiHost;
host_cpu: never;
host_crashdump: never;
host_metrics: XenApiHostMetrics;
host_patch: never;
message: XenApiMessage;
network: never;
network_sriov: never;
pool: XenApiPool;
pool_patch: never;
pool_update: never;
role: never;
secret: never;
subject: never;
task: XenApiTask;
tunnel: never;
};
export type RawTypeToRecord<T extends RawObjectType> = T extends "SR"
? XenApiSr
: T extends "VM"
? XenApiVm
: T extends "VM_guest_metrics"
? XenApiVmGuestMetrics
: T extends "VM_metrics"
? XenApiVmMetrics
: T extends "console"
? XenApiConsole
: T extends "host"
? XenApiHost
: T extends "host_metrics"
? XenApiHostMetrics
: T extends "message"
? XenApiMessage
: T extends "pool"
? XenApiPool
: T extends "task"
? XenApiTask
: never;

View File

@@ -19,7 +19,8 @@ import { useTaskStore } from "@/stores/task.store";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
const { pendingTasks, finishedTasks, isReady, hasError } = useTaskStore().subscribe();
const { pendingTasks, finishedTasks, isReady, hasError } =
useTaskStore().subscribe();
const { t } = useI18n();
const titleStore = usePageTitleStore();

View File

@@ -1,20 +1,43 @@
<template>
<div v-if="!isReady">Loading...</div>
<div v-else-if="!isVmRunning">Console is only available for running VMs.</div>
<RemoteConsole
v-else-if="vm && vmConsole"
:is-console-available="!isOperationsPending(vm, STOP_OPERATIONS)"
:location="vmConsole.location"
/>
<div :class="{ 'no-ui': !uiStore.hasUi }" class="vm-console-view">
<div v-if="hasError">{{ $t("error-occurred") }}</div>
<UiSpinner v-else-if="!isReady" class="spinner" />
<div v-else-if="!isVmRunning" class="not-running">
<div><img alt="" src="@/assets/monitor.svg" /></div>
{{ $t("power-on-for-console") }}
</div>
<template v-else-if="vm && vmConsole">
<RemoteConsole
:is-console-available="isConsoleAvailable"
:location="vmConsole.location"
class="remote-console"
/>
<div class="open-in-new-window">
<RouterLink
v-if="uiStore.hasUi"
:to="{ query: { ui: '0' } }"
class="link"
target="_blank"
>
<UiIcon :icon="faArrowUpRightFromSquare" />
{{ $t("open-in-new-window") }}
</RouterLink>
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import RemoteConsole from "@/components/RemoteConsole.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import { isOperationsPending } from "@/libs/utils";
import { POWER_STATE, VM_OPERATION, type XenApiVm } from "@/libs/xen-api";
import { useConsoleStore } from "@/stores/console.store";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useUiStore } from "@/stores/ui.store";
import { useVmStore } from "@/stores/vm.store";
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
@@ -32,14 +55,24 @@ const STOP_OPERATIONS = [
usePageTitleStore().setTitle(useI18n().t("console"));
const route = useRoute();
const uiStore = useUiStore();
const { isReady: isVmReady, getByUuid: getVmByUuid } = useVmStore().subscribe();
const {
isReady: isVmReady,
getByUuid: getVmByUuid,
hasError: hasVmError,
} = useVmStore().subscribe();
const { isReady: isConsoleReady, getByOpaqueRef: getConsoleByOpaqueRef } =
useConsoleStore().subscribe();
const {
isReady: isConsoleReady,
getByOpaqueRef: getConsoleByOpaqueRef,
hasError: hasConsoleError,
} = useConsoleStore().subscribe();
const isReady = computed(() => isVmReady.value && isConsoleReady.value);
const hasError = computed(() => hasVmError.value || hasConsoleError.value);
const vm = computed(() => getVmByUuid(route.params.uuid as XenApiVm["uuid"]));
const isVmRunning = computed(
@@ -55,4 +88,74 @@ const vmConsole = computed(() => {
return getConsoleByOpaqueRef(consoleOpaqueRef);
});
const isConsoleAvailable = computed(
() =>
vm.value !== undefined && !isOperationsPending(vm.value, STOP_OPERATIONS)
);
</script>
<style lang="postcss" scoped>
.vm-console-view {
display: flex;
align-items: center;
justify-content: center;
height: calc(100% - 14.5rem);
&.no-ui {
height: 100%;
}
}
.spinner {
color: var(--color-extra-blue-base);
display: flex;
margin: auto;
width: 10rem;
height: 10rem;
}
.remote-console {
flex: 1;
max-width: 100%;
height: 100%;
}
.not-running,
.not-available {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
gap: 4rem;
color: var(--color-extra-blue-base);
font-size: 3.6rem;
}
.open-in-new-window {
position: absolute;
top: 0;
right: 0;
overflow: hidden;
& > .link {
display: flex;
align-items: center;
gap: 1rem;
background-color: var(--color-extra-blue-base);
color: var(--color-blue-scale-500);
text-decoration: none;
padding: 1.5rem;
font-size: 1.6rem;
border-radius: 0 0 0 0.8rem;
white-space: nowrap;
transform: translateX(calc(100% - 4.5rem));
transition: transform 0.2s ease-in-out;
&:hover {
transform: translateX(0);
}
}
}
</style>

View File

@@ -1,7 +1,9 @@
<template>
<ObjectNotFoundWrapper :is-ready="isReady" :uuid-checker="hasUuid">
<VmHeader />
<VmTabBar :uuid="vm!.uuid" />
<template v-if="uiStore.hasUi">
<VmHeader />
<VmTabBar :uuid="vm!.uuid" />
</template>
<RouterView />
</ObjectNotFoundWrapper>
</template>

View File

@@ -14,7 +14,7 @@
"url": "https://vates.fr"
},
"license": "AGPL-3.0-or-later",
"version": "0.10.2",
"version": "0.11.0",
"engines": {
"node": ">=15.6"
},

View File

@@ -30,7 +30,7 @@
"rimraf": "^5.0.1"
},
"dependencies": {
"@vates/read-chunk": "^1.1.1"
"@vates/read-chunk": "^1.2.0"
},
"author": {
"name": "Vates SAS",

View File

@@ -26,7 +26,7 @@
},
"dependencies": {
"@iarna/toml": "^2.2.0",
"@vates/read-chunk": "^1.1.1",
"@vates/read-chunk": "^1.2.0",
"ansi-colors": "^4.1.1",
"app-conf": "^2.3.0",
"content-type": "^1.0.4",

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@xen-orchestra/proxy",
"version": "0.26.29",
"version": "0.26.30",
"license": "AGPL-3.0-or-later",
"description": "XO Proxy used to remotely execute backup jobs",
"keywords": [
@@ -32,18 +32,18 @@
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.4",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.39.0",
"@xen-orchestra/backups": "^0.40.0",
"@xen-orchestra/fs": "^4.0.1",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/mixin": "^0.1.0",
"@xen-orchestra/mixins": "^0.10.2",
"@xen-orchestra/mixins": "^0.11.0",
"@xen-orchestra/self-signed": "^0.1.3",
"@xen-orchestra/xapi": "^2.2.1",
"@xen-orchestra/xapi": "^3.0.0",
"ajv": "^8.0.3",
"app-conf": "^2.3.0",
"async-iterator-to-stream": "^1.1.0",
"fs-extra": "^11.1.0",
"get-stream": "^6.0.0",
"get-stream": "^7.0.1",
"getopts": "^2.2.3",
"golike-defer": "^0.5.1",
"http-server-plus": "^1.0.0",
@@ -60,7 +60,7 @@
"source-map-support": "^0.5.16",
"stoppable": "^1.0.6",
"xdg-basedir": "^5.1.0",
"xen-api": "^1.3.3",
"xen-api": "^1.3.4",
"xo-common": "^0.8.0"
},
"devDependencies": {

View File

@@ -1,11 +1,11 @@
{
"license": "ISC",
"private": false,
"version": "0.2.3",
"version": "0.3.0",
"name": "@xen-orchestra/vmware-explorer",
"dependencies": {
"@vates/node-vsphere-soap": "^1.0.0",
"@vates/read-chunk": "^1.1.1",
"@vates/node-vsphere-soap": "^2.0.0",
"@vates/read-chunk": "^1.2.0",
"@vates/task": "^0.2.0",
"@xen-orchestra/log": "^0.6.0",
"lodash": "^4.17.21",

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/xapi",
"version": "2.2.1",
"version": "3.0.0",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/xapi",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
@@ -16,7 +16,7 @@
},
"main": "./index.mjs",
"peerDependencies": {
"xen-api": "^1.3.3"
"xen-api": "^1.3.4"
},
"scripts": {
"postversion": "npm publish --access public",
@@ -25,10 +25,10 @@
"dependencies": {
"@vates/async-each": "^1.0.0",
"@vates/decorate-with": "^2.0.0",
"@vates/nbd-client": "^1.2.1",
"@vates/nbd-client": "^2.0.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"d3-time-format": "^3.0.0",
"d3-time-format": "^4.1.0",
"golike-defer": "^0.5.1",
"http-request-plus": "^1.0.0",
"json-rpc-protocol": "^0.13.2",

View File

@@ -6,7 +6,7 @@ import { decorateClass } from '@vates/decorate-with'
import { strict as assert } from 'node:assert'
import extractOpaqueRef from './_extractOpaqueRef.mjs'
import NbdClient from '@vates/nbd-client'
import NbdClient from '@vates/nbd-client/client.mjs'
import { createNbdRawStream, createNbdVhdStream } from 'vhd-lib/createStreamNbd.js'
import { VDI_FORMAT_RAW, VDI_FORMAT_VHD } from './index.mjs'

View File

@@ -1,8 +1,67 @@
# ChangeLog
## **5.85.0** (2023-07-31)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Highlights
- [Import/From VMWare] Support ESXi 6.5+ with snapshot (PR [#6909](https://github.com/vatesfr/xen-orchestra/pull/6909))
- [Netbox] New major version. BREAKING: in order for this new version to work, you need to assign the type `virtualization > vminterface` to the custom field `UUID` in your Netbox instance. [See documentation](https://xen-orchestra.com/docs/advanced.html#netbox). [#6038](https://github.com/vatesfr/xen-orchestra/issues/6038) [#6135](https://github.com/vatesfr/xen-orchestra/issues/6135) [#6024](https://github.com/vatesfr/xen-orchestra/issues/6024) [#6036](https://github.com/vatesfr/xen-orchestra/issues/6036) [Forum#6070](https://xcp-ng.org/forum/topic/6070) [Forum#6149](https://xcp-ng.org/forum/topic/6149) [Forum#6332](https://xcp-ng.org/forum/topic/6332) [Forum#6902](https://xcp-ng.org/forum/topic/6902) (PR [#6950](https://github.com/vatesfr/xen-orchestra/pull/6950))
- Synchronize VM description
- Synchronize VM platform
- 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
- [Backup/File restore] Faster and more robust ZIP export
- [Backup/File restore] Add faster tar+gzip (`.tgz`) export
### Enhancements
- [Backup/Restore] Button to open the raw log in the REST API (PR [#6936](https://github.com/vatesfr/xen-orchestra/pull/6936))
- [RPU] Avoid migration of VMs on hosts without missing patches (PR [#6943](https://github.com/vatesfr/xen-orchestra/pull/6943))
- [Settings/Users] Show users authentication methods (PR [#6962](https://github.com/vatesfr/xen-orchestra/pull/6962))
- [Settings/Users] User external authentication methods can be manually removed (PR [#6962](https://github.com/vatesfr/xen-orchestra/pull/6962))
### Bug fixes
- [Incremental Backup & Replication] Attempt to work around HVM multiplier issues when creating VMs on older XAPIs (PR [#6866](https://github.com/vatesfr/xen-orchestra/pull/6866))
- [REST API] Fix VDI export when NBD is enabled
- [XO Config Cloud Backup] Improve wording about passphrase (PR [#6938](https://github.com/vatesfr/xen-orchestra/pull/6938))
- [Pool] Fix IPv6 handling when adding hosts
- [New SR] Send provided NFS version to XAPI when probing a share
- [Backup/exports] Show more information on error ` stream has ended with not enough data (actual: xxx, expected: 512)` (PR [#6940](https://github.com/vatesfr/xen-orchestra/pull/6940))
- [Backup] Fix incremental replication with multiple SRs (PR [#6811](https://github.com/vatesfr/xen-orchestra/pull/6811))
- [New VM] Order interfaces by device as done on a VM Network tab (PR [#6944](https://github.com/vatesfr/xen-orchestra/pull/6944))
- Users can no longer sign in using their XO password if they are using other authentication providers (PR [#6962](https://github.com/vatesfr/xen-orchestra/pull/6962))
### Released packages
- @vates/read-chunk 1.2.0
- @vates/fuse-vhd 2.0.0
- xen-api 1.3.4
- @vates/nbd-client 2.0.0
- @vates/node-vsphere-soap 2.0.0
- @xen-orchestra/xapi 3.0.0
- @xen-orchestra/backups 0.40.0
- @xen-orchestra/backups-cli 1.0.10
- complex-matcher 0.7.1
- @xen-orchestra/mixins 0.11.0
- @xen-orchestra/proxy 0.26.30
- @xen-orchestra/vmware-explorer 0.3.0
- xo-server-audit 0.10.4
- xo-server-netbox 1.0.0
- xo-server-transport-xmpp 0.1.2
- xo-server-auth-github 0.3.0
- xo-server-auth-google 0.3.0
- xo-web 5.122.2
- xo-server 5.120.2
## **5.84.0** (2023-06-30)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
### Highlights
@@ -51,8 +110,6 @@
## **5.83.3** (2023-06-23)
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
### Bug fixes
- [Settings/Servers] Fix connecting using an explicit IPv6 address

View File

@@ -7,31 +7,12 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [Backup/Restore] Button to open the raw log in the REST API (PR [#6936](https://github.com/vatesfr/xen-orchestra/pull/6936))
- [Import/From VMWare] Support ESXi 6.5+ with snapshot (PR [#6909](https://github.com/vatesfr/xen-orchestra/pull/6909))
- [Netbox] New major version. BREAKING: in order for this new version to work, you need to assign the type `virtualization > vminterface` to the custom field `UUID` in your Netbox instance. [See documentation](https://xen-orchestra.com/docs/advanced.html#netbox). [#6038](https://github.com/vatesfr/xen-orchestra/issues/6038) [#6135](https://github.com/vatesfr/xen-orchestra/issues/6135) [#6024](https://github.com/vatesfr/xen-orchestra/issues/6024) [#6036](https://github.com/vatesfr/xen-orchestra/issues/6036) [Forum#6070](https://xcp-ng.org/forum/topic/6070) [Forum#6149](https://xcp-ng.org/forum/topic/6149) [Forum#6332](https://xcp-ng.org/forum/topic/6332) [Forum#6902](https://xcp-ng.org/forum/topic/6902) (PR [#6950](https://github.com/vatesfr/xen-orchestra/pull/6950))
- Synchronize VM description
- Synchronize VM platform
- 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
- [RPU] Avoid migration of VMs on hosts without missing patches (PR [#6943](https://github.com/vatesfr/xen-orchestra/pull/6943))
- [Backup/File restore] Faster and more robust ZIP export
- [Backup/File restore] Add faster tar+gzip (`.tgz`) export
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [Incremental Backup & Replication] Attempt to work around HVM multiplier issues when creating VMs on older XAPIs (PR [#6866](https://github.com/vatesfr/xen-orchestra/pull/6866))
- [REST API] Fix VDI export when NBD is enabled
- [XO Config Cloud Backup] Improve wording about passphrase (PR [#6938](https://github.com/vatesfr/xen-orchestra/pull/6938))
- [Pool] Fix IPv6 handling when adding hosts
- [New SR] Send provided NFS version to XAPI when probing a share
- [Backup/exports] Show more information on error ` stream has ended with not enough data (actual: xxx, expected: 512)` (PR [#6940](https://github.com/vatesfr/xen-orchestra/pull/6940))
- [Backup] Fix incremental replication with multiple SRs (PR [#6811](https://github.com/vatesfr/xen-orchestra/pull/6811))
- [LDAP] Mark the _Id attribute_ setting as required
- [Incremental Replication] Fix `TypeError: Cannot read properties of undefined (reading 'uuid') at #isAlreadyOnHealthCheckSr` [Forum#7492](https://xcp-ng.org/forum/topic/7492) (PR [#6969](https://github.com/vatesfr/xen-orchestra/pull/6969))
### Packages to release
@@ -49,20 +30,10 @@
<!--packages-start-->
- @vates/fuse-vhd major
- @vates/nbd-client major
- @vates/node-vsphere-soap major
- @xen-orchestra/backups minor
- @xen-orchestra/mixins minor
- @xen-orchestra/vmware-explorer minor
- @xen-orchestra/xapi major
- @vates/read-chunk minor
- complex-matcher patch
- @xen-orchestra/backups patch
- xen-api patch
- xo-server minor
- xo-server-transport-xmpp patch
- xo-server-audit patch
- xo-server-netbox major
- xo-web minor
- xo-server patch
- xo-server-auth-ldap patch
- xo-web patch
<!--packages-end-->

View File

@@ -25,7 +25,7 @@
"jest": "^29.0.3",
"lint-staged": "^13.0.3",
"lodash": "^4.17.4",
"prettier": "^2.0.5",
"prettier": "^3.0.1",
"promise-toolbox": "^0.21.0",
"semver": "^7.3.6",
"sorted-object": "^2.0.1",
@@ -98,7 +98,7 @@
"prettify": "prettier --ignore-path .gitignore --ignore-unknown --write .",
"test": "npm run test-lint && npm run test-unit",
"test-integration": "jest \".integ\\.spec\\.js$\" && scripts/run-script.js --parallel test-integration",
"test-lint": "eslint --ignore-path .gitignore --ignore-pattern packages/xo-web .",
"test-lint": "eslint --ignore-path .gitignore --ignore-pattern @xen-orchestra/lite --ignore-pattern packages/xo-web .",
"test-unit": "jest \"^(?!.*\\.integ\\.spec\\.js$)\" && scripts/run-script.js --bail test"
},
"workspaces": [

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "complex-matcher",
"version": "0.7.0",
"version": "0.7.1",
"license": "ISC",
"description": "Advanced search syntax used in XO",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/complex-matcher",

View File

@@ -17,7 +17,7 @@
"dependencies": {
"@vates/async-each": "^1.0.0",
"@vates/diff": "^0.1.0",
"@vates/read-chunk": "^1.1.1",
"@vates/read-chunk": "^1.2.0",
"@vates/stream-reader": "^0.1.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^4.0.1",

View File

@@ -40,7 +40,7 @@
"human-format": "^1.0.0",
"lodash": "^4.17.4",
"pw": "^0.0.4",
"xen-api": "^1.3.3"
"xen-api": "^1.3.4"
},
"devDependencies": {
"@babel/cli": "^7.1.5",

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xen-api",
"version": "1.3.3",
"version": "1.3.4",
"license": "ISC",
"description": "Connector to the Xen API",
"keywords": [

View File

@@ -419,7 +419,7 @@ export class Xapi extends EventEmitter {
signal: $cancelToken,
}),
{
when: error => error.response.statusCode === 302,
when: error => error.response !== undefined && error.response.statusCode === 302,
onRetry: async error => {
const response = error.response
if (response === undefined) {

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-audit",
"version": "0.10.3",
"version": "0.10.4",
"license": "AGPL-3.0-or-later",
"description": "Audit plugin for XO-Server",
"keywords": [

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-auth-github",
"version": "0.2.2",
"version": "0.3.0",
"license": "AGPL-3.0-or-later",
"description": "GitHub authentication plugin for XO-Server",
"keywords": [

View File

@@ -38,7 +38,7 @@ class AuthGitHubXoPlugin {
this._unregisterPassportStrategy = xo.registerPassportStrategy(
new Strategy(this._conf, async (accessToken, refreshToken, profile, done) => {
try {
done(null, await xo.registerUser('github', profile.username))
done(null, await xo.registerUser2('github', { id: profile.id, name: profile.username }))
} catch (error) {
done(error.message)
}

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-auth-google",
"version": "0.2.2",
"version": "0.3.0",
"license": "AGPL-3.0-or-later",
"description": "Google authentication plugin for XO-Server",
"keywords": [

View File

@@ -52,7 +52,10 @@ class AuthGoogleXoPlugin {
try {
done(
null,
await xo.registerUser('google', conf.scope === 'email' ? profile.emails[0].value : profile.displayName)
await xo.registerUser2('google', {
id: profile.id,
name: conf.scope === 'email' ? profile.emails[0].value : profile.displayName,
})
)
} catch (error) {
done(error.message)

View File

@@ -35,7 +35,7 @@
"ensure-array": "^1.0.0",
"exec-promise": "^0.7.0",
"inquirer": "^8.0.0",
"ldapts": "^4.1.0",
"ldapts": "^6.0.0",
"promise-toolbox": "^0.21.0"
},
"devDependencies": {

View File

@@ -11,11 +11,6 @@ const logger = createLogger('xo:xo-server-auth-ldap')
// ===================================================================
const DEFAULTS = {
checkCertificate: true,
filter: '(uid={{name}})',
}
const { escape } = Filter.prototype
const VAR_RE = /\{\{([^}]+)\}\}/g
@@ -55,7 +50,7 @@ If not specified, it will use a default set of well-known CAs.
description:
"Enforce the validity of the server's certificates. You can disable it when connecting to servers that use a self-signed certificate.",
type: 'boolean',
default: DEFAULTS.checkCertificate,
default: true,
},
startTls: {
title: 'Use StartTLS',
@@ -110,7 +105,7 @@ Or something like this if you also want to filter by group:
- \`(&(sAMAccountName={{name}})(memberOf=<group DN>))\`
`.trim(),
type: 'string',
default: DEFAULTS.filter,
default: '(uid={{name}})',
},
userIdAttribute: {
title: 'ID attribute',
@@ -164,7 +159,7 @@ Or something like this if you also want to filter by group:
required: ['base', 'filter', 'idAttribute', 'displayNameAttribute', 'membersMapping'],
},
},
required: ['uri', 'base'],
required: ['uri', 'base', 'userIdAttribute'],
}
export const testSchema = {
@@ -198,7 +193,7 @@ class AuthLdap {
})
{
const { checkCertificate = DEFAULTS.checkCertificate, certificateAuthorities } = conf
const { checkCertificate, certificateAuthorities } = conf
const tlsOptions = (this._tlsOptions = {})
@@ -212,15 +207,7 @@ class AuthLdap {
}
}
const {
bind: credentials,
base: searchBase,
filter: searchFilter = DEFAULTS.filter,
startTls = false,
groups,
uri,
userIdAttribute,
} = conf
const { bind: credentials, base: searchBase, filter: searchFilter, startTls, groups, uri, userIdAttribute } = conf
this._credentials = credentials
this._serverUri = uri
@@ -303,23 +290,17 @@ class AuthLdap {
return
}
let user
if (this._userIdAttribute === undefined) {
// Support legacy config
user = await this._xo.registerUser(undefined, username)
} else {
const ldapId = entry[this._userIdAttribute]
user = await this._xo.registerUser2('ldap', {
user: { id: ldapId, name: username },
})
const ldapId = entry[this._userIdAttribute]
const user = await this._xo.registerUser2('ldap', {
user: { id: ldapId, name: username },
})
const groupsConfig = this._groupsConfig
if (groupsConfig !== undefined) {
try {
await this._synchronizeGroups(user, entry[groupsConfig.membersMapping.userAttribute])
} catch (error) {
logger.error(`failed to synchronize groups: ${error.message}`)
}
const groupsConfig = this._groupsConfig
if (groupsConfig !== undefined) {
try {
await this._synchronizeGroups(user, entry[groupsConfig.membersMapping.userAttribute])
} catch (error) {
logger.error(`failed to synchronize groups: ${error.message}`)
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-netbox",
"version": "0.3.7",
"version": "1.0.0",
"license": "AGPL-3.0-or-later",
"description": "Synchronizes pools managed by Xen Orchestra with Netbox",
"keywords": [

View File

@@ -13,12 +13,27 @@ exports.configurationSchema = {
type: 'object',
properties: {
foo: {
// name of the setting to display in the UI instead of the raw name of property (here `foo`).
title: 'Foo',
// Markdown description for this setting
description: 'Value to use when doing foo',
type: 'string',
},
},
required: ['foo'],
}
// This (optional) dictionary provides example configurations that can be used to help
// configuring this plugin.
//
// The keys are the preset names, and the values are subset of the configuration.
exports.configurationPresets = {
'preset 1': { foo: 'foo value 1' },
'preset 2': { foo: 'foo value 2' },
}
// This (optional) schema is used to test the configuration
// of the plugin.
exports.testSchema = {

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-transport-xmpp",
"version": "0.1.1",
"version": "0.1.2",
"license": "AGPL-3.0-or-later",
"description": "Transport Xmpp plugin for XO-Server",
"keywords": [

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.118.0",
"version": "5.120.2",
"license": "AGPL-3.0-or-later",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -39,20 +39,20 @@
"@vates/otp": "^1.0.0",
"@vates/parse-duration": "^0.1.1",
"@vates/predicates": "^1.1.0",
"@vates/read-chunk": "^1.1.1",
"@vates/read-chunk": "^1.2.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.39.0",
"@xen-orchestra/backups": "^0.40.0",
"@xen-orchestra/cron": "^1.0.6",
"@xen-orchestra/defined": "^0.0.1",
"@xen-orchestra/emit-async": "^1.0.0",
"@xen-orchestra/fs": "^4.0.1",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/mixin": "^0.1.0",
"@xen-orchestra/mixins": "^0.10.2",
"@xen-orchestra/mixins": "^0.11.0",
"@xen-orchestra/self-signed": "^0.1.3",
"@xen-orchestra/template": "^0.1.0",
"@xen-orchestra/vmware-explorer": "^0.2.3",
"@xen-orchestra/xapi": "^2.2.1",
"@xen-orchestra/vmware-explorer": "^0.3.0",
"@xen-orchestra/xapi": "^3.0.0",
"ajv": "^8.0.3",
"app-conf": "^2.3.0",
"async-iterator-to-stream": "^1.0.1",
@@ -60,7 +60,7 @@
"blocked-at": "^1.2.0",
"bluebird": "^3.5.1",
"body-parser": "^1.18.2",
"complex-matcher": "^0.7.0",
"complex-matcher": "^0.7.1",
"compression": "^1.7.3",
"connect-flash": "^0.1.1",
"content-type": "^1.0.4",
@@ -76,7 +76,7 @@
"fast-xml-parser": "^4.0.0",
"fatfs": "^0.10.4",
"fs-extra": "^11.1.0",
"get-stream": "^6.0.0",
"get-stream": "^7.0.1",
"golike-defer": "^0.5.1",
"hashy": "^0.11.1",
"helmet": "^3.9.0",
@@ -131,7 +131,7 @@
"vhd-lib": "^4.5.0",
"ws": "^8.2.3",
"xdg-basedir": "^5.1.0",
"xen-api": "^1.3.3",
"xen-api": "^1.3.4",
"xo-acl-resolver": "^0.4.1",
"xo-collection": "^0.5.0",
"xo-common": "^0.8.0",

View File

@@ -1,10 +1,10 @@
import * as multiparty from 'multiparty'
import assert from 'assert'
import getStream from 'get-stream'
import hrp from 'http-request-plus'
import { createLogger } from '@xen-orchestra/log'
import { defer } from 'golike-defer'
import { format, JsonRpcError } from 'json-rpc-peer'
import { getStreamAsBuffer } from 'get-stream'
import { invalidParameters, noSuchObject } from 'xo-common/api-errors.js'
import { pipeline } from 'stream'
import { peekFooterFromVhdStream } from 'vhd-lib'
@@ -187,7 +187,7 @@ async function handleImport(req, res, { type, name, description, vmdkData, srId,
if (part.name !== 'file') {
promises.push(
(async () => {
const buffer = await getStream.buffer(part)
const buffer = await getStreamAsBuffer(part)
vmdkData[part.name] = new Uint32Array(
buffer.buffer,
buffer.byteOffset,

View File

@@ -80,7 +80,7 @@ set.params = {
},
shareByDefault: {
type: 'boolean',
optional: true
optional: true,
},
subjects: {
type: 'array',

View File

@@ -112,3 +112,16 @@ changePassword.params = {
oldPassword: { type: 'string' },
newPassword: { type: 'string' },
}
// -------------------------------------------------------------------
export async function removeAuthProvider({ id, authProvider }) {
await this.updateUser(id, { authProviders: { [authProvider]: null } })
}
removeAuthProvider.permission = 'admin'
removeAuthProvider.params = {
authProvider: { type: 'string' },
id: { type: 'string' },
}

View File

@@ -4,12 +4,12 @@ import { asyncEach } from '@vates/async-each'
import asyncMapSettled from '@xen-orchestra/async-map/legacy.js'
import { Task } from '@xen-orchestra/mixins/Tasks.mjs'
import concat from 'lodash/concat.js'
import getStream from 'get-stream'
import hrp from 'http-request-plus'
import { createLogger } from '@xen-orchestra/log'
import { defer } from 'golike-defer'
import { format } from 'json-rpc-peer'
import { FAIL_ON_QUEUE } from 'limit-concurrency-decorator'
import { getStreamAsBuffer } from 'get-stream'
import { ignoreErrors } from 'promise-toolbox'
import { invalidParameters, noSuchObject, operationFailed, unauthorized } from 'xo-common/api-errors.js'
import { Ref } from 'xen-api'
@@ -1224,7 +1224,7 @@ async function handleVmImport(req, res, { data, srId, type, xapi }) {
if (!(part.filename in tables)) {
tables[part.filename] = {}
}
const buffer = await getStream.buffer(part)
const buffer = await getStreamAsBuffer(part)
tables[part.filename][part.name] = new Uint32Array(
buffer.buffer,
buffer.byteOffset,

View File

@@ -1,6 +1,6 @@
import * as CM from 'complex-matcher'
import getStream from 'get-stream'
import { fromCallback } from 'promise-toolbox'
import { getStreamAsBuffer } from 'get-stream'
import { pipeline } from 'readable-stream'
import createNdJsonStream from '../_createNdJsonStream.mjs'
@@ -81,7 +81,7 @@ getAllObjects.params = {
export async function importConfig({ passphrase }) {
return {
$sendTo: await this.registerHttpRequest(async (req, res) => {
await this.importConfig(await getStream.buffer(req), { passphrase })
await this.importConfig(await getStreamAsBuffer(req), { passphrase })
res.end('config successfully imported')
}),

View File

@@ -46,6 +46,7 @@ xo-server-recover-account <user name or email>
const user = await xo.getUserByName(name, true)
if (user !== null) {
await xo.updateUser(user.id, {
authProviders: null,
password,
permission: 'admin',
preferences: { otp: null },

View File

@@ -113,8 +113,6 @@ export default class {
// - `userId`
// - optionally `expiration` to indicate when the session is no longer
// valid
// - an object with a property `username` containing the name
// of the authenticated user
const result = await provider(credentials, userData)
// No match.
@@ -122,10 +120,10 @@ export default class {
continue
}
const { userId, username, expiration } = result
const { userId, expiration } = result
return {
user: await (userId !== undefined ? this._app.getUser(userId) : this._app.registerUser(undefined, username)),
user: await this._app.getUser(userId),
expiration,
}
} catch (error) {

View File

@@ -150,8 +150,8 @@ export default class {
if (permission) {
user.permission = permission
}
if (password) {
user.pw_hash = await hash(password)
if (password !== undefined) {
user.pw_hash = password === null ? undefined : await hash(password)
}
const newPreferences = { ...user.preferences }
@@ -164,15 +164,33 @@ export default class {
})
user.preferences = isEmpty(newPreferences) ? undefined : newPreferences
const newAuthProviders = { ...user.authProviders }
forEach(authProviders, (value, name) => {
if (value == null) {
delete newAuthProviders[name]
} else {
newAuthProviders[name] = value
if (authProviders !== undefined) {
let newAuthProviders
if (authProviders !== null) {
newAuthProviders = { ...user.authProviders }
forEach(authProviders, (value, name) => {
if (value == null) {
delete newAuthProviders[name]
} else {
newAuthProviders[name] = value
}
})
}
})
user.authProviders = isEmpty(newAuthProviders) ? undefined : newAuthProviders
user.authProviders = isEmpty(newAuthProviders) ? undefined : newAuthProviders
}
// if updating either authProviders or password, check consistency
if (
(authProviders !== undefined || password !== undefined) &&
user.pw_hash !== undefined &&
!isEmpty(user.authProviders)
) {
throw new Error('user cannot have both password and auth providers')
}
if (user.pw_hash === undefined && isEmpty(user.authProviders) && id === this._app.apiContext?.user.id) {
throw new Error('current user cannot be without password and auth providers')
}
// TODO: remove
user.email = user.name
@@ -221,26 +239,8 @@ export default class {
throw noSuchObject(username, 'user')
}
// Deprecated: use registerUser2 instead
// Get or create a user associated with an auth provider.
async registerUser(provider, name) {
const user = await this.getUserByName(name, true)
if (user) {
if (user._provider !== provider) {
throw new Error(`the name ${name} is already taken`)
}
return user
}
if (!this._app.config.get('createUserOnFirstSignin')) {
throw new Error(`registering ${name} user is forbidden`)
}
return /* await */ this.createUser({
name,
_provider: provider,
})
async registerUser() {
throw new Error('use registerUser2 instead')
}
// New implementation of registerUser that:
@@ -306,6 +306,7 @@ export default class {
data: data !== undefined ? data : user.authProviders?.[providerId]?.data,
},
},
password: null,
})
}
@@ -321,8 +322,8 @@ export default class {
}
async checkUserPassword(userId, password, updateIfNecessary = true) {
const { pw_hash: hash } = await this.getUser(userId)
if (!(hash && (await verify(password, hash)))) {
const { authProviders, pw_hash: hash } = await this.getUser(userId)
if (!(hash !== undefined && isEmpty(authProviders) && (await verify(password, hash)))) {
return false
}

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-web",
"version": "5.121.0",
"version": "5.122.2",
"license": "AGPL-3.0-or-later",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -53,7 +53,7 @@
"chartist-plugin-legend": "^0.6.1",
"chartist-plugin-tooltip": "0.0.11",
"classnames": "^2.2.3",
"complex-matcher": "^0.7.0",
"complex-matcher": "^0.7.1",
"copy-to-clipboard": "^3.0.8",
"d3": "^5.0.0",
"debounce-input-decorator": "^1.0.0",

View File

@@ -733,7 +733,7 @@ const messages = {
userGroupsColumn: 'Member of',
userCountGroups: '{nGroups, number} group{nGroups, plural, one {} other {s}}',
userPermissionColumn: 'Permissions',
userPasswordColumn: 'Password',
userAuthColumn: 'Password / Authentication methods',
userName: 'Username',
userPassword: 'Password',
createUserButton: 'Create',

View File

@@ -533,8 +533,7 @@ const xoItemToRender = {
<span>
<Icon icon='xo-cloud-config' /> <ShortDate timestamp={createdAt} />
</span>
)
,
),
// XO objects.
pool: props => <Pool {...props} />,

View File

@@ -2964,6 +2964,14 @@ export const deleteUsers = users =>
export const editUser = (user, { email, password, permission }) =>
_call('user.set', { id: resolveId(user), email, password, permission })::tap(subscribeUsers.forceRefresh)
export const removeUserAuthProvider = ({ userId, authProviderId }) => {
_call('user.removeAuthProvider', { id: userId, authProvider: authProviderId })
::tap(subscribeUsers.forceRefresh)
.catch(e => {
error('user.removeAuthProvider', e.message)
})
}
const _signOutFromEverywhereElse = () =>
_call('token.delete', {
pattern: {

View File

@@ -546,12 +546,16 @@ export default class NewVm extends BaseComponent {
let VIFs = []
const defaultNetworkIds = this._getDefaultNetworkIds(template)
forEach(template.VIFs, vifId => {
const vif = getObject(storeState, vifId, resourceSet)
VIFs.push({
network: pool || isInResourceSet(vif.$network) ? vif.$network : defaultNetworkIds[0],
})
})
forEach(
// iterate template VIFs in device order
template.VIFs.map(id => getObject(storeState, id, resourceSet)).sort((a, b) => a.device - b.device),
vif => {
VIFs.push({
network: pool || isInResourceSet(vif.$network) ? vif.$network : defaultNetworkIds[0],
})
}
)
if (VIFs.length === 0) {
VIFs = defaultNetworkIds.map(id => ({ network: id }))
}

View File

@@ -11,6 +11,7 @@ import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import Link from 'link'
import map from 'lodash/map'
import merge from 'lodash/merge'
import orderBy from 'lodash/orderBy'
import pFinally from 'promise-toolbox/finally'
import React from 'react'
@@ -105,7 +106,7 @@ class Plugin extends Component {
_applyPredefinedConfiguration = () => {
const configName = this.refs.selectPredefinedConfiguration.value
this.setState({
editedConfig: this.props.configurationPresets[configName],
editedConfig: merge(undefined, this.state.editedConfig, this.props.configurationPresets[configName]),
})
}
@@ -167,7 +168,7 @@ class Plugin extends Component {
<p>{_('pluginConfigurationChoosePreset')}</p>
</span>
<div className='input-group'>
<select className='form-control' disabled={!editedConfig} ref='selectPredefinedConfiguration'>
<select className='form-control' ref='selectPredefinedConfiguration'>
{map(configurationPresets, (_, name) => (
<option key={name} value={name}>
{name}
@@ -175,7 +176,7 @@ class Plugin extends Component {
))}
</select>
<span className='input-group-btn'>
<Button btnStyle='primary' disabled={!editedConfig} onClick={this._applyPredefinedConfiguration}>
<Button btnStyle='primary' onClick={this._applyPredefinedConfiguration}>
{_('applyPluginPreset')}
</Button>
</span>

View File

@@ -6,6 +6,7 @@ import Component from 'base-component'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import keyBy from 'lodash/keyBy'
import Link from 'link'
import map from 'lodash/map'
import React from 'react'
import renderXoItem from 'render-xo-item'
@@ -16,7 +17,16 @@ import { get } from '@xen-orchestra/defined'
import { injectIntl } from 'react-intl'
import { Password, Select } from 'form'
import { createUser, deleteUser, deleteUsers, editUser, removeOtp, subscribeGroups, subscribeUsers } from 'xo'
import {
createUser,
deleteUser,
deleteUsers,
editUser,
removeOtp,
removeUserAuthProvider,
subscribeGroups,
subscribeUsers,
} from 'xo'
const permissions = {
none: {
@@ -76,9 +86,36 @@ const USER_COLUMNS = [
sortCriteria: user => user.permission,
},
{
name: _('userPasswordColumn'),
itemRenderer: user =>
isEmpty(user.authProviders) && <Editable.Password onChange={password => editUser(user, { password })} value='' />,
name: _('userAuthColumn'),
itemRenderer: user => {
const { authProviders } = user
return isEmpty(authProviders) ? (
<Editable.Password onChange={password => editUser(user, { password })} value='' />
) : (
<ul className='list-group'>
{Object.keys(authProviders)
.sort()
.map(id => {
const shortId = id.split(':')[0]
const plugin = 'auth-' + shortId
return (
<li key={id} className='list-group-item'>
<Link to={`/settings/plugins/?s=${encodeURIComponent(`name=^${plugin}$`)}`}>{shortId}</Link>
<ActionButton
className='pull-right'
btnStyle='warning'
size='small'
icon='remove'
handler={removeUserAuthProvider}
data-userId={user.id}
data-authProviderId={id}
/>
</li>
)
})}
</ul>
)
},
},
{
name: 'OTP',

3142
yarn.lock

File diff suppressed because it is too large Load Diff