Compare commits

..

78 Commits

Author SHA1 Message Date
Thierry
677a9c958c feat(lite/component): add support for nested modals 2023-07-04 08:59:16 +02:00
Pierre Donias
2978ad1486 feat(lite): 0.1.1 (#6930) 2023-07-03 15:58:17 +02:00
Julien Fontanet
c0d6dc48de feat(xo-web/XO tasks): better display of start date and duration 2023-07-01 10:30:44 +02:00
Julien Fontanet
f327422254 feat: release 5.84.0 2023-06-30 20:09:44 +02:00
Julien Fontanet
938d15d31b feat(xo-web): 5.121.0 2023-06-30 19:22:38 +02:00
Julien Fontanet
5ab1ddb9cb feat(xo-server): 5.118.0 2023-06-30 19:20:29 +02:00
Mathieu
01302d7a60 feat(xo-web/settings/config): cloud backup (#6917) 2023-06-30 19:09:56 +02:00
Julien Fontanet
c68630e2d6 feat(xo-server/rest-api): provide a way to extend it 2023-06-30 18:19:09 +02:00
Julien Fontanet
db082bfbe9 fix(xo-server/rest-api): handle ids that are numbers instead of strings 2023-06-30 18:19:09 +02:00
Julien Fontanet
650d88db46 feat(xo-server/configurePlugin): can update instead of replace existing config 2023-06-30 18:19:09 +02:00
Julien Fontanet
7d1ecca669 feat(xo-server): consider *passphrase* a sensitive value 2023-06-30 18:19:09 +02:00
Thierry Goettelmann
5f71e629ae fix(lite/components): app-menu doesn't allow more than 1 submenu (#6897) 2023-06-30 15:47:56 +02:00
rbarhtaoui
68205d4676 feat(xo-web/export,import VDI): explicit import/export raw VDI (#6925)
See zammad#15254
2023-06-30 15:10:30 +02:00
Mathieu
cdb466225d feat(xo-web,xo-server): import ISO VDI from url (#6924)
Related to zammad#15254
2023-06-30 13:47:43 +02:00
Julien Fontanet
0e7fbd598f feat(docs/rest-api): alpha → beta 2023-06-30 12:00:14 +02:00
Mathieu
99147c893d feat(xo-web): add tooltip on BulkIcons (#6895) 2023-06-29 10:56:26 +02:00
Mathieu
c63fb6173d feat(xo-web/import/disk): UI improvement for ISO files (#6874)
See https://xcp-ng.org/forum/topic/7243
2023-06-29 10:51:16 +02:00
Pierre Donias
5932ada717 chore(node-vsphere-soap): make pkg public (#6923)
Make package public and run normalize-packages on it to add the `postversion`
script to its `package.json`.
2023-06-29 10:45:06 +02:00
Mathieu
0d579748d6 fix(lite): replace 'change-power-state' by 'change-state' (#6922) 2023-06-29 10:13:02 +02:00
Pierre Donias
8c5ee4eafe feat: technical release (#6921)
* feat(@xen-orchestra/fs): 4.0.1

* feat(xen-api): 1.3.3

* feat(@vates/nbd-client): 1.2.1

* feat(@vates/node-vsphere-soap): 1.0.0

* feat(@vates/task): 0.2.0

* feat(@xen-orchestra/backups): 0.39.0

* feat(@xen-orchestra/backups-cli): 1.0.9

* feat(@xen-orchestra/mixins): 0.10.2

* feat(@xen-orchestra/proxy): 0.26.29

* feat(@xen-orchestra/vmware-explorer): 0.2.3

* feat(xo-cli): 0.20.0

* feat(xo-server): 5.117.0

* feat(xo-server-auth-oidc): 0.3.0

* feat(xo-server-perf-alert): 0.3.6

* feat(xo-web): 5.120.0

* chore(CHANGELOG): update next
2023-06-28 17:10:22 +02:00
Florent BEAUCHAMP
b03935ad2f feat(backups): can limit parallel VDI transfers per VM per job (#6787) 2023-06-28 16:47:39 +02:00
Mathieu
38439cbc43 fix(xo-web): enhance RRD stats (#6903)
- fix infinite requests
- avoid duplicate requests
2023-06-28 15:17:00 +02:00
Florent BEAUCHAMP
161c20b534 feat(xo-server): add MBR to cloud-init drive (#6889) 2023-06-28 10:42:01 +02:00
Julien Fontanet
603696dad1 fix(xo-server/rest-api): reply with 204 when non content 2023-06-27 14:43:27 +02:00
Julien Fontanet
6b2ad5a7cc feat(xo-cli rest get): new --output parameter
It can be used to save the response in a file instead of parsing it.
2023-06-27 14:43:27 +02:00
Julien Fontanet
88063d4d87 fix(xo-cli rest): params now support the json: prefix
So that any values can be passed.
2023-06-26 16:21:01 +02:00
Julien Fontanet
8956a99745 feat(xo-cli rest): support patch method 2023-06-26 16:09:32 +02:00
Florent BEAUCHAMP
0f0c0ec0d0 fix(vmware-explorer): handle selef signed certifictae during download (#6908) 2023-06-26 14:24:37 +02:00
Florent BEAUCHAMP
e5932e2c33 fix(node-vsphere-soap): don't disable TLS1.2 used by ESXi (#6913) 2023-06-26 11:31:24 +02:00
Julien Fontanet
84ec8f5f3c fix(mixins/HttpProxy): fix premature close warning 2023-06-26 10:47:34 +02:00
Julien Fontanet
661c5a269f fix(mixins/HttpProxy): fix excess event listeners warning 2023-06-26 10:47:31 +02:00
Julien Fontanet
5c6d7cae66 feat(mixins/HttpProxy): debug when proxy is enabled/disabled 2023-06-26 10:41:57 +02:00
Julien Fontanet
fcc73859b7 test(node-vsphere-soap): use test
Instead of old `lab` which has a lot of vulnerable dependencies.
2023-06-23 17:42:28 +02:00
Julien Fontanet
36645b0319 test(node-vsphere-soap): use native assert
Instead of old `code` which has a lot of vulnerable dependencies.
2023-06-23 17:35:22 +02:00
Florent BEAUCHAMP
a62575e3cf docs(backups): new terminology and mirror backups (#6837)
Co-authored-by: Mathieu <70369997+MathieuRA@users.noreply.github.com>
Co-authored-by: Jon Sands <fohdeesha@gmail.com>
2023-06-23 16:33:10 +02:00
Julien Fontanet
d7af3d3c03 fix(CHANGELOG): 5.83.4 → 5.83.3 2023-06-23 14:21:45 +02:00
Julien Fontanet
130ebb7d5f Merge remote-tracking branch 'origin/5.83' 2023-06-23 14:15:34 +02:00
Julien Fontanet
2af845ebd3 feat: release 5.83.3 2023-06-23 11:11:33 +02:00
Julien Fontanet
8e4d1701e6 feat(xo-server): 5.116.4 2023-06-23 11:09:21 +02:00
Julien Fontanet
4d16b6708f feat(@xen-orchestra/proxy): 0.26.28 2023-06-23 11:09:21 +02:00
Julien Fontanet
34ee08be25 feat(@xen-orchestra/backups): 0.38.3 2023-06-23 11:09:20 +02:00
Julien Fontanet
d66a76a09e feat(xen-api): 1.3.2 2023-06-23 11:09:02 +02:00
Florent BEAUCHAMP
0d801c9766 fix(backups): fix DR not deleting older VM (#6912)
Introduced by aa36629def
2023-06-23 10:59:51 +02:00
Julien Fontanet
b82b676fdb fix(xen-api/transports/json-rpc): fix IPv6 address support
Introduced by ab96c549a
2023-06-23 10:59:27 +02:00
Gabriel Gunullu
3494c0f64f fix(xo-server-perf-alert): add conditional statement on entry (#6900)
* fix(xo-server-perf-alert): add conditional statement on entry

Test if the entry is null to handle the case where the object cannot be found,
which can happen when the user forgets to remove an element that doesn't exist anymore from
the list of the monitored machines.

Co-authored-by: Florent BEAUCHAMP <florent.beauchamp@vates.fr>

---------

Co-authored-by: Florent BEAUCHAMP <florent.beauchamp@vates.fr>
2023-06-23 09:04:32 +02:00
Florent BEAUCHAMP
311098adc2 feat(backups): use the right SR for health check during replication (#6902) 2023-06-22 11:35:47 +02:00
Julien Fontanet
58182e2083 fix(xen-api/transports/json-rpc): fix IPv6 address support
Introduced by ab96c549a
2023-06-22 11:08:50 +02:00
Julien Fontanet
a62ae43274 feat(xen-api/cli): allow specifying transport 2023-06-22 11:02:15 +02:00
Julien Fontanet
f256610e08 fix(xo-web): don't test a disabled remote after editing
Fixes https://team.vates.fr/vates/pl/xxezjup7efr7idcur9qtftcgfe
2023-06-22 08:43:04 +02:00
Gabriel Gunullu
983d048219 feat(xo-web/kubernetes): add version selection (#6880)
Fixes #6842
See xoa#122
2023-06-21 14:10:47 +02:00
Julien Fontanet
3c6033f904 fix(xo-server): close connections of deleted users
Fixes #5235
2023-06-21 12:03:06 +02:00
Julien Fontanet
ef2bd2b59d fix(xo-server): better token check on HTTP request
It now checks that the user associated with the authentication token really exists.

This fixes xo-web infinite refresh when the token stored in cookies belongs to a missing user.
2023-06-21 12:03:06 +02:00
Julien Fontanet
04d70e9aa8 chore: update dev deps 2023-06-20 18:09:09 +02:00
Julien Fontanet
a2587ffc0a fix(CHANGELOG.unreleased): missing release type for vmware-explorer
Introduced by 4c0506429
2023-06-19 09:40:33 +02:00
Julien Fontanet
6776e7bb3d fix(CHANGELOG.unreleased): missing release type for vmware-explorer
Introduced by 4c0506429
2023-06-19 09:39:53 +02:00
Florent BEAUCHAMP
4c05064294 feat(vmware-exporer): use @vates/node-vsphere-soap 2023-06-19 09:31:07 +02:00
Florent BEAUCHAMP
c135f1394f fix(node-vsphere-soap): disable tests since they need a running vsphere/esxi 2023-06-19 09:31:07 +02:00
Florent BEAUCHAMP
d68f4215f1 fix(node-vsphere-soap): better handling of self signed cert 2023-06-19 09:31:07 +02:00
Florent BEAUCHAMP
af562f3c3a chore(node-vsphere-soap): fix lint issues 2023-06-19 09:31:07 +02:00
Julien Fontanet
7b949716bc chore(node-vsphere-soap): format with Prettier 2023-06-19 09:31:07 +02:00
Florent BEAUCHAMP
d3e256289b feat(node-vsphere-soap): fork 2023-06-19 09:31:07 +02:00
Gabriel Gunullu
3688e762b1 fix(xo-web/kubernetes): change recipe description (#6878)
Introduced by eb84d4a7ef
2023-06-16 11:37:35 +02:00
Julien Fontanet
249f1a7af4 feat(backups/XO metadata): store data filename in metadata 2023-06-16 10:40:04 +02:00
Thierry Goettelmann
2de26030ff chore(lite): add type branding to XAPI record's $ref & uuid (#6884)
Type branding enhances our type safety by preventing the incorrect usage of
`XenApiRecord`'s `$ref` and `uuid`. It ensures that these types are not
interchangeable.
2023-06-15 14:01:27 +02:00
Mathieu
fcc76fb8d0 fix(xo-web/home): fix 'isHostTimeConsistentWithXoaTime.then is not a function' (#6896)
See xoa-support#15250
Introduced by 132b1a41db
2023-06-15 10:07:56 +02:00
Julien Fontanet
88d5b7095e feat(xo-web/dashboard/health): copiable orphan VDI UUIDs (#6893)
Fixes internal request by @Fohdeesha https://team.vates.fr/vates/pl/p1nsuy8gzpgxtxwrqhdzocpiaw
2023-06-15 09:45:19 +02:00
Julien Fontanet
b0e55d88de feat(xo-web): clearer display to choose new backup job type (#6894)
Fixes https://team.vates.fr/vates/pl/xsj49jtmdfgp5god81ninumr6o

- explicit replication
- separate VM and metadata backup types
- homogenize button labels
2023-06-14 10:50:59 +02:00
Mathieu
370ad3e928 feat(lite): implement "closing-confirmation" store (#6883) 2023-06-14 10:45:44 +02:00
rbarhtaoui
07bf77d2dd feat(lite/pool/VMs): ability to delete selected VMs (#6860) 2023-06-14 10:32:15 +02:00
Thierry Goettelmann
a5ec65f3c0 fix(lite): eslint error "duplicate key" (#6891) 2023-06-13 13:58:35 +02:00
Thierry Goettelmann
522b318fd9 feat(lite/dev): add keyboard shortcut to toggle language (#6888)
To make development easier, add the ability to toggle language between FR and EN
while in development mode by pressing the `L` key (the same way we can toggle
light/dark theme with `D` key)
2023-06-13 10:45:26 +02:00
Julien Fontanet
9eb2a4033f feat(xo-server-auth-oidc): make scopes configurable and include profile by default
Fixes https://xcp-ng.org/forum/post/62185
2023-06-12 22:22:47 +02:00
Julien Fontanet
e87b0c393a chore: update dev deps 2023-06-12 22:00:52 +02:00
Mathieu
1fb7e665fa fix(xo-web/home/pool): switch alert support from 'danger' to 'warning' (#6849)
Harmonize with the host home view.
2023-06-12 11:49:47 +02:00
Thierry Goettelmann
7ea476d787 feat(lite): add alarm store (#6814) 2023-06-12 10:39:37 +02:00
Thierry Goettelmann
8260d07d61 fix(lite/i18n): "coming soon" (#6887) 2023-06-12 10:39:11 +02:00
rbarhtaoui
ac0b4e6514 fix(lite/login): fix transparent login button (#6879) 2023-06-12 10:37:47 +02:00
Pierre Donias
27b2f8cf27 docs(netbox): troubleshooting tip for 403 Forbidden (#6882) 2023-06-12 09:42:25 +02:00
138 changed files with 4178 additions and 2341 deletions

View File

@@ -13,7 +13,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "1.2.0",
"version": "1.2.1",
"engines": {
"node": ">=14.0"
},
@@ -23,7 +23,7 @@
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"promise-toolbox": "^0.21.0",
"xen-api": "^1.3.1"
"xen-api": "^1.3.3"
},
"devDependencies": {
"tap": "^16.3.0",

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 reedog117
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,127 @@
forked from https://github.com/reedog117/node-vsphere-soap
# node-vsphere-soap
[![Join the chat at https://gitter.im/reedog117/node-vsphere-soap](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/reedog117/node-vsphere-soap?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
This is a Node.js module to connect to VMware vCenter servers and/or ESXi hosts and perform operations using the [vSphere Web Services API]. If you're feeling really adventurous, you can use this module to port vSphere operations from other languages (such as the Perl, Python, and Go libraries that exist) and have fully native Node.js code controlling your VMware virtual infrastructure!
This is very much in alpha.
## Authors
- Patrick C - [@reedog117]
## Version
0.0.2-5
## Installation
```sh
$ npm install node-vsphere-soap --save
```
## Sample Code
### To connect to a vCenter server:
var nvs = require('node-vsphere-soap');
var vc = new nvs.Client(host, user, password, sslVerify);
vc.once('ready', function() {
// perform work here
});
vc.once('error', function(err) {
// handle error here
});
#### Arguments
- host = hostname or IP of vCenter/ESX/ESXi server
- user = username
- password = password
- sslVerify = true|false - set to false if you have self-signed/unverified certificates
#### Events
- ready = emits when session authenticated with server
- error = emits when there's an error
- _err_ contains the error
#### Client instance variables
- serviceContent - ServiceContent object retrieved by RetrieveServiceContent API call
- userName - username of authenticated user
- fullName - full name of authenticated user
### To run a command:
var vcCmd = vc.runCommand( commandToRun, arguments );
vcCmd.once('result', function( result, raw, soapHeader) {
// handle results
});
vcCmd.once('error', function( err) {
// handle errors
});
#### Arguments
- commandToRun = Method from the vSphere API
- arguments = JSON document containing arguments to send
#### Events
- result = emits when session authenticated with server
- _result_ contains the JSON-formatted result from the server
- _raw_ contains the raw SOAP XML response from the server
- _soapHeader_ contains any soapHeaders from the server
- error = emits when there's an error
- _err_ contains the error
Make sure you check out tests/vsphere-soap.test.js for examples on how to create commands to run
## Development
node-vsphere-soap uses a number of open source projects to work properly:
- [node.js] - evented I/O for the backend
- [node-soap] - SOAP client for Node.js
- [soap-cookie] - cookie authentication for the node-soap module
- [lodash] - for quickly manipulating JSON
- [lab] - testing engine
- [code] - assertion engine used with lab
Want to contribute? Great!
### Todo's
- Write More Tests
- Create Travis CI test harness with a fake vCenter Instance
- Add Code Comments
### Testing
I have been testing on a Mac with node v0.10.36 and both ESXi and vCenter 5.5.
To edit tests, edit the file **test/vsphere-soap.test.js**
To point the module at your own vCenter/ESXi host, edit **config-test.stub.js** and save it as **config-test.js**
To run test scripts:
```sh
$ npm test
```
## License
MIT
[vSphere Web Services API]: http://pubs.vmware.com/vsphere-55/topic/com.vmware.wssdk.apiref.doc/right-pane.html
[node-soap]: https://github.com/vpulim/node-soap
[node.js]: http://nodejs.org/
[soap-cookie]: https://github.com/shanestillwell/soap-cookie
[code]: https://github.com/hapijs/code
[lab]: https://github.com/hapijs/lab
[lodash]: https://lodash.com/
[@reedog117]: http://www.twitter.com/reedog117

View File

@@ -0,0 +1,231 @@
'use strict'
/*
node-vsphere-soap
client.js
This file creates the Client class
- when the class is instantiated, a connection will be made to the ESXi/vCenter server to verify that the creds are good
- upon a bad login, the connnection will be terminated
*/
const EventEmitter = require('events').EventEmitter
const axios = require('axios')
const https = require('node:https')
const util = require('util')
const soap = require('soap')
const Cookie = require('soap-cookie') // required for session persistence
// Client class
// inherits from EventEmitter
// possible events: connect, error, ready
function Client(vCenterHostname, username, password, sslVerify) {
this.status = 'disconnected'
this.reconnectCount = 0
sslVerify = typeof sslVerify !== 'undefined' ? sslVerify : false
EventEmitter.call(this)
// sslVerify argument handling
if (sslVerify) {
this.clientopts = {}
} else {
this.clientopts = {
request: axios.create({
httpsAgent: new https.Agent({
rejectUnauthorized: false,
}),
}),
}
}
this.connectionInfo = {
host: vCenterHostname,
user: username,
password,
sslVerify,
}
this._loginArgs = {
userName: this.connectionInfo.user,
password: this.connectionInfo.password,
}
this._vcUrl = 'https://' + this.connectionInfo.host + '/sdk/vimService.wsdl'
// connect to the vCenter / ESXi host
this.on('connect', this._connect)
this.emit('connect')
// close session
this.on('close', this._close)
return this
}
util.inherits(Client, EventEmitter)
Client.prototype.runCommand = function (command, args) {
const self = this
let cmdargs
if (!args || args === null) {
cmdargs = {}
} else {
cmdargs = args
}
const emitter = new EventEmitter()
// check if client has successfully connected
if (self.status === 'ready' || self.status === 'connecting') {
self.client.VimService.VimPort[command](cmdargs, function (err, result, raw, soapHeader) {
if (err) {
_soapErrorHandler(self, emitter, command, cmdargs, err)
}
if (command === 'Logout') {
self.status = 'disconnected'
process.removeAllListeners('beforeExit')
}
emitter.emit('result', result, raw, soapHeader)
})
} else {
// if connection not ready or connecting, reconnect to instance
if (self.status === 'disconnected') {
self.emit('connect')
}
self.once('ready', function () {
self.client.VimService.VimPort[command](cmdargs, function (err, result, raw, soapHeader) {
if (err) {
_soapErrorHandler(self, emitter, command, cmdargs, err)
}
if (command === 'Logout') {
self.status = 'disconnected'
process.removeAllListeners('beforeExit')
}
emitter.emit('result', result, raw, soapHeader)
})
})
}
return emitter
}
Client.prototype.close = function () {
const self = this
self.emit('close')
}
Client.prototype._connect = function () {
const self = this
if (self.status !== 'disconnected') {
return
}
self.status = 'connecting'
soap.createClient(
self._vcUrl,
self.clientopts,
function (err, client) {
if (err) {
self.emit('error', err)
throw err
}
self.client = client // save client for later use
self
.runCommand('RetrieveServiceContent', { _this: 'ServiceInstance' })
.once('result', function (result, raw, soapHeader) {
if (!result.returnval) {
self.status = 'disconnected'
self.emit('error', raw)
return
}
self.serviceContent = result.returnval
self.sessionManager = result.returnval.sessionManager
const loginArgs = { _this: self.sessionManager, ...self._loginArgs }
self
.runCommand('Login', loginArgs)
.once('result', function (result, raw, soapHeader) {
self.authCookie = new Cookie(client.lastResponseHeaders)
self.client.setSecurity(self.authCookie) // needed since vSphere SOAP WS uses cookies
self.userName = result.returnval.userName
self.fullName = result.returnval.fullName
self.reconnectCount = 0
self.status = 'ready'
self.emit('ready')
process.once('beforeExit', self._close)
})
.once('error', function (err) {
self.status = 'disconnected'
self.emit('error', err)
})
})
.once('error', function (err) {
self.status = 'disconnected'
self.emit('error', err)
})
},
self._vcUrl
)
}
Client.prototype._close = function () {
const self = this
if (self.status === 'ready') {
self
.runCommand('Logout', { _this: self.sessionManager })
.once('result', function () {
self.status = 'disconnected'
})
.once('error', function () {
/* don't care of error during disconnection */
self.status = 'disconnected'
})
} else {
self.status = 'disconnected'
}
}
function _soapErrorHandler(self, emitter, command, args, err) {
err = err || { body: 'general error' }
if (err.body.match(/session is not authenticated/)) {
self.status = 'disconnected'
process.removeAllListeners('beforeExit')
if (self.reconnectCount < 10) {
self.reconnectCount += 1
self
.runCommand(command, args)
.once('result', function (result, raw, soapHeader) {
emitter.emit('result', result, raw, soapHeader)
})
.once('error', function (err) {
emitter.emit('error', err.body)
throw err
})
} else {
emitter.emit('error', err.body)
throw err
}
} else {
emitter.emit('error', err.body)
throw err
}
}
// end
exports.Client = Client

View File

@@ -0,0 +1,38 @@
{
"name": "@vates/node-vsphere-soap",
"version": "1.0.0",
"description": "interface to vSphere SOAP/WSDL from node for interfacing with vCenter or ESXi, forked from node-vsphere-soap",
"main": "lib/client.js",
"author": "reedog117",
"repository": {
"directory": "@vates/node-vsphere-soap",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"axios": "^1.4.0",
"soap": "^1.0.0",
"soap-cookie": "^0.10.1"
},
"devDependencies": {
"test": "^3.3.0"
},
"keywords": [
"vsphere",
"vcenter",
"api",
"soap",
"wsdl"
],
"preferGlobal": false,
"license": "MIT",
"private": false,
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/node-vsphere-soap",
"engines": {
"node": ">=8.10"
},
"scripts": {
"postversion": "npm publish --access public"
}
}

View File

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

View File

@@ -0,0 +1,140 @@
'use strict'
/*
vsphere-soap.test.js
tests for the vCenterConnectionInstance class
*/
const assert = require('assert')
const { describe, it } = require('test')
const vc = require('../lib/client')
// eslint-disable-next-line n/no-missing-require
const TestCreds = require('../config-test.js').vCenterTestCreds
const VItest = new vc.Client(TestCreds.vCenterIP, TestCreds.vCenterUser, TestCreds.vCenterPassword, false)
describe('Client object initialization:', function () {
it('provides a successful login', { timeout: 5000 }, function (t, done) {
VItest.once('ready', function () {
assert.notEqual(VItest.userName, null)
assert.notEqual(VItest.fullName, null)
assert.notEqual(VItest.serviceContent, null)
done()
}).once('error', function (err) {
console.error(err)
// this should fail if there's a problem
assert.notEqual(VItest.userName, null)
assert.notEqual(VItest.fullName, null)
assert.notEqual(VItest.serviceContent, null)
done()
})
})
})
describe('Client reconnection test:', function () {
it('can successfully reconnect', { timeout: 5000 }, function (t, done) {
VItest.runCommand('Logout', { _this: VItest.serviceContent.sessionManager })
.once('result', function (result) {
// now we're logged out, so let's try running a command to test automatic re-login
VItest.runCommand('CurrentTime', { _this: 'ServiceInstance' })
.once('result', function (result) {
assert(result.returnval instanceof Date)
done()
})
.once('error', function (err) {
console.error(err)
})
})
.once('error', function (err) {
console.error(err)
})
})
})
// these tests don't work yet
describe('Client tests - query commands:', function () {
it('retrieves current time', { timeout: 5000 }, function (t, done) {
VItest.runCommand('CurrentTime', { _this: 'ServiceInstance' }).once('result', function (result) {
assert(result.returnval instanceof Date)
done()
})
})
it('retrieves current time 2 (check for event clobbering)', { timeout: 5000 }, function (t, done) {
VItest.runCommand('CurrentTime', { _this: 'ServiceInstance' }).once('result', function (result) {
assert(result.returnval instanceof Date)
done()
})
})
it('can obtain the names of all Virtual Machines in the inventory', { timeout: 20000 }, function (t, done) {
// get property collector
const propertyCollector = VItest.serviceContent.propertyCollector
// get view manager
const viewManager = VItest.serviceContent.viewManager
// get root folder
const rootFolder = VItest.serviceContent.rootFolder
let containerView, objectSpec, traversalSpec, propertySpec, propertyFilterSpec
// this is the equivalent to
VItest.runCommand('CreateContainerView', {
_this: viewManager,
container: rootFolder,
type: ['VirtualMachine'],
recursive: true,
}).once('result', function (result) {
// build all the data structures needed to query all the vm names
containerView = result.returnval
objectSpec = {
attributes: { 'xsi:type': 'ObjectSpec' }, // setting attributes xsi:type is important or else the server may mis-recognize types!
obj: containerView,
skip: true,
}
traversalSpec = {
attributes: { 'xsi:type': 'TraversalSpec' },
name: 'traverseEntities',
type: 'ContainerView',
path: 'view',
skip: false,
}
objectSpec = { ...objectSpec, selectSet: [traversalSpec] }
propertySpec = {
attributes: { 'xsi:type': 'PropertySpec' },
type: 'VirtualMachine',
pathSet: ['name'],
}
propertyFilterSpec = {
attributes: { 'xsi:type': 'PropertyFilterSpec' },
propSet: [propertySpec],
objectSet: [objectSpec],
}
// TODO: research why it fails if propSet is declared after objectSet
VItest.runCommand('RetrievePropertiesEx', {
_this: propertyCollector,
specSet: [propertyFilterSpec],
options: { attributes: { type: 'RetrieveOptions' } },
})
.once('result', function (result, raw) {
assert.notEqual(result.returnval.objects, null)
if (Array.isArray(result.returnval.objects)) {
assert.strictEqual(result.returnval.objects[0].obj.attributes.type, 'VirtualMachine')
} else {
assert.strictEqual(result.returnval.objects.obj.attributes.type, 'VirtualMachine')
}
done()
})
.once('error', function (err) {
console.error('\n\nlast request : ' + VItest.client.lastRequest, err)
})
})
})
})

View File

@@ -13,7 +13,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.1.2",
"version": "0.2.0",
"engines": {
"node": ">=14"
},

View File

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

View File

@@ -1,5 +1,7 @@
'use strict'
const { join, resolve } = require('node:path/posix')
const { DIR_XO_POOL_METADATA_BACKUPS } = require('./RemoteAdapter.js')
const { PATH_DB_DUMP } = require('./_runners/_PoolMetadataBackup.js')
@@ -20,7 +22,8 @@ exports.RestoreMetadataBackup = class RestoreMetadataBackup {
task: xapi.task_create('Import pool metadata'),
})
} else {
return String(await handler.readFile(`${backupId}/data.json`))
const metadata = JSON.parse(await handler.readFile(join(backupId, 'metadata.json')))
return String(await handler.readFile(resolve(backupId, metadata.data ?? 'data.json')))
}
}
}

View File

@@ -19,6 +19,7 @@ const DEFAULT_XAPI_VM_SETTINGS = {
concurrency: 2,
copyRetention: 0,
deleteFirst: false,
diskPerVmConcurrency: 0, // not limited by default
exportRetention: 0,
fullInterval: 0,
healthCheckSr: undefined,

View File

@@ -1,6 +1,7 @@
'use strict'
const { asyncMap } = require('@xen-orchestra/async-map')
const { join } = require('@xen-orchestra/fs/path')
const { DIR_XO_CONFIG_BACKUPS } = require('../RemoteAdapter.js')
const { formatFilenameDate } = require('../_filenameDate.js')
@@ -23,10 +24,11 @@ exports.XoMetadataBackup = class XoMetadataBackup {
const dir = `${scheduleDir}/${formatFilenameDate(timestamp)}`
const data = job.xoMetadata
const fileName = `${dir}/data.json`
const dataBaseName = './data.json'
const metadata = JSON.stringify(
{
data: dataBaseName,
jobId: job.id,
jobName: job.name,
scheduleId: schedule.id,
@@ -36,6 +38,8 @@ exports.XoMetadataBackup = class XoMetadataBackup {
null,
2
)
const dataFileName = join(dir, dataBaseName)
const metaDataFileName = `${dir}/metadata.json`
await asyncMap(
@@ -52,7 +56,7 @@ exports.XoMetadataBackup = class XoMetadataBackup {
async () => {
const handler = adapter.handler
const dirMode = this._config.dirMode
await handler.outputFile(fileName, data, { dirMode })
await handler.outputFile(dataFileName, data, { dirMode })
await handler.outputFile(metaDataFileName, metadata, {
dirMode,
})

View File

@@ -36,7 +36,7 @@ exports.FullXapiWriter = class FullXapiWriter extends MixinXapiWriter(AbstractFu
const sr = this._sr
const settings = this._settings
const job = this._job
const scheduleId = this.scheduleId
const scheduleId = this._scheduleId
const { uuid: srUuid, $xapi: xapi } = sr

View File

@@ -1,9 +1,9 @@
'use strict'
const assert = require('assert')
const map = require('lodash/map.js')
const mapValues = require('lodash/mapValues.js')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const { asyncEach } = require('@vates/async-each')
const { asyncMap } = require('@xen-orchestra/async-map')
const { chainVhd, checkVhdChain, openVhd, VhdAbstract } = require('vhd-lib')
const { createLogger } = require('@xen-orchestra/log')
@@ -138,7 +138,7 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
const adapter = this._adapter
const job = this._job
const scheduleId = this._scheduleId
const settings = this._settings
const jobId = job.id
const handler = adapter.handler
@@ -176,8 +176,9 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
}
const { size } = await Task.run({ name: 'transfer' }, async () => {
let transferSize = 0
await Promise.all(
map(deltaExport.vdis, async (vdi, id) => {
await asyncEach(
Object.entries(deltaExport.vdis),
async ([id, vdi]) => {
const path = `${this._vmBackupDir}/${vhds[id]}`
const isDelta = differentialVhds[`${id}.vhd`]
@@ -211,7 +212,6 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
checksum: false,
validator: tmpPath => checkVhd(handler, tmpPath),
writeBlockConcurrency: this._config.writeBlockConcurrency,
isDelta,
})
if (isDelta) {
@@ -224,8 +224,12 @@ class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrementalWrite
await vhd.readBlockAllocationTable() // required by writeFooter()
await vhd.writeFooter()
})
})
},
{
concurrency: settings.diskPerVmConcurrency,
}
)
return { size: transferSize }
})
metadataContent.size = size

View File

@@ -14,6 +14,19 @@ exports.MixinXapiWriter = (BaseClass = Object) =>
this._sr = sr
}
// check if the base Vm has all its disk on health check sr
async #isAlreadyOnHealthCheckSr(baseVm) {
const xapi = baseVm.$xapi
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) {
return false
}
}
return true
}
healthCheck() {
const sr = this._healthCheckSr
assert.notStrictEqual(sr, undefined, 'SR should be defined before making a health check')
@@ -25,20 +38,35 @@ exports.MixinXapiWriter = (BaseClass = Object) =>
},
async () => {
const { $xapi: xapi } = sr
let clonedVm
let healthCheckVmRef
try {
const baseVm = xapi.getObject(this._targetVmRef) ?? (await xapi.waitObject(this._targetVmRef))
const clonedRef = await xapi
.callAsync('VM.clone', this._targetVmRef, `Health Check - ${baseVm.name_label}`)
.then(extractOpaqueRef)
clonedVm = xapi.getObject(clonedRef) ?? (await xapi.waitObject(clonedRef))
if (await this.#isAlreadyOnHealthCheckSr(baseVm)) {
healthCheckVmRef = await Task.run(
{ name: 'cloning-vm' },
async () =>
await xapi
.callAsync('VM.clone', this._targetVmRef, `Health Check - ${baseVm.name_label}`)
.then(extractOpaqueRef)
)
} else {
healthCheckVmRef = await Task.run(
{ name: 'copying-vm' },
async () =>
await xapi
.callAsync('VM.copy', this._targetVmRef, `Health Check - ${baseVm.name_label}`, sr.$ref)
.then(extractOpaqueRef)
)
}
const healthCheckVm = xapi.getObject(healthCheckVmRef) ?? (await xapi.waitObject(healthCheckVmRef))
await new HealthCheckVmBackup({
restoredVm: clonedVm,
restoredVm: healthCheckVm,
xapi,
}).run()
} finally {
clonedVm && (await xapi.VM_destroy(clonedVm.$ref))
healthCheckVmRef && (await xapi.VM_destroy(healthCheckVmRef))
}
}
)

View File

@@ -171,13 +171,16 @@ job:
# For replication jobs, indicates which SRs to use
srs: IdPattern
# Here for historical reasons
type: 'backup'
type: 'backup' | 'mirrorBackup'
# Indicates which VMs to backup/replicate
# Indicates which VMs to backup/replicate for a xapi to remote backup job
vms: IdPattern
# Indicates which remote to read from for a mirror backup job
sourceRemote: IdPattern
# Indicates which XAPI to use to connect to a specific VM or SR
# for remote to remote backup job,this is only needed if there is healtcheck
recordToXapi:
[ObjectId]: XapiId

View File

@@ -8,9 +8,9 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.38.2",
"version": "0.39.0",
"engines": {
"node": ">=14.6"
"node": ">=14.18"
},
"scripts": {
"postversion": "npm publish --access public",
@@ -24,10 +24,10 @@
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.4",
"@vates/fuse-vhd": "^1.0.0",
"@vates/nbd-client": "^1.2.0",
"@vates/nbd-client": "^1.2.1",
"@vates/parse-duration": "^0.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^4.0.0",
"@xen-orchestra/fs": "^4.0.1",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/template": "^0.1.0",
"compare-versions": "^5.0.1",
@@ -43,6 +43,7 @@
"proper-lockfile": "^4.1.2",
"uuid": "^9.0.0",
"vhd-lib": "^4.5.0",
"xen-api": "^1.3.3",
"yazl": "^2.5.1"
},
"devDependencies": {

View File

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

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/fs",
"version": "4.0.0",
"version": "4.0.1",
"license": "AGPL-3.0-or-later",
"description": "The File System for Xen Orchestra backups.",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",
@@ -30,7 +30,6 @@
"@vates/coalesce-calls": "^0.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/read-chunk": "^1.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"bind-property-descriptor": "^2.0.0",
"decorator-synchronized": "^0.6.0",

View File

@@ -1,6 +1,6 @@
import asyncMapSettled from '@xen-orchestra/async-map/legacy'
import assert from 'assert'
import getStream from 'get-stream'
import { asyncEach } from '@vates/async-each'
import { coalesceCalls } from '@vates/coalesce-calls'
import { createLogger } from '@xen-orchestra/log'
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
@@ -623,7 +623,7 @@ export default class RemoteHandlerAbstract {
}
const files = await this._list(dir)
await asyncMapSettled(files, file =>
await asyncEach(files, file =>
this._unlink(`${dir}/${file}`).catch(error => {
// Unlink dir behavior is not consistent across platforms
// https://github.com/nodejs/node-v0.x-archive/issues/5791

View File

@@ -1,6 +1,8 @@
# ChangeLog
## **0.2.0**
## **next**
## **0.1.1** (2023-07-03)
- Invalidate sessionId token after logout (PR [#6480](https://github.com/vatesfr/xen-orchestra/pull/6480))
- Settings page (PR [#6418](https://github.com/vatesfr/xen-orchestra/pull/6418))
@@ -17,6 +19,7 @@
- Add "Under Construction" views (PR [#6673](https://github.com/vatesfr/xen-orchestra/pull/6673))
- Ability to change the state of selected VMs from the pool's list of VMs (PR [#6782](https://github.com/vatesfr/xen-orchestra/pull/6782))
- Ability to copy selected VMs from the pool's list of VMs (PR [#6847](https://github.com/vatesfr/xen-orchestra/pull/6847))
- Ability to delete selected VMs from the pool's list of VMs (PR [#6673](https://github.com/vatesfr/xen-orchestra/pull/6860))
## **0.1.0**

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/lite",
"version": "0.1.0",
"version": "0.1.1",
"scripts": {
"dev": "GIT_HEAD=$(git rev-parse HEAD) vite",
"build": "run-p type-check build-only",

View File

@@ -29,6 +29,7 @@ import { useXenApiStore } from "@/stores/xen-api.store";
import { useActiveElement, useMagicKeys, whenever } from "@vueuse/core";
import { logicAnd } from "@vueuse/math";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
let link = document.querySelector(
"link[rel~='icon']"
@@ -48,10 +49,11 @@ useChartTheme();
const uiStore = useUiStore();
if (import.meta.env.DEV) {
const { locale } = useI18n();
const activeElement = useActiveElement();
const { D } = useMagicKeys();
const { D, L } = useMagicKeys();
const canToggleDarkMode = computed(() => {
const canToggle = computed(() => {
if (activeElement.value == null) {
return true;
}
@@ -60,9 +62,14 @@ if (import.meta.env.DEV) {
});
whenever(
logicAnd(D, canToggleDarkMode),
logicAnd(D, canToggle),
() => (uiStore.colorMode = uiStore.colorMode === "dark" ? "light" : "dark")
);
whenever(
logicAnd(L, canToggle),
() => (locale.value = locale.value === "en" ? "fr" : "en")
);
}
whenever(

View File

@@ -13,13 +13,10 @@
v-model="password"
:placeholder="$t('password')"
:readonly="isConnecting"
required
/>
</FormInputWrapper>
<UiButton
type="submit"
:busy="isConnecting"
:disabled="password.trim().length < 1"
>
<UiButton type="submit" :busy="isConnecting">
{{ $t("login") }}
</UiButton>
</form>

View File

@@ -6,23 +6,26 @@
<slot v-else />
</template>
<script lang="ts" setup>
<script
generic="T extends XenApiRecord<string>, I extends T['uuid']"
lang="ts"
setup
>
import UiSpinner from "@/components/ui/UiSpinner.vue";
import type { XenApiRecord } from "@/libs/xen-api";
import ObjectNotFoundView from "@/views/ObjectNotFoundView.vue";
import { computed } from "vue";
import { useRouter } from "vue-router";
const props = defineProps<{
isReady: boolean;
uuidChecker: (uuid: string) => boolean;
id?: string;
uuidChecker: (uuid: I) => boolean;
id?: I;
}>();
const { currentRoute } = useRouter();
const id = computed(
() => props.id ?? (currentRoute.value.params.uuid as string)
);
const id = computed(() => props.id ?? (currentRoute.value.params.uuid as I));
const isRecordNotFound = computed(
() => props.isReady && !props.uuidChecker(id.value)

View File

@@ -5,6 +5,7 @@
v-model="value"
:class="inputClass"
:disabled="disabled || isLabelDisabled"
:required="required"
class="select"
ref="inputElement"
v-bind="$attrs"
@@ -21,6 +22,7 @@
v-model="value"
:class="inputClass"
:disabled="disabled || isLabelDisabled"
:required="required"
class="textarea"
v-bind="$attrs"
/>
@@ -29,6 +31,7 @@
v-model="value"
:class="inputClass"
:disabled="disabled || isLabelDisabled"
:required="required"
class="input"
ref="inputElement"
v-bind="$attrs"
@@ -70,6 +73,7 @@ const props = withDefaults(
beforeWidth?: string;
afterWidth?: string;
disabled?: boolean;
required?: boolean;
right?: boolean;
wrapperAttrs?: HTMLAttributes;
}>(),
@@ -88,7 +92,7 @@ const isEmpty = computed(
);
const inputType = inject("inputType", "input");
const isLabelDisabled = inject("isLabelDisabled", ref(false));
const color = inject(
const parentColor = inject(
"color",
computed(() => undefined)
);
@@ -102,7 +106,7 @@ const wrapperClass = computed(() => [
]);
const inputClass = computed(() => [
color.value ?? props.color,
parentColor.value ?? props.color,
{
right: props.right,
"has-before": props.before !== undefined,

View File

@@ -29,6 +29,7 @@ import InfraAction from "@/components/infra/InfraAction.vue";
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
import InfraVmList from "@/components/infra/InfraVmList.vue";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiHost } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store";
import { usePoolStore } from "@/stores/pool.store";
import { useUiStore } from "@/stores/ui.store";
@@ -42,7 +43,7 @@ import { useToggle } from "@vueuse/core";
import { computed } from "vue";
const props = defineProps<{
hostOpaqueRef: string;
hostOpaqueRef: XenApiHost["$ref"];
}>();
const { getByOpaqueRef } = useHostStore().subscribe();

View File

@@ -19,13 +19,14 @@
import InfraAction from "@/components/infra/InfraAction.vue";
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
import PowerStateIcon from "@/components/PowerStateIcon.vue";
import type { XenApiVm } from "@/libs/xen-api";
import { useVmStore } from "@/stores/vm.store";
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
import { useIntersectionObserver } from "@vueuse/core";
import { computed, ref } from "vue";
const props = defineProps<{
vmOpaqueRef: string;
vmOpaqueRef: XenApiVm["$ref"];
}>();
const { getByOpaqueRef } = useVmStore().subscribe();

View File

@@ -11,18 +11,21 @@
<script lang="ts" setup>
import InfraLoadingItem from "@/components/infra/InfraLoadingItem.vue";
import InfraVmItem from "@/components/infra/InfraVmItem.vue";
import type { XenApiHost } from "@/libs/xen-api";
import { useVmStore } from "@/stores/vm.store";
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
const props = defineProps<{
hostOpaqueRef?: string;
hostOpaqueRef?: XenApiHost["$ref"];
}>();
const { isReady, recordsByHostRef, hasError } = useVmStore().subscribe();
const vms = computed(() =>
recordsByHostRef.value.get(props.hostOpaqueRef ?? "OpaqueRef:NULL")
recordsByHostRef.value.get(
props.hostOpaqueRef ?? ("OpaqueRef:NULL" as XenApiHost["$ref"])
)
);
</script>

View File

@@ -1,8 +1,8 @@
<template>
<slot :is-open="isOpen" :open="open" name="trigger" />
<Teleport to="body" :disabled="!slots.trigger">
<Teleport to="body" :disabled="!shouldTeleport">
<ul
v-if="!$slots.trigger || isOpen"
v-if="!hasTrigger || isOpen"
ref="menu"
:class="{ horizontal, shadow }"
class="app-menu"
@@ -14,7 +14,8 @@
</template>
<script lang="ts" setup>
import placement, { type Options } from "placement.js";
import { IK_MENU_TELEPORTED } from "@/types/injection-keys";
import placementJs, { type Options } from "placement.js";
import { inject, nextTick, provide, ref, toRef, unref, useSlots } from "vue";
import { onClickOutside, unrefElement, whenever } from "@vueuse/core";
@@ -37,6 +38,14 @@ provide("isMenuHorizontal", toRef(props, "horizontal"));
provide("isMenuDisabled", toRef(props, "disabled"));
let clearClickOutsideEvent: (() => void) | undefined;
const hasTrigger = useSlots().trigger !== undefined;
const shouldTeleport = hasTrigger && !inject(IK_MENU_TELEPORTED, false);
if (shouldTeleport) {
provide(IK_MENU_TELEPORTED, true);
}
whenever(
() => !isOpen.value,
() => clearClickOutsideEvent?.()
@@ -62,7 +71,7 @@ const open = (event: MouseEvent) => {
}
);
placement(event.currentTarget as HTMLElement, unrefElement(menu), {
placementJs(event.currentTarget as HTMLElement, unrefElement(menu), {
placement:
props.placement ??
(unref(isParentHorizontal) !== false ? "bottom-start" : "right-start"),

View File

@@ -4,7 +4,7 @@
<div class="content">
<img alt="" src="@/assets/under-construction.svg" />
</div>
<div class="content">Coming soon</div>
<div class="content">{{ $t("coming-soon") }}</div>
</UiCard>
</template>

View File

@@ -1,12 +1,13 @@
<template>
<Teleport to="body">
<form
<Teleport :disabled="isNested" to="body">
<component
:is="isNested ? 'div' : 'form'"
:class="className"
class="ui-modal"
v-bind="$attrs"
@click.self="emit('close')"
@click.self="!isNested && emit('close')"
>
<div class="container">
<div :class="{ nested: isNested }" class="container">
<span v-if="onClose" class="close-icon" @click="emit('close')">
<UiIcon :icon="faXmark" />
</span>
@@ -24,22 +25,23 @@
<div v-if="$slots.default" class="content">
<slot />
</div>
<UiButtonGroup :color="color">
<UiButtonGroup v-if="!isNested" :color="color">
<slot name="buttons" />
</UiButtonGroup>
</div>
</form>
</component>
</Teleport>
</template>
<script lang="ts" setup>
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import { IK_MODAL_NESTED } from "@/types/injection-keys";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { useMagicKeys, whenever } from "@vueuse/core";
import { computed } from "vue";
import { computed, inject, provide } from "vue";
const props = withDefaults(
defineProps<{
@@ -54,27 +56,39 @@ const emit = defineEmits<{
(event: "close"): void;
}>();
const isNested = inject(IK_MODAL_NESTED, false);
provide(IK_MODAL_NESTED, true);
const { escape } = useMagicKeys();
whenever(escape, () => emit("close"));
const className = computed(() => {
return [`color-${props.color}`, { "has-icon": props.icon !== undefined }];
return [
`color-${props.color}`,
{
"has-icon": props.icon !== undefined,
nested: isNested,
},
];
});
</script>
<style lang="postcss" scoped>
.ui-modal {
position: fixed;
z-index: 2;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
overflow: auto;
align-items: center;
justify-content: center;
background-color: #00000080;
&:not(.nested) {
background-color: #00000080;
position: fixed;
z-index: 2;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}
.color-success {
@@ -103,11 +117,23 @@ const className = computed(() => {
flex-direction: column;
justify-content: center;
min-width: 40rem;
padding: 4.2rem;
text-align: center;
border-radius: 1rem;
background-color: var(--modal-background-color);
box-shadow: var(--shadow-400);
margin: 1rem 2rem;
&.nested {
width: 100%;
}
&:not(.nested) {
box-shadow: var(--shadow-400);
padding: 4.2rem;
}
}
.container > div:last-child {
padding-bottom: 1rem;
}
.close-icon {
@@ -120,7 +146,7 @@ const className = computed(() => {
color: var(--modal-color);
}
.container :slotted(.accent) {
.container :deep(.accent) {
color: var(--modal-color);
}

View File

@@ -21,7 +21,7 @@ import { faCopy } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
const props = defineProps<{
selectedRefs: string[];
selectedRefs: XenApiVm["$ref"][];
}>();
const { getByOpaqueRef } = useVmStore().subscribe();

View File

@@ -0,0 +1,73 @@
<template>
<MenuItem
:disabled="areSomeVmsInExecution"
:icon="faTrashCan"
v-tooltip="areSomeVmsInExecution && $t('selected-vms-in-execution')"
@click="openDeleteModal"
>
{{ $t("delete") }}
</MenuItem>
<UiModal
v-if="isDeleteModalOpen"
:icon="faSatellite"
@close="closeDeleteModal"
>
<template #title>
<i18n-t keypath="confirm-delete" scope="global" tag="div">
<span class="accent">
{{ $t("n-vms", { n: vmRefs.length }) }}
</span>
</i18n-t>
</template>
<template #subtitle>
{{ $t("please-confirm") }}
</template>
<template #buttons>
<UiButton outlined @click="closeDeleteModal">
{{ $t("go-back") }}
</UiButton>
<UiButton @click="deleteVms">
{{ $t("delete-vms", { n: vmRefs.length }) }}
</UiButton>
</template>
</UiModal>
</template>
<script lang="ts" setup>
import MenuItem from "@/components/menu/MenuItem.vue";
import { POWER_STATE } from "@/libs/xen-api";
import UiButton from "@/components/ui/UiButton.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import { useVmStore } from "@/stores/vm.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import { faSatellite, faTrashCan } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api";
const props = defineProps<{
vmRefs: XenApiVm["$ref"][];
}>();
const xenApi = useXenApiStore().getXapi();
const { getByOpaqueRef: getVm } = useVmStore().subscribe();
const {
open: openDeleteModal,
close: closeDeleteModal,
isOpen: isDeleteModalOpen,
} = useModal();
const vms = computed<XenApiVm[]>(() =>
props.vmRefs.map(getVm).filter((vm): vm is XenApiVm => vm !== undefined)
);
const areSomeVmsInExecution = computed(() =>
vms.value.some((vm) => vm.power_state !== POWER_STATE.HALTED)
);
const deleteVms = async () => {
await xenApi.vm.delete(props.vmRefs);
closeDeleteModal();
};
</script>

View File

@@ -118,7 +118,7 @@ import {
import { computed } from "vue";
const props = defineProps<{
vmRefs: string[];
vmRefs: XenApiVm["$ref"][];
}>();
const { getByOpaqueRef: getVm } = useVmStore().subscribe();

View File

@@ -22,6 +22,7 @@ import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { useVmStore } from "@/stores/vm.store";
import VmActionPowerStateItems from "@/components/vm/VmActionItems/VmActionPowerStateItems.vue";
import type { XenApiVm } from "@/libs/xen-api";
import {
faAngleDown,
faDisplay,
@@ -34,7 +35,7 @@ const { getByUuid: getVmByUuid } = useVmStore().subscribe();
const { currentRoute } = useRouter();
const vm = computed(() =>
getVmByUuid(currentRoute.value.params.uuid as string)
getVmByUuid(currentRoute.value.params.uuid as XenApiVm["uuid"])
);
const name = computed(() => vm.value?.name_label);

View File

@@ -10,7 +10,7 @@
<UiButton :active="isOpen" :icon="faEllipsis" transparent @click="open" />
</template>
<MenuItem :icon="faPowerOff">
{{ $t("change-power-state") }}
{{ $t("change-state") }}
<template #submenu>
<VmActionPowerStateItems :vm-refs="selectedRefs" />
</template>
@@ -25,9 +25,7 @@
<MenuItem v-tooltip="$t('coming-soon')" :icon="faCamera">
{{ $t("snapshot") }}
</MenuItem>
<MenuItem v-tooltip="$t('coming-soon')" :icon="faTrashCan">
{{ $t("delete") }}
</MenuItem>
<VmActionDeleteItem :vm-refs="selectedRefs" />
<MenuItem :icon="faFileExport">
{{ $t("export") }}
<template #submenu>
@@ -59,9 +57,11 @@ import AppMenu from "@/components/menu/AppMenu.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import UiButton from "@/components/ui/UiButton.vue";
import VmActionCopyItem from "@/components/vm/VmActionItems/VmActionCopyItem.vue";
import { useUiStore } from "@/stores/ui.store";
import VmActionDeleteItem from "@/components/vm/VmActionItems/VmActionDeleteItem.vue";
import VmActionPowerStateItems from "@/components/vm/VmActionItems/VmActionPowerStateItems.vue";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api";
import { useUiStore } from "@/stores/ui.store";
import {
faCamera,
faCode,
@@ -72,13 +72,12 @@ import {
faFileExport,
faPowerOff,
faRoute,
faTrashCan,
} from "@fortawesome/free-solid-svg-icons";
import { storeToRefs } from "pinia";
defineProps<{
disabled?: boolean;
selectedRefs: string[];
selectedRefs: XenApiVm["$ref"][];
}>();
const { isMobile } = storeToRefs(useUiStore());

View File

@@ -16,10 +16,13 @@ export type Stat<T> = {
pausable: Pausable;
};
type GetStats<T extends HostStats | VmStats> = (
uuid: string,
type GetStats<
T extends XenApiHost | XenApiVm,
S extends HostStats | VmStats
> = (
uuid: T["uuid"],
granularity: GRANULARITY
) => Promise<XapiStatsResponse<T>> | undefined;
) => Promise<XapiStatsResponse<S>> | undefined;
export type FetchedStats<
T extends XenApiHost | XenApiVm,
@@ -35,7 +38,7 @@ export type FetchedStats<
export default function useFetchStats<
T extends XenApiHost | XenApiVm,
S extends HostStats | VmStats
>(getStats: GetStats<S>, granularity: GRANULARITY): FetchedStats<T, S> {
>(getStats: GetStats<T, S>, granularity: GRANULARITY): FetchedStats<T, S> {
const stats = ref<Map<string, Stat<S>>>(new Map());
const timestamp = ref<number[]>([0, 0]);

View File

@@ -136,9 +136,9 @@ export function getHostMemory(
}
}
export const buildXoObject = <T extends XenApiRecord>(
export const buildXoObject = <T extends XenApiRecord<string>>(
record: RawXenApiRecord<T>,
params: { opaqueRef: string }
params: { opaqueRef: T["$ref"] }
) => {
return {
...record,

View File

@@ -46,6 +46,7 @@ const OBJECT_TYPES = {
host_crashdump: "host_crashdump",
host_metrics: "host_metrics",
host_patch: "host_patch",
message: "message",
network: "network",
network_sriov: "network_sriov",
pool: "pool",
@@ -87,83 +88,90 @@ export enum VM_OPERATION {
SUSPEND = "suspend",
}
export interface XenApiRecord {
$ref: string;
uuid: string;
declare const __brand: unique symbol;
export interface XenApiRecord<Name extends string> {
$ref: string & { [__brand]: `${Name}Ref` };
uuid: string & { [__brand]: `${Name}Uuid` };
}
export type RawXenApiRecord<T extends XenApiRecord> = Omit<T, "$ref">;
export type RawXenApiRecord<T extends XenApiRecord<string>> = Omit<T, "$ref">;
export interface XenApiPool extends XenApiRecord {
export interface XenApiPool extends XenApiRecord<"Pool"> {
cpu_info: {
cpu_count: string;
};
master: string;
master: XenApiHost["$ref"];
name_label: string;
}
export interface XenApiHost extends XenApiRecord {
export interface XenApiHost extends XenApiRecord<"Host"> {
address: string;
name_label: string;
metrics: string;
resident_VMs: string[];
metrics: XenApiHostMetrics["$ref"];
resident_VMs: XenApiVm["$ref"][];
cpu_info: { cpu_count: string };
software_version: { product_version: string };
}
export interface XenApiSr extends XenApiRecord {
export interface XenApiSr extends XenApiRecord<"Sr"> {
name_label: string;
physical_size: number;
physical_utilisation: number;
}
export interface XenApiVm extends XenApiRecord {
export interface XenApiVm extends XenApiRecord<"Vm"> {
current_operations: Record<string, VM_OPERATION>;
guest_metrics: string;
metrics: string;
metrics: XenApiVmMetrics["$ref"];
name_label: string;
name_description: string;
power_state: POWER_STATE;
resident_on: string;
consoles: string[];
resident_on: XenApiHost["$ref"];
consoles: XenApiConsole["$ref"][];
is_control_domain: boolean;
is_a_snapshot: boolean;
is_a_template: boolean;
VCPUs_at_startup: number;
}
export interface XenApiConsole extends XenApiRecord {
export interface XenApiConsole extends XenApiRecord<"Console"> {
protocol: string;
location: string;
}
export interface XenApiHostMetrics extends XenApiRecord {
export interface XenApiHostMetrics extends XenApiRecord<"HostMetrics"> {
live: boolean;
memory_free: number;
memory_total: number;
}
export interface XenApiVmMetrics extends XenApiRecord {
export interface XenApiVmMetrics extends XenApiRecord<"VmMetrics"> {
VCPUs_number: number;
}
export type XenApiVmGuestMetrics = XenApiRecord;
export type XenApiVmGuestMetrics = XenApiRecord<"VmGuestMetrics">;
export interface XenApiTask extends XenApiRecord {
export interface XenApiTask extends XenApiRecord<"Task"> {
name_label: string;
resident_on: string;
resident_on: XenApiHost["$ref"];
created: string;
finished: string;
status: string;
progress: number;
}
export interface XenApiMessage extends XenApiRecord<"Message"> {
name: string;
cls: RawObjectType;
}
type WatchCallbackResult = {
id: string;
class: ObjectType;
operation: "add" | "mod" | "del";
ref: string;
snapshot: RawXenApiRecord<XenApiRecord>;
ref: XenApiRecord<string>["$ref"];
snapshot: RawXenApiRecord<XenApiRecord<string>>;
};
type WatchCallback = (results: WatchCallbackResult[]) => void;
@@ -234,8 +242,7 @@ export default class XenApi {
async loadTypes() {
this.#types = (await this.#call<string[]>("system.listMethods"))
.filter((method: string) => method.endsWith(".get_all_records"))
.map((method: string) => method.slice(0, method.indexOf(".")))
.filter((type: string) => type !== "message");
.map((method: string) => method.slice(0, method.indexOf(".")));
}
get sessionId() {
@@ -273,14 +280,16 @@ export default class XenApi {
return fetch(url);
}
async loadRecords<T extends XenApiRecord>(type: RawObjectType): Promise<T[]> {
async loadRecords<T extends XenApiRecord<string>>(
type: RawObjectType
): Promise<T[]> {
const result = await this.#call<{ [key: string]: RawXenApiRecord<T> }>(
`${type}.get_all_records`,
[this.sessionId]
);
return Object.entries(result).map(([opaqueRef, record]) =>
buildXoObject(record, { opaqueRef })
buildXoObject(record, { opaqueRef: opaqueRef as T["$ref"] })
);
}
@@ -319,7 +328,7 @@ export default class XenApi {
this.#watchCallBack = callback;
}
async injectWatchEvent(poolRef: string) {
async injectWatchEvent(poolRef: XenApiPool["$ref"]) {
this.#fromToken = await this.#call("event.inject", [
this.sessionId,
"pool",
@@ -336,6 +345,10 @@ export default class XenApi {
type VmRefsToClone = Record<XenApiVm["$ref"], /* Cloned VM name */ string>;
return {
delete: (vmRefs: VmRefs) =>
Promise.all(
castArray(vmRefs).map((vmRef) => this._call("VM.destroy", [vmRef]))
),
start: (vmRefs: VmRefs) =>
Promise.all(
castArray(vmRefs).map((vmRef) =>
@@ -358,7 +371,7 @@ export default class XenApi {
);
},
resume: (vmRefsWithPowerState: VmRefsWithPowerState) => {
const vmRefs = Object.keys(vmRefsWithPowerState);
const vmRefs = Object.keys(vmRefsWithPowerState) as XenApiVm["$ref"][];
return Promise.all(
vmRefs.map((vmRef) => {
@@ -385,7 +398,7 @@ export default class XenApi {
);
},
clone: (vmRefsToClone: VmRefsToClone) => {
const vmRefs = Object.keys(vmRefsToClone);
const vmRefs = Object.keys(vmRefsToClone) as XenApiVm["$ref"][];
return Promise.all(
vmRefs.map((vmRef) =>

View File

@@ -12,8 +12,8 @@
"back-pool-dashboard": "Go back to your Pool dashboard",
"backup": "Backup",
"cancel": "Cancel",
"change-power-state": "Change power state",
"change-state": "Change state",
"confirm-delete": "You're about to delete {0}",
"coming-soon": "Coming soon!",
"community": "Community",
"community-name": "{name} community",
@@ -23,6 +23,7 @@
"cpu-usage": "CPU usage",
"dashboard": "Dashboard",
"delete": "Delete",
"delete-vms": "Delete 1 VM | Delete {n} VMs",
"descending": "descending",
"description": "Description",
"display": "Display",
@@ -56,6 +57,7 @@
"following-hosts-unreachable": "The following hosts are unreachable",
"force-reboot": "Force reboot",
"force-shutdown": "Force shutdown",
"go-back": "Go back",
"here": "Here",
"hosts": "Hosts",
"language": "Language",
@@ -64,6 +66,7 @@
"log-out": "Log out",
"login": "Login",
"migrate": "Migrate",
"n-vms": "1 VM | {n} VMs",
"name": "Name",
"network": "Network",
"network-download": "Download",
@@ -79,6 +82,7 @@
"password": "Password",
"password-invalid": "Password invalid",
"pause": "Pause",
"please-confirm": "Please confirm",
"pool-cpu-usage": "Pool CPU Usage",
"pool-ram-usage": "Pool RAM Usage",
"power-state": "Power state",
@@ -104,6 +108,7 @@
"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

@@ -12,7 +12,7 @@
"back-pool-dashboard": "Revenez au tableau de bord de votre pool",
"backup": "Sauvegarde",
"cancel": "Annuler",
"change-power-state": "Changer l'état d'alimentation",
"confirm-delete": "Vous êtes sur le point de supprimer {0}",
"change-state": "Changer l'état",
"coming-soon": "Bientôt disponible !",
"community": "Communauté",
@@ -23,6 +23,7 @@
"cpu-usage": "Utilisation CPU",
"dashboard": "Tableau de bord",
"delete": "Supprimer",
"delete-vms": "Supprimer 1 VM | Supprimer {n} VMs",
"descending": "descendant",
"description": "Description",
"display": "Affichage",
@@ -56,6 +57,7 @@
"following-hosts-unreachable": "Les hôtes suivants sont inaccessibles",
"force-reboot": "Forcer le redémarrage",
"force-shutdown": "Forcer l'arrêt",
"go-back": "Revenir en arrière",
"here": "Ici",
"hosts": "Hôtes",
"language": "Langue",
@@ -64,6 +66,7 @@
"log-out": "Se déconnecter",
"login": "Connexion",
"migrate": "Migrer",
"n-vms": "1 VM | {n} VMs",
"name": "Nom",
"network": "Réseau",
"network-download": "Descendant",
@@ -79,6 +82,7 @@
"password": "Mot de passe",
"password-invalid": "Mot de passe incorrect",
"pause": "Pause",
"please-confirm": "Veuillez confirmer",
"pool-cpu-usage": "Utilisation CPU du Pool",
"pool-ram-usage": "Utilisation RAM du Pool",
"power-state": "État d'alimentation",
@@ -104,6 +108,7 @@
"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

@@ -0,0 +1,31 @@
import type { XenApiMessage } from "@/libs/xen-api";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { createSubscribe } from "@/types/xapi-collection";
import { defineStore } from "pinia";
import { computed } from "vue";
export const useAlarmStore = defineStore("alarm", () => {
const messageCollection = useXapiCollectionStore().get("message");
const subscribe = createSubscribe<XenApiMessage, []>((options) => {
const originalSubscription = messageCollection.subscribe(options);
const extendedSubscription = {
records: computed(() =>
originalSubscription.records.value.filter(
(record) => record.name === "alarm"
)
),
};
return {
...originalSubscription,
...extendedSubscription,
};
});
return {
...messageCollection,
subscribe,
};
});

View File

@@ -0,0 +1,39 @@
import { defineStore } from "pinia";
import { onBeforeUnmount, ref, watch } from "vue";
const beforeUnloadListener = function (e: BeforeUnloadEvent) {
e.preventDefault();
e.returnValue = ""; // Required to trigger the modal on some browser. https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#browser_compatibility
};
export const useClosingConfirmationStore = defineStore(
"closing-confirmation",
() => {
const registeredIds = ref(new Set<symbol>());
watch(
() => registeredIds.value.size > 0,
(isConfirmationNeeded) => {
const eventMethod = isConfirmationNeeded
? "addEventListener"
: "removeEventListener";
window[eventMethod]("beforeunload", beforeUnloadListener);
}
);
const register = () => {
const id = Symbol();
registeredIds.value.add(id);
const unregister = () => registeredIds.value.delete(id);
onBeforeUnmount(unregister);
return unregister;
};
return {
register,
};
}
);

View File

@@ -10,7 +10,7 @@ import { computed, type ComputedRef } from "vue";
type GetStatsExtension = {
getStats: (
hostUuid: string,
hostUuid: XenApiHost["uuid"],
granularity: GRANULARITY
) => Promise<XapiStatsResponse<any>> | undefined;
};
@@ -31,7 +31,10 @@ export const useHostStore = defineStore("host", () => {
const subscribe = createSubscribe<XenApiHost, Extensions>((options) => {
const originalSubscription = hostCollection.subscribe(options);
const getStats = (hostUuid: string, granularity: GRANULARITY) => {
const getStats = (
hostUuid: XenApiHost["uuid"],
granularity: GRANULARITY
) => {
const host = originalSubscription.getByUuid(hostUuid);
if (host === undefined) {

View File

@@ -1,7 +1,7 @@
import { sortRecordsByNameLabel } from "@/libs/utils";
import type { GRANULARITY, XapiStatsResponse } from "@/libs/xapi-stats";
import { POWER_STATE } from "@/libs/xen-api";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api";
import { POWER_STATE } from "@/libs/xen-api";
import { useXapiCollectionStore } from "@/stores/xapi-collection.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import { createSubscribe, type Subscription } from "@/types/xapi-collection";
@@ -9,14 +9,14 @@ import { defineStore } from "pinia";
import { computed, type ComputedRef } from "vue";
type DefaultExtension = {
recordsByHostRef: ComputedRef<Map<string, XenApiVm[]>>;
recordsByHostRef: ComputedRef<Map<XenApiHost["$ref"], XenApiVm[]>>;
runningVms: ComputedRef<XenApiVm[]>;
};
type GetStatsExtension = [
{
getStats: (
id: string,
id: XenApiVm["uuid"],
granularity: GRANULARITY
) => Promise<XapiStatsResponse<any>>;
},
@@ -39,7 +39,7 @@ export const useVmStore = defineStore("vm", () => {
const extendedSubscription = {
recordsByHostRef: computed(() => {
const vmsByHostOpaqueRef = new Map<string, XenApiVm[]>();
const vmsByHostOpaqueRef = new Map<XenApiHost["$ref"], XenApiVm[]>();
originalSubscription.records.value.forEach((vm) => {
if (!vmsByHostOpaqueRef.has(vm.resident_on)) {
@@ -61,23 +61,23 @@ export const useVmStore = defineStore("vm", () => {
const hostSubscription = options?.hostSubscription;
const getStatsSubscription = hostSubscription !== undefined && {
getStats: (id: string, granularity: GRANULARITY) => {
getStats: (vmUuid: XenApiVm["uuid"], granularity: GRANULARITY) => {
const xenApiStore = useXenApiStore();
if (!xenApiStore.isConnected) {
return undefined;
}
const vm = originalSubscription.getByUuid(id);
const vm = originalSubscription.getByUuid(vmUuid);
if (vm === undefined) {
throw new Error(`VM ${id} could not be found.`);
throw new Error(`VM ${vmUuid} could not be found.`);
}
const host = hostSubscription.getByOpaqueRef(vm.resident_on);
if (host === undefined) {
throw new Error(`VM ${id} is halted or host could not be found.`);
throw new Error(`VM ${vmUuid} is halted or host could not be found.`);
}
return xenApiStore.getXapiStats()._getAndUpdateStats({

View File

@@ -16,7 +16,7 @@ export const useXapiCollectionStore = defineStore("xapiCollection", () => {
function get<
T extends RawObjectType,
S extends XenApiRecord = RawTypeToObject[T]
S extends XenApiRecord<string> = RawTypeToObject[T]
>(type: T): ReturnType<typeof createXapiCollection<S>> {
if (!collections.value.has(type)) {
collections.value.set(type, createXapiCollection<S>(type));
@@ -28,15 +28,17 @@ export const useXapiCollectionStore = defineStore("xapiCollection", () => {
return { get };
});
const createXapiCollection = <T extends XenApiRecord>(type: RawObjectType) => {
const createXapiCollection = <T extends XenApiRecord<string>>(
type: RawObjectType
) => {
const isReady = ref(false);
const isFetching = ref(false);
const isReloading = computed(() => isReady.value && isFetching.value);
const lastError = ref<string>();
const hasError = computed(() => lastError.value !== undefined);
const subscriptions = ref(new Set<symbol>());
const recordsByOpaqueRef = ref(new Map<string, T>());
const recordsByUuid = ref(new Map<string, T>());
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 xenApiStore = useXenApiStore();
@@ -54,12 +56,12 @@ const createXapiCollection = <T extends XenApiRecord>(type: RawObjectType) => {
return filter.value !== undefined ? records.filter(filter.value) : records;
});
const getByOpaqueRef = (opaqueRef: string) =>
const getByOpaqueRef = (opaqueRef: T["$ref"]) =>
recordsByOpaqueRef.value.get(opaqueRef);
const getByUuid = (uuid: string) => recordsByUuid.value.get(uuid);
const getByUuid = (uuid: T["uuid"]) => recordsByUuid.value.get(uuid);
const hasUuid = (uuid: string) => recordsByUuid.value.has(uuid);
const hasUuid = (uuid: T["uuid"]) => recordsByUuid.value.has(uuid);
const hasSubscriptions = computed(() => subscriptions.value.size > 0);
@@ -89,7 +91,7 @@ const createXapiCollection = <T extends XenApiRecord>(type: RawObjectType) => {
recordsByUuid.value.set(record.uuid, record);
};
const remove = (opaqueRef: string) => {
const remove = (opaqueRef: T["$ref"]) => {
if (!recordsByOpaqueRef.value.has(opaqueRef)) {
return;
}
@@ -129,7 +131,7 @@ const createXapiCollection = <T extends XenApiRecord>(type: RawObjectType) => {
if (options?.immediate !== false) {
start();
return subscription as Subscription<T, O>;
return subscription as unknown as Subscription<T, O>;
}
return {

View File

@@ -39,17 +39,16 @@ export const useXenApiStore = defineStore("xen-api", () => {
return;
}
const buildObject = () =>
buildXoObject(result.snapshot, { opaqueRef: result.ref }) as any;
switch (result.operation) {
case "add":
return collection.add(
buildXoObject(result.snapshot, { opaqueRef: result.ref })
);
return collection.add(buildObject());
case "mod":
return collection.update(
buildXoObject(result.snapshot, { opaqueRef: result.ref })
);
return collection.update(buildObject());
case "del":
return collection.remove(result.ref);
return collection.remove(result.ref as any);
}
});
});

View File

@@ -1,5 +1,6 @@
<template>
<ComponentStory
v-slot="{ properties, settings }"
:params="[
colorProp(),
iconProp(),
@@ -11,17 +12,31 @@
slot('buttons').help('Meant to receive UiButton components'),
setting('title').preset('Modal Title').widget(),
setting('subtitle').preset('Modal Subtitle').widget(),
setting('nested_modal').widget(boolean()),
]"
v-slot="{ properties, settings }"
>
<UiButton type="button" @click="open">Open Modal</UiButton>
<UiModal v-bind="properties" v-if="isOpen">
<UiModal v-if="isOpen" v-bind="properties">
<template #title>{{ settings.title }}</template>
<template #subtitle>{{ settings.subtitle }}</template>
<template #buttons>
<UiButton @click="close">Discard</UiButton>
</template>
<template v-if="settings.nested_modal">
<UiModal :icon="faWarning" color="warning">
<template #title>Warning</template>
<template #subtitle> This is a warning "nested" modal.</template>
<UiModal :icon="faInfoCircle" color="info">
<template #title>Info</template>
<template #subtitle> This is an info "nested" modal.</template>
</UiModal>
</UiModal>
<UiModal :icon="faCheck" color="success">
<template #title>Success</template>
<template #subtitle> This is a success "deep nested" modal.</template>
</UiModal>
</template>
</UiModal>
</ComponentStory>
</template>
@@ -38,6 +53,12 @@ import {
setting,
slot,
} from "@/libs/story/story-param";
import {
faCheck,
faInfoCircle,
faWarning,
} from "@fortawesome/free-solid-svg-icons";
import { boolean } from "@/libs/story/story-widget";
const { open, close, isOpen } = useModal();
</script>

View File

@@ -0,0 +1,4 @@
import type { InjectionKey } from "vue";
export const IK_MENU_TELEPORTED = Symbol() as InjectionKey<boolean>;
export const IK_MODAL_NESTED = Symbol() as InjectionKey<boolean>;

View File

@@ -2,6 +2,7 @@ import type {
XenApiConsole,
XenApiHost,
XenApiHostMetrics,
XenApiMessage,
XenApiPool,
XenApiRecord,
XenApiSr,
@@ -12,11 +13,11 @@ import type {
} from "@/libs/xen-api";
import type { ComputedRef, Ref } from "vue";
type DefaultExtension<T extends XenApiRecord> = {
type DefaultExtension<T extends XenApiRecord<string>> = {
records: ComputedRef<T[]>;
getByOpaqueRef: (opaqueRef: string) => T | undefined;
getByUuid: (uuid: string) => T | undefined;
hasUuid: (uuid: string) => boolean;
getByOpaqueRef: (opaqueRef: T["$ref"]) => T | undefined;
getByUuid: (uuid: T["uuid"]) => T | undefined;
hasUuid: (uuid: T["uuid"]) => boolean;
isReady: Readonly<Ref<boolean>>;
isFetching: Readonly<Ref<boolean>>;
isReloading: ComputedRef<boolean>;
@@ -32,7 +33,7 @@ type DeferExtension = [
{ immediate: false }
];
type DefaultExtensions<T extends XenApiRecord> = [
type DefaultExtensions<T extends XenApiRecord<string>> = [
DefaultExtension<T>,
DeferExtension
];
@@ -63,14 +64,14 @@ type GenerateSubscription<
: object;
export type Subscription<
T extends XenApiRecord,
T extends XenApiRecord<string>,
Options extends object,
Extensions extends any[] = []
> = GenerateSubscription<Options, Extensions> &
GenerateSubscription<Options, DefaultExtensions<T>>;
export function createSubscribe<
T extends XenApiRecord,
T extends XenApiRecord<string>,
Extensions extends any[],
Options extends object = SubscribeOptions<Extensions>
>(builder: (options?: Options) => Subscription<T, Options, Extensions>) {
@@ -125,6 +126,7 @@ export type RawTypeToObject = {
host_crashdump: never;
host_metrics: XenApiHostMetrics;
host_patch: never;
message: XenApiMessage;
network: never;
network_sriov: never;
pool: XenApiPool;

View File

@@ -6,6 +6,7 @@
<script lang="ts" setup>
import ObjectNotFoundWrapper from "@/components/ObjectNotFoundWrapper.vue";
import type { XenApiHost } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store";
import { useUiStore } from "@/stores/ui.store";
import { watchEffect } from "vue";
@@ -16,6 +17,8 @@ const route = useRoute();
const uiStore = useUiStore();
watchEffect(() => {
uiStore.currentHostOpaqueRef = getByUuid(route.params.uuid as string)?.$ref;
uiStore.currentHostOpaqueRef = getByUuid(
route.params.uuid as XenApiHost["uuid"]
)?.$ref;
});
</script>

View File

@@ -9,7 +9,7 @@
</template>
<script lang="ts" setup>
import { POWER_STATE, VM_OPERATION } from "@/libs/xen-api";
import { POWER_STATE, VM_OPERATION, type XenApiVm } from "@/libs/xen-api";
import { computed } from "vue";
import { useRoute } from "vue-router";
import RemoteConsole from "@/components/RemoteConsole.vue";
@@ -36,7 +36,7 @@ const { isReady: isConsoleReady, getByOpaqueRef: getConsoleByOpaqueRef } =
const isReady = computed(() => isVmReady.value && isConsoleReady.value);
const vm = computed(() => getVmByUuid(route.params.uuid as string));
const vm = computed(() => getVmByUuid(route.params.uuid as XenApiVm["uuid"]));
const isVmRunning = computed(
() => vm.value?.power_state === POWER_STATE.RUNNING

View File

@@ -10,6 +10,7 @@
import ObjectNotFoundWrapper from "@/components/ObjectNotFoundWrapper.vue";
import VmHeader from "@/components/vm/VmHeader.vue";
import VmTabBar from "@/components/vm/VmTabBar.vue";
import type { XenApiVm } from "@/libs/xen-api";
import { useUiStore } from "@/stores/ui.store";
import { useVmStore } from "@/stores/vm.store";
import { whenever } from "@vueuse/core";
@@ -19,6 +20,6 @@ import { useRoute } from "vue-router";
const route = useRoute();
const { getByUuid, hasUuid, isReady } = useVmStore().subscribe();
const uiStore = useUiStore();
const vm = computed(() => getByUuid(route.params.uuid as string));
const vm = computed(() => getByUuid(route.params.uuid as XenApiVm["uuid"]));
whenever(vm, (vm) => (uiStore.currentHostOpaqueRef = vm.resident_on));
</script>

View File

@@ -45,6 +45,7 @@ export default class HttpProxy {
if (enabled) {
events.add('connect', this.#handleConnect.bind(this)).add('request', this.#handleRequest.bind(this))
}
debug(enabled ? 'enabled' : 'disabled')
})
}
@@ -90,6 +91,9 @@ export default class HttpProxy {
try {
await this.#handleAuthentication(req, res, async () => {
// ServerResponse is no longer necessary
res.detachSocket(clientSocket)
const { port, hostname } = new URL('http://' + req.url)
const serverSocket = net.connect(port || 80, hostname)
@@ -97,12 +101,15 @@ export default class HttpProxy {
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n')
serverSocket.write(head)
fromCallback(pipeline, clientSocket, serverSocket).catch(warn)
fromCallback(pipeline, serverSocket, clientSocket).catch(warn)
await fromCallback(pipeline, serverSocket, clientSocket, serverSocket)
})
} catch (error) {
warn(error)
clientSocket.end()
// Ignore premature close errors, which simply means that either the client or server
// socket has closed the connection without waiting proper connection termination
if (error.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
warn(error)
}
}
}

View File

@@ -14,14 +14,14 @@
"url": "https://vates.fr"
},
"license": "AGPL-3.0-or-later",
"version": "0.10.1",
"version": "0.10.2",
"engines": {
"node": ">=15.6"
},
"dependencies": {
"@vates/event-listeners-manager": "^1.0.1",
"@vates/parse-duration": "^0.1.1",
"@vates/task": "^0.1.2",
"@vates/task": "^0.2.0",
"@xen-orchestra/log": "^0.6.0",
"acme-client": "^5.0.0",
"app-conf": "^2.3.0",

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@xen-orchestra/proxy",
"version": "0.26.27",
"version": "0.26.29",
"license": "AGPL-3.0-or-later",
"description": "XO Proxy used to remotely execute backup jobs",
"keywords": [
@@ -32,11 +32,11 @@
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.4",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.38.2",
"@xen-orchestra/fs": "^4.0.0",
"@xen-orchestra/backups": "^0.39.0",
"@xen-orchestra/fs": "^4.0.1",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/mixin": "^0.1.0",
"@xen-orchestra/mixins": "^0.10.1",
"@xen-orchestra/mixins": "^0.10.2",
"@xen-orchestra/self-signed": "^0.1.3",
"@xen-orchestra/xapi": "^2.2.1",
"ajv": "^8.0.3",
@@ -60,7 +60,7 @@
"source-map-support": "^0.5.16",
"stoppable": "^1.0.6",
"xdg-basedir": "^5.1.0",
"xen-api": "^1.3.1",
"xen-api": "^1.3.3",
"xo-common": "^0.8.0"
},
"devDependencies": {

View File

@@ -1,8 +1,9 @@
import { Client } from 'node-vsphere-soap'
import { Client } from '@vates/node-vsphere-soap'
import { dirname } from 'node:path'
import { EventEmitter } from 'node:events'
import { strictEqual, notStrictEqual } from 'node:assert'
import fetch from 'node-fetch'
import https from 'https'
import parseVmdk from './parsers/vmdk.mjs'
import parseVmsd from './parsers/vmsd.mjs'
@@ -13,6 +14,7 @@ export default class Esxi extends EventEmitter {
#cookies
#dcPath
#host
#httpsAgent
#user
#password
#ready = false
@@ -22,9 +24,12 @@ export default class Esxi extends EventEmitter {
this.#host = host
this.#user = user
this.#password = password
// @FIXME this module inject NODE_TLS_REJECT_UNAUTHORIZED into the process env, which is problematic because it disables globally SSL certificate verification
//
// we need to find a fix for this, maybe forking the library
if (!sslVerify) {
this.#httpsAgent = new https.Agent({
rejectUnauthorized: false,
})
}
this.#client = new Client(host, user, password, sslVerify)
this.#client.once('ready', async () => {
try {
@@ -78,6 +83,7 @@ export default class Esxi extends EventEmitter {
headers.Range = 'bytes=' + range
}
const res = await fetch(url, {
agent: this.#httpsAgent,
method: 'GET',
headers,
highWaterMark: 10 * 1024 * 1024,

View File

@@ -1,14 +1,14 @@
{
"license": "ISC",
"private": false,
"version": "0.2.2",
"version": "0.2.3",
"name": "@xen-orchestra/vmware-explorer",
"dependencies": {
"@vates/task": "^0.1.2",
"@vates/task": "^0.2.0",
"@vates/read-chunk": "^1.1.1",
"lodash": "^4.17.21",
"node-fetch": "^3.3.0",
"node-vsphere-soap": "^0.0.2-5",
"@vates/node-vsphere-soap": "^1.0.0",
"vhd-lib": "^4.5.0"
},
"engines": {

View File

@@ -15,7 +15,7 @@
"node": ">=14"
},
"peerDependencies": {
"xen-api": "^1.3.1"
"xen-api": "^1.3.3"
},
"scripts": {
"postversion": "npm publish --access public",
@@ -24,7 +24,7 @@
"dependencies": {
"@vates/async-each": "^1.0.0",
"@vates/decorate-with": "^2.0.0",
"@vates/nbd-client": "^1.2.0",
"@vates/nbd-client": "^1.2.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"d3-time-format": "^3.0.0",

View File

@@ -1,9 +1,69 @@
# ChangeLog
## **5.83.2** (2023-06-01)
## **5.84.0** (2023-06-30)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Enhancements
- [XO Tasks] Abortion can now be requested, note that not all tasks will respond to it
- [Home/Pool] `No XCP-ng Pro support enabled on this pool` alert is considered a warning instead of an error (PR [#6849](https://github.com/vatesfr/xen-orchestra/pull/6849))
- [Plugin/auth-iodc] OpenID Connect scopes are now configurable and `profile` is included by default
- [Dashboard/Health] Button to copy UUID of an orphan VDI to the clipboard (PR [#6893](https://github.com/vatesfr/xen-orchestra/pull/6893))
- [Kubernetes recipe] Add the possibility to choose the version for the cluster [#6842](https://github.com/vatesfr/xen-orchestra/issues/6842) (PR [#6880](https://github.com/vatesfr/xen-orchestra/pull/6880))
- [New VM] cloud-init drives are now bootable in a Windows VM (PR [#6889](https://github.com/vatesfr/xen-orchestra/pull/6889))
- [Backups] Add setting `backups.metadata.defaultSettings.diskPerVmConcurrency` in xo-server's configuration file to limit the number of disks transferred in parallel per VM, this is useful to avoid transfer overloading remote and Sr (PR [#6787](https://github.com/vatesfr/xen-orchestra/pull/6787))
- [Settings/Config] Add the possibility to backup/import/download XO config from/to the XO cloud (PR [#6917](https://github.com/vatesfr/xen-orchestra/pull/6917))
- [Import/Disk] Enhance clarity for importing ISO files [Forum#61480](https://xcp-ng.org/forum/post/61480) (PR [#6874](https://github.com/vatesfr/xen-orchestra/pull/6874))
- [Import/Disk] Ability to import ISO from a URL (PR [#6924](https://github.com/vatesfr/xen-orchestra/pull/6924))
- [Import/export VDI] Ability to export/import disks in RAW format (PR [#6925](https://github.com/vatesfr/xen-orchestra/pull/6925))
### Bug fixes
- [Home/Host] Fix "isHostTimeConsistentWithXoaTime.then is not a function" (PR [#6896](https://github.com/vatesfr/xen-orchestra/pull/6896))
- [ESXi Import] was depending on an older unmaintened library that was downgrading the global security level of XO (PR [#6859](https://github.com/vatesfr/xen-orchestra/pull/6859))
- [Backup] Fix memory consumption when deleting _VHD directory_ incremental backups
- [Remote] Fix `remote is disabled` error when editing a disabled remote
- [Settings/Servers] Fix connectiong using an explicit IPv6 address
- [Backups/Health check] Use the right SR for health check during replication job (PR [#6902](https://github.com/vatesfr/xen-orchestra/pull/6902))
- [RRD stats] Improve RRD stats performance (PR [#6903](https://github.com/vatesfr/xen-orchestra/pull/6903))
### Released packages
- @xen-orchestra/fs 4.0.1
- xen-api 1.3.3
- @vates/nbd-client 1.2.1
- @vates/node-vsphere-soap 1.0.0
- @vates/task 0.2.0
- @xen-orchestra/backups 0.39.0
- @xen-orchestra/backups-cli 1.0.9
- @xen-orchestra/mixins 0.10.2
- @xen-orchestra/proxy 0.26.29
- @xen-orchestra/vmware-explorer 0.2.3
- xo-cli 0.20.0
- xo-server-auth-oidc 0.3.0
- xo-server-perf-alert 0.3.6
- xo-server 5.118.0
- xo-web 5.121.0
## **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
- [Full Replication] Fix garbage collecting previous replications
### Released packages
- xen-api 1.3.2
- @xen-orchestra/backups 0.38.3
- @xen-orchestra/proxy 0.26.28
- xo-server 5.116.4
## **5.83.2** (2023-06-01)
## Bug fixes
- [Backup] Fix `Cannot read properties of undefined (reading 'vm')` (PR [#6873](https://github.com/vatesfr/xen-orchestra/pull/6873))
@@ -71,8 +131,6 @@
## **5.82.2** (2023-05-17)
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
### Bug fixes
- [New/VM] Fix stuck Cloud Config import ([GitHub comment](https://github.com/vatesfr/xen-orchestra/issues/5896#issuecomment-1465253774))

View File

@@ -7,8 +7,6 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [XO Tasks] Abortion can now be requested, note that not all tasks will respond to it
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
@@ -29,8 +27,4 @@
<!--packages-start-->
- @vates/nbd-client patch
- @vates/task minor
- xo-web minor
<!--packages-end-->

View File

@@ -373,6 +373,10 @@ In Netbox 2.x, custom fields can be created from the Admin panel > Custom fields
- Load the plugin (button next to the plugin's name)
- Manual synchronization: if you correctly configured and loaded the plugin, a "Synchronize with Netbox" button will appear in every pool's Advanced tab, which allows you to manually synchronize it with Netbox
:::tip
If you get a `403 Forbidden` error when testing the plugin, make sure you correctly configured the "Allowed IPs" for the token you are using.
:::
## Recipes
:::tip

View File

@@ -8,11 +8,12 @@ Alternatively, here is a video recap on different backup capabilities:
- [Rolling Snapshots](rolling_snapshots.md)
- [Full Backups](full_backups.md)
- [Delta Backups](delta_backups.md)
- [Disaster Recovery](disaster_recovery.md)
- [Incremental Backups](incremental_backups.md)
- [Full Replication](full_replication.md)
- [Metadata Backups](metadata_backup.md)
- [Continuous Replication](continuous_replication.md)
- [Incremental Replication](incremental_replication.md)
- [File Level Restore](file_level_restore.md)
- [Mirror backup](mirror_backup.md)
:::tip
Don't forget to take a look at the [backup troubleshooting](backup_troubleshooting.md) section. You can also take a look at the [backup reports](backup_reports.md) section for configuring notifications.

View File

@@ -1,128 +0,0 @@
# Continuous Replication
This feature is a continuous replication system for your XenServer VMs **without any storage vendor lock-in**. You can replicate a VM every _X_ minutes/hours to any storage repository. It could be to a distant XenServer host or just another local storage target.
This feature covers multiple objectives:
- no storage vendor lock-in
- no configuration (agent-less)
- low Recovery Point Objective, from 10 minutes to 24 hours (or more)
- flexibility
- no intermediate storage needed
- atomic replication
- efficient DR (disaster recovery) process
If you lose your main pool, you can start the copy on the other side, with very recent data.
![](https://xen-orchestra.com/blog/content/images/2016/01/replication.png)
:::warning
It is normal that you can't boot the copied VM directly: we protect it. The normal workflow is to make a clone and then work on it.
This also affects VMs with "Auto Power On" enabled, because of our protections you can ensure these won't start on your CR destination if you happen to reboot it.
:::
## Configure it
As you'll see, it is trivial to configure. Inside the "Backup/new" section, select "Continuous Replication".
Then:
1. Select the VMs you want to protect
1. Schedule the replication interval
1. Select the destination storage (could be any storage connected to any XenServer host!)
That's it! Your VMs are protected and replicated as requested.
To protect the replication, we removed the possibility to boot your copied VM directly, because if you do that, it will break the next delta. The solution is to clone it if you need it (a clone is really quick). You can then do whatever you want with this clone!
## Manual initial seed
**If you can't transfer the first backup through your network because it's too large**, you can make a seed locally. In order to do this, follow this procedure (until we make it accessible directly in XO).
:::tip
This is **only** if you need to make the initial copy without making the whole transfer through your network. Otherwise, **you don't need this**. These instructions are for Backup-NG jobs, and will not work to seed a legacy backup job. Please migrate any legacy jobs to Backup-NG!
:::
### Job creation
Create the Continuous Replication backup job, and leave it disabled for now. On the main Backup-NG page, copy the job's `backupJobId` by hovering to the left of the shortened ID and clicking the copy to clipboard button:
![](./assets/cr-seed-1.png)
Copy it somewhere temporarily. Now we need to also copy the ID of the job schedule, `backupScheduleId`. Do this by hovering over the schedule name in the same panel as before, and clicking the copy to clipboard button. Keep it with the `backupJobId` you copied previously as we will need them all later:
![](./assets/cr-seed-2.png)
### Seed creation
Manually create a snapshot on the VM being backed up, then copy this snapshot UUID, `snapshotUuid` from the snapshot panel of the VM:
![](./assets/cr-seed-3.png)
:::warning
DO NOT ever delete or alter this snapshot, feel free to rename it to make that clear.
:::
### Seed copy
Export this snapshot to a file, then import it on the target SR.
We need to copy the UUID of this newly created VM as well, `targetVmUuid`:
![](./assets/cr-seed-4.png)
:::warning
DO not start this VM or it will break the Continuous Replication job! You can rename this VM to more easily remember this.
:::
### Set up metadata
The XOA backup system requires metadata to correctly associate the source snapshot and the target VM to the backup job. We're going to use the `xo-cr-seed` utility to help us set them up.
First install the tool (all the following is done from the XOA VM CLI):
```sh
sudo npm i -g --unsafe-perm @xen-orchestra/cr-seed-cli
```
Here is an example of how the utility expects the UUIDs and info passed to it:
```console
$ xo-cr-seed
Usage: xo-cr-seed <source XAPI URL> <source snapshot UUID> <target XAPI URL> <target VM UUID> <backup job id> <backup schedule id>
xo-cr-seed v0.2.0
```
Putting it altogether and putting our values and UUID's into the command, it will look like this (it is a long command):
```console
$ xo-cr-seed https://root:password@xen1.company.tld 4a21c1cd-e8bd-4466-910a-f7524ecc07b1 https://root:password@xen2.company.tld 5aaf86ca-ae06-4a4e-b6e1-d04f0609e64d 90d11a94-a88f-4a84-b7c1-ed207d3de2f9 369a26f0-da77-41ab-a998-fa6b02c69b9a
```
:::warning
If the username or the password for your XCP-ng/XenServer hosts contains special characters, they must use [percent encoding](https://en.wikipedia.org/wiki/Percent-encoding).
An easy way to do this with Node in command line:
```console
$ node -p 'encodeURIComponent(process.argv[1])' -- 'password with special chars :#@'
password%20with%20special%20chars%20%3A%23%40
```
:::
### Finished
Your backup job should now be working correctly! Manually run the job the first time to check if everything is OK. Then, enable the job. **Now, only the deltas are sent, your initial seed saved you a LOT of time if you have a slow network.**
### Failover process
In the situation where you need to failover to your destination host, you simply need to start all your VMs on the destination host.
:::tip
If you want to start a VM on your destination host without breaking the CR jobs on the other side, you will need to make a copy of the VM and start the copy. Otherwise, you will be asked if you would like to force start the VMs.
:::
![](./assets/force-start.jpg)

View File

@@ -0,0 +1 @@
incremental_replication.md

View File

@@ -1,66 +0,0 @@
# Continuous Delta backups
You can export only the delta (difference) between your current VM disks and a previous snapshot (called here the _reference_). They are called _continuous_ because you'll **never export a full backup** after the first one.
## Introduction
Full backups can be represented like this:
![](./assets/nodelta.png)
It means huge files for each backup. Delta backups will only export the difference between the previous backup:
![](./assets/delta_final.png)
You can imagine making your first initial full backup during a weekend, and then only delta backups every night. It combines the flexibility of snapshots and the power of full backups, because:
- delta are stored somewhere else than the current VM storage
- they are small
- quick to create
- easy to restore
So, if you want to rollback your VM to a previous state, the cost is only one snapshot on your SR (far less than the [rolling snapshot](rolling_snapshot.md) mechanism).
Even if you lost your whole SR or VM, XOA will restore your VM entirely and automatically, at any date of backup.
You can even imagine using this to backup more often! Because deltas will be smaller, and will **always be deltas**.
### Continuous
They are called continuous because you'll **never export a full backup** after the first one. We'll merge the oldest delta into the full:
![](./assets/deltamerge1.png)
This way we can go "forward" and remove this oldest VHD after the merge:
![](./assets/deltamerge2.png)
## Create Delta backup
Just go into your "Backup" view, and select Delta Backup. Then, it's the same as a normal backup.
## Snapshots
Unlike other types of backup jobs which delete the associated snapshot when the job is done and it has been exported, delta backups always keep a snapshot of every VM in the backup job, and uses it for the delta. Do not delete these snapshots!
## Delta backup initial seed
If you don't want to do an initial full directly toward the destination, you can create a local delta backup first, then transfer the files to your destination.
Then, only the diff will be sent.
1. create a delta backup job to the first remote
1. run the backup (full)
1. edit the job to target the other remote
1. copy files from the first remote to the other one
1. run the backup (delta)
## Full backup interval
This advanced setting defines the number of backups after which a full backup is triggered, ie the maximum length of a delta chain.
For example, with a value of 2, the first two backups will be a full and a delta, and the third will start a new chain with a full backup.
This is important because on rare occasions a backup can be corrupted, and in the case of delta backups, this corruption might impact all the following backups in the chain. Occasionally performing a full backup limits how far a corrupted delta backup can propagate.
The value to use depends on your storage constraints and the frequency of your backups, but a value of 20 is a good start.

1
docs/delta_backups.md Symbolic link
View File

@@ -0,0 +1 @@
incremental_backups.md

View File

@@ -1,41 +0,0 @@
# Disaster recovery
Disaster Recovery (DR) encompasses all the ways to recover after losing hosts or storage repositories.
In this guide we'll only see the technical aspect of DR, which is a small part of this vast topic.
## Best practices
We strongly encourage you to read some literature on this topic. Basically, you should be able to recover from a major disaster within an appropriate amount of time and minimal acceptable data loss.
To avoid a potentially very long import process (restoring all your backup VMs), we implemented a streaming feature. [Streaming allows exporting and importing at the same time](https://xen-orchestra.com/blog/vm-streaming-export-in-xenserver/).
**The goal is to have your DR VMs ready to boot on a dedicated host. This also provides a way to check if you export was successful (if the VM boots).**
![](https://xen-orchestra.com/blog/content/images/2015/10/newsolution.png)
## Schedule a DR task
Planning a DR task is very similar to planning a backup or a snapshot. The only difference is that you select a storage destination.
You DR VMs will be visible "on the other side" as soon the task is done.
### Retention
Retention, or **depth**, applies to the VM name. **If you change the VM name for any reason, it won't be rotated anymore.** This way, you can play with your DR VM without the fear of losing it.
Also, by default, the DR VM will have a "Disaster Recovery" tag.
:::warning
A higher retention number will lead to huge space occupation on your SR.
:::
## Network conflicts
If you boot a copy of your production VM, be careful: if they share the same static IP, you'll have troubles.
A good way to avoid this kind of problem is to remove the network interface on the DR VM and check if the export is correctly done.
:::warning
For each DR replicated VM, we add "start" as a blocked operation, meaning even VMs with "Auto power on" enabled will not be started on your DR destination if it reboots.
:::

1
docs/disaster_recovery.md Symbolic link
View File

@@ -0,0 +1 @@
full_replication.md

41
docs/full_replication.md Normal file
View File

@@ -0,0 +1,41 @@
# Full Replication (formerly: Disaster recovery)
Full Replication (FR) encompasses all the ways to recover after losing hosts or storage repositories.
In this guide we'll only see the technical aspect of DR, which is a small part of this vast topic.
## Best practices
We strongly encourage you to read some literature on this topic. Basically, you should be able to recover from a major disaster within an appropriate amount of time and minimal acceptable data loss.
To avoid a potentially very long import process (restoring all your backup VMs), we implemented a streaming feature. [Streaming allows exporting and importing at the same time](https://xen-orchestra.com/blog/vm-streaming-export-in-xenserver/).
**The goal is to have your DR VMs ready to boot on a dedicated host. This also provides a way to check if you export was successful (if the VM boots).**
![](https://xen-orchestra.com/blog/content/images/2015/10/newsolution.png)
## Schedule a DR task
Planning a DR task is very similar to planning a backup or a snapshot. The only difference is that you select a storage destination.
You DR VMs will be visible "on the other side" as soon the task is done.
### Retention
Retention, or **depth**, applies to the VM name. **If you change the VM name for any reason, it won't be rotated anymore.** This way, you can play with your DR VM without the fear of losing it.
Also, by default, the DR VM will have a "Disaster Recovery" tag.
:::warning
A higher retention number will lead to huge space occupation on your SR.
:::
## Network conflicts
If you boot a copy of your production VM, be careful: if they share the same static IP, you'll have troubles.
A good way to avoid this kind of problem is to remove the network interface on the DR VM and check if the export is correctly done.
:::warning
For each DR replicated VM, we add "start" as a blocked operation, meaning even VMs with "Auto power on" enabled will not be started on your DR destination if it reboots.
:::

View File

@@ -0,0 +1,66 @@
# Incremental Backups (formerly: Continuous Delta backups)
You can export only the delta (difference) between your current VM disks and a previous snapshot (called here the _reference_). They are called _continuous_ because you'll **never export a full backup** after the first one.
## Introduction
Full backups can be represented like this:
![](./assets/nodelta.png)
It means huge files for each backup. Incremental backups will only export the difference between the previous backup:
![](./assets/delta_final.png)
You can imagine making your first initial key(complete) backup during a weekend, and then only delta backups every night. It combines the flexibility of snapshots and the power of full backups, because:
- delta are stored somewhere else than the current VM storage
- they are small
- quick to create
- easy to restore
So, if you want to rollback your VM to a previous state, the cost is only one snapshot on your SR (far less than the [rolling snapshot](rolling_snapshot.md) mechanism).
Even if you lost your whole SR or VM, XOA will restore your VM entirely and automatically, at any date of backup.
You can even imagine using this to backup more often! Because deltas will be smaller, and will **always be deltas**.
### Continuous
They are called continuous because you'll **never export a full backup** after the first one. We'll merge the oldest delta into the full:
![](./assets/deltamerge1.png)
This way we can go "forward" and remove this oldest VHD after the merge:
![](./assets/deltamerge2.png)
## Create Delta backup
Just go into your "Backup" view, and select Delta Backup. Then, it's the same as a normal backup.
## Snapshots
Unlike other types of backup jobs which delete the associated snapshot when the job is done and it has been exported, delta backups always keep a snapshot of every VM in the backup job, and uses it for the delta. Do not delete these snapshots!
## Incremental backup initial seed
If you don't want to do an initial full directly toward the destination, you can create a local delta backup first, then transfer the files to your destination.
Then, only the diff will be sent.
1. create a incremental backup job to the first remote
1. run the backup (full)
1. edit the job to target the other remote
1. copy files from the first remote to the other one
1. run the backup (incremental)
## Key backup interval
This advanced setting defines the number of backups after which a key backup is triggered, ie the maximum length of a delta chain.
For example, with a value of 2, the first two backups will be a key and a delta, and the third will start a new chain with a full backup.
This is important because on rare occasions a backup can be corrupted, and in the case of incremetnal backups, this corruption might impact all the following backups in the chain. Occasionally performing a full backup limits how far a corrupted delta backup can propagate.
The value to use depends on your storage constraints and the frequency of your backups, but a value of 20 is a good start.

View File

@@ -0,0 +1,128 @@
# Incremental Replication (formerly : Continuous Replication)
This feature is an incremental replication system for your XCP-ng or Xenserver VMs **without any storage vendor lock-in**. You can replicate a VM every _X_ minutes/hours to any storage repository. It could be to a distant XCP-ng or XenServer host or just another local storage target.
This feature covers multiple objectives:
- no storage vendor lock-in
- no configuration (agent-less)
- low Recovery Point Objective, from 10 minutes to 24 hours (or more)
- flexibility
- no intermediate storage needed
- atomic replication
- efficient DR (disaster recovery) process
If you lose your main pool, you can start the copy on the other side, with very recent data.
![](https://xen-orchestra.com/blog/content/images/2016/01/replication.png)
:::warning
It is normal that you can't boot the copied VM directly: we protect it. The normal workflow is to make a clone and then work on it.
This also affects VMs with "Auto Power On" enabled, because of our protections you can ensure these won't start on your CR destination if you happen to reboot it.
:::
## Configure it
As you'll see, it is trivial to configure. Inside the "Backup/new" section, select "Incremental replication".
Then:
1. Select the VMs you want to protect
1. Schedule the replication interval
1. Select the destination storage (could be any storage connected to any host!)
That's it! Your VMs are protected and replicated as requested.
To protect the replication, we removed the possibility to boot your copied VM directly, because if you do that, it will break the next delta. The solution is to clone it if you need it (a clone is really quick). You can then do whatever you want with this clone!
## Manual initial seed
**If you can't transfer the first backup through your network because it's too large**, you can make a seed locally. In order to do this, follow this procedure (until we make it accessible directly in XO).
:::tip
This is **only** if you need to make the initial copy without making the whole transfer through your network. Otherwise, **you don't need this**.
:::
### Job creation
Create the Incremental Replication backup job, and leave it disabled for now. On the main Backup page, copy the job's `backupJobId` by hovering to the left of the shortened ID and clicking the copy to clipboard button:
![](./assets/cr-seed-1.png)
Copy it somewhere temporarily. Now we need to also copy the ID of the job schedule, `backupScheduleId`. Do this by hovering over the schedule name in the same panel as before, and clicking the copy to clipboard button. Keep it with the `backupJobId` you copied previously as we will need them all later:
![](./assets/cr-seed-2.png)
### Seed creation
Manually create a snapshot on the VM being backed up, then copy this snapshot UUID, `snapshotUuid` from the snapshot panel of the VM:
![](./assets/cr-seed-3.png)
:::warning
DO NOT ever delete or alter this snapshot, feel free to rename it to make that clear.
:::
### Seed copy
Export this snapshot to a file, then import it on the target SR.
We need to copy the UUID of this newly created VM as well, `targetVmUuid`:
![](./assets/cr-seed-4.png)
:::warning
DO not start this VM or it will break the Incremental Replication job! You can rename this VM to more easily remember this.
:::
### Set up metadata
The XOA backup system requires metadata to correctly associate the source snapshot and the target VM to the backup job. We're going to use the `xo-cr-seed` utility to help us set them up.
First install the tool (all the following is done from the XOA VM CLI):
```sh
sudo npm i -g --unsafe-perm @xen-orchestra/cr-seed-cli
```
Here is an example of how the utility expects the UUIDs and info passed to it:
```console
$ xo-cr-seed
Usage: xo-cr-seed <source XAPI URL> <source snapshot UUID> <target XAPI URL> <target VM UUID> <backup job id> <backup schedule id>
xo-cr-seed v0.2.0
```
Putting it altogether and putting our values and UUID's into the command, it will look like this (it is a long command):
```console
$ xo-cr-seed https://root:password@xen1.company.tld 4a21c1cd-e8bd-4466-910a-f7524ecc07b1 https://root:password@xen2.company.tld 5aaf86ca-ae06-4a4e-b6e1-d04f0609e64d 90d11a94-a88f-4a84-b7c1-ed207d3de2f9 369a26f0-da77-41ab-a998-fa6b02c69b9a
```
:::warning
If the username or the password for your XCP-ng/XenServer hosts contains special characters, they must use [percent encoding](https://en.wikipedia.org/wiki/Percent-encoding).
An easy way to do this with Node in command line:
```console
$ node -p 'encodeURIComponent(process.argv[1])' -- 'password with special chars :#@'
password%20with%20special%20chars%20%3A%23%40
```
:::
### Finished
Your backup job should now be working correctly! Manually run the job the first time to check if everything is OK. Then, enable the job. **Now only the deltas are sent, your initial seed saved you a LOT of time if you have a slow network.**
### Failover process
In the situation where you need to failover to your destination host, you simply need to start all your VMs on the destination host.
:::tip
If you want to start a VM on your destination host without breaking the CR jobs on the other side, you will need to make a copy of the VM and start the copy. Otherwise, you will be asked if you would like to force start the VMs.
:::
![](./assets/force-start.jpg)

105
docs/mirror_backup.md Normal file
View File

@@ -0,0 +1,105 @@
# Mirror Backups
The goal is to replicate a backup from one remote to another. For instance, you make your backup to in-house NFS storage, and then replicate to bigger, slower and cheaper storage with a longer retention.
The source and destination can have different settings for encryption, VHD storage mode, retention, or compression.
## Creation
Just go into your "Backup" view, and select Vm Mirror Backup.
Then, select if you want to mirror incremental backups or full backups.
You must have exactly one source remote, you must have one or more destinations. The mirroring speed will be limited by the slower remote.
Most options of the full/incremental backups applies here, like concurrency (number of VM transferred in parallel), report, proxy and speed limit. You can also add a health check on schedules.
:::tip
If you have full and incremental backups on a remote, you must configure 2 mirror backup jobs, one full and one incremental.
:::
## synchronizing algorithm for full backups
Any new backup on the source is transfered to the remote
_key backup(full) are in upper case, delta backup are in lowercase_ . _Source has a retention of 3, destination has 4_
### First transfer
```
- source : ABC
- destination: empty
```
will transfer in order A , then B, and C. Destination will contains ABC
> **Limitation:** if the mirror retention is lower than the backup retention on the source remote, more data than necessary may be transferred during the first run, since all the backups of the source will be transfered to the destinations. Then the older backups will be purged on the destinations.
### Second transfer
```
- source : BCD
- destination: ABC
```
will transfer D. Destination will contains ABCD
### Third transfer
```
- source : CDE
- destination: ABCD
```
will transfer E and delete A from remote. Destination will contains BCDE
### if there is too much change on source
```
- source : IJK
- destination: BCDE
```
will transfer in order IJK and delete BCD from remote. Destination will contains EIJK
## Synchronizing algorithm for incremental backups
this will only transfer new backups, and then run the same merge algorithm than in [Incremental Backups](incremental_backups.md).
_key backup(full) are in upper case, delta backup are in lowercase_ . _Source has a retention of 3, destination has 4_
### First transfer
```
- source : Abc # one key, two delta
- destination: empty
```
will transfer in order A , then b, and c. Destination will contains Abc
> **Limitation:** if the mirror retention is lower than the backup retention on the source remote, more data than necessary may be transferred during the first run, since all the backups of the source will be transfered to the destinations. Then the older backups will be purged on the destinations.
### Second transfer
```
- source : Bcd # A and b have been merged
- destination: Abc
```
transfer only the delta d , destination will contains Abcd (no merge)
### Third transfer
```
- source : Cde # B and c have been merged
- destination: Abcd
```
transfer only the delta e , destination will contains Bcde (merge A into b)
### if there is too much change on source
```
- source : Ijk
- destination: Bcde
```
transfer all the chain in order, destination will contains EIjk (merge B,c and d into e)

View File

@@ -1,7 +1,7 @@
# REST API
:::warning ⚠️ Alpha feature
This is an alpha feature that may change significantly in the coming months - do not use this feature in a production environment. The fully up-to-date README [is available here](https://github.com/vatesfr/xen-orchestra/blob/master/packages/xo-server/docs/rest-api.md). This page is only here to explain how it works, it's not intended to be used as an accurate guide.
:::warning ⚠️ Beta feature
This is a beta feature that may see some changes in the future. The fully up-to-date README [is available here](https://github.com/vatesfr/xen-orchestra/blob/master/packages/xo-server/docs/rest-api.md). This page is only here to explain how it works, it's not intended to be used as an accurate guide.
:::
We originally developed [our existing API](architecture.html#api) to be used between the Web UI `xo-web` and the server backend, `xo-server`. That's why it's a JSON-RPC API connected via websockets, allowing us to update objects live in the browser. This is perfect for our usage, but a bit complicated for others.

View File

@@ -62,6 +62,7 @@
"/@vates/decorate-with/",
"/@vates/event-listeners-manager/",
"/@vates/nbd-client/",
"/@vates/node-vsphere-soap/",
"/@vates/predicates/",
"/@xen-orchestra/audit-core/",
"/dist/",

View File

@@ -23,7 +23,7 @@
"node": ">=10"
},
"dependencies": {
"@xen-orchestra/fs": "^4.0.0",
"@xen-orchestra/fs": "^4.0.1",
"cli-progress": "^3.1.0",
"exec-promise": "^0.7.0",
"getopts": "^2.2.3",

View File

@@ -8,9 +8,8 @@ const { asyncEach } = require('@vates/async-each')
const { warn } = createLogger('vhd-lib:createVhdDirectoryFromStream')
const buildVhd = Disposable.wrap(async function* (handler, path, inputStream, { concurrency, compression, isDelta }) {
const buildVhd = Disposable.wrap(async function* (handler, path, inputStream, { concurrency, compression }) {
const vhd = yield VhdDirectory.create(handler, path, { compression })
const emptyBlock = Buffer.alloc(2 * 1024 * 1024, 0)
await asyncEach(
parseVhdStream(inputStream),
async function (item) {
@@ -25,11 +24,7 @@ const buildVhd = Disposable.wrap(async function* (handler, path, inputStream, {
await vhd.writeParentLocator({ ...item, data: item.buffer })
break
case 'block':
// automatically thin blocks of key backup
// we can't thin block of delta backup since it can be an empty block whom parent block contains data
if (isDelta || !emptyBlock.equals(item.buffer)) {
await vhd.writeEntireBlock(item)
}
await vhd.writeEntireBlock(item)
break
case 'bat':
// it exists but I don't care
@@ -50,10 +45,10 @@ exports.createVhdDirectoryFromStream = async function createVhdDirectoryFromStre
handler,
path,
inputStream,
{ validator, concurrency = 16, compression, isDelta } = {}
{ validator, concurrency = 16, compression } = {}
) {
try {
const size = await buildVhd(handler, path, inputStream, { concurrency, compression, isDelta })
const size = await buildVhd(handler, path, inputStream, { concurrency, compression })
if (validator !== undefined) {
await validator.call(this, path)
}

View File

@@ -20,7 +20,7 @@
"@vates/read-chunk": "^1.1.1",
"@vates/stream-reader": "^0.1.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^4.0.0",
"@xen-orchestra/fs": "^4.0.1",
"@xen-orchestra/log": "^0.6.0",
"async-iterator-to-stream": "^1.0.2",
"decorator-synchronized": "^0.6.0",
@@ -33,7 +33,7 @@
"uuid": "^9.0.0"
},
"devDependencies": {
"@xen-orchestra/fs": "^4.0.0",
"@xen-orchestra/fs": "^4.0.1",
"execa": "^5.0.0",
"get-stream": "^6.0.0",
"rimraf": "^5.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.1"
"xen-api": "^1.3.3"
},
"devDependencies": {
"@babel/cli": "^7.1.5",

View File

@@ -11,7 +11,7 @@ const data = {
protocol: 'https:',
},
'[::1]': {
hostname: '::1',
hostname: '[::1]',
pathname: '/',
protocol: 'https:',
},

View File

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

View File

@@ -1,4 +1,4 @@
const URL_RE = /^(?:(https?:)\/*)?(?:(([^:]*)(?::([^@]*))?)@)?(?:\[([^\]]+)\]|([^:/]+))(?::([0-9]+))?(\/[^?#]*)?$/
const URL_RE = /^(?:(https?:)\/*)?(?:(([^:]*)(?::([^@]*))?)@)?(\[[^\]]+\]|[^:/]+)(?::([0-9]+))?(\/[^?#]*)?$/
export default url => {
const matches = URL_RE.exec(url)
@@ -6,8 +6,7 @@ export default url => {
throw new Error('invalid URL: ' + url)
}
const [, protocol = 'https:', auth, username = '', password = '', ipv6, hostname = ipv6, port, pathname = '/'] =
matches
const [, protocol = 'https:', auth, username = '', password = '', hostname, port, pathname = '/'] = matches
const parsedUrl = {
protocol,
hostname,

View File

@@ -44,7 +44,7 @@ const usage = 'Usage: xen-api <url> [<user> [<password>]]'
async function main(createClient) {
const opts = minimist(process.argv.slice(2), {
string: ['proxy', 'session-id'],
string: ['proxy', 'session-id', 'transport'],
boolean: ['allow-unauthorized', 'help', 'read-only', 'verbose'],
alias: {
@@ -54,6 +54,7 @@ async function main(createClient) {
proxy: 'p',
'read-only': 'ro',
verbose: 'v',
transport: 't',
},
})
@@ -91,6 +92,7 @@ async function main(createClient) {
httpProxy: opts.proxy,
readOnly: opts.ro,
syncStackTraces: true,
transport: opts.transport || undefined,
})
await xapi.connect()

View File

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

View File

@@ -76,9 +76,15 @@ Usage:
xo-cli rest get tasks filter='status:pending'
xo-cli rest get vms fields=name_label,power_state
xo-cli rest get <object> [wait | wait=result]
xo-cli rest get [--output <file>] <object> [wait | wait=result]
Show an object from the REST API.
--output <file>
If specified, the response will be saved in <file> instead of being parsed.
If <file> ends with `/`, it will be considered as the directory in which
to save the response, and the filename will be last part of the <object> path.
<object>
Full path of the object to show
@@ -92,6 +98,18 @@ Usage:
xo-cli rest get vms/<VM UUID>
xo-cli rest get tasks/<task id>/actions wait=result
xo-cli rest patch <object> <name>=<value>...
Update properties of an object (not all properties are writable).
<object>
Full path of the object to update
<name>=<value>...
Properties to update on the object
Examples:
xo-cli rest patch vms/<VM UUID> name_label='My VM' name_description='Its description
xo-cli rest post <action> <name>=<value>...
Execute an action.

View File

@@ -110,6 +110,18 @@ Usage:
xo-cli rest get vms/<VM UUID>
xo-cli rest get tasks/<task id>/actions wait=result
xo-cli rest patch <object> <name>=<value>...
Update properties of an object (not all properties are writable).
<object>
Full path of the object to update
<name>=<value>...
Properties to update on the object
Examples:
xo-cli rest patch vms/<VM UUID> name_label='My VM' name_description='Its description
xo-cli rest post <action> <name>=<value>...
Execute an action.

View File

@@ -297,9 +297,15 @@ const help = wrap(
$name rest get tasks filter='status:pending'
$name rest get vms fields=name_label,power_state
$name rest get <object> [wait | wait=result]
$name rest get [--output <file>] <object> [wait | wait=result]
Show an object from the REST API.
--output <file>
If specified, the response will be saved in <file> instead of being parsed.
If <file> ends with \`/\`, it will be considered as the directory in which
to save the response, and the filename will be last part of the <object> path.
<object>
Full path of the object to show
@@ -313,6 +319,18 @@ const help = wrap(
$name rest get vms/<VM UUID>
$name rest get tasks/<task id>/actions wait=result
$name rest patch <object> <name>=<value>...
Update properties of an object (not all properties are writable).
<object>
Full path of the object to update
<name>=<value>...
Properties to update on the object
Examples:
$name rest patch vms/<VM UUID> name_label='My VM' name_description='Its description
$name rest post <action> <name>=<value>...
Execute an action.

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-cli",
"version": "0.19.0",
"version": "0.20.0",
"license": "AGPL-3.0-or-later",
"description": "Basic CLI for Xen-Orchestra",
"keywords": [
@@ -30,6 +30,7 @@
},
"dependencies": {
"chalk": "^5.0.1",
"content-type": "^1.0.5",
"fs-extra": "^11.1.0",
"getopts": "^2.3.0",
"http-request-plus": "^1.0.0",

View File

@@ -1,4 +1,8 @@
import { basename, join } from 'node:path'
import { createWriteStream } from 'node:fs'
import { normalize } from 'node:path/posix'
import { parse as parseContentType } from 'content-type'
import { pipeline } from 'node:stream/promises'
import getopts from 'getopts'
import hrp from 'http-request-plus'
import merge from 'lodash/merge.js'
@@ -22,7 +26,8 @@ function parseParams(args) {
if (i === -1) {
params[arg] = ''
} else {
params[arg.slice(0, i)] = arg.slice(i + 1)
const value = arg.slice(i + 1)
params[arg.slice(0, i)] = value.startsWith('json:') ? JSON.parse(value.slice(5)) : value
}
}
return params
@@ -42,23 +47,58 @@ const COMMANDS = {
return await response.text()
},
async get([path, ...args]) {
const response = await this.exec(path, { query: parseParams(args) })
async get(args) {
const {
_: [path, ...rest],
output,
} = getopts(args, {
alias: { output: 'o' },
string: 'output',
stopEarly: true,
})
const result = await response.json()
const response = await this.exec(path, { query: parseParams(rest) })
if (Array.isArray(result)) {
for (let i = 0, n = result.length; i < n; ++i) {
const value = result[i]
if (typeof value === 'string') {
result[i] = stripPrefix(value)
} else if (value != null && typeof value.href === 'string') {
value.href = stripPrefix(value.href)
}
}
if (output !== '') {
return pipeline(
response,
output === '-'
? process.stdout
: createWriteStream(output.endsWith('/') ? join(output, basename(path)) : output, { flags: 'wx' })
)
}
return this.json ? JSON.stringify(result, null, 2) : result
const { type } = parseContentType(response)
if (type === 'application/json') {
const result = await response.json()
if (Array.isArray(result)) {
for (let i = 0, n = result.length; i < n; ++i) {
const value = result[i]
if (typeof value === 'string') {
result[i] = stripPrefix(value)
} else if (value != null && typeof value.href === 'string') {
value.href = stripPrefix(value.href)
}
}
}
return this.json ? JSON.stringify(result, null, 2) : result
} else {
throw new Error('unsupported content-type ' + type)
}
},
async patch([path, ...params]) {
const response = await this.exec(path, {
body: JSON.stringify(parseParams(params)),
headers: {
'content-type': 'application/json',
},
method: 'PATCH',
})
return await response.text()
},
async post([path, ...params]) {

View File

@@ -38,6 +38,17 @@ exports.configurationSchema = {
title: 'Username field',
type: 'string',
},
scope: {
description: `List of scopes from which to request profile information.
Scopes should be listed separated by a single whitespace.
Note: The \`openid\` scope is implicitely included.
`,
default: 'profile',
title: 'Scopes',
type: 'string',
},
},
},
},

View File

@@ -25,7 +25,7 @@
"url": "https://vates.fr"
},
"license": "AGPL-3.0-or-later",
"version": "0.2.0",
"version": "0.3.0",
"engines": {
"node": ">=12"
},

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-perf-alert",
"version": "0.3.5",
"version": "0.3.6",
"license": "AGPL-3.0-or-later",
"description": "Sends alerts based on performance criteria",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-perf-alert",

View File

@@ -589,12 +589,14 @@ ${monitorBodies.join('\n')}`
const entriesWithMissingStats = []
for (const entry of snapshot) {
// Ignore special SRs (e.g. *XCP-ng Tools*, *DVD drives*, etc) as their usage is always 100%
if (entry.object.physical_size <= 0 && entry.object.content_type === 'iso') continue
// can happen when the user forgets to remove an element that doesn't exist anymore from the list of the monitored machines
if (entry.object === null) continue
if (entry.value === undefined) {
entriesWithMissingStats.push(entry)
continue
}
// Ignore special SRs (e.g. *XCP-ng Tools*, *DVD drives*, etc) as their usage is always 100%
if (entry.object.physical_size <= 0 && entry.object.content_type === 'iso') continue
const raiseAlarm = _alarmId => {
// sample XenCenter message (linebreaks are meaningful):

View File

@@ -13,7 +13,7 @@
- [Start an action](#start-an-action)
- [The future](#the-future)
> This [REST](https://en.wikipedia.org/wiki/Representational_state_transfer)-oriented API is experimental. Non-backward compatible changes or removal may occur in any future release. Use of the feature is not recommended in production environments.
> This [REST](https://en.wikipedia.org/wiki/Representational_state_transfer)-oriented API is in beta, some minor changes may occur in the future.
## Authentication

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.116.3",
"version": "5.118.0",
"license": "AGPL-3.0-or-later",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -41,17 +41,17 @@
"@vates/predicates": "^1.1.0",
"@vates/read-chunk": "^1.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.38.2",
"@xen-orchestra/backups": "^0.39.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.0",
"@xen-orchestra/fs": "^4.0.1",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/mixin": "^0.1.0",
"@xen-orchestra/mixins": "^0.10.1",
"@xen-orchestra/mixins": "^0.10.2",
"@xen-orchestra/self-signed": "^0.1.3",
"@xen-orchestra/template": "^0.1.0",
"@xen-orchestra/vmware-explorer": "^0.2.2",
"@xen-orchestra/vmware-explorer": "^0.2.3",
"@xen-orchestra/xapi": "^2.2.1",
"ajv": "^8.0.3",
"app-conf": "^2.3.0",
@@ -131,7 +131,7 @@
"vhd-lib": "^4.5.0",
"ws": "^8.2.3",
"xdg-basedir": "^5.1.0",
"xen-api": "^1.3.1",
"xen-api": "^1.3.3",
"xo-acl-resolver": "^0.4.1",
"xo-collection": "^0.5.0",
"xo-common": "^0.8.0",

View File

@@ -1,10 +1,11 @@
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 { noSuchObject } from 'xo-common/api-errors.js'
import { invalidParameters, noSuchObject } from 'xo-common/api-errors.js'
import { pipeline } from 'stream'
import { peekFooterFromVhdStream } from 'vhd-lib'
import { vmdkToVhd } from 'xo-vmdk-to-vhd'
@@ -261,8 +262,31 @@ async function handleImport(req, res, { type, name, description, vmdkData, srId,
})
}
// type is 'vhd' or 'vmdk'
async function importDisk({ sr, type, name, description, vmdkData }) {
// type is 'vhd', 'vmdk', 'raw' or 'iso'
async function importDisk({ sr, type, name, description, url, vmdkData }) {
if (url !== undefined) {
const isRaw = type === 'raw' || type === 'iso'
if (!(isRaw || type === 'vhd')) {
throw invalidParameters('URL import is only compatible with VHD and raw formats')
}
const stream = await hrp(url)
const length = stream.headers['content-length']
if (length !== undefined) {
stream.length = length
}
sr = this.getXapiObject(sr)
const vdiRef = await sr.$importVdi(stream, {
format: isRaw ? VDI_FORMAT_RAW : VDI_FORMAT_VHD,
name_label: name,
name_description: description,
})
return await sr.$xapi.getField('VDI', vdiRef, 'uuid')
}
return {
$sendTo: await this.registerHttpRequest(handleImport, {
description,
@@ -281,6 +305,7 @@ importDisk.params = {
description: { type: 'string', minLength: 0, optional: true },
name: { type: 'string' },
sr: { type: 'string' },
url: { type: 'string', optional: true },
type: { type: 'string' },
vmdkData: {
type: 'object',

View File

@@ -23,6 +23,29 @@ const SECTOR_SIZE = 512
const TEN_MIB = 10 * 1024 * 1024
// https://en.wikipedia.org/wiki/Master_boot_record
// we'll add a classical generic mbr
// with one FAT16 partition addressed by LBA
export function addMbr(buf) {
const mbr = Buffer.alloc(SECTOR_SIZE, 0)
// 0 - 446 is bootstrap code , keep it empty
// entry
mbr[446] = 0x80 // entry is bootable
mbr[450] = 0x0e // FAT16 LBA
mbr.writeInt32LE(1, 454) // LBA address of first sector
assert.strictEqual(buf.length % SECTOR_SIZE, 0, 'buffer length must be aligned to sector size')
mbr.writeInt32LE(buf.length / SECTOR_SIZE + 1, 458) // LBA address of last sector
// 3 more 16 bytes entry we don't need
// boot signature
mbr[510] = 0x55
mbr[511] = 0xaa
return Buffer.concat([mbr, buf])
}
// Creates a 10MB buffer and initializes it as a FAT 16 volume.
export function init({ label = 'NO LABEL ' } = {}) {
assert.strictEqual(typeof label, 'string')

View File

@@ -26,7 +26,7 @@ export const merge = (newValue, oldValue) => {
export const obfuscate = value => replace(value, OBFUSCATED_VALUE)
const SENSITIVE_PARAMS = ['token', /password/i, 'encryptionKey']
const SENSITIVE_PARAMS = ['token', 'passphrase', /password/i, 'encryptionKey']
const isSensitiveParam = name =>
SENSITIVE_PARAMS.some(pattern => (typeof pattern === 'string' ? pattern === name : pattern.test(name)))

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