joyent/node-triton#54 a start at 'triton rbac info', add 'triton rbac instance-role-tags'
This commit is contained in:
		
							parent
							
								
									74b8f3e42e
								
							
						
					
					
						commit
						4e45e4061f
					
				| @ -18,6 +18,10 @@ | ||||
|     - `triton rbac policy ...` to show, create, edit and delete policies. | ||||
|     - `triton rbac keys` to list all RBAC user SSH keys. | ||||
|     - `triton rbac key ...` to show, create, edit and delete user keys. | ||||
|     - `triton rbac info` will dump a summary of the full current RBAC | ||||
|       configuration. This command is still in development. | ||||
|     - `triton rbac instance-role-tags ...` to list and manage role tags | ||||
|       on an instance. | ||||
| 
 | ||||
| 
 | ||||
| ## 2.1.4 | ||||
|  | ||||
| @ -224,7 +224,7 @@ CloudApi.prototype._request = function _request(options, callback) { | ||||
|     assert.optionalObject(options.data, 'options.data'); | ||||
| 
 | ||||
|     var method = (options.method || 'GET').toLowerCase(); | ||||
|     assert.ok(['get', 'post', 'delete', 'head'].indexOf(method) >= 0, | ||||
|     assert.ok(['get', 'post', 'put', 'delete', 'head'].indexOf(method) >= 0, | ||||
|         'invalid method given'); | ||||
|     switch (method) { | ||||
|     case 'delete': | ||||
| @ -1170,6 +1170,69 @@ CloudApi.prototype.deletePolicy = function deletePolicy(opts, cb) { | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * <http://apidocs.joyent.com/cloudapi/#SetRoleTags>
 | ||||
|  * Set RBAC role-tags on a resource. | ||||
|  * | ||||
|  * @param {Object} opts (object): | ||||
|  *      - {String} resource (required) The resource URL. E.g. | ||||
|  *        '/:account/machines/:uuid' to tag a particular machine instance. | ||||
|  *      - {Array} roleTags (required) the array of role tags to set. Each | ||||
|  *        role tag string is the name of a RBAC role. See `ListRoles`. | ||||
|  * @param {Function} cb of the form `function (err, body, res)` | ||||
|  *      Where `body` is of the form `{name: <resource url>,
 | ||||
|  *      'role-tag': <array of added role tags>}`.
 | ||||
|  * @throws {AssertionError, TypeError} on invalid inputs | ||||
|  */ | ||||
| CloudApi.prototype.setRoleTags = function setRoleTags(opts, cb) { | ||||
|     assert.object(opts, 'opts'); | ||||
|     assert.string(opts.resource, 'opts.resource'); | ||||
|     assert.arrayOfString(opts.roleTags, 'opts.roleTags'); | ||||
|     assert.func(cb, 'cb'); | ||||
| 
 | ||||
|     // Validate `resource`.
 | ||||
|     // XXX Do we need to massage '/my' to '/:account' as old cloudapi.js does?
 | ||||
|     var resourceRe = new RegExp('^/[^/]{2,}/[^/]+'); | ||||
|     if (! resourceRe.test(opts.resource)) { | ||||
|         throw new TypeError(format('invalid resource "%s": must match ' + | ||||
|             '"/:account/:type..."', opts.resource)); | ||||
|     } | ||||
| 
 | ||||
|     var validResources = [ | ||||
|         'machines', | ||||
|         'packages', | ||||
|         'images', | ||||
|         'fwrules', | ||||
|         'networks', | ||||
|         // TODO: validate/test role tags on these rbac resources
 | ||||
|         'users', | ||||
|         'roles', | ||||
|         'policies', | ||||
|         // TODO: validate, test
 | ||||
|         'keys', | ||||
|         'datacenters', | ||||
|         'analytics', | ||||
|         'instrumentations' | ||||
|     ]; | ||||
|     var parts = opts.resource.split('/'); | ||||
|     if (validResources.indexOf(parts[2]) === -1) { | ||||
|         throw new TypeError(format('invalid resource "%s": resource type ' + | ||||
|             'must be one of: %s', opts.resource, validResources.join(', '))); | ||||
|     } | ||||
| 
 | ||||
|     this._request({ | ||||
|         method: 'PUT', | ||||
|         path: opts.resource, | ||||
|         data: { | ||||
|             'role-tag': opts.roleTags | ||||
|         } | ||||
|     }, function (err, req, res, body) { | ||||
|         cb(err, body, res); | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| // --- Exports
 | ||||
| 
 | ||||
| module.exports.createClient = function (options) { | ||||
|  | ||||
							
								
								
									
										272
									
								
								lib/do_rbac/do_info.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										272
									
								
								lib/do_rbac/do_info.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,272 @@ | ||||
| /* | ||||
|  * This Source Code Form is subject to the terms of the Mozilla Public | ||||
|  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | ||||
|  */ | ||||
| 
 | ||||
| /* | ||||
|  * Copyright 2015 Joyent, Inc. | ||||
|  * | ||||
|  * `triton rbac info ...` | ||||
|  */ | ||||
| 
 | ||||
| /* BEGIN JSSTYLED */ | ||||
| /* | ||||
| 
 | ||||
| Sample output for development/discussion: | ||||
| 
 | ||||
| ``` | ||||
| users ($numUsers): | ||||
|     bob ($fullname, **no ssh keys**): roles web* | ||||
|     carl (Carl Fogel): roles eng, operator*, cibot*, monbot* | ||||
|     ... | ||||
| roles ($numRoles): | ||||
|     eng: policies write, read                   # include users here? | ||||
|     ops: policies delete, read, write | ||||
|     support: policies read | ||||
| policies ($numPolicies): | ||||
|     delete ($desc): | ||||
|         can deletemachine | ||||
|     read (cloudapi read-only actions): | ||||
|         can listmachines, getmachine and listimages | ||||
|     write (cloudapi write (non-delete) actions): | ||||
|         can createmachine, updatemachine, stopmachine and startmachine | ||||
| resources:   # or call this 'resources'? role-tags? | ||||
|     # some dump of all resources (perhaps not default to *all*) and their | ||||
|     # role-tags | ||||
|     instance foo0 ($uuid): role-tags eng | ||||
|     image bar@1.2.3 ($uuid): role-tags ops | ||||
| ``` | ||||
| 
 | ||||
| Ideas: | ||||
| - red warning about users with no keys | ||||
| - `triton rbac info -u bob`   Show everything from bob's p.o.v. | ||||
| - `triton rbac info -r readonly`   Show everything from this role's p.o.v. | ||||
|     `... --instance foo0`, etc. | ||||
| - `-t|--role-tags` to include the role tag info. Perhaps with arg for which? | ||||
|   E.g. do we traverse all machines, images, networks? That could too much... | ||||
|   Might need cloudapi support for returning those optionally. | ||||
|     ListImages?fields=*,role_tags   # perhaps don't support '*' | ||||
| */ | ||||
| /* END JSSTYLED */ | ||||
| 
 | ||||
| 
 | ||||
| var assert = require('assert-plus'); | ||||
| var format = require('util').format; | ||||
| var tabula = require('tabula'); | ||||
| var vasync = require('vasync'); | ||||
| 
 | ||||
| var common = require('../common'); | ||||
| var errors = require('../errors'); | ||||
| 
 | ||||
| var ansiStylize = common.ansiStylize; | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| /* | ||||
|  * Gather RBAC users, policies, roles and add those to the given `ctx` object. | ||||
|  */ | ||||
| function gatherRbacBasicInfo(ctx, cb) { | ||||
|     assert.object(ctx.cloudapi, 'ctx.cloudapi'); | ||||
|     assert.func(cb, 'cb'); | ||||
| 
 | ||||
|     vasync.parallel({funcs: [ | ||||
|         function listUsers(next) { | ||||
|             ctx.cloudapi.listUsers(function (err, users) { | ||||
|                 ctx.users = users; | ||||
|                 next(err); | ||||
|             }); | ||||
|         }, | ||||
|         function listPolicies(next) { | ||||
|             ctx.cloudapi.listPolicies(function (err, policies) { | ||||
|                 ctx.policies = policies; | ||||
|                 next(err); | ||||
|             }); | ||||
|         }, | ||||
|         function listRoles(next) { | ||||
|             ctx.cloudapi.listRoles(function (err, roles) { | ||||
|                 ctx.roles = roles; | ||||
|                 next(err); | ||||
|             }); | ||||
|         } | ||||
|     ]}, cb); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function do_info(subcmd, opts, args, cb) { | ||||
|     if (opts.help) { | ||||
|         this.do_help('help', {}, [subcmd], cb); | ||||
|         return; | ||||
|     } else if (args.length !== 0) { | ||||
|         cb(new errors.UsageError('invalid args: ' + args)); | ||||
|         return; | ||||
|     } | ||||
|     var log = this.log; | ||||
| 
 | ||||
|     var context = { | ||||
|         tritonapi: this.top.tritonapi, | ||||
|         cloudapi: this.top.tritonapi.cloudapi | ||||
|     }; | ||||
|     vasync.pipeline({arg: context, funcs: [ | ||||
|         gatherRbacBasicInfo, | ||||
|         function gatherUserKeys(ctx, next) { | ||||
|             if (!opts.all) { | ||||
|                 next(); | ||||
|                 return; | ||||
|             } | ||||
|             // XXX Q! or concurrency forEachParallel
 | ||||
|             vasync.forEachParallel({ | ||||
|                 inputs: ctx.users, | ||||
|                 func: function oneUser(user, nextUser) { | ||||
|                     ctx.cloudapi.listUserKeys({userId: user.id}, | ||||
|                             function (err, userKeys) { | ||||
|                         user.keys = userKeys; | ||||
|                         nextUser(err); | ||||
|                     }); | ||||
|                 } | ||||
|             }, next); | ||||
|         }, | ||||
|         function fillInUserRoles(ctx, next) { | ||||
|             var i; | ||||
|             var userFromLogin = {}; | ||||
|             for (i = 0; i < ctx.users.length; i++) { | ||||
|                 var user = ctx.users[i]; | ||||
|                 user.default_roles = []; | ||||
|                 user.roles = []; | ||||
|                 userFromLogin[user.login] = user; | ||||
|             } | ||||
|             for (i = 0; i < ctx.roles.length; i++) { | ||||
|                 var role = ctx.roles[i]; | ||||
|                 role.default_members.forEach(function (login) { | ||||
|                     userFromLogin[login].default_roles.push(role.name); | ||||
|                 }); | ||||
|                 role.members.forEach(function (login) { | ||||
|                     userFromLogin[login].roles.push(role.name); | ||||
|                 }); | ||||
|             } | ||||
|             next(); | ||||
|         }, | ||||
|         function printInfo(ctx, next) { | ||||
|             var i; | ||||
|             log.trace({ | ||||
|                 users: ctx.users, | ||||
|                 policies: ctx.policies, | ||||
|                 roles: ctx.roles | ||||
|             }, 'rbac info data'); | ||||
| 
 | ||||
|             console.log('users (%d):', ctx.users.length); | ||||
|             tabula.sortArrayOfObjects(ctx.users, ['name']); | ||||
|             for (i = 0; i < ctx.users.length; i++) { | ||||
|                 var user = ctx.users[i]; | ||||
| 
 | ||||
|                 var userExtra = []; | ||||
|                 if (user.firstName || user.lastName) { | ||||
|                     userExtra.push(((user.firstName || '') + ' ' + | ||||
|                         (user.lastName || '')).trim()); | ||||
|                 } | ||||
|                 if (user.keys && user.keys.length === 0) { | ||||
|                     userExtra.push(ansiStylize('no ssh keys', 'red')); | ||||
|                 } | ||||
|                 if (userExtra.length > 0) { | ||||
|                     userExtra = format(' (%s)', userExtra.join(', ')); | ||||
|                 } else { | ||||
|                     userExtra = ''; | ||||
|                 } | ||||
| 
 | ||||
|                 var roleInfo = []; | ||||
|                 user.default_roles.sort(); | ||||
|                 user.roles.sort(); | ||||
|                 var roleSeen = {}; | ||||
|                 user.default_roles.forEach(function (r) { | ||||
|                     roleSeen[r] = true; | ||||
|                     roleInfo.push(r); | ||||
|                 }); | ||||
|                 user.roles.forEach(function (r) { | ||||
|                     if (!roleSeen[r]) { | ||||
|                         roleInfo.push(r + '*'); // marker for non-default role
 | ||||
|                     } | ||||
|                 }); | ||||
|                 if (roleInfo.length === 1) { | ||||
|                     roleInfo = 'role ' + roleInfo.join(', '); | ||||
|                 } else if (roleInfo.length > 0) { | ||||
|                     roleInfo = 'roles ' + roleInfo.join(', '); | ||||
|                 } else { | ||||
|                     roleInfo = ansiStylize('no roles', 'red'); | ||||
|                 } | ||||
|                 console.log('    %s%s: %s', ansiStylize(user.login, 'bold'), | ||||
|                     userExtra, roleInfo); | ||||
|             } | ||||
| 
 | ||||
|             console.log('roles (%d):', ctx.roles.length); | ||||
|             tabula.sortArrayOfObjects(ctx.roles, ['name']); | ||||
|             for (i = 0; i < ctx.roles.length; i++) { | ||||
|                 var role = ctx.roles[i]; | ||||
| 
 | ||||
|                 var policyInfo; | ||||
|                 if (role.policies.length === 1) { | ||||
|                     policyInfo = 'policy ' + role.policies.join(', '); | ||||
|                 } else if (role.policies.length > 0) { | ||||
|                     policyInfo = 'policies ' + role.policies.join(', '); | ||||
|                 } else { | ||||
|                     policyInfo = ansiStylize('no policies', 'red'); | ||||
|                 } | ||||
|                 console.log('    %s: %s', ansiStylize(role.name, 'bold'), | ||||
|                     policyInfo); | ||||
|             } | ||||
| 
 | ||||
|             console.log('policies (%d):', ctx.policies.length); | ||||
|             tabula.sortArrayOfObjects(ctx.policies, ['name']); | ||||
|             for (i = 0; i < ctx.policies.length; i++) { | ||||
|                 var policy = ctx.policies[i]; | ||||
|                 var noRules = ''; | ||||
|                 if (policy.rules.length === 0) { | ||||
|                     noRules = ' ' + ansiStylize('no rules', 'red'); | ||||
|                 } | ||||
|                 if (policy.description) { | ||||
|                     console.log('    %s (%s) rules:%s', | ||||
|                         ansiStylize(policy.name, 'bold'), | ||||
|                         policy.description, noRules); | ||||
|                 } else { | ||||
|                     console.log('    %s rules:%s', | ||||
|                         ansiStylize(policy.name, 'bold'), noRules); | ||||
|                 } | ||||
|                 policy.rules.forEach(function (r) { | ||||
|                     console.log('        %s', r); | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             next(); | ||||
|         } | ||||
|     ]}, function (err) { | ||||
|         cb(err); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| do_info.options = [ | ||||
|     { | ||||
|         names: ['help', 'h'], | ||||
|         type: 'bool', | ||||
|         help: 'Show this help.' | ||||
|     }, | ||||
|     { | ||||
|         names: ['all', 'a'], | ||||
|         type: 'bool', | ||||
|         help: 'Include all info for a more full report. This requires more ' + | ||||
|             'work to gather all info.' | ||||
|     } | ||||
| ]; | ||||
| 
 | ||||
| do_info.help = ( | ||||
|     /* BEGIN JSSTYLED */ | ||||
|     'Print an account RBAC summary.\n' + | ||||
|     '\n' + | ||||
|     'Usage:\n' + | ||||
|     '    {{name}} info [<options>]\n' + | ||||
|     '\n' + | ||||
|     '{{options}}' | ||||
|     /* END JSSTYLED */ | ||||
| ); | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| module.exports = do_info; | ||||
							
								
								
									
										582
									
								
								lib/do_rbac/do_role_tags.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										582
									
								
								lib/do_rbac/do_role_tags.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,582 @@ | ||||
| /* | ||||
|  * This Source Code Form is subject to the terms of the Mozilla Public | ||||
|  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | ||||
|  */ | ||||
| 
 | ||||
| /* | ||||
|  * Copyright 2015 Joyent, Inc. | ||||
|  * | ||||
|  * `triton rbac role-tags ...`    # hidden lower-level command | ||||
|  * `triton rbac instance-role-tags ...` | ||||
|  * `triton rbac image-role-tags ...` | ||||
|  * `triton rbac package-role-tags ...` | ||||
|  * `triton rbac network-role-tags ...` | ||||
|  * etc. | ||||
|  */ | ||||
| 
 | ||||
| var assert = require('assert-plus'); | ||||
| var format = require('util').format; | ||||
| var fs = require('fs'); | ||||
| var strsplit = require('strsplit'); | ||||
| var vasync = require('vasync'); | ||||
| 
 | ||||
| var common = require('../common'); | ||||
| var errors = require('../errors'); | ||||
| 
 | ||||
| 
 | ||||
| // ---- internal support stuff
 | ||||
| 
 | ||||
| function _listRoleTags(opts, cb) { | ||||
|     assert.object(opts.cli, 'opts.cli'); | ||||
|     assert.string(opts.resourceId, 'opts.resourceId'); | ||||
|     assert.optionalBool(opts.json, 'opts.json'); | ||||
|     assert.func(cb, 'cb'); | ||||
|     var cli = opts.cli; | ||||
| 
 | ||||
|     cli.tritonapi.getInstanceRoleTags(opts.resourceId, | ||||
|             function (err, roleTags) { | ||||
|         if (err) { | ||||
|             cb(err); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (opts.json) { | ||||
|             console.log(JSON.stringify(roleTags)); | ||||
|         } else { | ||||
|             roleTags.forEach(function (r) { | ||||
|                 console.log(r); | ||||
|             }); | ||||
|         } | ||||
|         cb(); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| function _addRoleTags(opts, cb) { | ||||
|     assert.object(opts.cli, 'opts.cli'); | ||||
|     assert.string(opts.resourceId, 'opts.resourceId'); | ||||
|     assert.arrayOfString(opts.roleTags, 'opts.roleTags'); | ||||
|     assert.func(cb, 'cb'); | ||||
|     var cli = opts.cli; | ||||
|     var log = cli.log; | ||||
| 
 | ||||
|     vasync.pipeline({arg: {}, funcs: [ | ||||
|         function getCurrRoleTags(ctx, next) { | ||||
|             cli.tritonapi.getInstanceRoleTags(opts.resourceId, | ||||
|                     function (err, roleTags, inst) { | ||||
|                 if (err) { | ||||
|                     next(err); | ||||
|                     return; | ||||
|                 } | ||||
|                 ctx.roleTags = roleTags; | ||||
|                 ctx.inst = inst; | ||||
|                 log.trace({inst: inst, roleTags: roleTags}, 'curr role tags'); | ||||
|                 next(); | ||||
|             }); | ||||
|         }, | ||||
| 
 | ||||
|         function addRoleTags(ctx, next) { | ||||
|             var adding = []; | ||||
|             for (var i = 0; i < opts.roleTags.length; i++) { | ||||
|                 var r = opts.roleTags[i]; | ||||
|                 if (ctx.roleTags.indexOf(r) === -1) { | ||||
|                     ctx.roleTags.push(r); | ||||
|                     adding.push(r); | ||||
|                 } | ||||
|             } | ||||
|             if (adding.length === 0) { | ||||
|                 next(); | ||||
|                 return; | ||||
|             } else { | ||||
|                 console.log('Adding %d role tag%s (%s) to instance "%s"', | ||||
|                     adding.length, adding.length === 1 ? '' : 's', | ||||
|                     adding.join(', '), ctx.inst.name); | ||||
|             } | ||||
|             cli.tritonapi.cloudapi.setRoleTags({ | ||||
|                 resource: _resourceUrlFromId( | ||||
|                     cli.tritonapi.profile.account, ctx.inst.id), | ||||
|                 roleTags: ctx.roleTags | ||||
|             }, next); | ||||
|         } | ||||
|     ]}, function (err) { | ||||
|         cb(err); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| // TODO: resource URL should be in tritonapi.js,
 | ||||
| //      E.g. perhaps `TritonApi.setInstanceRoleTags`?
 | ||||
| function _resourceUrlFromId(account, id) { | ||||
|     return format('/%s/machines/%s', account, id); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function _reprFromRoleTags(roleTags) { | ||||
|     assert.arrayOfString(roleTags, 'roleTags'); | ||||
| 
 | ||||
|     if (roleTags.length === 0) { | ||||
|         return ''; | ||||
|     } | ||||
| 
 | ||||
|     // Make this somewhat canonical by sorting.
 | ||||
|     roleTags.sort(); | ||||
|     return roleTags.join('\n') + '\n'; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function _roleTagsFromRepr(repr) { | ||||
|     assert.string(repr, 'repr'); | ||||
| 
 | ||||
|     var roleTags = []; | ||||
|     var lines = repr.split(/\n/g); | ||||
|     lines.forEach(function (line) { | ||||
|         var commentIdx = line.indexOf('#'); | ||||
|         if (commentIdx !== -1) { | ||||
|             line = line.slice(0, commentIdx); | ||||
|         } | ||||
|         line = line.trim(); | ||||
|         if (!line) { | ||||
|             return; | ||||
|         } | ||||
|         roleTags.push(line); | ||||
|     }); | ||||
| 
 | ||||
|     roleTags.sort(); | ||||
|     return roleTags; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function _editRoleTags(opts, cb) { | ||||
|     assert.object(opts.cli, 'opts.cli'); | ||||
|     assert.string(opts.resourceId, 'opts.resourceId'); | ||||
|     assert.func(cb, 'cb'); | ||||
|     var cli = opts.cli; | ||||
| 
 | ||||
|     var account = cli.tritonapi.profile.account; | ||||
|     var id; | ||||
|     var roleTags; | ||||
|     var filename; | ||||
|     var origText; | ||||
| 
 | ||||
|     function offerRetry(afterText) { | ||||
|         common.promptEnter( | ||||
|             'Press <Enter> to re-edit, Ctrl+C to abort.', | ||||
|             function (aborted) { | ||||
|                 if (aborted) { | ||||
|                     console.log('\nAborting. No change made.'); | ||||
|                     cb(); | ||||
|                 } else { | ||||
|                     editAttempt(afterText); | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     function editAttempt(text) { | ||||
|         common.editInEditor({ | ||||
|             text: text, | ||||
|             filename: filename | ||||
|         }, function (err, afterText, changed) { | ||||
|             if (err) { | ||||
|                 return cb(new errors.TritonError(err)); | ||||
|             } | ||||
|             // We don't use this `changed` in case it is a second attempt.
 | ||||
| 
 | ||||
|             try { | ||||
|                 var edited = _roleTagsFromRepr(afterText); | ||||
| 
 | ||||
|                 if (_reprFromRoleTags(edited) === origText) { | ||||
|                     // This repr is the closest to a canonical form we have.
 | ||||
|                     console.log('No change'); | ||||
|                     cb(); | ||||
|                     return; | ||||
|                 } | ||||
|             } catch (textErr) { | ||||
|                 console.error('Error with your changes: %s', textErr); | ||||
|                 offerRetry(afterText); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Save changes.
 | ||||
|             cli.tritonapi.cloudapi.setRoleTags({ | ||||
|                 resource: _resourceUrlFromId(account, id), | ||||
|                 roleTags: edited | ||||
|             }, function (setErr) { | ||||
|                 if (setErr) { | ||||
|                     console.error('Error updating role tags with ' + | ||||
|                         'your changes: %s', setErr); | ||||
|                     offerRetry(afterText); | ||||
|                     return; | ||||
|                 } | ||||
|                 console.log('Edited role tags on instance "%s"', | ||||
|                     opts.resourceId); | ||||
|                 cb(); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     cli.tritonapi.getInstanceRoleTags(opts.resourceId, | ||||
|             function (err, roleTags_, inst) { | ||||
|         if (err) { | ||||
|             cb(err); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         id = inst.id; | ||||
|         roleTags = roleTags_; | ||||
|         filename = format('%s-inst-%s-roleTags.txt', | ||||
|             cli.tritonapi.profile.account, | ||||
|             opts.resourceId); | ||||
|         origText = _reprFromRoleTags(roleTags); | ||||
|         editAttempt(origText); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function _setRoleTags(opts, cb) { | ||||
|     assert.object(opts.cli, 'opts.cli'); | ||||
|     assert.string(opts.resourceId, 'opts.resourceId'); | ||||
|     assert.arrayOfString(opts.roleTags, 'opts.roleTags'); | ||||
|     assert.optionalBool(opts.yes, 'opts.yes'); | ||||
|     assert.func(cb, 'cb'); | ||||
|     var cli = opts.cli; | ||||
| 
 | ||||
|     vasync.pipeline({arg: {}, funcs: [ | ||||
|         // TODO: consider shorter path if the instance UUID is given
 | ||||
|         //       (but what if the instance has a UUID for an *alias*)?
 | ||||
|         function getResource(ctx, next) { | ||||
|             cli.tritonapi.getInstance(opts.resourceId, function (err, inst) { | ||||
|                 if (err) { | ||||
|                     next(err); | ||||
|                     return; | ||||
|                 } | ||||
|                 ctx.inst = inst; | ||||
|                 next(); | ||||
|             }); | ||||
|         }, | ||||
| 
 | ||||
|         function confirm(ctx, next) { | ||||
|             if (opts.yes) { | ||||
|                 return next(); | ||||
|             } | ||||
|             var msg = format('Set role tags on instance "%s"? [y/n] ', | ||||
|                 ctx.inst.name); | ||||
|             common.promptYesNo({msg: msg}, function (answer) { | ||||
|                 if (answer !== 'y') { | ||||
|                     console.error('Aborting'); | ||||
|                     next(true); // early abort signal
 | ||||
|                 } else { | ||||
|                     next(); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
| 
 | ||||
|         function setThem(ctx, next) { | ||||
|             console.log('Setting role tags on instance "%s"', ctx.inst.name); | ||||
|             cli.tritonapi.cloudapi.setRoleTags({ | ||||
|                 resource: _resourceUrlFromId( | ||||
|                     cli.tritonapi.profile.account, ctx.inst.id), | ||||
|                 roleTags: opts.roleTags | ||||
|             }, next); | ||||
|         } | ||||
|     ]}, function (err) { | ||||
|         cb(err); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| function _deleteRoleTags(opts, cb) { | ||||
|     assert.object(opts.cli, 'opts.cli'); | ||||
|     assert.string(opts.resourceId, 'opts.resourceId'); | ||||
|     assert.arrayOfString(opts.roleTags, 'opts.roleTags'); | ||||
|     assert.func(cb, 'cb'); | ||||
|     var cli = opts.cli; | ||||
|     var log = cli.log; | ||||
| 
 | ||||
|     vasync.pipeline({arg: {}, funcs: [ | ||||
|         function getCurrRoleTags(ctx, next) { | ||||
|             cli.tritonapi.getInstanceRoleTags(opts.resourceId, | ||||
|                     function (err, roleTags, inst) { | ||||
|                 if (err) { | ||||
|                     next(err); | ||||
|                     return; | ||||
|                 } | ||||
|                 ctx.roleTags = roleTags; | ||||
|                 ctx.inst = inst; | ||||
|                 log.trace({inst: inst, roleTags: roleTags}, 'curr role tags'); | ||||
|                 next(); | ||||
|             }); | ||||
|         }, | ||||
| 
 | ||||
|         function determineToDelete(ctx, next) { | ||||
|             ctx.toDelete = []; | ||||
|             ctx.roleTagsToKeep = []; | ||||
|             for (var i = 0; i < ctx.roleTags.length; i++) { | ||||
|                 var r = ctx.roleTags[i]; | ||||
|                 if (opts.roleTags.indexOf(r) !== -1) { | ||||
|                     ctx.toDelete.push(r); | ||||
|                 } else { | ||||
|                     ctx.roleTagsToKeep.push(r); | ||||
|                 } | ||||
|             } | ||||
|             next(); | ||||
|         }, | ||||
| 
 | ||||
|         function confirm(ctx, next) { | ||||
|             if (ctx.toDelete.length === 0 || opts.yes) { | ||||
|                 return next(); | ||||
|             } | ||||
|             var msg = format( | ||||
|                 'Delete %d role tag%s (%s) from instance "%s"? [y/n] ', | ||||
|                 ctx.toDelete.length, ctx.toDelete.length === 1 ? '' : 's', | ||||
|                 ctx.toDelete.join(', '), ctx.inst.name); | ||||
|             common.promptYesNo({msg: msg}, function (answer) { | ||||
|                 if (answer !== 'y') { | ||||
|                     console.error('Aborting'); | ||||
|                     next(true); // early abort signal
 | ||||
|                 } else { | ||||
|                     next(); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
| 
 | ||||
|         function deleteRoleTags(ctx, next) { | ||||
|             if (ctx.toDelete.length === 0) { | ||||
|                 next(); | ||||
|                 return; | ||||
|             } | ||||
|             console.log('Deleting %d role tag%s (%s) from instance "%s"', | ||||
|                 ctx.toDelete.length, ctx.toDelete.length === 1 ? '' : 's', | ||||
|                 ctx.toDelete.join(', '), ctx.inst.name); | ||||
|             cli.tritonapi.cloudapi.setRoleTags({ | ||||
|                 resource: _resourceUrlFromId( | ||||
|                     cli.tritonapi.profile.account, ctx.inst.id), | ||||
|                 roleTags: ctx.roleTagsToKeep | ||||
|             }, next); | ||||
|         } | ||||
|     ]}, function (err) { | ||||
|         cb(err); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function _deleteAllRoleTags(opts, cb) { | ||||
|     assert.object(opts.cli, 'opts.cli'); | ||||
|     assert.string(opts.resourceId, 'opts.resourceId'); | ||||
|     assert.func(cb, 'cb'); | ||||
|     var cli = opts.cli; | ||||
| 
 | ||||
|     vasync.pipeline({arg: {}, funcs: [ | ||||
|         // TODO: consider shorter path if the instance UUID is given
 | ||||
|         //       (but what if the instance has a UUID for an *alias*)?
 | ||||
|         function getResource(ctx, next) { | ||||
|             cli.tritonapi.getInstance(opts.resourceId, function (err, inst) { | ||||
|                 if (err) { | ||||
|                     next(err); | ||||
|                     return; | ||||
|                 } | ||||
|                 ctx.inst = inst; | ||||
|                 next(); | ||||
|             }); | ||||
|         }, | ||||
| 
 | ||||
|         function confirm(ctx, next) { | ||||
|             if (opts.yes) { | ||||
|                 return next(); | ||||
|             } | ||||
|             var msg = format('Delete all role tags from instance "%s"? [y/n] ', | ||||
|                 ctx.inst.name); | ||||
|             common.promptYesNo({msg: msg}, function (answer) { | ||||
|                 if (answer !== 'y') { | ||||
|                     console.error('Aborting'); | ||||
|                     next(true); // early abort signal
 | ||||
|                 } else { | ||||
|                     next(); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
| 
 | ||||
|         function deleteAllRoleTags(ctx, next) { | ||||
|             console.log('Deleting all role tags from instance "%s"', | ||||
|                 ctx.inst.name); | ||||
|             cli.tritonapi.cloudapi.setRoleTags({ | ||||
|                 resource: _resourceUrlFromId( | ||||
|                     cli.tritonapi.profile.account, ctx.inst.id), | ||||
|                 roleTags: [] | ||||
|             }, next); | ||||
|         } | ||||
|     ]}, function (err) { | ||||
|         cb(err); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function _roleTagsFromArrayOfString(arr) { | ||||
|     assert.arrayOfString(arr, arr); | ||||
|     var allRoleTags = []; | ||||
|     for (var i = 0; i < arr.length; i++) { | ||||
|         var roleTags = arr[i] | ||||
|             /* JSSTYLED */ | ||||
|             .split(/\s*,\s*/) | ||||
|             .filter(function (r) { return r.trim(); }); | ||||
|         allRoleTags = allRoleTags.concat(roleTags); | ||||
|     } | ||||
|     return allRoleTags; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| // ---- `triton rbac instance-role-tags`
 | ||||
| 
 | ||||
| function do_instance_role_tags(subcmd, opts, args, cb) { | ||||
|     if (opts.help) { | ||||
|         this.do_help('help', {}, [subcmd], cb); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     // Which action?
 | ||||
|     var actions = []; | ||||
|     if (opts.add) { actions.push('add'); } | ||||
|     if (opts.edit) { actions.push('edit'); } | ||||
|     if (opts.set) { actions.push('set'); } | ||||
|     if (opts['delete']) { actions.push('delete'); } | ||||
|     if (opts.delete_all) { actions.push('deleteAll'); } | ||||
|     var action; | ||||
|     if (actions.length === 0) { | ||||
|         action = 'list'; | ||||
|     } else if (actions.length > 1) { | ||||
|         return cb(new errors.UsageError( | ||||
|             'only one action option may be used at once')); | ||||
|     } else { | ||||
|         action = actions[0]; | ||||
|     } | ||||
| 
 | ||||
|     // Arg count validation.
 | ||||
|     if (args.length === 0) { | ||||
|         return cb(new errors.UsageError('INST argument is required')); | ||||
|     } else if (args.length > 1) { | ||||
|         return cb(new errors.UsageError('too many arguments')); | ||||
|     } | ||||
| 
 | ||||
|     switch (action) { | ||||
|     case 'list': | ||||
|         _listRoleTags({ | ||||
|             cli: this.top, | ||||
|             resourceId: args[0], | ||||
|             json: opts.json | ||||
|         }, cb); | ||||
|         break; | ||||
|     case 'add': | ||||
|         _addRoleTags({ | ||||
|             cli: this.top, | ||||
|             resourceId: args[0], | ||||
|             roleTags: _roleTagsFromArrayOfString(opts.add) | ||||
|         }, cb); | ||||
|         break; | ||||
|     case 'edit': | ||||
|         _editRoleTags({ | ||||
|             cli: this.top, | ||||
|             resourceId: args[0] | ||||
|         }, cb); | ||||
|         break; | ||||
|     case 'set': | ||||
|         _setRoleTags({ | ||||
|             cli: this.top, | ||||
|             resourceId: args[0], | ||||
|             roleTags: _roleTagsFromArrayOfString(opts.set), | ||||
|             yes: opts.yes | ||||
|         }, cb); | ||||
|         break; | ||||
|     case 'delete': | ||||
|         _deleteRoleTags({ | ||||
|             cli: this.top, | ||||
|             resourceId: args[0], | ||||
|             roleTags: _roleTagsFromArrayOfString(opts['delete']), | ||||
|             yes: opts.yes | ||||
|         }, cb); | ||||
|         break; | ||||
|     case 'deleteAll': | ||||
|         _deleteAllRoleTags({ | ||||
|             cli: this.top, | ||||
|             resourceId: args[0], | ||||
|             yes: opts.yes | ||||
|         }, cb); | ||||
|         break; | ||||
|     default: | ||||
|         return cb(new errors.InternalError('unknown action: ' + action)); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| do_instance_role_tags.options = [ | ||||
|     { | ||||
|         names: ['help', 'h'], | ||||
|         type: 'bool', | ||||
|         help: 'Show this help.' | ||||
|     }, | ||||
|     { | ||||
|         names: ['json', 'j'], | ||||
|         type: 'bool', | ||||
|         help: 'JSON stream output.' | ||||
|     }, | ||||
|     { | ||||
|         names: ['yes', 'y'], | ||||
|         type: 'bool', | ||||
|         help: 'Answer yes to confirmations, e.g. confirmation of deletion.' | ||||
|     }, | ||||
|     { | ||||
|         group: 'Action Options' | ||||
|     }, | ||||
|     { | ||||
|         names: ['add', 'a'], | ||||
|         type: 'arrayOfString', | ||||
|         helpArg: 'ROLE[,ROLE...]', | ||||
|         help: 'Add the given role tags. Can be specified multiple times.' | ||||
|     }, | ||||
|     { | ||||
|         names: ['set', 's'], | ||||
|         type: 'arrayOfString', | ||||
|         helpArg: 'ROLE[,ROLE...]', | ||||
|         help: 'Set role tags to the given value(s). Can be specified ' + | ||||
|             'multiple times.' | ||||
|     }, | ||||
|     { | ||||
|         names: ['edit', 'e'], | ||||
|         type: 'bool', | ||||
|         help: 'Edit role tags in your $EDITOR.' | ||||
|     }, | ||||
|     { | ||||
|         names: ['delete', 'd'], | ||||
|         type: 'arrayOfString', | ||||
|         helpArg: 'ROLE[,ROLE...]', | ||||
|         help: 'Delete the given role tags. Can be specified multiple times.' | ||||
|     }, | ||||
|     { | ||||
|         names: ['delete-all', 'D'], | ||||
|         type: 'bool', | ||||
|         help: 'Delete all role tags from the given resource.' | ||||
|     } | ||||
| ]; | ||||
| do_instance_role_tags.help = [ | ||||
|     /* BEGIN JSSTYLED */ | ||||
|     'List and manage role tags for the given instance.', | ||||
|     '', | ||||
|     'Usage:', | ||||
|     '     {{name}} instance-role-tags INST                      # list role tags', | ||||
|     '     {{name}} instance-role-tags -a ROLE[,ROLE...] INST    # add', | ||||
|     '     {{name}} instance-role-tags -s ROLE[,ROLE...] INST    # set/replace', | ||||
|     '     {{name}} instance-role-tags -e INST                   # edit in $EDITOR', | ||||
|     '     {{name}} instance-role-tags -d ROLE[,ROLE...] INST    # delete', | ||||
|     '     {{name}} instance-role-tags -D INST                   # delete all', | ||||
|     '', | ||||
|     '{{options}}', | ||||
|     'Where "ROLE" is a role tag name (see `triton rbac roles`) and INST is', | ||||
|     'an instance "id", "name" or short id.' | ||||
|     /* END JSSTYLED */ | ||||
| ].join('\n'); | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| module.exports = { | ||||
|     //do_role_tags: do_role_tags,
 | ||||
|     do_instance_role_tags: do_instance_role_tags | ||||
| }; | ||||
| @ -28,7 +28,22 @@ function RbacCLI(top) { | ||||
|         /* END JSSTYLED */ | ||||
|         helpOpts: { | ||||
|             minHelpCol: 24 /* line up with option help */ | ||||
|         } | ||||
|         }, | ||||
|         helpSubcmds: [ | ||||
|             'help', | ||||
|             'info', | ||||
|             { group: 'RBAC Resources' }, | ||||
|             'users', | ||||
|             'user', | ||||
|             'keys', | ||||
|             'key', | ||||
|             'policies', | ||||
|             'policy', | ||||
|             'roles', | ||||
|             'role', | ||||
|             { group: 'Role Tags' }, | ||||
|             'instance-role-tags' | ||||
|         ] | ||||
|     }); | ||||
| } | ||||
| util.inherits(RbacCLI, Cmdln); | ||||
| @ -40,17 +55,19 @@ RbacCLI.prototype.init = function init(opts, args, cb) { | ||||
|     Cmdln.prototype.init.apply(this, arguments); | ||||
| }; | ||||
| 
 | ||||
| RbacCLI.prototype.do_info = require('./do_info'); | ||||
| 
 | ||||
| RbacCLI.prototype.do_users = require('./do_users'); | ||||
| RbacCLI.prototype.do_user = require('./do_user'); | ||||
| 
 | ||||
| RbacCLI.prototype.do_keys = require('./do_keys'); | ||||
| RbacCLI.prototype.do_key = require('./do_key'); | ||||
| RbacCLI.prototype.do_policies = require('./do_policies'); | ||||
| RbacCLI.prototype.do_policy = require('./do_policy'); | ||||
| RbacCLI.prototype.do_roles = require('./do_roles'); | ||||
| RbacCLI.prototype.do_role = require('./do_role'); | ||||
| 
 | ||||
| RbacCLI.prototype.do_policies = require('./do_policies'); | ||||
| RbacCLI.prototype.do_policy = require('./do_policy'); | ||||
| 
 | ||||
| RbacCLI.prototype.do_keys = require('./do_keys'); | ||||
| RbacCLI.prototype.do_key = require('./do_key'); | ||||
| var doRoleTags = require('./do_role_tags'); | ||||
| //RbacCLI.prototype.do_role_tags = doRoleTags.do_role_tags;
 | ||||
| RbacCLI.prototype.do_instance_role_tags = doRoleTags.do_instance_role_tags; | ||||
| 
 | ||||
| module.exports = RbacCLI; | ||||
|  | ||||
| @ -487,13 +487,16 @@ TritonApi.prototype.getNetwork = function getNetwork(name, cb) { | ||||
|  * Get an instance by ID, exact name, or short ID, in that order. | ||||
|  * | ||||
|  * @param {String} name | ||||
|  * @param {Function} callback `function (err, inst)` | ||||
|  * @param {Function} callback `function (err, inst, res)` | ||||
|  *      Where, on success, `res` is the response object from a `GetMachine` call | ||||
|  *      if one was made. | ||||
|  */ | ||||
| TritonApi.prototype.getInstance = function getInstance(name, cb) { | ||||
|     var self = this; | ||||
|     assert.string(name, 'name'); | ||||
|     assert.func(cb, 'cb'); | ||||
| 
 | ||||
|     var res; | ||||
|     var shortId; | ||||
|     var inst; | ||||
| 
 | ||||
| @ -511,7 +514,8 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) { | ||||
|                     return next(); | ||||
|                 } | ||||
|             } | ||||
|             self.cloudapi.getMachine(uuid, function (err, inst_) { | ||||
|             self.cloudapi.getMachine(uuid, function (err, inst_, res_) { | ||||
|                 res = res_; | ||||
|                 inst = inst_; | ||||
|                 if (err && err.restCode === 'ResourceNotFound') { | ||||
|                     // The CloudApi 404 error message sucks: "VM not found".
 | ||||
| @ -579,7 +583,7 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) { | ||||
|         if (err) { | ||||
|             cb(err); | ||||
|         } else if (inst) { | ||||
|             cb(null, inst); | ||||
|             cb(null, inst, res); | ||||
|         } else { | ||||
|             cb(new errors.ResourceNotFoundError(format( | ||||
|                 'no instance with name or short id "%s" was found', name))); | ||||
| @ -588,6 +592,62 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) { | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Get instance role tags. | ||||
|  * | ||||
|  * @param {String} name  The instance id, name, or short id. | ||||
|  * @param {Function} callback `function (err, roleTags, inst)` | ||||
|  */ | ||||
| TritonApi.prototype.getInstanceRoleTags = | ||||
| function getInstanceRoleTags(name, cb) { | ||||
|     var self = this; | ||||
|     assert.string(name, 'name'); | ||||
|     assert.func(cb, 'cb'); | ||||
| 
 | ||||
|     var roleTags; | ||||
|     var inst; | ||||
| 
 | ||||
|     vasync.pipeline({arg: {}, funcs: [ | ||||
|         function getInst(ctx, next) { | ||||
|             self.getInstance(name, function (err, inst_, res) { | ||||
|                 if (err) { | ||||
|                     next(err); | ||||
|                     return; | ||||
|                 } | ||||
|                 inst = inst_; | ||||
|                 ctx.res = res; | ||||
|                 next(); | ||||
|             }); | ||||
|         }, | ||||
|         function getMachineIfNecessary(ctx, next) { | ||||
|             // Sometimes `getInstance` returns a CloudAPI `GetMachine` res on
 | ||||
|             // which there is a 'role-tag' header that we want. Sometimes not.
 | ||||
|             if (ctx.res) { | ||||
|                 next(); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             self.cloudapi.getMachine(inst.id, function (err, inst_, res) { | ||||
|                 if (err) { | ||||
|                     next(err); | ||||
|                     return; | ||||
|                 } | ||||
|                 ctx.res = res; | ||||
|                 next(); | ||||
|             }); | ||||
|         }, | ||||
|         function getRolesFromRes(ctx, next) { | ||||
|             roleTags = (ctx.res.headers['role-tag'] || '') | ||||
|                 /* JSSTYLED */ | ||||
|                 .split(/\s*,\s*/) | ||||
|                 .filter(function (r) { return r.trim(); }); | ||||
|             next(); | ||||
|         } | ||||
|     ]}, function (err) { | ||||
|         cb(err, roleTags, inst); | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Get an RBAC user by ID, login, or short ID, in that order. | ||||
| @ -715,7 +775,8 @@ TritonApi.prototype.getUser = function getUser(opts, cb) { | ||||
|                 next(); | ||||
|                 return; | ||||
|             } | ||||
|             self.cloudapi.listUserKeys({id: ctx.user.id}, function (err, keys) { | ||||
|             self.cloudapi.listUserKeys({userId: ctx.user.id}, | ||||
|                     function (err, keys) { | ||||
|                 if (err) { | ||||
|                     next(err); | ||||
|                     return; | ||||
|  | ||||
		Reference in New Issue
	
	Block a user