Merge remote-tracking branch 'xo-cli/master'

This commit is contained in:
Julien Fontanet 2017-01-13 14:00:45 +01:00
commit a703ecc7e1
8 changed files with 750 additions and 0 deletions

View File

@ -0,0 +1,12 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespaces = true

1
packages/xo-cli/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/node_modules/

90
packages/xo-cli/.jshintrc Normal file
View File

@ -0,0 +1,90 @@
{
// Julien Fontanet JSHint configuration
//
// Changes from defaults:
// - all enforcing options (except `++` & `--`) enabled
// - single quotes
// - indentation set to 2 instead of 4
// - almost all relaxing options disabled
// - allow expression statements (necessary for chai.expect())
// - allow global strict (most of my devs are in Node.js or Browserify)
// - environments are set to Browserify, mocha & Node.js
//
// See http://jshint.com/docs/ for more details
"maxerr" : 50, // {int} Maximum error before stopping
// Enforcing
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
"camelcase" : true, // true: Identifiers must be in camelCase
"curly" : true, // true: Require {} for every new block or scope
"eqeqeq" : true, // true: Require triple equals (===) for comparison
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
"immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
"indent" : 2, // {int} Number of spaces to use for indentation
"latedef" : true, // true: Require variables/functions to be defined before being used
"newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()`
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
"noempty" : true, // true: Prohibit use of empty blocks
"nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment)
"plusplus" : false, // true: Prohibit use of `++` & `--`
"quotmark" : "single", // Quotation mark consistency:
// false : do nothing (default)
// true : ensure whatever is used is consistent
// "single" : require single quotes
// "double" : require double quotes
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
"unused" : true, // true: Require all defined variables be used
"strict" : true, // true: Requires all functions run in ES5 Strict Mode
"maxparams" : 4, // {int} Max number of formal params allowed per function
"maxdepth" : 3, // {int} Max depth of nested blocks (within functions)
"maxstatements" : 20, // {int} Max number statements per function
"maxcomplexity" : 7, // {int} Max cyclomatic complexity per function
"maxlen" : 80, // {int} Max number of characters per line
// Relaxing
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
"boss" : false, // true: Tolerate assignments where comparisons would be expected
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
"eqnull" : false, // true: Tolerate use of `== null`
"es5" : false, // true: Allow ES5 syntax (ex: getters and setters)
"esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`)
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
// (ex: `for each`, multiple try/catch, function expression…)
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
"expr" : true, // true: Tolerate `ExpressionStatement` as Programs
"funcscope" : false, // true: Tolerate defining variables inside control statements
"globalstrict" : true, // true: Allow global "use strict" (also enables 'strict')
"iterator" : false, // true: Tolerate using the `__iterator__` property
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
"laxcomma" : false, // true: Tolerate comma-first style coding
"loopfunc" : false, // true: Tolerate functions being defined in loops
"multistr" : false, // true: Tolerate multi-line strings
"proto" : false, // true: Tolerate using the `__proto__` property
"scripturl" : false, // true: Tolerate script-targeted URLs
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
"validthis" : false, // true: Tolerate using this in a non-constructor function
// Environments
"browser" : false, // Web Browser (window, document, etc)
"browserify" : true, // Browserify (node.js code in the browser)
"couch" : false, // CouchDB
"devel" : true, // Development/debugging (alert, confirm, etc)
"dojo" : false, // Dojo Toolkit
"jquery" : false, // jQuery
"mocha" : true, // mocha
"mootools" : false, // MooTools
"node" : true, // Node.js
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
"prototypejs" : false, // Prototype and Scriptaculous
"rhino" : false, // Rhino
"worker" : false, // Web Workers
"wsh" : false, // Windows Scripting Host
"yui" : false, // Yahoo User Interface
// Custom Globals
"globals" : {} // additional predefined global variables
}

View File

@ -0,0 +1,9 @@
language: node_js
node_js:
- 'stable'
- '6'
- '4'
# Use containers.
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
sudo: false

130
packages/xo-cli/README.md Normal file
View File

@ -0,0 +1,130 @@
# XO-CLI
[![Build Status](https://img.shields.io/travis/vatesfr/xo-cli/master.svg)](http://travis-ci.org/vatesfr/xo-cli)
[![Dependency Status](https://david-dm.org/vatesfr/xo-cli/status.svg?theme=shields.io)](https://david-dm.org/vatesfr/xo-cli)
[![devDependency Status](https://david-dm.org/vatesfr/xo-cli/dev-status.svg?theme=shields.io)](https://david-dm.org/vatesfr/xo-cli#info=devDependencies)
> Basic CLI for Xen-Orchestra
## Installation
#### [npm](https://npmjs.org/package/xo-cli)
```
npm install -g xo-cli
```
## Usage
```
> xo-cli --help
Usage:
xo-cli --register [<XO-Server URL>] [<username>] [<password>]
Registers the XO instance to use.
xo-cli --unregister
Remove stored credentials.
xo-cli --list-commands [--json] [<pattern>]...
Returns the list of available commands on the current XO instance.
The patterns can be used to filter on command names.
xo-cli --list-objects [--<property>]… [<property>=<value>]...
Returns a list of XO objects.
--<property>
Restricts displayed properties to those listed.
<property>=<value>
Restricted displayed objects to those matching the patterns.
xo-cli <command> [<name>=<value>]...
Executes a command on the current XO instance.
```
#### Register your XO instance
```
> xo-cli --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.
#### List available objects
Prints all objects:
```
> xo-cli --list-objects
```
It is possible to filter on object properties, for instance to prints
all VM templates:
```
> xo-cli --list-objects type=VM-template
```
#### List available commands
```
> xo-cli --list-commands
```
Commands can be filtered using patterns:
```
> xo-cli --list-commands '{user,group}.*'
```
#### Execute a command
The same syntax is used for all commands: `xo-cli <command> <param
name>=<value>...`
E.g., adding a new server:
```
> xo-cli server.add host=my.server.net username=root password=secret-password
42
```
The return value is the identifier of this new server in XO.
Parameters (except `true` and `false` which are correctly parsed as
booleans) are assumed to be strings, for other types, you may use JSON
encoding by prefixing with `json:`:
```
> xo-cli foo.bar baz='json:[1, 2, 3]'
```
##### VM export
```
> xo-cli vm.export vm=a01667e0-8e29-49fc-a550-17be4226783c @=vm.xva
```
##### VM import
```
> xo-cli vm.import host=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
```
## Contributing
Contributions are *very* welcome, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xo-cli/issues)
you've encountered;
- fork and create a pull request.
## License
XO-CLI is released under the [AGPL
v3](http://www.gnu.org/licenses/agpl-3.0-standalone.html).

54
packages/xo-cli/config.js Normal file
View File

@ -0,0 +1,54 @@
'use strict'
// ===================================================================
var promisify = require('bluebird').promisify
var readFile = promisify(require('fs').readFile)
var writeFile = promisify(require('fs').writeFile)
var assign = require('lodash/assign')
var l33t = require('l33teral')
var mkdirp = promisify(require('mkdirp'))
var xdgBasedir = require('xdg-basedir')
// ===================================================================
var configPath = xdgBasedir.config + '/xo-cli'
var configFile = configPath + '/config.json'
// ===================================================================
var load = exports.load = function () {
return readFile(configFile).then(JSON.parse).catch(function () {
return {}
})
}
exports.get = function (path) {
return load().then(function (config) {
return l33t(config).tap(path)
})
}
var save = exports.save = function (config) {
return mkdirp(configPath).then(function () {
return writeFile(configFile, JSON.stringify(config))
})
}
exports.set = function (data) {
return load().then(function (config) {
return save(assign(config, data))
})
}
exports.unset = function (paths) {
return load().then(function (config) {
var l33tConfig = l33t(config)
;[].concat(paths).forEach(function (path) {
l33tConfig.purge(path, true)
})
return save(config)
})
}

397
packages/xo-cli/index.js Executable file
View File

@ -0,0 +1,397 @@
#!/usr/bin/env node
'use strict'
var Bluebird = require('bluebird')
Bluebird.longStackTraces()
var createReadStream = require('fs').createReadStream
var createWriteStream = require('fs').createWriteStream
var resolveUrl = require('url').resolve
var stat = require('fs-promise').stat
var chalk = require('chalk')
var eventToPromise = require('event-to-promise')
var filter = require('lodash/filter')
var forEach = require('lodash/forEach')
var getKeys = require('lodash/keys')
var got = require('got')
var humanFormat = require('human-format')
var identity = require('lodash/identity')
var isObject = require('lodash/isObject')
var micromatch = require('micromatch')
var multiline = require('multiline')
var nicePipe = require('nice-pipe')
var pairs = require('lodash/toPairs')
var pick = require('lodash/pick')
var prettyMs = require('pretty-ms')
var progressStream = require('progress-stream')
var Xo = require('xo-lib').default
// -------------------------------------------------------------------
var config = require('./config')
// ===================================================================
function connect () {
return config.load().bind({}).then(function (config) {
if (!config.server) {
throw new Error('no server to connect to!')
}
if (!config.token) {
throw new Error('no token available')
}
var xo = new Xo({ url: config.server })
return xo.open().then(function () {
return xo.signIn({ token: config.token })
}).then(function () {
return xo
})
})
}
function _startsWith (string, search) {
return string.lastIndexOf(search, 0) === 0
}
var FLAG_RE = /^--([^=]+)(?:=([^]*))?$/
function extractFlags (args) {
var flags = {}
var i = 0
var n = args.length
var matches
while (i < n && (matches = args[i].match(FLAG_RE))) {
var value = matches[2]
flags[matches[1]] = value === undefined ? true : value
++i
}
args.splice(0, i)
return flags
}
var PARAM_RE = /^([^=]+)=([^]*)$/
function parseParameters (args) {
var params = {}
forEach(args, function (arg) {
var matches
if (!(matches = arg.match(PARAM_RE))) {
throw new Error('invalid arg: ' + arg)
}
var name = matches[1]
var value = matches[2]
if (_startsWith(value, 'json:')) {
value = JSON.parse(value.slice(5))
}
if (name === '@') {
params['@'] = value
return
}
if (value === 'true') {
value = true
} else if (value === 'false') {
value = false
}
params[name] = value
})
return params
}
var 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)
)
}
}
function wrap (val) {
return function wrappedValue () {
return val
}
}
// ===================================================================
var help = wrap((function (pkg) {
return multiline.stripIndent(function () { /*
Usage:
$name --register [<XO-Server URL>] [<username>] [<password>]
Registers the XO instance to use.
$name --unregister
Remove stored credentials.
$name --list-commands [--json] [<pattern>]...
Returns the list of available commands on the current XO instance.
The patterns can be used to filter on command names.
$name --list-objects [--<property>] [<property>=<value>]...
Returns a list of XO objects.
--<property>
Restricts displayed properties to those listed.
<property>=<value>
Restricted displayed objects to those matching the patterns.
$name <command> [<name>=<value>]...
Executes a command on the current XO instance.
$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]
})
})(require('./package')))
// -------------------------------------------------------------------
function main (args) {
if (!args || !args.length || args[0] === '-h') {
return help()
}
var fnName = args[0].replace(/^--|-\w/g, function (match) {
if (match === '--') {
return ''
}
return match[1].toUpperCase()
})
if (fnName in exports) {
return exports[fnName](args.slice(1))
}
return exports.call(args)
}
exports = module.exports = main
// -------------------------------------------------------------------
exports.help = help
function register (args) {
var xo = new Xo({ url: args[0] })
return xo.open().then(function () {
return xo.signIn({
email: args[1],
password: args[2]
})
}).then(function () {
console.log('Successfully logged with', xo.user.email)
return xo.call('token.create')
}).then(function (token) {
return config.set({
server: args[0],
token: token
})
})
}
exports.register = register
function unregister () {
return config.unset([
'server',
'token'
])
}
exports.unregister = unregister
function listCommands (args) {
return connect().then(function getMethodsInfo (xo) {
return xo.call('system.getMethodsInfo')
}).then(function formatMethodsInfo (methods) {
var json = false
var patterns = []
forEach(args, function (arg) {
if (arg === '--json') {
json = true
} else {
patterns.push(arg)
}
})
if (patterns.length) {
methods = pick(methods, micromatch(Object.keys(methods), patterns))
}
if (json) {
return methods
}
methods = pairs(methods)
methods.sort(function (a, b) {
a = a[0]
b = b[0]
if (a < b) {
return -1
}
return +(a > b)
})
var str = []
forEach(methods, function (method) {
var name = method[0]
var info = method[1]
str.push(chalk.bold.blue(name))
forEach(info.params || [], function (info, name) {
str.push(' ')
if (info.optional) {
str.push('[')
}
str.push(name, '=<', info.type || 'unknown', '>')
if (info.optional) {
str.push(']')
}
})
str.push('\n')
if (info.description) {
str.push(' ', info.description, '\n')
}
})
return str.join('')
})
}
exports.listCommands = listCommands
function listObjects (args) {
var properties = getKeys(extractFlags(args))
var filterProperties = properties.length
? function (object) {
return pick(object, properties)
}
: identity
var sieve = args.length ? parseParameters(args) : null
return connect().then(function getXoObjects (xo) {
return xo.call('xo.getAllObjects')
}).then(function filterObjects (objects) {
objects = filter(objects, sieve)
const stdout = process.stdout
stdout.write('[\n')
for (var i = 0, n = objects.length; i < n;) {
stdout.write(JSON.stringify(filterProperties(objects[i]), null, 2))
stdout.write(++i < n ? ',\n' : '\n')
}
stdout.write(']')
})
}
exports.listObjects = listObjects
function call (args) {
if (!args.length) {
throw new Error('missing command name')
}
var method = args.shift()
var params = parseParameters(args)
var file = params['@']
delete params['@']
var baseUrl
return connect().then(function (xo) {
// FIXME: do not use private properties.
baseUrl = xo._url.replace(/^ws/, 'http')
return xo.call(method, params)
}).then(function handleResult (result) {
var keys, key, url
if (
isObject(result) &&
(keys = getKeys(result)).length === 1
) {
key = keys[0]
if (key === '$getFrom') {
url = resolveUrl(baseUrl, result[key])
var output = createWriteStream(file)
var progress = progressStream({ time: 1e3 }, printProgress)
return eventToPromise(nicePipe([
got.stream(url).on('response', function (response) {
var length = response.headers['content-length']
if (length) {
progress.length(length)
}
}),
progress,
output
]), 'finish')
}
if (key === '$sendTo') {
url = resolveUrl(baseUrl, result[key])
return stat(file).then(function (stats) {
var length = stats.size
var input = nicePipe([
createReadStream(file),
progressStream({
length: length,
time: 1e3
}, printProgress)
])
return got.post(url, {
body: input,
headers: {
'content-length': length
},
method: 'POST'
}).then(function (response) {
return response.body
})
})
}
}
return result
})
}
exports.call = call
// ===================================================================
if (!module.parent) {
require('exec-promise')(exports)
}

View File

@ -0,0 +1,57 @@
{
"name": "xo-cli",
"version": "0.8.2",
"license": "AGPL-3.0",
"description": "Basic CLI for Xen-Orchestra",
"keywords": [
"xo",
"xen-orchestra",
"xen",
"orchestra"
],
"homepage": "https://github.com/vatesfr/xo-cli",
"bugs": "https://github.com/vatesfr/xo-cli/issues",
"author": "Julien Fontanet <julien.fontanet@vates.fr>",
"preferGlobal": true,
"bin": {
"xo-cli": "index.js"
},
"files": [
"*.js"
],
"repository": {
"type": "git",
"url": "https://github.com/vatesfr/xo-cli"
},
"dependencies": {
"bluebird": "^3.4.6",
"chalk": "^1.1.1",
"event-to-promise": "^0.7.0",
"exec-promise": "^0.6.1",
"fs-promise": "^1.0.0",
"got": "^6.5.0",
"human-format": "^0.7.0",
"l33teral": "^3.0.2",
"lodash": "^4.16.4",
"micromatch": "^2.2.0",
"mkdirp": "^0.5.0",
"multiline": "^1.0.2",
"nice-pipe": "0.0.0",
"pretty-ms": "^2.1.0",
"progress-stream": "^1.1.1",
"xdg-basedir": "^2.0.0",
"xo-lib": "^0.8.0"
},
"devDependencies": {
"standard": "^8.1.0"
},
"scripts": {
"lint": "standard",
"posttest": "npm run lint"
},
"greenkeeper": {
"ignore": [
"nice-pipe"
]
}
}