feat(upload-ova): new CLI to import OVA VMs (#3630)
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||||
|
require('./package.json')
|
||||||
|
)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/benchmark/
|
||||||
|
/benchmarks/
|
||||||
|
*.bench.js
|
||||||
|
*.bench.js.map
|
||||||
|
|
||||||
|
/examples/
|
||||||
|
example.js
|
||||||
|
example.js.map
|
||||||
|
*.example.js
|
||||||
|
*.example.js.map
|
||||||
|
|
||||||
|
/fixture/
|
||||||
|
/fixtures/
|
||||||
|
*.fixture.js
|
||||||
|
*.fixture.js.map
|
||||||
|
*.fixtures.js
|
||||||
|
*.fixtures.js.map
|
||||||
|
|
||||||
|
/test/
|
||||||
|
/tests/
|
||||||
|
*.spec.js
|
||||||
|
*.spec.js.map
|
||||||
|
|
||||||
|
__snapshots__/
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# XO-UPLOAD-OVA
|
||||||
|
|
||||||
|
> Basic CLI to upload ova files to Xen-Orchestra
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
xo-upload-ova --register [--expiresIn duration] <XO-Server URL> <username> [<password>]
|
||||||
|
Registers the XO instance to use.
|
||||||
|
|
||||||
|
--expiresIn duration
|
||||||
|
Can be used to change the validity duration of the
|
||||||
|
authorization token (default: one month).
|
||||||
|
|
||||||
|
xo-upload-ova --unregister
|
||||||
|
Remove stored credentials.
|
||||||
|
|
||||||
|
xo-upload-ova --inspect <file>
|
||||||
|
Displays the data that would be imported from the ova.
|
||||||
|
|
||||||
|
xo-upload-ova --upload <file> <sr> [--override <key>=<value> [<key>=<value>]+]
|
||||||
|
Actually imports the VM contained in <file> to the Storage Repository <sr>.
|
||||||
|
Some parameters can be overridden from the file, consult --inspect to get the list.
|
||||||
|
Note: --override has to come last. By default arguments are string, prefix them with <json:> to type
|
||||||
|
them, ex. " --override nameLabel='new VM' memory=json:67108864 disks.vmdisk1.capacity=json:134217728"
|
||||||
|
|
||||||
|
xo-upload-ova v0.1.0
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Register your XO instance
|
||||||
|
|
||||||
|
```
|
||||||
|
> xo-upload-ova --register http://xo.my-company.net admin@admin.net admin
|
||||||
|
Successfully logged with admin@admin.net
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: only a token will be saved in the configuration file.
|
||||||
|
|
||||||
|
#### Import your .ova file
|
||||||
|
|
||||||
|
```
|
||||||
|
> xo-upload-ova --upload dsl.ova a7c630bf-b38c-489e-d3c3-e62507948980 --override 'nameLabel=dsl ' descriptionLabel='short desc' memory=json:671088640 disks.vmdisk1.descriptionLabel='disk description' disks.vmdisk1.capacity=json:1342177280
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributions
|
||||||
|
|
||||||
|
Contributions are _very_ welcomed, either on the documentation or on
|
||||||
|
the code.
|
||||||
|
|
||||||
|
You may:
|
||||||
|
|
||||||
|
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||||
|
you've encountered;
|
||||||
|
- fork and create a pull request.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
XO-UPLOAD-OVA is released under the [AGPL
|
||||||
|
v3](http://www.gnu.org/licenses/agpl-3.0-standalone.html).
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"name": "@xen-orchestra/upload-ova",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"description": "Basic CLI to upload ova files to Xen-Orchestra",
|
||||||
|
"keywords": [
|
||||||
|
"import",
|
||||||
|
"orchestra",
|
||||||
|
"ova",
|
||||||
|
"xcp-ng",
|
||||||
|
"xcp",
|
||||||
|
"xen-orchestra",
|
||||||
|
"xen-server",
|
||||||
|
"xen",
|
||||||
|
"xo"
|
||||||
|
],
|
||||||
|
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/upload-ova",
|
||||||
|
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||||
|
"repository": {
|
||||||
|
"directory": "@xen-orchestra/upload-ova",
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||||
|
},
|
||||||
|
"preferGlobal": true,
|
||||||
|
"main": "dist/",
|
||||||
|
"bin": {
|
||||||
|
"xo-upload-ova": "dist/index.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/polyfill": "^7.0.0",
|
||||||
|
"chalk": "^2.2.0",
|
||||||
|
"exec-promise": "^0.7.0",
|
||||||
|
"fs-extra": "^7.0.0",
|
||||||
|
"fs-promise": "^2.0.3",
|
||||||
|
"get-stream": "^4.1.0",
|
||||||
|
"http-request-plus": "^0.8.0",
|
||||||
|
"human-format": "^0.10.0",
|
||||||
|
"l33teral": "^3.0.3",
|
||||||
|
"lodash": "^4.17.4",
|
||||||
|
"nice-pipe": "0.0.0",
|
||||||
|
"pretty-ms": "^4.0.0",
|
||||||
|
"progress-stream": "^2.0.0",
|
||||||
|
"pw": "^0.0.4",
|
||||||
|
"strip-indent": "^2.0.0",
|
||||||
|
"xdg-basedir": "^3.0.0",
|
||||||
|
"xo-lib": "^0.9.0",
|
||||||
|
"xo-vmdk-to-vhd": "^0.1.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/cli": "^7.0.0",
|
||||||
|
"@babel/core": "^7.0.0",
|
||||||
|
"@babel/preset-env": "^7.0.0",
|
||||||
|
"@babel/preset-flow": "^7.0.0",
|
||||||
|
"babel-plugin-lodash": "^3.3.2",
|
||||||
|
"cross-env": "^5.1.3",
|
||||||
|
"rimraf": "^2.6.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||||
|
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||||
|
"prebuild": "rimraf dist/",
|
||||||
|
"predev": "yarn run prebuild",
|
||||||
|
"prepublishOnly": "yarn run build"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
import assign from 'lodash/assign'
|
||||||
|
import l33t from 'l33teral'
|
||||||
|
import xdgBasedir from 'xdg-basedir'
|
||||||
|
|
||||||
|
import { mkdirp, readFile, writeFile } from 'fs-extra'
|
||||||
|
|
||||||
|
const configPath = xdgBasedir.config + '/xo-upload-ova'
|
||||||
|
const configFile = configPath + '/config.json'
|
||||||
|
|
||||||
|
export async function load() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(await readFile(configFile))
|
||||||
|
} catch (e) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get(path) {
|
||||||
|
const config = await load()
|
||||||
|
return l33t(config).tap(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function save(config) {
|
||||||
|
await mkdirp(configPath)
|
||||||
|
await writeFile(configFile, JSON.stringify(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function set(data) {
|
||||||
|
const config = await load()
|
||||||
|
await save(assign(config, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unset(paths) {
|
||||||
|
const config = await load()
|
||||||
|
const l33tConfig = l33t(config)
|
||||||
|
;[].concat(paths).forEach(function(path) {
|
||||||
|
l33tConfig.purge(path, true)
|
||||||
|
})
|
||||||
|
return save(config)
|
||||||
|
}
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
/* eslint no-console: "off" */
|
||||||
|
|
||||||
|
import chalk from 'chalk'
|
||||||
|
import execPromise from 'exec-promise'
|
||||||
|
import { createReadStream } from 'fs'
|
||||||
|
import { stat } from 'fs-promise'
|
||||||
|
import getStream from 'get-stream'
|
||||||
|
import hrp from 'http-request-plus'
|
||||||
|
import humanFormat from 'human-format'
|
||||||
|
import l33t from 'l33teral'
|
||||||
|
import isObject from 'lodash/isObject'
|
||||||
|
import getKeys from 'lodash/keys'
|
||||||
|
import startsWith from 'lodash/startsWith'
|
||||||
|
import nicePipe from 'nice-pipe'
|
||||||
|
import prettyMs from 'pretty-ms'
|
||||||
|
import progressStream from 'progress-stream'
|
||||||
|
import pw from 'pw'
|
||||||
|
import stripIndent from 'strip-indent'
|
||||||
|
import { URL } from 'url'
|
||||||
|
import Xo from 'xo-lib'
|
||||||
|
import { parseOVAFile } from 'xo-vmdk-to-vhd'
|
||||||
|
|
||||||
|
import pkg from '../package'
|
||||||
|
import {
|
||||||
|
load as loadConfig,
|
||||||
|
set as setConfig,
|
||||||
|
unset as unsetConfig,
|
||||||
|
} from './config'
|
||||||
|
|
||||||
|
function help() {
|
||||||
|
return stripIndent(
|
||||||
|
`
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
$name --register [--expiresIn duration] <XO-Server URL> <username> [<password>]
|
||||||
|
Registers the XO instance to use.
|
||||||
|
|
||||||
|
--expiresIn duration
|
||||||
|
Can be used to change the validity duration of the
|
||||||
|
authorization token (default: one month).
|
||||||
|
|
||||||
|
$name --unregister
|
||||||
|
Remove stored credentials.
|
||||||
|
|
||||||
|
$name --inspect <file>
|
||||||
|
Displays the data that would be imported from the ova.
|
||||||
|
|
||||||
|
$name --upload <file> <sr> [--override <key>=<value> [<key>=<value>]+]
|
||||||
|
Actually imports the VM contained in <file> to the Storage Repository <sr>.
|
||||||
|
Some parameters can be overridden from the file, consult --inspect to get the list.
|
||||||
|
Note: --override has to come last. By default arguments are string, prefix them with <json:> to type
|
||||||
|
them, ex. " --override nameLabel='new VM' memory=json:67108864 disks.vmdisk1.capacity=json:134217728"
|
||||||
|
|
||||||
|
$name v$version
|
||||||
|
`
|
||||||
|
).replace(/<([^>]+)>|\$(\w+)/g, function(_, arg, key) {
|
||||||
|
if (arg) {
|
||||||
|
return '<' + chalk.yellow(arg) + '>'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'name') {
|
||||||
|
return chalk.bold(pkg[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkg[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
|
const { server, token } = await loadConfig()
|
||||||
|
if (server === undefined) {
|
||||||
|
throw new Error('no server to connect to!')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token === undefined) {
|
||||||
|
throw new Error('no token available')
|
||||||
|
}
|
||||||
|
|
||||||
|
const xo = new Xo({ url: server })
|
||||||
|
await xo.open()
|
||||||
|
await xo.signIn({ token })
|
||||||
|
return xo
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregister() {
|
||||||
|
return unsetConfig(['server', 'token'])
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function register(args) {
|
||||||
|
let expiresIn
|
||||||
|
if (args[0] === '--expiresIn') {
|
||||||
|
expiresIn = args[1]
|
||||||
|
args = args.slice(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [
|
||||||
|
url,
|
||||||
|
email,
|
||||||
|
password = await new Promise(resolve => {
|
||||||
|
process.stdout.write('Password: ')
|
||||||
|
pw(resolve)
|
||||||
|
}),
|
||||||
|
] = args
|
||||||
|
|
||||||
|
const xo = new Xo({ url })
|
||||||
|
await xo.open()
|
||||||
|
await xo.signIn({ email, password })
|
||||||
|
console.log('Successfully logged with', xo.user.email)
|
||||||
|
|
||||||
|
await setConfig({
|
||||||
|
server: url,
|
||||||
|
token: await xo.call('token.create', { expiresIn }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeStringDecoder(buffer, encoder) {
|
||||||
|
return Buffer.from(buffer).toString(encoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function inspect(args) {
|
||||||
|
const file = args[0]
|
||||||
|
const data = await parseOVAFile(
|
||||||
|
new NodeParsableFile(file, (await stat(file)).size),
|
||||||
|
nodeStringDecoder,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
console.log('file metadata:', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOverride(args) {
|
||||||
|
const flag = args.shift()
|
||||||
|
if (flag !== '--override') {
|
||||||
|
throw new Error('Third argument has to be --override')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length === 0) {
|
||||||
|
throw new Error('Missing actual override')
|
||||||
|
}
|
||||||
|
const overrides = {}
|
||||||
|
for (const definition of args) {
|
||||||
|
const index = definition.indexOf('=')
|
||||||
|
const key = definition.slice(0, index)
|
||||||
|
let value = definition.slice(index + 1)
|
||||||
|
if (startsWith(value, 'json:')) {
|
||||||
|
value = JSON.parse(value.slice(5))
|
||||||
|
}
|
||||||
|
overrides[key] = value
|
||||||
|
}
|
||||||
|
return overrides
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upload(args) {
|
||||||
|
const file = args.shift()
|
||||||
|
const srId = args.shift()
|
||||||
|
let overrides = {}
|
||||||
|
if (args.length > 1) {
|
||||||
|
overrides = parseOverride(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await parseOVAFile(
|
||||||
|
new NodeParsableFile(file, (await stat(file)).size),
|
||||||
|
nodeStringDecoder
|
||||||
|
)
|
||||||
|
const params = { sr: srId }
|
||||||
|
const xo = await connect()
|
||||||
|
const getXoObject = async filter =>
|
||||||
|
Object.values(await xo.call('xo.getAllObjects', { filter }))[0]
|
||||||
|
const sr = await getXoObject({ id: srId })
|
||||||
|
const pool = await getXoObject({ id: sr.$poolId })
|
||||||
|
const master = await getXoObject({ id: pool.master })
|
||||||
|
const pif = await getXoObject({
|
||||||
|
type: 'PIF',
|
||||||
|
management: true,
|
||||||
|
$host: master.id,
|
||||||
|
})
|
||||||
|
data.networks = data.networks.map(() => pif.$network)
|
||||||
|
console.log('data', data)
|
||||||
|
const l33tData = l33t(data)
|
||||||
|
const overridesKeys = Object.keys(overrides)
|
||||||
|
const missingKeys = overridesKeys.filter(k => !l33tData.probe(k))
|
||||||
|
if (missingKeys.length) {
|
||||||
|
// eslint-disable-next-line no-throw-literal
|
||||||
|
throw `those override keys don't exist in the metadata: ${missingKeys}`
|
||||||
|
}
|
||||||
|
for (const key of overridesKeys) {
|
||||||
|
l33tData.plant(key, overrides[key])
|
||||||
|
}
|
||||||
|
data.disks = Object.values(data.disks)
|
||||||
|
params.data = l33tData.obj
|
||||||
|
params.type = 'ova'
|
||||||
|
const method = 'vm.import'
|
||||||
|
|
||||||
|
// FIXME: do not use private properties.
|
||||||
|
const baseUrl = xo._url.replace(/^ws/, 'http')
|
||||||
|
|
||||||
|
const result = await xo.call(method, params)
|
||||||
|
let keys, key, url
|
||||||
|
if (isObject(result) && (keys = getKeys(result)).length === 1) {
|
||||||
|
key = keys[0]
|
||||||
|
|
||||||
|
if (key === '$sendTo') {
|
||||||
|
if (typeof file !== 'string') {
|
||||||
|
// eslint-disable-next-line no-throw-literal
|
||||||
|
throw 'file parameter should be a path'
|
||||||
|
}
|
||||||
|
url = new URL(result[key], baseUrl)
|
||||||
|
|
||||||
|
const { size: length } = await stat(file)
|
||||||
|
const input = nicePipe([
|
||||||
|
createReadStream(file),
|
||||||
|
progressStream(
|
||||||
|
{
|
||||||
|
length,
|
||||||
|
time: 1e3,
|
||||||
|
},
|
||||||
|
printProgress
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await hrp
|
||||||
|
.post(url.toString(), {
|
||||||
|
body: input,
|
||||||
|
headers: {
|
||||||
|
'content-length': length,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.readAll('utf-8')
|
||||||
|
} catch (e) {
|
||||||
|
console.log('ERROR', e)
|
||||||
|
console.log('ERROR content', await e.response.readAll('utf-8'))
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NodeParsableFile {
|
||||||
|
constructor(fileName, fileLength = Infinity) {
|
||||||
|
this._fileName = fileName
|
||||||
|
this._start = 0
|
||||||
|
this._end = fileLength
|
||||||
|
}
|
||||||
|
|
||||||
|
slice(start, end) {
|
||||||
|
const newFile = new NodeParsableFile(this._fileName)
|
||||||
|
newFile._start = start < 0 ? this._end + start : this._start + start
|
||||||
|
newFile._end = end < 0 ? this._end + end : this._start + end
|
||||||
|
return newFile
|
||||||
|
}
|
||||||
|
|
||||||
|
async read() {
|
||||||
|
const result = await getStream.buffer(
|
||||||
|
createReadStream(this._fileName, {
|
||||||
|
start: this._start,
|
||||||
|
end: this._end - 1,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// crazy stuff to get a browser-compatible ArrayBuffer from a node buffer
|
||||||
|
// https://stackoverflow.com/a/31394257/72637
|
||||||
|
return result.buffer.slice(
|
||||||
|
result.byteOffset,
|
||||||
|
result.byteOffset + result.byteLength
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const humanFormatOpts = {
|
||||||
|
unit: 'B',
|
||||||
|
scale: 'binary',
|
||||||
|
}
|
||||||
|
|
||||||
|
function printProgress(progress) {
|
||||||
|
if (progress.length) {
|
||||||
|
console.warn(
|
||||||
|
'%s% of %s @ %s/s - ETA %s',
|
||||||
|
Math.round(progress.percentage),
|
||||||
|
humanFormat(progress.length, humanFormatOpts),
|
||||||
|
humanFormat(progress.speed, humanFormatOpts),
|
||||||
|
prettyMs(progress.eta * 1e3)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
'%s @ %s/s',
|
||||||
|
humanFormat(progress.transferred, humanFormatOpts),
|
||||||
|
humanFormat(progress.speed, humanFormatOpts)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function main(args) {
|
||||||
|
if (!args || !args.length || args[0] === '-h' || args[0] === '--help') {
|
||||||
|
return help()
|
||||||
|
}
|
||||||
|
const fnName = args[0].replace(/^--|-\w/g, match =>
|
||||||
|
match === '--' ? '' : match[1].toUpperCase()
|
||||||
|
)
|
||||||
|
if (fnName in exports) {
|
||||||
|
return exports[fnName](args.slice(1))
|
||||||
|
}
|
||||||
|
return help()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!module.parent) {
|
||||||
|
execPromise(main)
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||||
|
|
||||||
- [Backup] **BETA** Ability to backup running VMs with their memory [#645](https://github.com/vatesfr/xen-orchestra/issues/645) (PR [#4252](https://github.com/vatesfr/xen-orchestra/pull/4252))
|
- [Backup] **BETA** Ability to backup running VMs with their memory [#645](https://github.com/vatesfr/xen-orchestra/issues/645) (PR [#4252](https://github.com/vatesfr/xen-orchestra/pull/4252))
|
||||||
|
- [Import] add CLI tool to import OVA files (PR [#3630](https://github.com/vatesfr/xen-orchestra/pull/3630))
|
||||||
|
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
|
|
||||||
@@ -20,5 +21,7 @@
|
|||||||
>
|
>
|
||||||
> Rule of thumb: add packages on top.
|
> Rule of thumb: add packages on top.
|
||||||
|
|
||||||
|
- xo-vmdk-to-vhd major
|
||||||
|
- @xen-orchestra/upload-ova major
|
||||||
- xo-server minor
|
- xo-server minor
|
||||||
- xo-web minor
|
- xo-web minor
|
||||||
|
|||||||
@@ -117,12 +117,18 @@ encoding by prefixing with `json:`:
|
|||||||
> xo-cli vm.export vm=a01667e0-8e29-49fc-a550-17be4226783c @=vm.xva
|
> xo-cli vm.export vm=a01667e0-8e29-49fc-a550-17be4226783c @=vm.xva
|
||||||
```
|
```
|
||||||
|
|
||||||
##### VM import
|
##### XVA VM import
|
||||||
|
|
||||||
```
|
```
|
||||||
> xo-cli vm.import sr=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
|
> xo-cli vm.import sr=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Note: `xo-cli` only supports the import of XVA files. It will not import OVA files.
|
||||||
|
|
||||||
|
##### OVA VM import
|
||||||
|
|
||||||
|
A separate utility, [`xo-upload-ova`](https://github.com/vatesfr/xen-orchestra/blob/master/packages/@xen-orchestra/upload-ova/README.md), can be used to import `.ova` files.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import mixin from '@xen-orchestra/mixin'
|
|||||||
import ms from 'ms'
|
import ms from 'ms'
|
||||||
import synchronized from 'decorator-synchronized'
|
import synchronized from 'decorator-synchronized'
|
||||||
import tarStream from 'tar-stream'
|
import tarStream from 'tar-stream'
|
||||||
import vmdkToVhd from 'xo-vmdk-to-vhd'
|
import { vmdkToVhd } from 'xo-vmdk-to-vhd'
|
||||||
import {
|
import {
|
||||||
cancelable,
|
cancelable,
|
||||||
defer,
|
defer,
|
||||||
@@ -1461,11 +1461,15 @@ export default class Xapi extends XapiBase {
|
|||||||
table.grainLogicalAddressList,
|
table.grainLogicalAddressList,
|
||||||
table.grainFileOffsetList
|
table.grainFileOffsetList
|
||||||
)
|
)
|
||||||
await this._importVdiContent(vdi, vhdStream, VDI_FORMAT_VHD)
|
try {
|
||||||
|
await this._importVdiContent(vdi, vhdStream, VDI_FORMAT_VHD)
|
||||||
// See: https://github.com/mafintosh/tar-stream#extracting
|
// See: https://github.com/mafintosh/tar-stream#extracting
|
||||||
// No import parallelization.
|
// No import parallelization.
|
||||||
cb()
|
} catch (e) {
|
||||||
|
reject(e)
|
||||||
|
} finally {
|
||||||
|
cb()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
stream.pipe(extract)
|
stream.pipe(extract)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,11 +15,10 @@ Installation of the [npm package](https://npmjs.org/package/xo-vmdk-to-vhd):
|
|||||||
To convert a VMDK stream to a Fixed VHD stream without buffering the entire input or output:
|
To convert a VMDK stream to a Fixed VHD stream without buffering the entire input or output:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import convertFromVMDK from 'xo-vmdk-to-vhd'
|
import { vmdkToVhd } from 'xo-vmdk-to-vhd'
|
||||||
import { createReadStream, createWriteStream } from 'fs'
|
import { createReadStream, createWriteStream } from 'fs'
|
||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const stream = await convertFromVMDK(fs.createReadStream(vmdkFileName))
|
const stream = await vmdkToVhd(fs.createReadStream(vmdkFileName))
|
||||||
|
|
||||||
stream.pipe(fs.createWriteStream(vhdFileName))
|
stream.pipe(fs.createWriteStream(vhdFileName))
|
||||||
})()
|
})()
|
||||||
@@ -28,11 +27,11 @@ import { createReadStream, createWriteStream } from 'fs'
|
|||||||
or:
|
or:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var convertFromVMDK = require('xo-vmdk-to-vhd').default
|
var vmdkToVhd = require('xo-vmdk-to-vhd').vmdkToVhd
|
||||||
var createReadStream = require('fs').createReadStream
|
var createReadStream = require('fs').createReadStream
|
||||||
var createWriteStream = require('fs').createWriteStream
|
var createWriteStream = require('fs').createWriteStream
|
||||||
|
|
||||||
convertFromVMDK(fs.createReadStream(vmdkFileName)).then(function(stream) {
|
vmdkToVhd(fs.createReadStream(vmdkFileName)).then(function(stream) {
|
||||||
stream.pipe(fs.createWriteStream(vhdFileName))
|
stream.pipe(fs.createWriteStream(vhdFileName))
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { createReadableSparseStream } from 'vhd-lib'
|
import { createReadableSparseStream } from 'vhd-lib'
|
||||||
|
import { parseOVAFile, ParsableFile } from './ova'
|
||||||
|
|
||||||
import VMDKDirectParser from './vmdk-read'
|
import VMDKDirectParser from './vmdk-read'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
default as readVmdkGrainTable,
|
default as readVmdkGrainTable,
|
||||||
readCapacityAndGrainTable,
|
readCapacityAndGrainTable,
|
||||||
} from './vmdk-read-table'
|
} from './vmdk-read-table'
|
||||||
|
|
||||||
async function convertFromVMDK(
|
async function vmdkToVhd(
|
||||||
vmdkReadStream,
|
vmdkReadStream,
|
||||||
grainLogicalAddressList,
|
grainLogicalAddressList,
|
||||||
grainFileOffsetList
|
grainFileOffsetList
|
||||||
@@ -24,4 +26,5 @@ async function convertFromVMDK(
|
|||||||
parser.blockIterator()
|
parser.blockIterator()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
export { convertFromVMDK as default }
|
|
||||||
|
export { ParsableFile, parseOVAFile, vmdkToVhd }
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
/* eslint-env jest */
|
||||||
|
|
||||||
|
import { exec } from 'child-process-promise'
|
||||||
|
import { createReadStream } from 'fs'
|
||||||
|
import { stat, writeFile } from 'fs-extra'
|
||||||
|
import getStream from 'get-stream'
|
||||||
|
import { pFromCallback } from 'promise-toolbox'
|
||||||
|
import rimraf from 'rimraf'
|
||||||
|
import tmp from 'tmp'
|
||||||
|
|
||||||
|
import { ParsableFile, parseOVAFile } from './ova'
|
||||||
|
import readVmdkGrainTable from './vmdk-read-table'
|
||||||
|
|
||||||
|
const initialDir = process.cwd()
|
||||||
|
beforeEach(async () => {
|
||||||
|
const dir = await pFromCallback(cb => tmp.dir(cb))
|
||||||
|
process.chdir(dir)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const tmpDir = process.cwd()
|
||||||
|
process.chdir(initialDir)
|
||||||
|
await pFromCallback(cb => rimraf(tmpDir, cb))
|
||||||
|
})
|
||||||
|
|
||||||
|
export class NodeParsableFile extends ParsableFile {
|
||||||
|
constructor(fileName, fileLength = Infinity) {
|
||||||
|
super()
|
||||||
|
this._fileName = fileName
|
||||||
|
this._start = 0
|
||||||
|
this._end = fileLength
|
||||||
|
}
|
||||||
|
|
||||||
|
slice(start, end) {
|
||||||
|
const newFile = new NodeParsableFile(this._fileName)
|
||||||
|
newFile._start = start < 0 ? this._end + start : this._start + start
|
||||||
|
newFile._end = end < 0 ? this._end + end : this._start + end
|
||||||
|
return newFile
|
||||||
|
}
|
||||||
|
|
||||||
|
async read() {
|
||||||
|
const result = await getStream.buffer(
|
||||||
|
createReadStream(this._fileName, {
|
||||||
|
start: this._start,
|
||||||
|
end: this._end - 1,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// crazy stuff to get a browser-compatible ArrayBuffer from a node buffer
|
||||||
|
// https://stackoverflow.com/a/31394257/72637
|
||||||
|
return result.buffer.slice(
|
||||||
|
result.byteOffset,
|
||||||
|
result.byteOffset + result.byteLength
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const vmdkFileName = 'random-data.vmdk'
|
||||||
|
test('An ova file is parsed correctly', async () => {
|
||||||
|
const ovfName = 'test.ovf'
|
||||||
|
await writeFile(ovfName, xmlContent)
|
||||||
|
const rawFileName = 'random-data'
|
||||||
|
await exec(`base64 /dev/urandom | head -c 104448 > ${rawFileName}`)
|
||||||
|
await exec(
|
||||||
|
`rm -f ${vmdkFileName} && python /usr/share/pyshared/VMDKstream.py ${rawFileName} ${vmdkFileName}`
|
||||||
|
)
|
||||||
|
const ovaName = `test.ova`
|
||||||
|
await exec(`tar cf ${ovaName} ${ovfName} ${vmdkFileName}`)
|
||||||
|
const vmdkParsableFile = new NodeParsableFile(
|
||||||
|
vmdkFileName,
|
||||||
|
(await stat(vmdkFileName)).size
|
||||||
|
)
|
||||||
|
const directGrainTableFetch = await readVmdkGrainTable(async (start, end) =>
|
||||||
|
vmdkParsableFile.slice(start, end).read()
|
||||||
|
)
|
||||||
|
expect(directGrainTableFetch).toEqual(expectedResult.tables[vmdkFileName])
|
||||||
|
const data = await parseOVAFile(
|
||||||
|
new NodeParsableFile(ovaName),
|
||||||
|
(buffer, encoder) => {
|
||||||
|
return Buffer.from(buffer).toString(encoder)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(data).toEqual(expectedResult)
|
||||||
|
})
|
||||||
|
|
||||||
|
const expectedResult = {
|
||||||
|
tables: {
|
||||||
|
[vmdkFileName]: {
|
||||||
|
grainFileOffsetList: [65536, 115712],
|
||||||
|
grainLogicalAddressList: [0, 65536],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disks: {
|
||||||
|
vmdisk1: {
|
||||||
|
capacity: 134217728,
|
||||||
|
path: 'random-data.vmdk',
|
||||||
|
descriptionLabel: 'No description',
|
||||||
|
nameLabel: 'Hard Disk 1',
|
||||||
|
position: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
networks: ['LAN'],
|
||||||
|
nameLabel: 'dsl',
|
||||||
|
descriptionLabel: "NetworkJutsu's Damn Small Linux OVA",
|
||||||
|
nCpus: 1,
|
||||||
|
memory: 67108864,
|
||||||
|
}
|
||||||
|
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--Generated by VMware ESX Server, User: root, UTC time: 2015-09-20T04:41:19.332937Z-->
|
||||||
|
<Envelope vmw:buildId="build-2615704" xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:cim="http://schemas.dmtf.org/wbem/wscim/1/common" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vmw="http://www.vmware.com/schema/ovf" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<References>
|
||||||
|
<File ovf:href="${vmdkFileName}" ovf:id="file1" ovf:size="76934656" />
|
||||||
|
</References>
|
||||||
|
<DiskSection>
|
||||||
|
<Info>Virtual disk information</Info>
|
||||||
|
<Disk ovf:capacity="128" ovf:capacityAllocationUnits="byte * 2^20" ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized" ovf:populatedSize="82313216" />
|
||||||
|
</DiskSection>
|
||||||
|
<NetworkSection>
|
||||||
|
<Info>The list of logical networks</Info>
|
||||||
|
<Network ovf:name="LAN">
|
||||||
|
<Description>The LAN network</Description>
|
||||||
|
</Network>
|
||||||
|
</NetworkSection>
|
||||||
|
<VirtualSystem ovf:id="dsl">
|
||||||
|
<Info>A virtual machine</Info>
|
||||||
|
<Name>dsl</Name>
|
||||||
|
<OperatingSystemSection ovf:id="1" vmw:osType="otherGuest">
|
||||||
|
<Info>The kind of installed guest operating system</Info>
|
||||||
|
</OperatingSystemSection>
|
||||||
|
<VirtualHardwareSection>
|
||||||
|
<Info>Virtual hardware requirements</Info>
|
||||||
|
<System>
|
||||||
|
<vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
|
||||||
|
<vssd:InstanceID>0</vssd:InstanceID>
|
||||||
|
<vssd:VirtualSystemIdentifier>dsl</vssd:VirtualSystemIdentifier>
|
||||||
|
<vssd:VirtualSystemType>vmx-11</vssd:VirtualSystemType>
|
||||||
|
</System>
|
||||||
|
<Item>
|
||||||
|
<rasd:AllocationUnits>hertz * 10^6</rasd:AllocationUnits>
|
||||||
|
<rasd:Description>Number of Virtual CPUs</rasd:Description>
|
||||||
|
<rasd:ElementName>1 virtual CPU(s)</rasd:ElementName>
|
||||||
|
<rasd:InstanceID>1</rasd:InstanceID>
|
||||||
|
<rasd:ResourceType>3</rasd:ResourceType>
|
||||||
|
<rasd:VirtualQuantity>1</rasd:VirtualQuantity>
|
||||||
|
</Item>
|
||||||
|
<Item>
|
||||||
|
<rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>
|
||||||
|
<rasd:Description>Memory Size</rasd:Description>
|
||||||
|
<rasd:ElementName>64MB of memory</rasd:ElementName>
|
||||||
|
<rasd:InstanceID>2</rasd:InstanceID>
|
||||||
|
<rasd:ResourceType>4</rasd:ResourceType>
|
||||||
|
<rasd:VirtualQuantity>64</rasd:VirtualQuantity>
|
||||||
|
</Item>
|
||||||
|
<Item>
|
||||||
|
<rasd:Address>1</rasd:Address>
|
||||||
|
<rasd:Description>IDE Controller</rasd:Description>
|
||||||
|
<rasd:ElementName>VirtualIDEController 1</rasd:ElementName>
|
||||||
|
<rasd:InstanceID>3</rasd:InstanceID>
|
||||||
|
<rasd:ResourceType>5</rasd:ResourceType>
|
||||||
|
</Item>
|
||||||
|
<Item>
|
||||||
|
<rasd:Address>0</rasd:Address>
|
||||||
|
<rasd:Description>IDE Controller</rasd:Description>
|
||||||
|
<rasd:ElementName>VirtualIDEController 0</rasd:ElementName>
|
||||||
|
<rasd:InstanceID>4</rasd:InstanceID>
|
||||||
|
<rasd:ResourceType>5</rasd:ResourceType>
|
||||||
|
</Item>
|
||||||
|
<Item ovf:required="false">
|
||||||
|
<rasd:AutomaticAllocation>false</rasd:AutomaticAllocation>
|
||||||
|
<rasd:ElementName>VirtualVideoCard</rasd:ElementName>
|
||||||
|
<rasd:InstanceID>5</rasd:InstanceID>
|
||||||
|
<rasd:ResourceType>24</rasd:ResourceType>
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="enable3DSupport" vmw:value="false" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="enableMPTSupport" vmw:value="false" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="use3dRenderer" vmw:value="automatic" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="useAutoDetect" vmw:value="false" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="videoRamSizeInKB" vmw:value="4096" />
|
||||||
|
</Item>
|
||||||
|
<Item ovf:required="false">
|
||||||
|
<rasd:AutomaticAllocation>false</rasd:AutomaticAllocation>
|
||||||
|
<rasd:ElementName>VirtualVMCIDevice</rasd:ElementName>
|
||||||
|
<rasd:InstanceID>6</rasd:InstanceID>
|
||||||
|
<rasd:ResourceSubType>vmware.vmci</rasd:ResourceSubType>
|
||||||
|
<rasd:ResourceType>1</rasd:ResourceType>
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="allowUnrestrictedCommunication" vmw:value="false" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="slotInfo.pciSlotNumber" vmw:value="33" />
|
||||||
|
</Item>
|
||||||
|
<Item ovf:required="false">
|
||||||
|
<rasd:AddressOnParent>0</rasd:AddressOnParent>
|
||||||
|
<rasd:AutomaticAllocation>false</rasd:AutomaticAllocation>
|
||||||
|
<rasd:ElementName>CD-ROM 1</rasd:ElementName>
|
||||||
|
<rasd:InstanceID>7</rasd:InstanceID>
|
||||||
|
<rasd:Parent>3</rasd:Parent>
|
||||||
|
<rasd:ResourceSubType>vmware.cdrom.remoteatapi</rasd:ResourceSubType>
|
||||||
|
<rasd:ResourceType>15</rasd:ResourceType>
|
||||||
|
</Item>
|
||||||
|
<Item>
|
||||||
|
<rasd:AddressOnParent>0</rasd:AddressOnParent>
|
||||||
|
<rasd:ElementName>Hard Disk 1</rasd:ElementName>
|
||||||
|
<rasd:HostResource>ovf:/disk/vmdisk1</rasd:HostResource>
|
||||||
|
<rasd:InstanceID>8</rasd:InstanceID>
|
||||||
|
<rasd:Parent>4</rasd:Parent>
|
||||||
|
<rasd:ResourceType>17</rasd:ResourceType>
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="backing.writeThrough" vmw:value="false" />
|
||||||
|
</Item>
|
||||||
|
<Item>
|
||||||
|
<rasd:AddressOnParent>7</rasd:AddressOnParent>
|
||||||
|
<rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
|
||||||
|
<rasd:Connection>LAN</rasd:Connection>
|
||||||
|
<rasd:Description>PCNet32 ethernet adapter on "LAN"</rasd:Description>
|
||||||
|
<rasd:ElementName>Ethernet 1</rasd:ElementName>
|
||||||
|
<rasd:InstanceID>9</rasd:InstanceID>
|
||||||
|
<rasd:ResourceSubType>PCNet32</rasd:ResourceSubType>
|
||||||
|
<rasd:ResourceType>10</rasd:ResourceType>
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="slotInfo.pciSlotNumber" vmw:value="32" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="wakeOnLanEnabled" vmw:value="true" />
|
||||||
|
</Item>
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="cpuHotAddEnabled" vmw:value="false" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="cpuHotRemoveEnabled" vmw:value="false" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="firmware" vmw:value="bios" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="virtualICH7MPresent" vmw:value="false" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="virtualSMCPresent" vmw:value="false" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="memoryHotAddEnabled" vmw:value="false" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="nestedHVEnabled" vmw:value="false" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="powerOpInfo.powerOffType" vmw:value="soft" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="powerOpInfo.resetType" vmw:value="soft" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="powerOpInfo.standbyAction" vmw:value="powerOnSuspend" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="powerOpInfo.suspendType" vmw:value="hard" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="tools.afterPowerOn" vmw:value="true" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="tools.afterResume" vmw:value="true" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="tools.beforeGuestShutdown" vmw:value="true" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="tools.beforeGuestStandby" vmw:value="true" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="tools.syncTimeWithHost" vmw:value="false" />
|
||||||
|
<vmw:Config ovf:required="false" vmw:key="tools.toolsUpgradePolicy" vmw:value="manual" />
|
||||||
|
</VirtualHardwareSection>
|
||||||
|
<AnnotationSection ovf:required="false">
|
||||||
|
<Info>A human-readable annotation</Info>
|
||||||
|
<Annotation>NetworkJutsu's Damn Small Linux OVA</Annotation>
|
||||||
|
</AnnotationSection>
|
||||||
|
</VirtualSystem>
|
||||||
|
</Envelope>`
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import find from 'lodash/find'
|
||||||
|
import forEach from 'lodash/forEach'
|
||||||
|
import xml2js from 'xml2js'
|
||||||
|
|
||||||
|
import { readVmdkGrainTable } from '.'
|
||||||
|
|
||||||
|
/********
|
||||||
|
*
|
||||||
|
* THIS FILE HAS TO WORK IN BOTH THE BROWSER AND NODE
|
||||||
|
*
|
||||||
|
********/
|
||||||
|
|
||||||
|
// See: http://opennodecloud.com/howto/2013/12/25/howto-ON-ovf-reference.html
|
||||||
|
// See: http://www.dmtf.org/sites/default/files/standards/documents/DSP0243_1.0.0.pdf
|
||||||
|
// See: http://www.dmtf.org/sites/default/files/standards/documents/DSP0243_2.1.0.pdf
|
||||||
|
|
||||||
|
const MEMORY_UNIT_TO_FACTOR = {
|
||||||
|
k: 1024,
|
||||||
|
m: 1048576,
|
||||||
|
g: 1073741824,
|
||||||
|
t: 1099511627776,
|
||||||
|
}
|
||||||
|
const RESOURCE_TYPE_TO_HANDLER = {
|
||||||
|
// CPU.
|
||||||
|
'3': (data, { 'rasd:VirtualQuantity': nCpus }) => {
|
||||||
|
data.nCpus = +nCpus
|
||||||
|
},
|
||||||
|
// RAM.
|
||||||
|
'4': (
|
||||||
|
data,
|
||||||
|
{ 'rasd:AllocationUnits': unit, 'rasd:VirtualQuantity': quantity }
|
||||||
|
) => {
|
||||||
|
data.memory = quantity * allocationUnitsToFactor(unit)
|
||||||
|
},
|
||||||
|
// Network.
|
||||||
|
'10': (
|
||||||
|
{ networks },
|
||||||
|
{ 'rasd:AutomaticAllocation': enabled, 'rasd:Connection': name }
|
||||||
|
) => {
|
||||||
|
if (enabled) {
|
||||||
|
networks.push(name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Disk.
|
||||||
|
'17': (
|
||||||
|
{ disks },
|
||||||
|
{
|
||||||
|
'rasd:AddressOnParent': position,
|
||||||
|
'rasd:Description': description = 'No description',
|
||||||
|
'rasd:ElementName': name,
|
||||||
|
'rasd:HostResource': resource,
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const diskId = resource.match(/^(?:ovf:)?\/disk\/(.+)$/)
|
||||||
|
const disk = diskId && disks[diskId[1]]
|
||||||
|
|
||||||
|
if (disk) {
|
||||||
|
disk.descriptionLabel = description
|
||||||
|
disk.nameLabel = name
|
||||||
|
disk.position = +position
|
||||||
|
} else {
|
||||||
|
// TODO: Log error in U.I.
|
||||||
|
console.error(`No disk found: '${diskId}'.`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTarHeader(header, stringDeserializer) {
|
||||||
|
const fileName = stringDeserializer(header.slice(0, 100), 'ascii').split(
|
||||||
|
'\0'
|
||||||
|
)[0]
|
||||||
|
if (fileName.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const fileSize = parseInt(
|
||||||
|
stringDeserializer(header.slice(124, 124 + 11), 'ascii'),
|
||||||
|
8
|
||||||
|
)
|
||||||
|
return { fileName, fileSize }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ParsableFile {
|
||||||
|
// noinspection JSMethodCanBeStatic
|
||||||
|
get size() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** returns a ParsableFile */
|
||||||
|
slice(start, end) {}
|
||||||
|
|
||||||
|
/** reads the fragment, returns an ArrayBuffer */
|
||||||
|
async read() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ensureArray = value => {
|
||||||
|
if (value === undefined) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.isArray(value) ? value : [value]
|
||||||
|
}
|
||||||
|
|
||||||
|
const allocationUnitsToFactor = unit => {
|
||||||
|
const intValue = unit.match(/\^([0-9]+)$/)
|
||||||
|
return intValue != null
|
||||||
|
? Math.pow(2, intValue[1])
|
||||||
|
: MEMORY_UNIT_TO_FACTOR[unit.charAt(0).toLowerCase()]
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterDisks = disks => {
|
||||||
|
for (const diskId in disks) {
|
||||||
|
if (disks[diskId].position == null) {
|
||||||
|
// TODO: Log error in U.I.
|
||||||
|
console.error(`No position specified for '${diskId}'.`)
|
||||||
|
delete disks[diskId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function parseOVF(fileFragment, stringDeserializer) {
|
||||||
|
const xmlString = stringDeserializer(await fileFragment.read(), 'utf-8')
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
xml2js.parseString(
|
||||||
|
xmlString,
|
||||||
|
{ mergeAttrs: true, explicitArray: false },
|
||||||
|
(err, res) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
Envelope: {
|
||||||
|
DiskSection: { Disk: disks },
|
||||||
|
References: { File: files },
|
||||||
|
VirtualSystem: system,
|
||||||
|
},
|
||||||
|
} = res
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
disks: {},
|
||||||
|
networks: [],
|
||||||
|
}
|
||||||
|
const hardware = system.VirtualHardwareSection
|
||||||
|
|
||||||
|
// Get VM name/description.
|
||||||
|
data.nameLabel = hardware.System['vssd:VirtualSystemIdentifier']
|
||||||
|
data.descriptionLabel =
|
||||||
|
(system.AnnotationSection && system.AnnotationSection.Annotation) ||
|
||||||
|
(system.OperatingSystemSection &&
|
||||||
|
system.OperatingSystemSection.Description)
|
||||||
|
|
||||||
|
// Get disks.
|
||||||
|
forEach(ensureArray(disks), disk => {
|
||||||
|
const file = find(
|
||||||
|
ensureArray(files),
|
||||||
|
file => file['ovf:id'] === disk['ovf:fileRef']
|
||||||
|
)
|
||||||
|
const unit = disk['ovf:capacityAllocationUnits']
|
||||||
|
|
||||||
|
data.disks[disk['ovf:diskId']] = {
|
||||||
|
capacity:
|
||||||
|
disk['ovf:capacity'] *
|
||||||
|
((unit && allocationUnitsToFactor(unit)) || 1),
|
||||||
|
path: file && file['ovf:href'],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get hardware info: CPU, RAM, disks, networks...
|
||||||
|
forEach(ensureArray(hardware.Item), item => {
|
||||||
|
const handler = RESOURCE_TYPE_TO_HANDLER[item['rasd:ResourceType']]
|
||||||
|
if (!handler) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handler(data, item)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove disks which not have a position.
|
||||||
|
// (i.e. no info in hardware.Item section.)
|
||||||
|
filterDisks(data.disks)
|
||||||
|
resolve(data)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param parsableFile: ParsableFile
|
||||||
|
* @param stringDeserializer function (ArrayBuffer, encoding) => String
|
||||||
|
* @param skipVmdk if true avoid parsing the VMDK file tables
|
||||||
|
* @returns {Promise<{tables: {}}>}
|
||||||
|
*/
|
||||||
|
export async function parseOVAFile(
|
||||||
|
parsableFile,
|
||||||
|
stringDeserializer,
|
||||||
|
skipVmdk = false
|
||||||
|
) {
|
||||||
|
let offset = 0
|
||||||
|
const HEADER_SIZE = 512
|
||||||
|
let data = { tables: {} }
|
||||||
|
while (true) {
|
||||||
|
const header = parseTarHeader(
|
||||||
|
await parsableFile.slice(offset, offset + HEADER_SIZE).read(),
|
||||||
|
stringDeserializer
|
||||||
|
)
|
||||||
|
offset += HEADER_SIZE
|
||||||
|
if (header === null) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (header.fileName.toLowerCase().endsWith('.ovf')) {
|
||||||
|
const res = await parseOVF(
|
||||||
|
parsableFile.slice(offset, offset + header.fileSize),
|
||||||
|
stringDeserializer
|
||||||
|
)
|
||||||
|
data = { ...data, ...res }
|
||||||
|
}
|
||||||
|
if (!skipVmdk && header.fileName.toLowerCase().endsWith('.vmdk')) {
|
||||||
|
const fileSlice = parsableFile.slice(offset, offset + header.fileSize)
|
||||||
|
const readFile = async (start, end) => fileSlice.slice(start, end).read()
|
||||||
|
data.tables[header.fileName] = await readVmdkGrainTable(readFile)
|
||||||
|
}
|
||||||
|
offset += Math.ceil(header.fileSize / 512) * 512
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import tmp from 'tmp'
|
|||||||
|
|
||||||
import { createReadStream, createWriteStream, stat } from 'fs-extra'
|
import { createReadStream, createWriteStream, stat } from 'fs-extra'
|
||||||
import { pFromCallback } from 'promise-toolbox'
|
import { pFromCallback } from 'promise-toolbox'
|
||||||
import convertFromVMDK, { readVmdkGrainTable } from '.'
|
import { vmdkToVhd, readVmdkGrainTable } from '.'
|
||||||
|
|
||||||
const initialDir = process.cwd()
|
const initialDir = process.cwd()
|
||||||
jest.setTimeout(100000)
|
jest.setTimeout(100000)
|
||||||
@@ -66,7 +66,7 @@ test('VMDK to VHD can convert a random data file with VMDKDirectParser', async (
|
|||||||
)
|
)
|
||||||
const result = await readVmdkGrainTable(createFileAccessor(vmdkFileName))
|
const result = await readVmdkGrainTable(createFileAccessor(vmdkFileName))
|
||||||
const pipe = (
|
const pipe = (
|
||||||
await convertFromVMDK(
|
await vmdkToVhd(
|
||||||
createReadStream(vmdkFileName),
|
createReadStream(vmdkFileName),
|
||||||
result.grainLogicalAddressList,
|
result.grainLogicalAddressList,
|
||||||
result.grainFileOffsetList
|
result.grainFileOffsetList
|
||||||
|
|||||||
@@ -7271,7 +7271,7 @@ get-stdin@^7.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-7.0.0.tgz#8d5de98f15171a125c5e516643c7a6d0ea8a96f6"
|
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-7.0.0.tgz#8d5de98f15171a125c5e516643c7a6d0ea8a96f6"
|
||||||
integrity sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==
|
integrity sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==
|
||||||
|
|
||||||
get-stream@^4.0.0:
|
get-stream@^4.0.0, get-stream@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
|
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
|
||||||
integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
|
integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
|
||||||
|
|||||||
Reference in New Issue
Block a user