Compare commits
78 Commits
feat_thin_
...
lite/neste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
677a9c958c | ||
|
|
2978ad1486 | ||
|
|
c0d6dc48de | ||
|
|
f327422254 | ||
|
|
938d15d31b | ||
|
|
5ab1ddb9cb | ||
|
|
01302d7a60 | ||
|
|
c68630e2d6 | ||
|
|
db082bfbe9 | ||
|
|
650d88db46 | ||
|
|
7d1ecca669 | ||
|
|
5f71e629ae | ||
|
|
68205d4676 | ||
|
|
cdb466225d | ||
|
|
0e7fbd598f | ||
|
|
99147c893d | ||
|
|
c63fb6173d | ||
|
|
5932ada717 | ||
|
|
0d579748d6 | ||
|
|
8c5ee4eafe | ||
|
|
b03935ad2f | ||
|
|
38439cbc43 | ||
|
|
161c20b534 | ||
|
|
603696dad1 | ||
|
|
6b2ad5a7cc | ||
|
|
88063d4d87 | ||
|
|
8956a99745 | ||
|
|
0f0c0ec0d0 | ||
|
|
e5932e2c33 | ||
|
|
84ec8f5f3c | ||
|
|
661c5a269f | ||
|
|
5c6d7cae66 | ||
|
|
fcc73859b7 | ||
|
|
36645b0319 | ||
|
|
a62575e3cf | ||
|
|
d7af3d3c03 | ||
|
|
130ebb7d5f | ||
|
|
2af845ebd3 | ||
|
|
8e4d1701e6 | ||
|
|
4d16b6708f | ||
|
|
34ee08be25 | ||
|
|
d66a76a09e | ||
|
|
0d801c9766 | ||
|
|
b82b676fdb | ||
|
|
3494c0f64f | ||
|
|
311098adc2 | ||
|
|
58182e2083 | ||
|
|
a62ae43274 | ||
|
|
f256610e08 | ||
|
|
983d048219 | ||
|
|
3c6033f904 | ||
|
|
ef2bd2b59d | ||
|
|
04d70e9aa8 | ||
|
|
a2587ffc0a | ||
|
|
6776e7bb3d | ||
|
|
4c05064294 | ||
|
|
c135f1394f | ||
|
|
d68f4215f1 | ||
|
|
af562f3c3a | ||
|
|
7b949716bc | ||
|
|
d3e256289b | ||
|
|
3688e762b1 | ||
|
|
249f1a7af4 | ||
|
|
2de26030ff | ||
|
|
fcc76fb8d0 | ||
|
|
88d5b7095e | ||
|
|
b0e55d88de | ||
|
|
370ad3e928 | ||
|
|
07bf77d2dd | ||
|
|
a5ec65f3c0 | ||
|
|
522b318fd9 | ||
|
|
9eb2a4033f | ||
|
|
e87b0c393a | ||
|
|
1fb7e665fa | ||
|
|
7ea476d787 | ||
|
|
8260d07d61 | ||
|
|
ac0b4e6514 | ||
|
|
27b2f8cf27 |
@@ -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",
|
||||
|
||||
1
@vates/node-vsphere-soap/.npmignore
Symbolic link
1
@vates/node-vsphere-soap/.npmignore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../scripts/npmignore
|
||||
22
@vates/node-vsphere-soap/LICENSE
Normal file
22
@vates/node-vsphere-soap/LICENSE
Normal 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.
|
||||
|
||||
127
@vates/node-vsphere-soap/README.md
Normal file
127
@vates/node-vsphere-soap/README.md
Normal file
@@ -0,0 +1,127 @@
|
||||
forked from https://github.com/reedog117/node-vsphere-soap
|
||||
|
||||
# node-vsphere-soap
|
||||
|
||||
[](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
|
||||
231
@vates/node-vsphere-soap/lib/client.js
Normal file
231
@vates/node-vsphere-soap/lib/client.js
Normal 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
|
||||
38
@vates/node-vsphere-soap/package.json
Normal file
38
@vates/node-vsphere-soap/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
15
@vates/node-vsphere-soap/test/config-test.stub.js
Normal file
15
@vates/node-vsphere-soap/test/config-test.stub.js
Normal 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
|
||||
140
@vates/node-vsphere-soap/test/vsphere-soap.test.js
Normal file
140
@vates/node-vsphere-soap/test/vsphere-soap.test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -13,7 +13,7 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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')))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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**
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
@@ -118,7 +118,7 @@ import {
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
vmRefs: string[];
|
||||
vmRefs: XenApiVm["$ref"][];
|
||||
}>();
|
||||
|
||||
const { getByOpaqueRef: getVm } = useVmStore().subscribe();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
31
@xen-orchestra/lite/src/stores/alarm.store.ts
Normal file
31
@xen-orchestra/lite/src/stores/alarm.store.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
39
@xen-orchestra/lite/src/stores/closing-confirmation.store.ts
Normal file
39
@xen-orchestra/lite/src/stores/closing-confirmation.store.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -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) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
4
@xen-orchestra/lite/src/types/injection-keys.ts
Normal file
4
@xen-orchestra/lite/src/types/injection-keys.ts
Normal 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>;
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
64
CHANGELOG.md
64
CHANGELOG.md
@@ -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))
|
||||
|
||||
@@ -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-->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
:::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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
### 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:
|
||||
|
||||

|
||||
|
||||
:::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`:
|
||||
|
||||

|
||||
|
||||
:::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.
|
||||
:::
|
||||
|
||||

|
||||
1
docs/continuous_replication.md
Symbolic link
1
docs/continuous_replication.md
Symbolic link
@@ -0,0 +1 @@
|
||||
incremental_replication.md
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
It means huge files for each backup. Delta backups will only export the difference between the previous backup:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
This way we can go "forward" and remove this oldest VHD after the merge:
|
||||
|
||||

|
||||
|
||||
## 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
1
docs/delta_backups.md
Symbolic link
@@ -0,0 +1 @@
|
||||
incremental_backups.md
|
||||
@@ -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).**
|
||||
|
||||

|
||||
|
||||
## 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
1
docs/disaster_recovery.md
Symbolic link
@@ -0,0 +1 @@
|
||||
full_replication.md
|
||||
41
docs/full_replication.md
Normal file
41
docs/full_replication.md
Normal 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).**
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
:::
|
||||
66
docs/incremental_backups.md
Normal file
66
docs/incremental_backups.md
Normal 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:
|
||||
|
||||

|
||||
|
||||
It means huge files for each backup. Incremental backups will only export the difference between the previous backup:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
This way we can go "forward" and remove this oldest VHD after the merge:
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
128
docs/incremental_replication.md
Normal file
128
docs/incremental_replication.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
:::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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
### 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:
|
||||
|
||||

|
||||
|
||||
:::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`:
|
||||
|
||||

|
||||
|
||||
:::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.
|
||||
:::
|
||||
|
||||

|
||||
105
docs/mirror_backup.md
Normal file
105
docs/mirror_backup.md
Normal 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)
|
||||
@@ -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.
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -11,7 +11,7 @@ const data = {
|
||||
protocol: 'https:',
|
||||
},
|
||||
'[::1]': {
|
||||
hostname: '::1',
|
||||
hostname: '[::1]',
|
||||
pathname: '/',
|
||||
protocol: 'https:',
|
||||
},
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user