diff --git a/CHANGES.md b/CHANGES.md index 8b00156..258509e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,8 @@ Known issues: ## not yet released - [joyent/node-triton#226] added new `triton volume sizes` subcommand. +- [PUBAPI-1420] added support for mounting volumes in LX and SmartOS instances. + E.g., `triton instance create --volume VOLUME ...`. ## 5.3.1 diff --git a/lib/do_instance/do_create.js b/lib/do_instance/do_create.js index cc0c636..47b7e18 100644 --- a/lib/do_instance/do_create.js +++ b/lib/do_instance/do_create.js @@ -5,7 +5,7 @@ */ /* - * Copyright 2016 Joyent, Inc. + * Copyright 2017 Joyent, Inc. * * `triton instance create ...` */ @@ -20,6 +20,62 @@ var distractions = require('../distractions'); var errors = require('../errors'); var mat = require('../metadataandtags'); +function parseVolMount(volume) { + var components; + var volMode; + var volMountpoint; + var volName; + var VALID_MODES = ['ro', 'rw']; + var VALID_VOLUME_NAME_REGEXP = /^[a-zA-Z0-9][a-zA-Z0-9_\.\-]+$/; + + assert.string(volume, 'volume'); + + components = volume.split(':'); + if (components.length !== 2 && components.length !== 3) { + return new errors.UsageError('invalid volume specified, must be in ' + + 'the form ":[:]", got: "' + volume + + '"'); + } + + volName = components[0]; + volMountpoint = components[1]; + volMode = components[2]; + + // first component should be a volume name. We only check here that it + // syntactically looks like a volume name, we'll leave the upstream to + // determine if it's not actually a volume. + if (!VALID_VOLUME_NAME_REGEXP.test(volName)) { + return new errors.UsageError('invalid volume name, got: "' + volume + + '"'); + } + + // second component should be an absolute path + // NOTE: if we ever move past node 0.10, we could use path.isAbsolute(path) + if (volMountpoint.length === 0 || volMountpoint[0] !== '/') { + return new errors.UsageError('invalid volume mountpoint, must be ' + + 'absolute path, got: "' + volume + '"'); + } + if (volMountpoint.indexOf('\0') !== -1) { + return new errors.UsageError('invalid volume mountpoint, contains ' + + 'invalid characters, got: "' + volume + '"'); + } + if (volMountpoint.search(/[^\/]/) === -1) { + return new errors.UsageError('invalid volume mountpoint, must contain' + + ' at least one non-/ character, got: "' + volume + '"'); + } + + // third component is optional mode: 'ro' or 'rw' + if (components.length === 3 && VALID_MODES.indexOf(volMode) === -1) { + return new errors.UsageError('invalid volume mode, got: "' + volume + + '"'); + } + + return { + mode: volMode || 'rw', + mountpoint: volMountpoint, + name: volName + }; +} function do_create(subcmd, opts, args, cb) { if (opts.help) { @@ -132,6 +188,42 @@ function do_create(subcmd, opts, args, cb) { next(); }, + /* + * Make sure if volumes were passed, they're in the correct form. + */ + function parseVolMounts(ctx, next) { + var idx; + var validationErrs = []; + var parsedObj; + var volMounts = []; + + if (!opts.volume) { + next(); + return; + } + + for (idx = 0; idx < opts.volume.length; idx++) { + parsedObj = parseVolMount(opts.volume[idx]); + if (parsedObj instanceof Error) { + validationErrs.push(parsedObj); + } else { + // if it's not an error, it's a volume + volMounts.push(parsedObj); + } + } + + if (validationErrs.length > 0) { + next(new errors.MultiError(validationErrs)); + return; + } + + if (volMounts.length > 0) { + ctx.volMounts = volMounts; + } + + next(); + }, + /* * Determine `ctx.locality` according to what CloudAPI supports * based on `ctx.affinities` parsed earlier. @@ -274,6 +366,8 @@ function do_create(subcmd, opts, args, cb) { }, function createInst(ctx, next) { + assert.optionalArrayOfObject(ctx.volMounts, 'ctx.volMounts'); + var createOpts = { name: opts.name, image: ctx.img.id, @@ -281,6 +375,10 @@ function do_create(subcmd, opts, args, cb) { networks: ctx.nets && ctx.nets.map( function (net) { return net.id; }) }; + + if (ctx.volMounts) { + createOpts.volumes = ctx.volMounts; + } if (ctx.locality) { createOpts.locality = ctx.locality; } @@ -446,6 +544,17 @@ do_create.options = [ help: 'Enable Cloud Firewall on this instance. See ' + '' }, + { + names: ['volume', 'v'], + type: 'arrayOfString', + help: 'Mount a volume into the instance (non-KVM only). VOLMOUNT is ' + + '"[:access-mode]" where access mode is ' + + 'one of "ro" for read-only or "rw" for read-write (default). For ' + + 'example: "-v myvolume:/mnt:ro" to mount "myvolume" read-only on ' + + '/mnt in this instance.', + helpArg: 'VOLMOUNT', + hidden: true + }, { group: ''