Compare commits
1 Commits
master
...
dave.eddy-
Author | SHA1 | Date | |
---|---|---|---|
|
5cff14f261 |
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,8 +1,3 @@
|
||||
/node_modules
|
||||
/tmp
|
||||
/test/*.json
|
||||
/npm-debug.log
|
||||
/triton-*.tgz
|
||||
.DS_Store
|
||||
.git
|
||||
*.swp
|
||||
/build
|
||||
|
12
.npmignore
12
.npmignore
@ -1,12 +0,0 @@
|
||||
/.gitmodules
|
||||
/node_modules
|
||||
/tmp
|
||||
/test/*.json
|
||||
/npm-debug.log
|
||||
/deps
|
||||
/examples
|
||||
/tools
|
||||
/Makefile
|
||||
/TODO.txt
|
||||
/test
|
||||
/triton-*.tgz
|
892
CHANGES.md
892
CHANGES.md
@ -1,893 +1 @@
|
||||
# node-triton changelog
|
||||
|
||||
Known issues:
|
||||
|
||||
- `triton ssh ...` disables ssh ControlMaster to avoid issue #52.
|
||||
|
||||
## not yet released
|
||||
|
||||
(nothing)
|
||||
|
||||
## 7.0.0
|
||||
|
||||
- [Backward incompatible.] `triton image get NAME|SHORTID` will now *exclude*
|
||||
inactive images by default. Before this change inactive images (e.g. those
|
||||
with a state of "creating" or "unactivated" or "disabled") would be
|
||||
included. Use the new `-a,--all` option to include inactive images. This
|
||||
matches the behavior of `triton image list [-a,--all] ...`.
|
||||
|
||||
- [joyent/node-triton#258] `triton instance create IMAGE ...` will now exclude
|
||||
inactive images when looking for an image with the given name.
|
||||
|
||||
## 6.3.0
|
||||
|
||||
- [joyent/node-triton#259] Added basic support for use of SSH bastion hosts
|
||||
to access zones on private fabrics. If the `tritoncli.ssh.proxy` tag is set
|
||||
on an instance, `triton ssh` will look up the name or UUID of the proxy
|
||||
instance and use `ssh -o ProxyJump` to tunnel the connection to the target.
|
||||
If the `tritoncli.ssh.ip` tag is set on an instance, `triton ssh` will use
|
||||
that IP address instead of the `primaryIp` when making its connection.
|
||||
|
||||
## 6.2.0
|
||||
|
||||
- [joyent/node-triton#255, joyent/node-triton#257] Improved the interface
|
||||
and documentation of `triton network create` and `triton vlan create`. In
|
||||
particular, it is now possible to specify static routes and DNS resolvers.
|
||||
|
||||
## 6.1.2
|
||||
|
||||
- [joyent/node-triton#249] Error when creating or deleting profiles when
|
||||
using node v10.
|
||||
|
||||
## 6.1.1
|
||||
|
||||
- [TRITON-598] Fix error handling for `triton network get-default` when
|
||||
no default network is set on the account.
|
||||
|
||||
## 6.1.0
|
||||
|
||||
- [joyent/node-triton#250] Avoid an error from `triton profile list` if
|
||||
only *some* of the minimal `TRITON_` or `SDC_` envvars are defined.
|
||||
- [TRITON-401] Add `triton network` and `triton vlan` commands, for
|
||||
creating/changing/removing network fabrics and VLANs.
|
||||
- [TRITON-524] Add `triton inst get --credentials ...` option to match
|
||||
`triton inst list --credentials ...` for including generated credentials
|
||||
in instance metadata.
|
||||
- [joyent/node-triton#245] `triton profile` now generates fresh new keys during
|
||||
Docker setup and signs them with an account key, rather than copying (and
|
||||
decrypting) the account key itself. This makes using Docker simpler with keys
|
||||
in an SSH Agent.
|
||||
- [TRITON-53] x-account image clone. A user can make a copy of a shared image
|
||||
using the `triton image clone` command.
|
||||
- [TRITON-53] A shared image (i.e. when the user is on the image.acl) is no
|
||||
longer provisionable by default - you will need to explicitly add the
|
||||
--allow-shared-images cli option when calling `triton create` command to
|
||||
provision from a shared image (or clone the image then provision from the
|
||||
clone).
|
||||
- [TRITON-52] x-DC image copy. A user can copy an image that they own into
|
||||
another datacenter within the same cloud using the `triton image copy` cli
|
||||
command. Example:
|
||||
|
||||
```
|
||||
triton -p us-east-1 image cp my-custom-image us-sw-1
|
||||
```
|
||||
|
||||
## 6.0.0
|
||||
|
||||
This release containes some breaking changes with the --affinity flag to
|
||||
`triton instance create`. It also does not work with cloudapi endpoints older
|
||||
than 8.0.0 (mid 2016); for an older cloudapi endpoint, use node-triton 5.9.0.
|
||||
|
||||
- [TRITON-167, TRITON-168] update support for
|
||||
`triton instance create --affinity=...`. It now fully supports regular
|
||||
expressions, tags and globs, and works across a wider variety of situations.
|
||||
Examples:
|
||||
|
||||
```
|
||||
# regular expressions
|
||||
triton instance create --affinity='instance!=/^production-db/' ...
|
||||
|
||||
# globs
|
||||
triton instance create --affinity='instance!=production-db*' ...
|
||||
|
||||
# tags
|
||||
triton instance create --affinity='role!=db'
|
||||
```
|
||||
|
||||
See <https://apidocs.joyent.com/cloudapi/#affinity-rules> for more details
|
||||
how affinities work.
|
||||
|
||||
However:
|
||||
- Use of regular expressions requires a cloudapi version of 8.8.0 or later.
|
||||
- 'inst' as a affinity shorthand no longer works. Use 'instance' instead.
|
||||
E.g.: --affinity='instance==db1' instead of --affinity='inst==db1'
|
||||
- The shorthand --affinity=<INST> no longer works. Use
|
||||
--affinity='instance===<INST>' instead.
|
||||
|
||||
## 5.10.0
|
||||
|
||||
- [TRITON-19] add support for deletion protection on instances. An instance with
|
||||
the deletion protection flag set true cannot be destroyed until the flag is
|
||||
set false. It is exposed through
|
||||
`triton instance create --deletion-protection ...`,
|
||||
`triton instance enable-deletion-protection ...`, and
|
||||
`triton instance disable-deletion-protection ...`. This flag is only supported
|
||||
on cloudapi versions 8.7.0 or above.
|
||||
- [TRITON-59] node-triton should support nic operations
|
||||
`triton instance nic get ...`
|
||||
`triton instance nic create ...`
|
||||
`triton instance nic list ...`
|
||||
`triton instance nic delete ...`
|
||||
- [TRITON-42] node-triton should support nics when creating an instance, e.g.
|
||||
`triton instance create --nic <Network Object> IMAGE PACKAGE`
|
||||
|
||||
## 5.9.0
|
||||
|
||||
- [TRITON-190] remove support for `triton instance create --brand=bhyve ...`.
|
||||
The rest of bhyve support will remain, but selection of bhyve brand will
|
||||
happen via images or packages that are bhyve-specific.
|
||||
|
||||
## 5.8.0
|
||||
|
||||
- [TRITON-124] add node-triton support for bhyve. This adds a `triton instance
|
||||
create --brand=bhyve ...` option that can be used for zvol images that support
|
||||
it. Note that bhyve support is alpha in TritonDC -- most datacenters won't yet
|
||||
support this option.
|
||||
|
||||
## 5.7.0
|
||||
|
||||
- [TRITON-116] node-triton image sharing. Adds `triton image share` and
|
||||
`triton image unshare` commands.
|
||||
|
||||
## 5.6.1
|
||||
|
||||
- [PUBAPI-1470] volume objects should expose their creation timestamp in a
|
||||
property named "created" instead of "create_timestamp".
|
||||
|
||||
## 5.6.0
|
||||
|
||||
- [TRITON-30] Add UpdateNetworkIP to node-triton, e.g.
|
||||
`triton network ip update`
|
||||
- [TRITON-24] node-triton ListNetworkIPs has unordered results, e.g.
|
||||
`triton network ip list NETWORK`
|
||||
- [TRITON-88] node-triton "env" doesn't call its callback
|
||||
|
||||
## 5.5.0
|
||||
|
||||
- [PUBAPI-1452] Add ip subcommand to network, e.g.
|
||||
`triton network ip`.
|
||||
|
||||
## 5.4.0
|
||||
|
||||
- [joyent/node-triton#74, TOOLS-1872] Filter instance list by tag, e.g.
|
||||
`triton instance list tag.foo=bar`.
|
||||
|
||||
## 5.3.2
|
||||
|
||||
- [joyent/node-triton#187] DTraceProviderBindings errors on FreeBSD.
|
||||
- [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
|
||||
|
||||
- [joyent/node-triton#222] Fix the matching environment variable for the
|
||||
`triton -r,--role ROLE ...` option to be `TRITON_ROLE` instead of
|
||||
`MANTA_ROLE`.
|
||||
- [joyent/node-triton#201] Fix `triton -r,--role ROLE ...` option for taking up
|
||||
an RBAC role. This was introduced in v4.12.0 and was accidentally broken
|
||||
in v5.0.0.
|
||||
- [joyent/node-triton#217] `triton volume ls -l` should output a `RESOURCE`
|
||||
column.
|
||||
|
||||
## 5.3.0
|
||||
|
||||
- [joyent/node-triton#173], [joyent/node-triton#174] and
|
||||
[joyent/node-triton#175] Add support for creating and managing NFS shared
|
||||
volumes. New `triton volume` commands are available:
|
||||
|
||||
* `triton volume create` to create NFS shared volumes
|
||||
* `triton volume list` to list existing volumes
|
||||
* `triton volume get` to get information about a given volume
|
||||
* `triton volume delete` to delete one or more volumes
|
||||
|
||||
Use `triton volume --help` to get help on all of these commands.
|
||||
|
||||
Note that these commands are hidden for now. They will be made visible by
|
||||
default once the server-side support for volumes is shipped in Triton.
|
||||
|
||||
## 5.2.1
|
||||
|
||||
- [joyent/node-triton#193] Fix possible CLI crash with `triton ssh ...` if the
|
||||
instance's image doesn't have any tags.
|
||||
- [joyent/node-triton#213] commands fail unhelpfully when `cliSetupTritonApi`
|
||||
returns error (this includes e.g. supplying an incorrect key fingerprint,
|
||||
which no longer results in a cryptic stack trace and crash)
|
||||
|
||||
## 5.2.0
|
||||
|
||||
- [joyent/node-triton#197] Create triton image export command
|
||||
|
||||
## 5.1.1
|
||||
|
||||
- [joyent/node-triton#190] Fix `triton profile create|docker-setup` breakage
|
||||
with latest "17.03.\*" versions of `docker` installed.
|
||||
- [joyent/node-triton#148] Fix `triton profile edit ...` to work with an
|
||||
"EDITOR" environment variable with quotes and spaces.
|
||||
- [joyent/node-triton#183] `triton profile create` will no longer use ANSI
|
||||
codes for styling if stdout isn't a TTY.
|
||||
|
||||
## 5.1.0
|
||||
|
||||
- [joyent/node-triton#182] Add `-y, --yes` options to `triton profile create`
|
||||
and `triton profile docker-setup` to allow non-interactive setup.
|
||||
|
||||
## 5.0.0
|
||||
|
||||
- [joyent/node-triton#108] Support for passphrase-protected private keys.
|
||||
Before this work, an encrypted private SSH key (i.e. protected by a
|
||||
passphrase) would have to be loaded in an ssh-agent for the `triton`
|
||||
CLI to use it. Now `triton` will prompt for the passphrase to unlock
|
||||
the private key (in memory), if needed. For example:
|
||||
|
||||
$ triton package list
|
||||
Enter passphrase for id_rsa: <passphrase entered interactively here>
|
||||
SHORTID NAME MEMORY SWAP DISK VCPUS
|
||||
14ad9d54 g4-highcpu-128M 128M 512M 3G -
|
||||
14ae2634 g4-highcpu-256M 256M 1G 5G -
|
||||
...
|
||||
|
||||
- **BREAKING CHANGE for module usage of node-triton.**
|
||||
To implement joyent/node-triton#108, the way a TritonApi client is
|
||||
setup for use has changed from being (unrealistically) sync to async.
|
||||
|
||||
Client preparation is now a multi-step process:
|
||||
|
||||
1. create the client object;
|
||||
2. initialize it (mainly involves finding the SSH key identified by the
|
||||
`keyId`); and,
|
||||
3. optionally unlock the SSH key (if it is passphrase-protected and not in
|
||||
an ssh-agent).
|
||||
|
||||
`createClient` has changed to take a callback argument. It will create and
|
||||
init the client (steps 1 and 2) and takes an optional `unlockKeyFn` parameter
|
||||
to handle step 3. A new `mod_triton.promptPassphraseUnlockKey` export can be
|
||||
used for `unlockKeyFn` for command-line tools to handle prompting for a
|
||||
passphrase on stdin, if required. Therefore what used to be:
|
||||
|
||||
var mod_triton = require('triton');
|
||||
try {
|
||||
var client = mod_triton.createClient({ # No longer works.
|
||||
profileName: 'env'
|
||||
});
|
||||
} catch (initErr) {
|
||||
// handle err
|
||||
}
|
||||
|
||||
// use `client`
|
||||
|
||||
is now:
|
||||
|
||||
var mod_triton = require('triton');
|
||||
mod_triton.createClient({
|
||||
profileName: 'env',
|
||||
unlockKeyFn: mod_triton.promptPassphraseUnlockKey
|
||||
}, function (err, client) {
|
||||
if (err) {
|
||||
// handle err
|
||||
}
|
||||
|
||||
// use `client`
|
||||
});
|
||||
|
||||
See [the examples/ directory](examples/) for more complete examples.
|
||||
|
||||
Low-level/raw handling of the three steps above is possible as follows
|
||||
(error handling is elided):
|
||||
|
||||
var mod_bunyan = require('bunyan');
|
||||
var mod_triton = require('triton');
|
||||
|
||||
// 1. create
|
||||
var client = mod_triton.createTritonApiClient({
|
||||
log: mod_bunyan.createLogger({name: 'my-tool'}),
|
||||
config: {},
|
||||
profile: mod_triton.loadProfile('env')
|
||||
});
|
||||
|
||||
// 2. init
|
||||
client.init(function (initErr) {
|
||||
// 3. unlock key
|
||||
// See top-comment in "lib/tritonapi.js".
|
||||
});
|
||||
|
||||
- [joyent/node-triton#143] Fix duplicate output from 'triton rbac key ...'.
|
||||
|
||||
- [joyent/node-triton#157] Add `triton instance resize ...` command and
|
||||
`TritonApi.resizeInstance` method.
|
||||
|
||||
- [joyent/node-triton#129] Fix `triton reboot --wait` to properly wait. Before
|
||||
it would often return immediately, before the instance started rebooting.
|
||||
Add `--wait-timeout N` option to `triton reboot`.
|
||||
Also add `TritonApi#rebootInstance()` api method.
|
||||
|
||||
- [joyent/node-triton#166] Update sshpk to fix issue with the TLS client cert
|
||||
created by `triton profile docker-setup` so that it doesn't create a cert that
|
||||
Go's TLS library doesn't like.
|
||||
|
||||
- [joyent/node-triton#156] Providing all required profile options as command
|
||||
line flags (account, url, keyId) no longer produces an incomplete profile
|
||||
error.
|
||||
|
||||
- PUBAPI-1171/PUBAPI-1205/PUBAPI-1351 The handling of legacy `SDC_*`
|
||||
environment variables has been cleaned up. These environment
|
||||
variables are used for compatibility with the node-smartdc toolset.
|
||||
* `SDC_TESTING` is now evaluated the same way as node-smartdc. Any
|
||||
set value but the empty string is true.
|
||||
* Errors on boolean environment variables will now identify the
|
||||
variable at fault.
|
||||
* `triton env` will emit additional comments grouping variables.
|
||||
|
||||
- [joyent/node-triton#80] Add `triton network list public=true|false`
|
||||
filtering. Note that this filtering is client-side.
|
||||
|
||||
- [joyent/node-triton#146] Add `--wait` flag to `triton instance rename`.
|
||||
|
||||
- [joyent/node-triton#133] Add `triton inst fwrule list` and `triton fwrules`
|
||||
shortcuts for the existing `triton inst fwrules` and `triton fwrule list`,
|
||||
respectively.
|
||||
|
||||
- [joyent/node-triton#3] triton ssh command not aware of "ubuntu" login for
|
||||
ubuntu-certified images.
|
||||
|
||||
- [joyent/node-triton#137] Improve the handling for the getting started case
|
||||
when a user may not have envvars or a profile setup.
|
||||
|
||||
- [joyent/node-triton#158] tritonapi image cache never expires
|
||||
|
||||
- [joyent/node-triton#153] Bump restify-clients dep. Thanks, github.com/tomgco.
|
||||
|
||||
|
||||
## 4.15.0
|
||||
|
||||
- [joyent/node-triton#64] Support 'triton instance rename ...' (by
|
||||
github.com/YangYong3).
|
||||
- [trentm/node-dashdash#30, joyent/node-triton#144] Change the output used by
|
||||
Bash completion support to indicate "there are no completions for this
|
||||
argument" to cope with different sorting rules on different Bash/platforms.
|
||||
For example:
|
||||
|
||||
$ triton -p test2 package get <TAB> # before
|
||||
##-no -tritonpackage- completions-##
|
||||
|
||||
$ triton -p test2 package get <TAB> # after
|
||||
##-no-completion- -results-##
|
||||
|
||||
## 4.14.2
|
||||
|
||||
- TOOLS-1592 First workaround for a possible BadDigestError when using
|
||||
node v6.
|
||||
|
||||
## 4.14.1
|
||||
|
||||
- TOOLS-1587 'triton profile docker-setup' fails when path to 'docker' has
|
||||
spaces. This can help on Windows where Docker Toolbox installs docker.exe
|
||||
to "C:\Program Files\Docker Toolbox".
|
||||
- [#136] bash completion for `triton profile create --copy <TAB>`
|
||||
|
||||
## 4.14.0
|
||||
|
||||
- [#130] Include disabled images when using an image cache (e.g. for filling in
|
||||
image name and version details in `triton ls` output.
|
||||
|
||||
|
||||
## 4.13.0
|
||||
|
||||
- [#120] Don't fail `triton instance list` if the retrieval of *image* info
|
||||
(retrieved to get image name and version, as a bonus) fails with an
|
||||
authorization error -- in case it is an RBAC failure where a subuser can
|
||||
ListMachines, but not ListImages.
|
||||
|
||||
- [#113] *Usage* errors now some "error help", including option or command
|
||||
synopses. Some examples (the new thing is marked with `>`):
|
||||
|
||||
- Command synopses when argument errors:
|
||||
|
||||
```
|
||||
$ triton create
|
||||
triton instance create: error (Usage): incorrect number of args
|
||||
> usage: triton instance create [OPTIONS] IMAGE PACKAGE
|
||||
```
|
||||
|
||||
- Option synopsis with option errors:
|
||||
|
||||
```
|
||||
$ triton image ls --bogus
|
||||
triton image ls: error (Option): unknown option: "--bogus"
|
||||
> usage: triton image ls [ --help | -h ] [ --all | -a ] [ -H ] [ -o field1,... ]
|
||||
> [ --long | -l ] [ -s field1,... ] [ --json | -j ] ...
|
||||
```
|
||||
|
||||
- Suggested command name misspellings:
|
||||
|
||||
```
|
||||
$ triton in
|
||||
triton: error (UnknownCommand): unknown command: "in"
|
||||
> Did you mean this?
|
||||
> info
|
||||
> inst
|
||||
```
|
||||
|
||||
|
||||
## 4.12.0
|
||||
|
||||
- [#120] `triton -r,--role ROLE ...` option to take up an RBAC role(s).
|
||||
|
||||
|
||||
## 4.11.0
|
||||
|
||||
- [#112] Fix `triton completion`, broke a while back.
|
||||
- [#111] `triton env --unset,-u` option to emit environment commands to *unset*
|
||||
relevant envvars.
|
||||
- Unhide `triton env` from `triton --help` output.
|
||||
|
||||
|
||||
## 4.10.0
|
||||
|
||||
- [#82] Affinity (a.k.a. locality hints) support for instance creation, e.g.:
|
||||
|
||||
# Use same server as instance 'db0':
|
||||
triton create -a instance==db0 ...
|
||||
triton create -a db0 ... # shortcut for same thing
|
||||
|
||||
# Use different server than instance 'db0':
|
||||
triton create -a 'instance!=db0' ...
|
||||
|
||||
# *Attempt* to use same server as instance 'db0', but don't fail
|
||||
# if cannot. This is called a "non-strict" or "soft" rule.
|
||||
triton create -a instance==~db0 ...
|
||||
|
||||
# *Attempt* to use a different server than instance 'db0':
|
||||
triton create -a 'instance!=~db0' ...
|
||||
|
||||
"Affinity" here refers to providing rules for deciding on which server
|
||||
a new instance should by provisioned. Rules are in terms of existing
|
||||
instances. As a shortcut, 'inst' can be used in place of 'instance'
|
||||
above (e.g. `triton create -a 'inst!=db0' ...`).
|
||||
|
||||
## 4.9.0
|
||||
|
||||
- [#46] Initial support for `triton` helping setup and manage configuration for
|
||||
using `docker` against a Triton datacenter. Triton datacenters can provide
|
||||
a Docker Remote API endpoint against which you can run the normal `docker`
|
||||
client. See <https://www.joyent.com/triton> for and overview and
|
||||
<https://github.com/joyent/sdc-docker> for developer details.
|
||||
|
||||
- `triton profile create` will now setup the profile for running Docker,
|
||||
if the Triton datacenter provides a docker endpoint. The typical flow
|
||||
would be:
|
||||
|
||||
$ triton profile create
|
||||
name: foo
|
||||
...
|
||||
$ triton profile set foo # make foo my default profile
|
||||
$ eval "$(triton env --docker)" # set 'DOCKER_' envvars
|
||||
$ docker info
|
||||
|
||||
This profile setup for Docker requires making requests to the
|
||||
CloudAPI endpoint which can fail (e.g. if CloudAPI is down, credentials
|
||||
are invalid, etc.). You can use the `--no-docker` option to skip
|
||||
the Docker setup part of profile creation.
|
||||
|
||||
- For existing Triton CLI profiles, there is a new `triton profile
|
||||
docker-setup [PROFILE]`.
|
||||
|
||||
$ triton profile docker-setup
|
||||
$ eval "$(triton env --docker)"
|
||||
$ docker info
|
||||
|
||||
- `triton env` will now emit commands to setup `DOCKER_` envvars. That
|
||||
can be limited to just the Docker-relevant env via `triton env --docker`.
|
||||
|
||||
|
||||
## 4.8.0
|
||||
|
||||
- #103 `triton ip <inst>` to output the instance's primaryIp
|
||||
- #52 Workaround for `triton ssh ...`. In version 4.6.0, `triton ssh ...`
|
||||
interactive sessions were broken. This version reverts that change and adds
|
||||
a workaround for #52 (by disabling ControlMaster when spawning `ssh`).
|
||||
See <https://github.com/joyent/node-triton/issues/52> for details.
|
||||
- #97 `triton profile set -` to set the *last* profile as current.
|
||||
- PUBAPI-1266 Added `instance enable-firewall` and `instance disable-firewall`
|
||||
|
||||
|
||||
## 4.7.0
|
||||
|
||||
**Known issue: `triton ssh` interactive sessions are broken.
|
||||
Upgrade to v4.7.1.**
|
||||
|
||||
- #101 Bash completion for server-side data: instances, images, etc.
|
||||
Bash completion on TAB should now work for things like the following:
|
||||
`triton create <TAB to complete images> <TAB to complete packages`,
|
||||
`triton inst tag ls <TAB to complete instances>`. Cached (with a 5 minute
|
||||
TTL) completions for the following data are supported: instances, images,
|
||||
packages, networks, fwrules, account keys.
|
||||
See `triton completion --help` for adding/updating Bash completion.
|
||||
- #99 `triton profile set ...` alias for `set-current`
|
||||
|
||||
|
||||
## 4.6.0
|
||||
|
||||
**Known issue: `triton ssh` interactive sessions are broken.
|
||||
Upgrade to v4.7.1.**
|
||||
|
||||
- #98 `triton inst get ID` for a deleted instance will now emit the instance
|
||||
object and error less obtusely. This adds a new `InstanceDeleted` error code
|
||||
from `TritonApi`.
|
||||
- PUBAPI-1233 firewalls: `triton fwrule ...`
|
||||
- PUBAPI-1234 instance snapshots: `triton inst snapshot ...`
|
||||
- #52 Fix 'triton ssh ...' stdout/stderr to fully flush with node >= 4.x.
|
||||
|
||||
|
||||
## 4.5.2
|
||||
|
||||
- #95 Fix breakage of `triton image create` in v4.5.0. (By Kris Shannon.)
|
||||
- #94, #93 `triton inst create ...` is broken if "images.json" cache file
|
||||
is missing. (By Kris Shannon.)
|
||||
|
||||
|
||||
## 4.5.1
|
||||
|
||||
- #92 `triton` CLI should summarize `err.body.errors` from CloudAPI
|
||||
Per <https://github.com/joyent/eng/blob/master/docs/index.md#error-handling>,
|
||||
CloudAPI error response will sometimes have extra error details to show.
|
||||
|
||||
|
||||
## 4.5.0
|
||||
|
||||
- #88 'triton inst tag ...' for managing instance tags.
|
||||
|
||||
|
||||
## 4.4.4
|
||||
|
||||
- #90 Update sshpk and smartdc-auth to attempt to deal with multiple package
|
||||
inter-deps.
|
||||
|
||||
|
||||
## 4.4.3
|
||||
|
||||
- #86 Ensure `triton profile ls` and `triton profile set-current` work
|
||||
when there is no current profile.
|
||||
|
||||
|
||||
## 4.4.2
|
||||
|
||||
- Support `triton.createClient(...)` creation without requiring a
|
||||
`configDir`. Basically this then fallsback to a `TritonApi` with the default
|
||||
config.
|
||||
|
||||
|
||||
## 4.4.1
|
||||
|
||||
- #83, #84 Fix running `triton` on Windows.
|
||||
Note: Triton config is stored in "%APPDATA%/Joyent/Triton/..." on Windows,
|
||||
"~/.triton/..." on other platforms.
|
||||
|
||||
|
||||
## 4.4.0
|
||||
|
||||
- #78 `triton image delete IMAGE`
|
||||
- #79 Fix `triton instance get NAME` to make sure it gets the `dns_names` CNS
|
||||
field.
|
||||
- PUBAPI-1227: Note that `triton image list` doesn't include Docker images, at
|
||||
least currently.
|
||||
|
||||
|
||||
## 4.3.1
|
||||
|
||||
- #77 triton create error in v4.3.0
|
||||
|
||||
|
||||
## 4.3.0
|
||||
|
||||
**Bad release. Use >=4.3.1.**
|
||||
|
||||
- #76 `triton image create ...` and `triton image wait ...`
|
||||
- #72 want `triton image` to still return image details even when it is not in 'active' state
|
||||
|
||||
|
||||
## 4.2.0
|
||||
|
||||
- Bash completion: Add completion for *args* to `triton account update <TAB>`.
|
||||
This isn't perfect because a space is added after completion of "FIELD=",
|
||||
but hopefully is helpful.
|
||||
- #75 `triton account update ...`
|
||||
|
||||
|
||||
## 4.1.0
|
||||
|
||||
- Unhide `triton completion` so hopefully more find it and use it.
|
||||
|
||||
- node-triton#73 `triton instance list --credentials` to include
|
||||
"metadata.credentials" in instance listing.
|
||||
|
||||
- node-triton#35 More easily distinguish KVM and LX and Docker images and
|
||||
instances.
|
||||
|
||||
In PUBAPI-1161 CloudAPI (v8.0.0) started exposing IMG.type, INST.brand and
|
||||
INST.docker. One of the main issues for users is that telling KVM ubuntu
|
||||
from LX ubuntu is confusing (see also joyent/smartos-live#532).
|
||||
|
||||
tl;dr:
|
||||
|
||||
- `triton image list` default output now includes the `type` instead of
|
||||
`state`. The `state` column is still in output with `-l`, `-j`,
|
||||
`-o state`.
|
||||
- `triton instance list` default output now includes a `flags` column
|
||||
instead of `primaryIp`. The 'D' and 'K' flags identify Docker and KVM
|
||||
instances.
|
||||
- `triton instance list -l` includes the brand.
|
||||
|
||||
Default output examples showing the various cases (and the attempt to
|
||||
stay within 80 columns):
|
||||
|
||||
```bash
|
||||
$ triton imgs
|
||||
SHORTID NAME VERSION FLAGS OS TYPE PUBDATE
|
||||
1bd84670 minimal-64-lts 14.4.2 P smartos zone-dataset 2015-05-28
|
||||
b67492c2 base-64-lts 14.4.2 P smartos zone-dataset 2015-05-28
|
||||
ffe82a0a ubuntu-15.04 20151105 P linux lx-dataset 2015-11-05
|
||||
8a1dbc62 centos-6 20160111 P linux zvol 2016-01-11
|
||||
|
||||
$ triton insts
|
||||
SHORTID NAME IMG STATE FLAGS AGE
|
||||
da7c6edd cocky_noyce 3d996aaa running DF 10m
|
||||
deedeb42 ubu0 ubuntu-15.04@20151105 running - 9m
|
||||
aa9ccfda mini2 minimal-64-lts@14.4.2 running - 9m
|
||||
e8fc0b96 centi0 centos-6@20160111 running K 8m
|
||||
```
|
||||
|
||||
- Filtering instances on `docker=true`:
|
||||
|
||||
```bash
|
||||
$ triton insts docker=true
|
||||
SHORTID NAME IMG STATE FLAGS AGE
|
||||
da7c6edd cocky_noyce 3d996aaa running DF 13m
|
||||
```
|
||||
|
||||
|
||||
## 4.0.1
|
||||
|
||||
- Add `triton env -t` to be able to emit a shell environment to configure `triton` itself.
|
||||
This allows one to have the following Bash function to select a Triton profile for
|
||||
`triton` and node-smartdc tooling:
|
||||
|
||||
function triton-select { eval $(triton env $1); }
|
||||
|
||||
|
||||
## 4.0.0
|
||||
|
||||
- [backwards incompat] #66 New consistent `triton` CLI style. See [the
|
||||
issue](https://github.com/joyent/node-triton/issues/66) for discussion.
|
||||
|
||||
The major changes is that where some sub-commands used to be some
|
||||
flavour of:
|
||||
|
||||
triton things # list all the things
|
||||
triton thing ID # get a thing
|
||||
triton thing -a ID # create a new thing
|
||||
|
||||
Now commands are consistently:
|
||||
|
||||
triton thing list # list all the things
|
||||
triton thing get ID # get a thing
|
||||
triton thing create ... # create a new thing
|
||||
...
|
||||
|
||||
The most annoying incompatility is the need for "get" to
|
||||
get a thing. E.g.:
|
||||
|
||||
BEFORE AFTER
|
||||
triton img blah triton img get blah
|
||||
triton inst web0 triton inst get web0
|
||||
|
||||
For *listing* things, there is typically a shortcut with
|
||||
the old form, e.g. `triton images` is a shortcut for
|
||||
`triton image list`.
|
||||
|
||||
Currently all of the CLI *except* the experimental `triton rbac ...`
|
||||
is converted to the new consistent style.
|
||||
|
||||
- [backwards incompat] `triton whoami` was dropped. This used to be a shortcut
|
||||
for `triton account get`. It could possibly come back.
|
||||
|
||||
- *Much* improved [Bash
|
||||
completion](https://github.com/joyent/node-triton#bash-completion). See
|
||||
`triton completion -h` for notes on how to install.
|
||||
|
||||
- Add the ability to create a profile copying from an existing profile,
|
||||
via `triton profile create --copy NAME`.
|
||||
|
||||
- `triton key add` was added (<https://apidocs.joyent.com/cloudapi/#CreateKey>).
|
||||
|
||||
|
||||
## 3.6.0
|
||||
|
||||
- #67 Add `triton create --network,-N NETWORK ...` option for specifying
|
||||
networks for instance creation. "NETWORK" is a network id, name, or
|
||||
short id; or a comma-separated array of networks.
|
||||
|
||||
|
||||
## 3.5.0
|
||||
|
||||
- #67 Add `triton create --tag|-t ...` option for adding tags on instance creation.
|
||||
E.g. `triton create -n NAME -t foo=bar -t @my-tags-file.json IMAGE PACKAGE`.
|
||||
|
||||
|
||||
## 3.4.2
|
||||
|
||||
- #63 "triton images" with a filter should not be cached.
|
||||
- #65 Fix `triton profile(s)` handling when the user has no profiles yet.
|
||||
|
||||
|
||||
## 3.4.1
|
||||
|
||||
- #60 Display `vcpus` in `triton packages` output.
|
||||
- Add `-d,--data <data>` option to `triton cloudapi`.
|
||||
- Fix `triton rbac role ROLE`. Also get that command to have a stable order for the
|
||||
displayed fields.
|
||||
|
||||
|
||||
## 3.4.0
|
||||
|
||||
- Improvements for using node-triton as a module. E.g. a simple example:
|
||||
|
||||
var triton = require('triton');
|
||||
var client = triton.createClient({profileName: 'env'});
|
||||
client.listImages(function (err, imgs) {
|
||||
console.log(err);
|
||||
console.log(imgs);
|
||||
});
|
||||
|
||||
See the README and "lib/index.js" for more info.
|
||||
|
||||
|
||||
## 3.3.0
|
||||
|
||||
- #59 CLI options to `triton create` to add metadata on instance creation:
|
||||
- `triton create -m,--metadata KEY=VALUE` to add a single value
|
||||
- `triton create -m,--metadata @FILE` to add values from a JSON
|
||||
or key/value-per-line file
|
||||
- `triton create -M,--metadata-file KEY=FILE` to set a key from a file
|
||||
- `triton create --script FILE` to set the special "user-script" key
|
||||
from a file
|
||||
|
||||
|
||||
## 3.2.0
|
||||
|
||||
- #58 `triton --act-as=ACCOUNT ...` for an operator account to auth as
|
||||
themself, but operator on another account's resources. Note that operator
|
||||
accesses like this are audited on the CloudAPI server side.
|
||||
- `triton --accept-version VER` hidden top-level option for development. This
|
||||
allows calling the target cloudapi with the given value for the
|
||||
"Accept-Version" header -- which is how CloudAPI does API versioning.
|
||||
By default `triton` is coded to a particular cloudapi version range, so
|
||||
forcing a different version *could* result in breaking in the triton client
|
||||
code that handles the response. IOW, this is just a tool for developers
|
||||
of this Triton client and CloudAPI itself.
|
||||
|
||||
|
||||
## 3.1.0
|
||||
|
||||
- New (hidden for now, i.e. experimental) `triton env ...` to dump
|
||||
`eval`able shell commands for
|
||||
[node-smartdc](https://github.com/joyent/node-smartdc) environment setup for
|
||||
a given Triton CLI profile. E.g.:
|
||||
|
||||
eval $(triton env east1)
|
||||
sdc-listmachines
|
||||
|
||||
I think this should grow to support setting up Docker env as well.
|
||||
- #54 `triton rbac role-tags` for now can't be hidden (as long we have the
|
||||
need to role-tag raw resource URLs like '/my/images').
|
||||
- #54 `triton rbac apply --dev-create-keys-and-profiles` for
|
||||
experimenting/dev/testing to quickly generate and add user keys and setup
|
||||
Triton CLI profiles for all users in the RBAC config.
|
||||
- #54 RBAC support, see <https://docs.joyent.com/public-cloud/rbac> to start.
|
||||
- `triton rbac info` improvements: better help, use brackets to show
|
||||
non-default roles.
|
||||
- `triton rbac reset`
|
||||
- change `triton rbac user USER` output a little for the 'keys' (show
|
||||
the key fingerprint and name instead of the key content), 'roles',
|
||||
and 'default_roles' fields.
|
||||
- #54 *Drop* support for shortIds for `triton rbac {users,roles,policies}`
|
||||
commands. They all have unique *`name`* fields, just use that.
|
||||
- #54 `triton rbac apply` will implicitly look for a user key file at
|
||||
"./rbac-user-keys/$login.pub" if no `keys` field is provided in the
|
||||
"rbac.json" config file.
|
||||
- Change default `triton keys` and `triton rbac keys` output to be tabular.
|
||||
Otherwise it is a little obtuse to see fingerprints (which is what currently
|
||||
must be included in a profile). `triton [rbac] keys -A` can be used to
|
||||
get the old behaviour (just the key content, i.e. output appropriate
|
||||
for "~/.ssh/authorized\_keys").
|
||||
|
||||
|
||||
## 3.0.0
|
||||
|
||||
- #54 RBAC support, see <https://docs.joyent.com/public-cloud/rbac> to start.
|
||||
- [Backward incompatible.] The `triton` CLI option for the cloudapi URL has
|
||||
changed from `--url,-u` to **`--url,-U`**.
|
||||
- Add `triton --user,-u USER` CLI option and `TRITON_USER` (or `SDC_USER`)
|
||||
environment variable support for specifying the RBAC user.
|
||||
- `triton profiles` now shows the optional `user` fields.
|
||||
- A (currently experimental and hidden) `triton rbac ...` command to
|
||||
house RBAC CLI functionality.
|
||||
- `triton rbac users` to list all users.
|
||||
- `triton rbac user ...` to show, create, edit and delete users.
|
||||
- `triton rbac roles` to list all roles.
|
||||
- `triton rbac role ...` to show, create, edit and delete roles.
|
||||
- `triton rbac policies` to list all policies.
|
||||
- `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 {instance,image,network,package,}role-tags ...` to list
|
||||
and manage role tags on each of those resources.
|
||||
- `triton rbac info` will dump a summary of the full current RBAC
|
||||
state. This command is still in development.
|
||||
- `triton rbac apply` will synchronize a local RBAC config (by default it
|
||||
looks for "./rbac.json") to live RBAC state. Current the RBAC config
|
||||
file format is undocumented. See "examples/rbac-\*" for examples.
|
||||
- #55 Update of smartdc-auth/sshpk deps, removal of duplicated code for
|
||||
composing Authorization headers
|
||||
|
||||
|
||||
## 2.1.4
|
||||
|
||||
- #51: Update deps to get dtrace-provider 0.6 build fix for node v4.2.x.
|
||||
- #49: `triton create ... --firewall` to enable [Cloud
|
||||
Firewall](https://docs.joyent.com/public-cloud/network/firewall).
|
||||
|
||||
|
||||
## 2.1.3
|
||||
|
||||
- #44 'triton rm' alias for delete
|
||||
- #43 `triton profile ...` doesn't use the profile from `TRITON_PROFILE` envvar
|
||||
|
||||
## 2.1.2
|
||||
|
||||
- #41 Add compatibility with ed25519 keys in ssh-agent
|
||||
- #42 Tools using sshpk should lock in an exact version
|
||||
|
||||
## 2.1.1
|
||||
|
||||
- #40 Update smartdc-auth so that newer OpenSSH `ssh-keygen` default
|
||||
fingerprint formats for setting `keyId` work.
|
||||
- #39 Test suite: Change the test config 'destructiveAllowed' var to
|
||||
'writeActionsAllowed'.
|
||||
|
||||
|
||||
## 2.1.0
|
||||
|
||||
- Errors and exit status: Change `Usage` errors to always have an exit status
|
||||
of `2` (per common practice in at least some tooling). Add `ResourceNotFound`
|
||||
error for `triton {instance,package,image,network}` with exit status `3`.
|
||||
This can help tooling (e.g. the test suite uses this in one place). Add
|
||||
`triton help` docs on exit status.
|
||||
|
||||
- Test suite: Integration tests always require a config file
|
||||
(either `$TRITON_TEST_CONFIG` path or "test/config.json").
|
||||
Drop the other `TRITON_TEST_*` envvars.
|
||||
|
||||
|
||||
## 2.0.0
|
||||
|
||||
- Changed name to `triton` npm package, graciously given up by
|
||||
[suguru](https://www.npmjs.com/~suguru) from his
|
||||
<https://github.com/ameba-proteus/node-triton> project. <3
|
||||
The latest previous release of the triton package was 1.0.7,
|
||||
so we'll separate with a major version bump for *this* triton
|
||||
package.
|
||||
|
||||
## 1.0.0
|
||||
|
||||
Initial release as `joyent-triton` npm package.
|
||||
|
59
Makefile
59
Makefile
@ -1,5 +1,5 @@
|
||||
#
|
||||
# Copyright (c) 2015, Joyent, Inc.
|
||||
# Copyright (c) 2015, Joyent, Inc. All rights reserved.
|
||||
#
|
||||
# Makefile for node-triton
|
||||
#
|
||||
@ -8,7 +8,7 @@
|
||||
# Vars, Tools, Files, Flags
|
||||
#
|
||||
JS_FILES := bin/triton \
|
||||
$(shell find lib test -name '*.js' | grep -v '/tmp/')
|
||||
$(shell find lib -name '*.js' | grep -v '/tmp/')
|
||||
JSL_CONF_NODE = tools/jsl.node.conf
|
||||
JSL_FILES_NODE = $(JS_FILES)
|
||||
JSSTYLE_FILES = $(JS_FILES)
|
||||
@ -25,57 +25,8 @@ all:
|
||||
npm install
|
||||
|
||||
.PHONY: test
|
||||
test: test-unit test-integration
|
||||
|
||||
.PHONY: test-unit
|
||||
test-unit:
|
||||
NODE_NDEBUG= ./node_modules/.bin/tape test/unit/*.test.js
|
||||
|
||||
.PHONY: test-integration
|
||||
test-integration:
|
||||
NODE_NDEBUG= ./node_modules/.bin/tape test/integration/*.test.js
|
||||
|
||||
.PHONY: test-in-parallel
|
||||
test-in-parallel:
|
||||
ls test/unit/*.test.js test/integration/*.test.js \
|
||||
| parallel ./node_modules/.bin/tape \
|
||||
| ./node_modules/.bin/tap-summary --no-ansi --no-progress
|
||||
|
||||
.PHONY: clean
|
||||
clean::
|
||||
rm -f triton-*.tgz
|
||||
|
||||
check:: versioncheck
|
||||
|
||||
# Ensure CHANGES.md and package.json have the same version.
|
||||
.PHONY: versioncheck
|
||||
versioncheck:
|
||||
@echo version is: $(shell cat package.json | json version)
|
||||
[[ `cat package.json | json version` == `grep '^## ' CHANGES.md | head -2 | tail -1 | awk '{print $$2}'` ]]
|
||||
|
||||
.PHONY: cutarelease
|
||||
cutarelease: versioncheck
|
||||
[[ -z `git status --short` ]] # If this fails, the working dir is dirty.
|
||||
@which json 2>/dev/null 1>/dev/null && \
|
||||
ver=$(shell json -f package.json version) && \
|
||||
name=$(shell json -f package.json name) && \
|
||||
publishedVer=$(shell npm view -j $(shell json -f package.json name)@$(shell json -f package.json version) version 2>/dev/null) && \
|
||||
if [[ -n "$$publishedVer" ]]; then \
|
||||
echo "error: $$name@$$ver is already published to npm"; \
|
||||
exit 1; \
|
||||
fi && \
|
||||
echo "** Are you sure you want to tag and publish $$name@$$ver to npm?" && \
|
||||
echo "** Enter to continue, Ctrl+C to abort." && \
|
||||
read
|
||||
ver=$(shell cat package.json | json version) && \
|
||||
date=$(shell date -u "+%Y-%m-%d") && \
|
||||
git tag -a "$$ver" -m "version $$ver ($$date)" && \
|
||||
git push --tags origin && \
|
||||
npm publish
|
||||
|
||||
.PHONY: git-hooks
|
||||
git-hooks:
|
||||
ln -sf ../../tools/pre-commit.sh .git/hooks/pre-commit
|
||||
test:
|
||||
./test/runtests
|
||||
|
||||
.PHONY: dumpvar
|
||||
dumpvar:
|
||||
@ -85,6 +36,6 @@ dumpvar:
|
||||
fi
|
||||
@echo "$(VAR) is '$($(VAR))'"
|
||||
|
||||
|
||||
include ./tools/mk/Makefile.deps
|
||||
include ./tools/mk/Makefile.targ
|
||||
JSL_FLAGS += --nofilelist
|
||||
|
64
README.md
64
README.md
@ -1,38 +1,52 @@
|
||||
![logo](https://code.spearhead.cloud/Spearhead/node-spearhead/raw/branch/master/tools/sphsp.png)
|
||||
`triton` is a tool for Joyent's Triton (a.k.a. SmartDataCenter), either for on-premises installations
|
||||
of Triton or Joyent's Public Cloud (<https://my.joyent.com>,
|
||||
<http://www.joyent.com/products/compute-service>).
|
||||
|
||||
# node-spearhead
|
||||
**This project is experimental and probably broken. For now, please look
|
||||
at [node-smartdc](https://github.com/joyent/node-smartdc).**
|
||||
|
||||
This repository holds the node-spearhead CLI tool to work with the Spearhead
|
||||
Cloud. It is a fork of [node-triton](https://github.com/joyent/node-triton).
|
||||
# Installation
|
||||
|
||||
## Installation and configuration
|
||||
1. Install [node.js](http://nodejs.org/).
|
||||
2. `npm install -g git://github.com/joyent/node-triton`
|
||||
|
||||
### Get a Spearhead Cloud account
|
||||
Verify that installed and is on your PATH:
|
||||
|
||||
Create an account on the Spearhead Cloud and upload your SSH key. You can create an account
|
||||
[here](https://spearhead.cloud/).
|
||||
$ triton --version
|
||||
Triton client 1.0.0
|
||||
|
||||
Before you can used the CLI you'll need a Joyent account, an SSH key uploaded
|
||||
and `triton` configured with those account details.
|
||||
|
||||
# Setup
|
||||
|
||||
TODO
|
||||
|
||||
# Getting Started
|
||||
|
||||
TODO
|
||||
|
||||
|
||||
### Data-centers
|
||||
# node-triton differences with node-smartdc
|
||||
|
||||
The list of available Spearhead Cloud data-centers is available
|
||||
[here](https://spearhead.cloud/datacenters).
|
||||
- There is a single `sdc` command instead of a number of `sdc-FOO` commands.
|
||||
- The `SDC_USER` envvar is accepted in preference to `SDC_ACCOUNT`.
|
||||
|
||||
|
||||
### Installation
|
||||
# cloudapi2.js differences with node-smartdc/lib/cloudapi.js
|
||||
|
||||
Install [node.js](http://nodejs.org/), then:
|
||||
The old node-smartdc module included an lib for talking directly to the SDC
|
||||
Cloud API (node-smartdc/lib/cloudapi.js). Part of this module (node-sdc) is a
|
||||
re-write of the Cloud API lib with some backward incompatibilities. The
|
||||
differences and backward incompatibilities are discussed here.
|
||||
|
||||
npm install -g spearhead
|
||||
- Currently no caching options in cloudapi2.js (this should be re-added in
|
||||
some form). The `noCache` option to many of the cloudapi.js methods will not
|
||||
be re-added, it was a wart.
|
||||
- The leading `account` option to each cloudapi.js method has been dropped. It
|
||||
was redundant for the constructor `account` option.
|
||||
- "account" is now "user" in the CloudAPI constructor.
|
||||
- All (all? at least at the time of this writing) methods in cloudapi2.js have
|
||||
a signature of `function (options, callback)` instead of the sometimes
|
||||
haphazard extra arguments.
|
||||
|
||||
Verify that it is installed and on your PATH:
|
||||
$ spearhead --version
|
||||
Spearhead CLI 6.1.4
|
||||
https://code.spearhead.cloud/Spearhead/node-spearhead
|
||||
|
||||
Now you ca use `spearhead` to interact with our Public Cloud. More details
|
||||
about installation and configuration are available
|
||||
[here](https://docs.spearhead.cloud).
|
||||
|
||||
## License
|
||||
MPL 2.0
|
||||
|
94
TODO.txt
94
TODO.txt
@ -1,60 +1,68 @@
|
||||
- The 'shortcut' commands use `handlerFromSubcmd(...).dispatch`. That
|
||||
doesn't run the subcmd class's `.init()` method. node-cmdln should provide
|
||||
a way to do this. ... basically want to call the *main()* but with preparsed
|
||||
options. Perhaps the init/fini should move into dispatch?
|
||||
bash completion
|
||||
|
||||
triton create affinity support for tag matching, globs, regex
|
||||
triton delete VM|IMAGE # substring matching? too dangerous
|
||||
triton delete --vm VM
|
||||
triton delete --image IMAGE
|
||||
|
||||
note in README that full UUIDs is much faster in the API
|
||||
"shortid" instead of full UUID "id" in default output, and then allow lookup
|
||||
by that shortid. Really nice for 80 columns.
|
||||
|
||||
*type*: cloudapi changes to clarify: LX, docker, smartos, kvm instances
|
||||
image "name@version" in 'triton insts' table. Optionally?
|
||||
|
||||
# maybe next
|
||||
# DONE
|
||||
|
||||
triton
|
||||
triton -v # bunyan trace logging
|
||||
triton images # list images
|
||||
triton image ID|NAME # get image
|
||||
triton packages # list packages
|
||||
triton package ID|NAME
|
||||
|
||||
triton instances|insts # list machines
|
||||
triton instance|inst ID|NAME # get machine
|
||||
|
||||
triton create # triton create-instance
|
||||
|
||||
triton cloudapi ...
|
||||
triton ssh ...
|
||||
|
||||
triton info
|
||||
|
||||
|
||||
|
||||
# maybe today
|
||||
|
||||
PUBAPI-1117 triton create -c|--count N
|
||||
|
||||
Rate limiting. Testing with non-op accounts. I suspect PUBAPI-1117 and other
|
||||
usage will lead to rate limiting errors from cloudapi. `triton` should
|
||||
(a) retry reasonably on those error codes and (b) proactively control rate
|
||||
of cloudapi requests (tunable).
|
||||
|
||||
triton images
|
||||
Drop 'state' in default columns. Add type to be able to see lx or not
|
||||
for 'linux' ones. That might hit that stupid naming problem.
|
||||
|
||||
|
||||
# profiles
|
||||
|
||||
triton profile # list all profiles
|
||||
triton profile NAME # show NAME profile
|
||||
triton profile -a NAME # sets it as active
|
||||
triton profile -n|--new # ???
|
||||
|
||||
For today: only the implicit 'env' profile.
|
||||
|
||||
|
||||
|
||||
# config
|
||||
|
||||
~/.triton/
|
||||
config.json
|
||||
{"currProfile": "east3b"}
|
||||
east3b/ # profile
|
||||
PROFILE2/
|
||||
...
|
||||
|
||||
|
||||
|
||||
# another day
|
||||
|
||||
triton config get|set|list # see 'npm config'
|
||||
|
||||
triton --shell # or whatever, repl
|
||||
|
||||
$ triton shell
|
||||
$profile> cd inst
|
||||
$profile inst> ls
|
||||
...
|
||||
$profile inst> cd vm0
|
||||
$profile inst/vm0> get
|
||||
...
|
||||
$profile inst/vm0> cd snapshot
|
||||
$profile inst/vm0/snapshot> ls
|
||||
...
|
||||
|
||||
extensible triton commands:
|
||||
~/.triton/plugins.d/$plugin.json
|
||||
|
||||
Would be nice to not have to read/parse all these files for every run,
|
||||
i.e. lazily. Is that a problem for `triton` showing commands list?
|
||||
|
||||
How do plugins in other node projects work? Is there an npm special thing
|
||||
that would work?
|
||||
|
||||
Should plugins just be commands? Perhaps for starters, but don't hardcode
|
||||
that.
|
||||
|
||||
$ cat build.json
|
||||
{
|
||||
"cmd": "build",
|
||||
"desc": "Build a Triton image from a Tritonfile"
|
||||
"require": "triton-plugin-build"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require('../lib/cli').main();
|
19
bin/triton
Executable file
19
bin/triton
Executable file
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* Copyright (c) 2015 Joyent Inc. All rights reserved.
|
||||
*
|
||||
* triton command
|
||||
*/
|
||||
|
||||
var p = console.log;
|
||||
var cmdln = require('cmdln');
|
||||
var CLI = require('../lib/cli');
|
||||
|
||||
if (require.main === module) {
|
||||
var cli = new CLI();
|
||||
cmdln.main(cli, {
|
||||
argv: process.argv,
|
||||
showCode: true,
|
||||
showNoCommandErr: false
|
||||
});
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"cacheDir": "cache"
|
||||
"defaultProfile": "env"
|
||||
}
|
||||
|
@ -1,189 +0,0 @@
|
||||
# Functions for Bash completion of some 'triton' option/arg types.
|
||||
|
||||
function complete_tritonprofile {
|
||||
local word="$1"
|
||||
local candidates
|
||||
candidates=$(ls -1 ~/.triton/profiles.d/*.json 2>/dev/null \
|
||||
| sed -E 's/^.*\/([^\/]+)\.json$/\1/')
|
||||
compgen $compgen_opts -W "$candidates" -- "$word"
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Get completions for a given type of Triton (server-side) data.
|
||||
#
|
||||
# Usage:
|
||||
# _complete_tritondata $type # e.g. _complete_tritondata images
|
||||
#
|
||||
# The easiest/slowest thing to do to complete images would be to just call:
|
||||
# triton [profile-related-args] images -Ho name
|
||||
# or similar. Too slow.
|
||||
#
|
||||
# The next easiest would be this:
|
||||
# candidates=$(TRITON_COMPLETE=$type $COMP_LINE)
|
||||
# where `triton` is setup to specially just handle completions if
|
||||
# `TRITON_COMPLETE` is set. That special handling writes out a cache file to
|
||||
# avoid hitting the server every time. This is still too slow because the
|
||||
# node.js startup time for `triton` is too slow (around 1s on my laptop).
|
||||
#
|
||||
# The next choice is to (a) use the special `TRITON_COMPLETE` handling to
|
||||
# fetch data from the server and write out a cache file, but (b) attempt to
|
||||
# find and use that cache file without calling node.js code. The win is
|
||||
# (at least in my usage) faster response time to a <TAB>. The cost is
|
||||
# reproducing (imperfectly) in Bash the logic for determining the Triton profile
|
||||
# info to find the cache.
|
||||
#
|
||||
function _complete_tritondata {
|
||||
local type=$1
|
||||
|
||||
# First, find the Triton CLI profile.
|
||||
local profile
|
||||
profile=$(echo "$COMP_LINE" | grep -- '\s\+-p\s*\w\+\s\+' | sed -E 's/.* +-p *([^ ]+) +.*/\1/')
|
||||
if [[ -z "$profile" ]]; then
|
||||
profile=$TRITON_PROFILE
|
||||
fi
|
||||
if [[ -z "$profile" ]]; then
|
||||
profile=$(grep '"profile":' ~/.triton/config.json | cut -d'"' -f4)
|
||||
fi
|
||||
if [[ -z "$profile" ]]; then
|
||||
profile=env
|
||||
fi
|
||||
trace " profile: $profile"
|
||||
|
||||
# Then, determine the account and url that go into the cache dir.
|
||||
# TODO: include -a/-U options that change from profile values
|
||||
# TODO: subuser support
|
||||
local url
|
||||
local account
|
||||
local profileFile
|
||||
profileFile=$HOME/.triton/profiles.d/$profile.json
|
||||
if [[ "$profile" == "env" ]]; then
|
||||
url=$TRITON_URL
|
||||
if [[ -z "$url" ]]; then
|
||||
url=$SDC_URL
|
||||
fi
|
||||
account=$TRITON_ACCOUNT
|
||||
if [[ -z "$account" ]]; then
|
||||
account=$SDC_ACCOUNT
|
||||
fi
|
||||
elif [[ -f $profileFile ]]; then
|
||||
url=$(grep '"url":' $profileFile | cut -d'"' -f4)
|
||||
account=$(grep '"account":' $profileFile | cut -d'"' -f4)
|
||||
fi
|
||||
trace " url: $url"
|
||||
trace " account: $account"
|
||||
|
||||
# Mimic node-triton/lib/common.js#profileSlug
|
||||
local profileSlug
|
||||
profileSlug="$(echo "$account" | sed -E 's/@/_/g')@$(echo "$url" | sed -E 's#^https?://##')"
|
||||
profileSlug="$(echo "$profileSlug" | sed -E 's/[^a-zA-Z0-9_@-]/_/g')"
|
||||
|
||||
local cacheFile
|
||||
cacheFile="$HOME/.triton/cache/$profileSlug/$type.completions"
|
||||
trace " cacheFile: $cacheFile"
|
||||
|
||||
# If we have a cache file, remove it and regenerate if it is >5 minutes old.
|
||||
#
|
||||
# Dev Note: This 5min TTL should match what `lib/cli.js#_emitCompletions()`
|
||||
# is using.
|
||||
local candidates
|
||||
if [[ ! -f "$cacheFile" ]]; then
|
||||
candidates=$(TRITON_COMPLETE=$type $COMP_LINE)
|
||||
else
|
||||
local mtime
|
||||
mtime=$(stat -r "$cacheFile" | awk '{print $10}')
|
||||
local ttl=300 # 5 minutes in seconds
|
||||
local age
|
||||
age=$(echo "$(date +%s) - $mtime" | bc)
|
||||
if [[ $age -gt $ttl ]]; then
|
||||
# Out of date. Regenerate the cache file.
|
||||
trace " cacheFile out-of-date (mtime=$mtime, age=$age, ttl=$ttl)"
|
||||
rm "$cacheFile"
|
||||
candidates=$(TRITON_COMPLETE=$type $COMP_LINE)
|
||||
else
|
||||
trace " cacheFile is in-date (mtime=$mtime, age=$age, ttl=$ttl)"
|
||||
candidates=$(cat "$cacheFile")
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$candidates"
|
||||
}
|
||||
|
||||
function complete_tritonpackage {
|
||||
local word="$1"
|
||||
candidates=$(_complete_tritondata packages)
|
||||
compgen $compgen_opts -W "$candidates" -- "$word"
|
||||
}
|
||||
|
||||
function complete_tritonimage {
|
||||
local word="$1"
|
||||
candidates=$(_complete_tritondata images)
|
||||
compgen $compgen_opts -W "$candidates" -- "$word"
|
||||
}
|
||||
|
||||
function complete_tritoninstance {
|
||||
local word="$1"
|
||||
candidates=$(_complete_tritondata instances)
|
||||
compgen $compgen_opts -W "$candidates" -- "$word"
|
||||
}
|
||||
|
||||
function complete_tritonnetwork {
|
||||
local word="$1"
|
||||
candidates=$(_complete_tritondata networks)
|
||||
compgen $compgen_opts -W "$candidates" -- "$word"
|
||||
}
|
||||
|
||||
function complete_tritonvolume {
|
||||
local word="$1"
|
||||
candidates=$(_complete_tritondata volumes)
|
||||
compgen $compgen_opts -W "$candidates" -- "$word"
|
||||
}
|
||||
|
||||
function complete_tritonfwrule {
|
||||
local word="$1"
|
||||
candidates=$(_complete_tritondata fwrules)
|
||||
compgen $compgen_opts -W "$candidates" -- "$word"
|
||||
}
|
||||
|
||||
function complete_tritonkey {
|
||||
local word="$1"
|
||||
candidates=$(_complete_tritondata keys)
|
||||
compgen $compgen_opts -W "$candidates" -- "$word"
|
||||
}
|
||||
|
||||
function complete_tritonaffinityrule {
|
||||
local word="$1"
|
||||
candidates=$(_complete_tritondata affinityrules)
|
||||
|
||||
# Triton affinity rules typically have '=' in them, e.g.:
|
||||
# triton create -a inst==db0 ...
|
||||
# This means we run afoul of the '=' in COMP_WORDBREAKS which results in
|
||||
# triton create -a inst=<TAB>
|
||||
# leading to:
|
||||
# triton create -a inst=inst==
|
||||
# The answer is to strip off at the last '=' in the returned completions.
|
||||
if [[ "$word" == *"="* ]]; then
|
||||
local uptolastequal
|
||||
uptolastequal="${word%=*}="
|
||||
compgen $compgen_opts -W "$candidates" -- "$word" \
|
||||
| cut -c$(( ${#uptolastequal} + 1 ))-
|
||||
else
|
||||
compgen $compgen_opts -W "$candidates" -- "$word"
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
|
||||
function complete_tritonupdateaccountfield {
|
||||
local word="$1"
|
||||
local candidates
|
||||
candidates="{{UPDATE_ACCOUNT_FIELDS}}"
|
||||
compgen $compgen_opts -W "$candidates" -- "$word"
|
||||
}
|
||||
|
||||
function complete_tritonupdatefwrulefield {
|
||||
local word="$1"
|
||||
local candidates
|
||||
candidates="{{UPDATE_FWRULE_FIELDS}}"
|
||||
compgen $compgen_opts -W "$candidates" -- "$word"
|
||||
}
|
40
examples/example-get-account.js
Executable file
40
examples/example-get-account.js
Executable file
@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Example using cloudapi2.js to call cloudapi's GetAccount endpoint.
|
||||
*
|
||||
* Usage:
|
||||
* ./example-get-account.js | bunyan
|
||||
*/
|
||||
|
||||
var p = console.log;
|
||||
var auth = require('smartdc-auth');
|
||||
var bunyan = require('bunyan');
|
||||
var cloudapi = require('../lib/cloudapi2');
|
||||
|
||||
var log = bunyan.createLogger({
|
||||
name: 'test-get-account',
|
||||
level: 'trace'
|
||||
})
|
||||
|
||||
var USER = process.env.SDC_USER || process.env.SDC_ACCOUNT || 'bob';
|
||||
var KEY_ID = process.env.SDC_KEY_ID || 'b4:f0:b4:6c:18:3b:44:63:b4:4e:58:22:74:43:d4:bc';
|
||||
|
||||
var sign = auth.cliSigner({
|
||||
keyId: KEY_ID,
|
||||
user: USER,
|
||||
log: log
|
||||
});
|
||||
var client = cloudapi.createClient({
|
||||
url: 'https://us-sw-1.api.joyent.com',
|
||||
user: USER,
|
||||
version: '*',
|
||||
sign: sign,
|
||||
agent: false, // don't want KeepAlive
|
||||
log: log
|
||||
});
|
||||
|
||||
log.info('start')
|
||||
client.getAccount(function (err, account) {
|
||||
p('getAccount: err', err)
|
||||
p('getAccount: account', account)
|
||||
});
|
@ -1,235 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012 Mark Cavage, Inc. All rights reserved.
|
||||
* Copyright (c) 2015, Joyent, Inc.
|
||||
*/
|
||||
|
||||
/*
|
||||
* TODO: this should be a separate module. Both node-triton and
|
||||
* node-docker-registry-client are using (slightly different versions of) this.
|
||||
*
|
||||
* Adapted from
|
||||
* <github.com/mcavage/node-restify/blob/master/lib/clients/string_client.js>
|
||||
* now at <https://github.com/restify/clients/blob/master/lib/StringClient.js>
|
||||
*
|
||||
* This subclasses the Restify StringClient to add the following features:
|
||||
*
|
||||
* 1. Extend the callback from
|
||||
* callback(err, req, res, <JSON-parsed-body>);
|
||||
* to:
|
||||
* callback(err, req, res, <JSON-parsed-body>, <raw-body (Buffer)>);
|
||||
* This allows one to work on the raw body for special case handling, if
|
||||
* wanted. I'm not sure I'd propose this for restify core because it
|
||||
* shouldn't add features that make it harder to go all streaming.
|
||||
*
|
||||
* 2. In restify.JsonClient, if the body is not parseable JSON, it log.trace's
|
||||
* an error, and returns `{}` (see mcavage/restify#388). I don't particularly
|
||||
* like that because it is ambiguous (and also disallows returning a JSON
|
||||
* body that is false-y: `false`, `0`, `null`).
|
||||
*
|
||||
* Instead this client will do the following:
|
||||
* (a) If the response is an error status (>=400), then return `undefined`
|
||||
* for the body. This allows the caller to know if the body was parsed
|
||||
* because `undefined` is not representable in JSON.
|
||||
* (b) If the response is a success (<400), then return an
|
||||
* InvalidContentError restify error.
|
||||
*
|
||||
* (TODO: I'd support this for restify code, but it *is* backward
|
||||
* incompatible.)
|
||||
*
|
||||
* 3. `.write()` doesn't default a null `body` to `{}`.
|
||||
* This change isn't because I came across the need for it, but because that
|
||||
* just seems wrong.
|
||||
*
|
||||
* 4. Doesn't set `res.body` which restify's StringClient.parse seems to do
|
||||
* ... as an accident of history I'm guessing?
|
||||
*/
|
||||
|
||||
/* jsl:ignore */
|
||||
'use strict';
|
||||
/* jsl:end */
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var crypto = require('crypto');
|
||||
var strsplit = require('strsplit').strsplit;
|
||||
var util = require('util');
|
||||
var zlib = require('zlib');
|
||||
|
||||
var errors = require('restify-errors');
|
||||
var codeToHttpError = errors.codeToHttpError;
|
||||
var RestError = errors.RestError;
|
||||
var StringClient = require('restify-clients').StringClient;
|
||||
|
||||
|
||||
// --- API
|
||||
|
||||
function SaferJsonClient(options) {
|
||||
assert.object(options, 'options');
|
||||
|
||||
options.accept = 'application/json';
|
||||
options.name = options.name || 'SaferJsonClient';
|
||||
options.contentType = 'application/json';
|
||||
|
||||
StringClient.call(this, options);
|
||||
|
||||
this._super = StringClient.prototype;
|
||||
}
|
||||
util.inherits(SaferJsonClient, StringClient);
|
||||
|
||||
|
||||
SaferJsonClient.prototype.write = function write(options, body, callback) {
|
||||
assert.object(body, 'body');
|
||||
|
||||
// This is change #3.
|
||||
var resBody = JSON.stringify(body);
|
||||
return (this._super.write.call(this, options, resBody, callback));
|
||||
};
|
||||
|
||||
|
||||
SaferJsonClient.prototype.parse = function parse(req, callback) {
|
||||
function parseResponse(err, res) {
|
||||
var chunks = []; // gunzipped response chunks (Buffer objects)
|
||||
var len = 0; // accumulated count of chunk lengths
|
||||
var contentMd5;
|
||||
var contentMd5Hash;
|
||||
var gz;
|
||||
var resErr = err;
|
||||
|
||||
function finish() {
|
||||
var body = Buffer.concat(chunks, len);
|
||||
|
||||
/*
|
||||
* Save the original response's body in case this is the best error
|
||||
* message we can output to the user. The responsibility to use this
|
||||
* property is left to the user of this custom JSON client.
|
||||
*
|
||||
* See lib/cli.js and joyent/node-triton#30 for a concrete use case.
|
||||
*/
|
||||
if (resErr) {
|
||||
resErr.originalBody = body;
|
||||
}
|
||||
|
||||
if (res.log.trace()) {
|
||||
res.log.trace({body: body.toString(), len: len},
|
||||
'body received');
|
||||
}
|
||||
|
||||
// Content-Length check
|
||||
var contentLength = Number(res.headers['content-length']);
|
||||
if (req.method !== 'HEAD' &&
|
||||
!isNaN(contentLength) && len !== contentLength)
|
||||
{
|
||||
resErr = new errors.InvalidContentError(util.format(
|
||||
'Incomplete content: Content-Length:%s but got %s bytes',
|
||||
contentLength, len));
|
||||
callback(resErr, req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// Content-MD5 check.
|
||||
if (contentMd5Hash &&
|
||||
contentMd5 !== contentMd5Hash.digest('base64'))
|
||||
{
|
||||
resErr = new errors.BadDigestError('Content-MD5');
|
||||
callback(resErr, req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the body as JSON, if we can.
|
||||
// Note: This regex-based trim works on a buffer. `trim()` doesn't.
|
||||
var obj;
|
||||
if (len && !/^\s*$/.test(body)) { // Skip all-whitespace body.
|
||||
try {
|
||||
obj = JSON.parse(body);
|
||||
} catch (jsonErr) {
|
||||
res.log.trace(jsonErr, 'Invalid JSON in response');
|
||||
if (!resErr) {
|
||||
// TODO: Does this mask other error statuses?
|
||||
resErr = new errors.InvalidContentError(
|
||||
'Invalid JSON in response');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Special error handling.
|
||||
if (res && res.statusCode >= 400) {
|
||||
// Upcast error to a RestError (if we can)
|
||||
// Be nice and handle errors like
|
||||
// { error: { code: '', message: '' } }
|
||||
// in addition to { code: '', message: '' }.
|
||||
if (obj && (obj.code || (obj.error && obj.error.code))) {
|
||||
var _c = obj.code ||
|
||||
(obj.error ? obj.error.code : '') ||
|
||||
'';
|
||||
var _m = obj.message ||
|
||||
(obj.error ? obj.error.message : '') ||
|
||||
'';
|
||||
|
||||
resErr = new RestError({
|
||||
message: _m,
|
||||
restCode: _c,
|
||||
statusCode: res.statusCode
|
||||
});
|
||||
resErr.name = resErr.restCode;
|
||||
|
||||
if (!/Error$/.test(resErr.name)) {
|
||||
resErr.name += 'Error';
|
||||
}
|
||||
} else if (!resErr) {
|
||||
resErr = codeToHttpError(res.statusCode,
|
||||
obj.message || '', body);
|
||||
}
|
||||
}
|
||||
if (resErr) {
|
||||
resErr.body = obj;
|
||||
}
|
||||
|
||||
callback(resErr, req, res, obj, body);
|
||||
}
|
||||
|
||||
|
||||
if (!res) {
|
||||
// Early out if we didn't even get a response.
|
||||
callback(resErr, req);
|
||||
return;
|
||||
}
|
||||
|
||||
// Content-MD5 setup.
|
||||
contentMd5 = res.headers['content-md5'];
|
||||
if (contentMd5 && req.method !== 'HEAD' && res.statusCode !== 206) {
|
||||
contentMd5Hash = crypto.createHash('md5');
|
||||
}
|
||||
|
||||
if (res.headers['content-encoding'] === 'gzip') {
|
||||
gz = zlib.createGunzip();
|
||||
gz.on('data', function (chunk) {
|
||||
chunks.push(chunk);
|
||||
len += chunk.length;
|
||||
});
|
||||
gz.once('end', finish);
|
||||
res.once('end', gz.end.bind(gz));
|
||||
} else {
|
||||
res.once('end', finish);
|
||||
}
|
||||
|
||||
res.on('data', function onData(chunk) {
|
||||
if (contentMd5Hash) {
|
||||
contentMd5Hash.update(chunk.toString('utf8'), 'binary');
|
||||
}
|
||||
|
||||
if (gz) {
|
||||
gz.write(chunk);
|
||||
} else {
|
||||
chunks.push(chunk);
|
||||
len += chunk.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (parseResponse);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// --- Exports
|
||||
|
||||
module.exports = SaferJsonClient;
|
@ -1,27 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* A stub for a `bunyan.createLogger()` that does no logging.
|
||||
*/
|
||||
function BunyanNoopLogger() {}
|
||||
BunyanNoopLogger.prototype.trace = function () {};
|
||||
BunyanNoopLogger.prototype.debug = function () {};
|
||||
BunyanNoopLogger.prototype.info = function () {};
|
||||
BunyanNoopLogger.prototype.warn = function () {};
|
||||
BunyanNoopLogger.prototype.error = function () {};
|
||||
BunyanNoopLogger.prototype.fatal = function () {};
|
||||
BunyanNoopLogger.prototype.child = function () { return this; };
|
||||
BunyanNoopLogger.prototype.end = function () {};
|
||||
|
||||
|
||||
module.exports = {
|
||||
BunyanNoopLogger: BunyanNoopLogger
|
||||
};
|
784
lib/cli.js
784
lib/cli.js
@ -1,11 +1,5 @@
|
||||
/*
|
||||
* 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 (c) 2017, Joyent, Inc.
|
||||
* Copyright (c) 2015 Joyent Inc. All rights reserved.
|
||||
*
|
||||
* The `triton` CLI class.
|
||||
*/
|
||||
@ -18,159 +12,27 @@ var child_process = require('child_process'),
|
||||
var cmdln = require('cmdln'),
|
||||
Cmdln = cmdln.Cmdln;
|
||||
var fs = require('fs');
|
||||
var mkdirp = require('mkdirp');
|
||||
var util = require('util'),
|
||||
format = util.format;
|
||||
var path = require('path');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('./common');
|
||||
var constants = require('./constants');
|
||||
var mod_config = require('./config');
|
||||
var errors = require('./errors');
|
||||
var lib_tritonapi = require('./tritonapi');
|
||||
var Triton = require('./triton');
|
||||
|
||||
|
||||
|
||||
//---- globals
|
||||
|
||||
var packageJson = require('../package.json');
|
||||
var p = console.log;
|
||||
|
||||
|
||||
var OPTIONS = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Print this help and exit.'
|
||||
},
|
||||
{
|
||||
name: 'version',
|
||||
type: 'bool',
|
||||
help: 'Print version and exit.'
|
||||
},
|
||||
{
|
||||
names: ['verbose', 'v'],
|
||||
type: 'bool',
|
||||
help: 'Verbose/debug output.'
|
||||
},
|
||||
|
||||
{
|
||||
names: ['profile', 'p'],
|
||||
type: 'string',
|
||||
completionType: 'tritonprofile',
|
||||
env: 'SC_PROFILE',
|
||||
helpArg: 'NAME',
|
||||
help: 'Spearhead Cloud client profile to use.'
|
||||
},
|
||||
|
||||
{
|
||||
group: 'CloudAPI Options'
|
||||
},
|
||||
|
||||
/*
|
||||
* Environment variable integration.
|
||||
*
|
||||
* While dashdash supports integrated envvar parsing with options
|
||||
* we don't use that with `triton` because (a) we want to apply *option*
|
||||
* usage (but not envvars) to profiles other than the default 'env'
|
||||
* profile, and (b) we want to support `TRITON_*` *and* `SDC_*` envvars,
|
||||
* which dashdash doesn't support.
|
||||
*
|
||||
* See <https://github.com/joyent/node-triton/issues/28> for some details.
|
||||
*/
|
||||
{
|
||||
names: ['account', 'a'],
|
||||
type: 'string',
|
||||
help: 'Account (login name). Environment: SC_ACCOUNT=ACCOUNT ',
|
||||
helpArg: 'ACCOUNT'
|
||||
},
|
||||
{
|
||||
names: ['act-as'],
|
||||
type: 'string',
|
||||
help: 'Masquerade as the given account login name. This can only ' +
|
||||
'succeed for operator accounts. Note that accesses like these ' +
|
||||
'are audited on the CloudAPI server side.',
|
||||
helpArg: 'ACCOUNT',
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
names: ['user', 'u'],
|
||||
type: 'string',
|
||||
help: 'RBAC user (login name). Environment: SC_USER=USER',
|
||||
helpArg: 'USER'
|
||||
},
|
||||
{
|
||||
names: ['role', 'r'],
|
||||
type: 'arrayOfCommaSepString',
|
||||
env: 'SC_ROLE',
|
||||
help: 'Assume an RBAC role. Use multiple times or once with a list',
|
||||
helpArg: 'ROLE,...'
|
||||
},
|
||||
{
|
||||
names: ['keyId', 'k'],
|
||||
type: 'string',
|
||||
help: 'SSH key fingerprint. Environment: SC_KEY_ID=FINGERPRINT.',
|
||||
helpArg: 'FP'
|
||||
},
|
||||
{
|
||||
names: ['url', 'U'],
|
||||
type: 'string',
|
||||
help: 'Spearhead Cloud Datacenter URL. Environment: SC_URL=URL.',
|
||||
helpArg: 'URL'
|
||||
},
|
||||
{
|
||||
names: ['J'],
|
||||
type: 'string',
|
||||
hidden: true,
|
||||
help: 'Spearhead Cloud (SC) datacenter name. This is ' +
|
||||
'a shortcut to the "https://$dc.api.spearhead.cloud" ' +
|
||||
'cloudapi URL.'
|
||||
},
|
||||
{
|
||||
names: ['insecure', 'i'],
|
||||
type: 'bool',
|
||||
help: 'Do not validate the SSL certificate. Environment: ' +
|
||||
'SC_TLS_INSECURE=1 (or the deprecated SC_TESTING=1).',
|
||||
'default': false
|
||||
},
|
||||
{
|
||||
names: ['accept-version'],
|
||||
type: 'string',
|
||||
helpArg: 'VER',
|
||||
help: 'A cloudapi API version, or semver range, to attempt to use. ' +
|
||||
'This is passed in the "Accept-Version" header. ' +
|
||||
'See `spearhead cloudapi /--ping` to list supported versions. ' +
|
||||
'The default is "' + lib_tritonapi.CLOUDAPI_ACCEPT_VERSION + '". ' +
|
||||
'*This is intended for development use only. It could cause ' +
|
||||
'`spearhead` processing of responses to break.*',
|
||||
hidden: true
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
|
||||
// ---- other support stuff
|
||||
|
||||
function parseCommaSepStringNoEmpties(option, optstr, arg) {
|
||||
// JSSTYLED
|
||||
return arg.trim().split(/\s*,\s*/g)
|
||||
.filter(function (part) { return part; });
|
||||
}
|
||||
|
||||
cmdln.dashdash.addOptionType({
|
||||
name: 'commaSepString',
|
||||
takesArg: true,
|
||||
helpArg: 'STRING',
|
||||
parseArg: parseCommaSepStringNoEmpties
|
||||
});
|
||||
|
||||
cmdln.dashdash.addOptionType({
|
||||
name: 'arrayOfCommaSepString',
|
||||
takesArg: true,
|
||||
helpArg: 'STRING',
|
||||
parseArg: parseCommaSepStringNoEmpties,
|
||||
array: true,
|
||||
arrayFlatten: true
|
||||
var pkg = require('../package.json');
|
||||
var name = 'triton';
|
||||
var log = bunyan.createLogger({
|
||||
name: name,
|
||||
serializers: bunyan.stdSerializers,
|
||||
stream: process.stderr,
|
||||
level: 'warn'
|
||||
});
|
||||
|
||||
|
||||
@ -179,505 +41,82 @@ cmdln.dashdash.addOptionType({
|
||||
|
||||
function CLI() {
|
||||
Cmdln.call(this, {
|
||||
name: 'spearhead',
|
||||
desc: packageJson.description,
|
||||
options: OPTIONS,
|
||||
name: pkg.name,
|
||||
desc: pkg.description,
|
||||
options: [
|
||||
{names: ['help', 'h'], type: 'bool', help: 'Print help and exit.'},
|
||||
{name: 'version', type: 'bool', help: 'Print version and exit.'},
|
||||
{names: ['verbose', 'v'], type: 'bool',
|
||||
help: 'Verbose/debug output.'},
|
||||
// XXX disable profile selection for now
|
||||
//{names: ['profile', 'p'], type: 'string', env: 'TRITON_PROFILE',
|
||||
// helpArg: 'NAME', help: 'Triton client profile to use.'}
|
||||
],
|
||||
helpOpts: {
|
||||
includeEnv: true,
|
||||
minHelpCol: 30
|
||||
},
|
||||
helpSubcmds: [
|
||||
'help',
|
||||
'profile',
|
||||
'env',
|
||||
'completion',
|
||||
{ group: 'Instances (aka VMs/Machines/Containers)' },
|
||||
'instance',
|
||||
'instances',
|
||||
'create',
|
||||
'delete',
|
||||
'start',
|
||||
'stop',
|
||||
'reboot',
|
||||
'ssh',
|
||||
'ip',
|
||||
{ group: 'Images, Packages, Networks, Firewall Rules' },
|
||||
'image',
|
||||
'package',
|
||||
'network',
|
||||
'fwrule',
|
||||
'vlan',
|
||||
{ group: 'Other Commands' },
|
||||
'info',
|
||||
{ group: 'Operator Commands' },
|
||||
'account',
|
||||
'key',
|
||||
'services',
|
||||
'datacenters'
|
||||
],
|
||||
helpBody: [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Exit Status:',
|
||||
' 0 Successful completion.',
|
||||
' 1 An error occurred.',
|
||||
' 2 Usage error.',
|
||||
' 3 "ResourceNotFound" error (when an instance, image, etc. with',
|
||||
' the given name or id is not found) or "InstanceDeleted" error.'
|
||||
/* END JSSTYLED */
|
||||
].join('\n')
|
||||
'info',
|
||||
'keys',
|
||||
{ group: 'Instances (aka VMs/Machines/Containers)' },
|
||||
'create-instance',
|
||||
'instances',
|
||||
'instance',
|
||||
'instance-audit',
|
||||
'start-instance',
|
||||
'stop-instance',
|
||||
'reboot-instance',
|
||||
'delete-instance',
|
||||
'ssh',
|
||||
{ group: 'Images' },
|
||||
'images',
|
||||
'image',
|
||||
{ group: 'Packages' },
|
||||
'packages',
|
||||
'package'
|
||||
]
|
||||
});
|
||||
}
|
||||
util.inherits(CLI, Cmdln);
|
||||
|
||||
CLI.prototype.init = function (opts, args, callback) {
|
||||
var self = this;
|
||||
this.opts = opts;
|
||||
|
||||
this.log = bunyan.createLogger({
|
||||
name: this.name,
|
||||
serializers: bunyan.stdSerializers,
|
||||
stream: process.stderr,
|
||||
level: 'warn'
|
||||
});
|
||||
if (opts.verbose) {
|
||||
this.log.level('trace');
|
||||
this.log.src = true;
|
||||
this.showErrStack = true;
|
||||
}
|
||||
|
||||
if (opts.version) {
|
||||
console.log('Spearhead CLI', packageJson.version);
|
||||
console.log(packageJson.homepage);
|
||||
p(this.name, pkg.version);
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.url && opts.J) {
|
||||
callback(new errors.UsageError(
|
||||
'cannot use both "--url" and "-J" options'));
|
||||
} else if (opts.J) {
|
||||
opts.url = format('https://%s.api.spearhead.cloud', opts.J);
|
||||
this.opts = opts;
|
||||
if (opts.verbose) {
|
||||
log.level('trace');
|
||||
log.src = true;
|
||||
}
|
||||
|
||||
this.configDir = constants.CLI_CONFIG_DIR;
|
||||
|
||||
this.__defineGetter__('config', function getConfig() {
|
||||
if (self._config === undefined) {
|
||||
self._config = mod_config.loadConfig({
|
||||
configDir: self.configDir
|
||||
});
|
||||
self.log.trace({config: self._config}, 'loaded config');
|
||||
this.__defineGetter__('triton', function () {
|
||||
if (self._triton === undefined) {
|
||||
self._triton = new Triton({log: log, profile: opts.profile});
|
||||
}
|
||||
return self._config;
|
||||
return self._triton;
|
||||
});
|
||||
|
||||
this.__defineGetter__('profileName', function getProfileName() {
|
||||
return (opts.profile || self.config.profile || 'env');
|
||||
});
|
||||
|
||||
this.__defineGetter__('profile', function getProfile() {
|
||||
if (self._profile === undefined) {
|
||||
try {
|
||||
self._profile = mod_config.loadProfile({
|
||||
configDir: self.configDir,
|
||||
name: self.profileName,
|
||||
profileOverrides: self._cliOptsAsProfile()
|
||||
});
|
||||
} catch (pErr) {
|
||||
/*
|
||||
* Let's be nice for the getting started use case where we
|
||||
* defaulted to 'env' profile (e.g. the user has never created
|
||||
* one) and the minimal envvars aren't set. I.e. The user just
|
||||
* installed and ran `triton ls` or some other command.
|
||||
*/
|
||||
if (pErr.code === 'Config' && self.profileName === 'env' &&
|
||||
!opts.profile && !self.config.profile)
|
||||
{
|
||||
/* BEGIN JSSTYLED */
|
||||
pErr.message += '\n'
|
||||
+ ' No profile information could be loaded.\n'
|
||||
+ ' Use "spearhead profile create" to create a profile or provide\n'
|
||||
+ ' the required "CloudAPI options" described in "spearhead --help".';
|
||||
/* END JSSTYLED */
|
||||
}
|
||||
throw pErr;
|
||||
}
|
||||
self.log.trace({profile: self._profile}, 'loaded profile');
|
||||
}
|
||||
return self._profile;
|
||||
});
|
||||
|
||||
this.__defineGetter__('tritonapi', function getTritonapi() {
|
||||
if (self._tritonapi === undefined) {
|
||||
self._tritonapi = lib_tritonapi.createClient({
|
||||
log: self.log,
|
||||
profile: self.profile,
|
||||
config: self.config
|
||||
});
|
||||
self.log.trace('created tritonapi');
|
||||
}
|
||||
return self._tritonapi;
|
||||
});
|
||||
|
||||
if (process.env.SC_COMPLETE) {
|
||||
/*
|
||||
* If `SC_COMPLETE=<type>` is set (typically only in the
|
||||
* Triton CLI bash completion driver, see
|
||||
* "etc/triton-bash-completion-types.sh"), then Bash completions are
|
||||
* fetched and printed, instead of the usual subcommand handling.
|
||||
*
|
||||
* Completion results are typically cached (under "~/.triton/cache")
|
||||
* to avoid hitting the server for data everytime.
|
||||
*
|
||||
* Example usage:
|
||||
* SC_COMPLETE=images triton -p my-profile create
|
||||
*/
|
||||
self._emitCompletions(process.env.SC_COMPLETE, function (err) {
|
||||
callback(err || false);
|
||||
});
|
||||
} else {
|
||||
// Cmdln class handles `opts.help`.
|
||||
Cmdln.prototype.init.call(self, opts, args, callback);
|
||||
}
|
||||
// Cmdln class handles `opts.help`.
|
||||
Cmdln.prototype.init.apply(this, arguments);
|
||||
};
|
||||
|
||||
|
||||
CLI.prototype.fini = function fini(subcmd, err, cb) {
|
||||
this.log.trace({err: err, subcmd: subcmd}, 'cli fini');
|
||||
if (this._tritonapi) {
|
||||
this._tritonapi.close();
|
||||
delete this._tritonapi;
|
||||
}
|
||||
cb();
|
||||
};
|
||||
|
||||
//CLI.prototype.do_profile = require('./do_profile');
|
||||
|
||||
/*
|
||||
* Fetch and display Bash completions (one completion per line) for the given
|
||||
* Triton data type (e.g. 'images', 'instances', 'packages', ...).
|
||||
* This caches results (per profile) with a 5 minute TTL.
|
||||
*
|
||||
* Dev Note: If the cache path logic changes, then the *Bash* implementation
|
||||
* of the same logic in "etc/triton-bash-completion-types.sh" must be updated
|
||||
* to match.
|
||||
*/
|
||||
CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
|
||||
assert.string(type, 'type');
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
var cacheFile = path.join(this.tritonapi.cacheDir, type + '.completions');
|
||||
var ttl = 5 * 60 * 1000; // timeout of cache file info (ms)
|
||||
var tritonapi = this.tritonapi;
|
||||
|
||||
vasync.pipeline({arg: {}, funcs: [
|
||||
function tryCacheFile(arg, next) {
|
||||
fs.stat(cacheFile, function (err, stats) {
|
||||
if (!err &&
|
||||
stats.mtime.getTime() + ttl >= (new Date()).getTime()) {
|
||||
process.stdout.write(fs.readFileSync(cacheFile));
|
||||
next(true); // early abort
|
||||
} else if (err && err.code !== 'ENOENT') {
|
||||
next(err);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
function initAuth(args, next) {
|
||||
tritonapi.init(function (initErr) {
|
||||
if (initErr) {
|
||||
next(initErr);
|
||||
}
|
||||
if (tritonapi.keyPair.isLocked()) {
|
||||
next(new errors.TritonError(
|
||||
'cannot unlock keys during completion'));
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
function gather(arg, next) {
|
||||
var completions;
|
||||
|
||||
switch (type) {
|
||||
case 'packages':
|
||||
tritonapi.cloudapi.listPackages({}, function (err, pkgs) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
completions = [];
|
||||
pkgs.forEach(function (pkg) {
|
||||
if (pkg.name.indexOf(' ') === -1) {
|
||||
// Cannot bash complete results with spaces, so
|
||||
// skip them here.
|
||||
completions.push(pkg.name);
|
||||
}
|
||||
completions.push(pkg.id);
|
||||
});
|
||||
arg.completions = completions.join('\n') + '\n';
|
||||
next();
|
||||
});
|
||||
break;
|
||||
case 'images':
|
||||
tritonapi.cloudapi.listImages({}, function (err, imgs) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
completions = [];
|
||||
imgs.forEach(function (img) {
|
||||
// Cannot bash complete results with spaces, so
|
||||
// skip them here.
|
||||
if (img.name.indexOf(' ') === -1) {
|
||||
completions.push(img.name);
|
||||
if (img.version.indexOf(' ') === -1) {
|
||||
completions.push(img.name + '@' + img.version);
|
||||
}
|
||||
}
|
||||
completions.push(img.id);
|
||||
});
|
||||
arg.completions = completions.join('\n') + '\n';
|
||||
next();
|
||||
});
|
||||
break;
|
||||
case 'instances':
|
||||
tritonapi.cloudapi.listMachines({}, function (err, insts) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
completions = [];
|
||||
insts.forEach(function (inst) {
|
||||
if (inst.name.indexOf(' ') === -1) {
|
||||
// Cannot bash complete results with spaces, so
|
||||
// skip them here.
|
||||
completions.push(inst.name);
|
||||
}
|
||||
completions.push(inst.id);
|
||||
});
|
||||
arg.completions = completions.join('\n') + '\n';
|
||||
next();
|
||||
});
|
||||
break;
|
||||
case 'volumes':
|
||||
tritonapi.cloudapi.listVolumes({}, function (err, vols) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
completions = [];
|
||||
vols.forEach(function (vol) {
|
||||
completions.push(vol.name);
|
||||
completions.push(vol.id);
|
||||
});
|
||||
arg.completions = completions.join('\n') + '\n';
|
||||
next();
|
||||
});
|
||||
break;
|
||||
case 'affinityrules':
|
||||
/*
|
||||
* We exclude ids, in favour of just inst names here. The only
|
||||
* justification for differing from other completion types
|
||||
* on that is that with the additional prefixes, there would
|
||||
* be too many.
|
||||
*/
|
||||
tritonapi.cloudapi.listMachines({}, function (err, insts) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
completions = [];
|
||||
insts.forEach(function (inst) {
|
||||
if (inst.name.indexOf(' ') === -1) {
|
||||
// Cannot bash complete results with spaces, so
|
||||
// skip them here.
|
||||
completions.push('inst==' + inst.name);
|
||||
completions.push('inst!=' + inst.name);
|
||||
completions.push('inst==~' + inst.name);
|
||||
completions.push('inst!=~' + inst.name);
|
||||
}
|
||||
});
|
||||
arg.completions = completions.join('\n') + '\n';
|
||||
next();
|
||||
});
|
||||
break;
|
||||
case 'networks':
|
||||
tritonapi.cloudapi.listNetworks({}, function (err, nets) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
completions = [];
|
||||
nets.forEach(function (net) {
|
||||
if (net.name.indexOf(' ') === -1) {
|
||||
// Cannot bash complete results with spaces, so
|
||||
// skip them here.
|
||||
completions.push(net.name);
|
||||
}
|
||||
completions.push(net.id);
|
||||
});
|
||||
arg.completions = completions.join('\n') + '\n';
|
||||
next();
|
||||
});
|
||||
break;
|
||||
case 'fwrules':
|
||||
tritonapi.cloudapi.listFirewallRules({}, function (err,
|
||||
fwrules) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
completions = [];
|
||||
fwrules.forEach(function (fwrule) {
|
||||
completions.push(fwrule.id);
|
||||
});
|
||||
arg.completions = completions.join('\n') + '\n';
|
||||
next();
|
||||
});
|
||||
break;
|
||||
case 'keys':
|
||||
tritonapi.cloudapi.listKeys({}, function (err, keys) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
completions = [];
|
||||
keys.forEach(function (key) {
|
||||
if (key.name.indexOf(' ') === -1) {
|
||||
// Cannot bash complete results with spaces, so
|
||||
// skip them here.
|
||||
completions.push(key.name);
|
||||
}
|
||||
completions.push(key.fingerprint);
|
||||
});
|
||||
arg.completions = completions.join('\n') + '\n';
|
||||
next();
|
||||
});
|
||||
break;
|
||||
default:
|
||||
process.stderr.write('warning: unknown spearhead completion type: '
|
||||
+ type + '\n');
|
||||
next();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
function saveCache(arg, next) {
|
||||
if (!arg.completions) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
fs.writeFile(cacheFile, arg.completions, next);
|
||||
},
|
||||
|
||||
function emit(arg, next) {
|
||||
if (arg.completions) {
|
||||
console.log(arg.completions);
|
||||
}
|
||||
next();
|
||||
}
|
||||
]}, function (err) {
|
||||
if (err === true) { // early abort signal
|
||||
err = null;
|
||||
}
|
||||
cb(err);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Apply overrides from CLI options to the given profile object *in place*.
|
||||
*/
|
||||
CLI.prototype._applyProfileOverrides =
|
||||
function _applyProfileOverrides(profile) {
|
||||
var optProfile = this._cliOptsAsProfile();
|
||||
for (var attr in optProfile) {
|
||||
profile[attr] = optProfile[attr];
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Create a profile dict from any cli override options specified.
|
||||
* Unless all profile flags are specified on the cli, this profile
|
||||
* will be incomplete and will need to be combined with another
|
||||
* configuration source.
|
||||
*/
|
||||
CLI.prototype._cliOptsAsProfile = function _cliOptsAsProfile() {
|
||||
var self = this;
|
||||
var profile = {};
|
||||
[
|
||||
{oname: 'account', pname: 'account'},
|
||||
{oname: 'user', pname: 'user'},
|
||||
{oname: 'role', pname: 'roles'},
|
||||
{oname: 'url', pname: 'url'},
|
||||
{oname: 'keyId', pname: 'keyId'},
|
||||
{oname: 'insecure', pname: 'insecure'},
|
||||
{oname: 'accept_version', pname: 'acceptVersion'},
|
||||
{oname: 'act_as', pname: 'actAsAccount'}
|
||||
].forEach(function (field) {
|
||||
// We need to check `opts._order` to know if boolean opts
|
||||
// were specified.
|
||||
var specified = self.opts._order.filter(
|
||||
function (opt) { return opt.key === field.oname; }).length > 0;
|
||||
if (specified) {
|
||||
profile[field.pname] = self.opts[field.oname];
|
||||
}
|
||||
});
|
||||
return profile;
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Create and return a TritonApi instance for the given profile name and using
|
||||
* the CLI's config. Callers of this should remember to `tritonapi.close()`
|
||||
* when complete... otherwise an HTTP Agent using keep-alive will keep node
|
||||
* from exiting until it times out.
|
||||
*/
|
||||
CLI.prototype.tritonapiFromProfileName =
|
||||
function tritonapiFromProfileName(opts) {
|
||||
assert.object(opts, 'opts');
|
||||
assert.string(opts.profileName, 'opts.profileName');
|
||||
|
||||
var profile;
|
||||
if (opts.profileName === this.profileName) {
|
||||
profile = this.profile;
|
||||
} else {
|
||||
profile = mod_config.loadProfile({
|
||||
configDir: this.configDir,
|
||||
name: opts.profileName
|
||||
});
|
||||
this.log.trace({profile: profile},
|
||||
'tritonapiFromProfileName: loaded profile');
|
||||
}
|
||||
|
||||
return lib_tritonapi.createClient({
|
||||
log: this.log,
|
||||
profile: profile,
|
||||
config: this.config
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Meta
|
||||
CLI.prototype.do_completion = require('./do_completion');
|
||||
CLI.prototype.do_profiles = require('./do_profiles');
|
||||
CLI.prototype.do_profile = require('./do_profile');
|
||||
CLI.prototype.do_env = require('./do_env');
|
||||
|
||||
// Other
|
||||
// Operator
|
||||
CLI.prototype.do_account = require('./do_account');
|
||||
CLI.prototype.do_services = require('./do_services');
|
||||
CLI.prototype.do_datacenters = require('./do_datacenters');
|
||||
CLI.prototype.do_info = require('./do_info');
|
||||
|
||||
// Account keys
|
||||
CLI.prototype.do_key = require('./do_key');
|
||||
CLI.prototype.do_keys = require('./do_keys');
|
||||
|
||||
// Firewall rules
|
||||
CLI.prototype.do_fwrule = require('./do_fwrule');
|
||||
CLI.prototype.do_fwrules = require('./do_fwrules');
|
||||
|
||||
// Images
|
||||
CLI.prototype.do_images = require('./do_images');
|
||||
CLI.prototype.do_image = require('./do_image');
|
||||
@ -685,126 +124,33 @@ CLI.prototype.do_image = require('./do_image');
|
||||
// Instances (aka VMs/containers/machines)
|
||||
CLI.prototype.do_instance = require('./do_instance');
|
||||
CLI.prototype.do_instances = require('./do_instances');
|
||||
CLI.prototype.do_create = require('./do_create');
|
||||
CLI.prototype.do_delete = require('./do_delete');
|
||||
CLI.prototype.do_start = require('./do_start');
|
||||
CLI.prototype.do_stop = require('./do_stop');
|
||||
CLI.prototype.do_reboot = require('./do_reboot');
|
||||
CLI.prototype.do_create_instance = require('./do_create_instance');
|
||||
CLI.prototype.do_instance_audit = require('./do_instance_audit');
|
||||
CLI.prototype.do_stop_instance = require('./do_startstop_instance')('stop');
|
||||
CLI.prototype.do_start_instance = require('./do_startstop_instance')('start');
|
||||
CLI.prototype.do_reboot_instance = require('./do_startstop_instance')('reboot');
|
||||
CLI.prototype.do_delete_instance = require('./do_delete_instance');
|
||||
CLI.prototype.do_ssh = require('./do_ssh');
|
||||
CLI.prototype.do_ip = require('./do_ip');
|
||||
|
||||
// Packages
|
||||
CLI.prototype.do_packages = require('./do_packages');
|
||||
CLI.prototype.do_package = require('./do_package');
|
||||
|
||||
// Networks
|
||||
CLI.prototype.do_networks = require('./do_networks');
|
||||
CLI.prototype.do_network = require('./do_network');
|
||||
|
||||
// VLANs
|
||||
CLI.prototype.do_vlan = require('./do_vlan');
|
||||
|
||||
// Hidden commands
|
||||
CLI.prototype.do_cloudapi = require('./do_cloudapi');
|
||||
CLI.prototype.do_badger = require('./do_badger');
|
||||
CLI.prototype.do_rbac = require('./do_rbac');
|
||||
|
||||
// Volumes
|
||||
CLI.prototype.do_volumes = require('./do_volumes');
|
||||
CLI.prototype.do_volume = require('./do_volume');
|
||||
|
||||
|
||||
|
||||
|
||||
//---- mainline
|
||||
|
||||
function main(argv) {
|
||||
if (!argv) {
|
||||
argv = process.argv;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
var cli = new CLI();
|
||||
cli.main(argv, function (err) {
|
||||
var exitStatus = (err ? err.exitStatus || 1 : 0);
|
||||
var showErr = (cli.showErr !== undefined ? cli.showErr : true);
|
||||
var errHelp;
|
||||
var errMessage;
|
||||
|
||||
if (err && showErr) {
|
||||
var code = (err.body ? err.body.code : err.code) || err.restCode;
|
||||
if (code === 'NoCommand') {
|
||||
/* jsl:pass */
|
||||
} else if (err.name === 'InternalServerError') {
|
||||
/*
|
||||
* Internal server error, we want to provide a useful error
|
||||
* message without exposing internals.
|
||||
*/
|
||||
console.error('%s: internal error. Please try again later, ' +
|
||||
'and contact support in case the error persists.',
|
||||
cmdln.nameFromErr(err));
|
||||
} else {
|
||||
/*
|
||||
* If the err has `body.errors`, as some Triton/SDC APIs do per
|
||||
* // JSSTYLED
|
||||
* https://github.com/joyent/eng/blob/master/docs/index.md#error-handling
|
||||
* then append a one-line summary for each error object.
|
||||
*/
|
||||
var bodyErrors = '';
|
||||
if (err.body && err.body.errors) {
|
||||
err.body.errors.forEach(function (e) {
|
||||
bodyErrors += format('\n %s: %s', e.field, e.code);
|
||||
if (e.message) {
|
||||
bodyErrors += ': ' + e.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Try to find the most descriptive message to output.
|
||||
*
|
||||
* 1. If there's a message property on the error object, we
|
||||
* assume this is suitable to output to the user.
|
||||
*
|
||||
* 2. Otherwise, if there's an "orignalBody" property, we output
|
||||
* its content per joyent/node-triton#30.
|
||||
*
|
||||
* 3. We fall back to using the error's name as the error
|
||||
* message.
|
||||
*/
|
||||
if (typeof (err.message) === 'string' && err.message !== '') {
|
||||
errMessage = err.message;
|
||||
} else if (err.originalBody !== undefined) {
|
||||
errMessage = err.originalBody.toString();
|
||||
} else {
|
||||
errMessage = err.name;
|
||||
}
|
||||
|
||||
console.error('%s: error%s: %s%s',
|
||||
cmdln.nameFromErr(err),
|
||||
(code ? format(' (%s)', code) : ''),
|
||||
(cli.showErrStack ? err.stack : errMessage),
|
||||
bodyErrors);
|
||||
}
|
||||
|
||||
errHelp = cmdln.errHelpFromErr(err);
|
||||
if (errHelp) {
|
||||
console.error(errHelp);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* We'd like to NOT use `process.exit` because that doesn't always
|
||||
* allow std handles to flush (e.g. all logging to complete). However
|
||||
* I don't know of another way to exit non-zero.
|
||||
*/
|
||||
if (exitStatus !== 0) {
|
||||
process.exit(exitStatus);
|
||||
}
|
||||
});
|
||||
cmdln.main(cli, {showNoCommandErr: false});
|
||||
}
|
||||
|
||||
|
||||
//---- exports
|
||||
|
||||
module.exports = {
|
||||
CLI: CLI,
|
||||
main: main
|
||||
};
|
||||
module.exports = CLI;
|
||||
|
2885
lib/cloudapi2.js
2885
lib/cloudapi2.js
File diff suppressed because it is too large
Load Diff
1404
lib/common.js
Normal file → Executable file
1404
lib/common.js
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
436
lib/config.js
Normal file → Executable file
436
lib/config.js
Normal file → Executable file
@ -1,125 +1,45 @@
|
||||
/*
|
||||
* 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 2017 Joyent, Inc.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This module provides functions to read and write (a) a TritonApi config
|
||||
* and (b) TritonApi profiles.
|
||||
*
|
||||
* The config is a JSON object loaded from "etc/defaults.json" (shipped with
|
||||
* node-triton) plus possibly overrides from "$configDir/config.json" --
|
||||
* which is "~/.triton/config.json" for the `triton` CLI. The config has
|
||||
* a strict set of allowed keys.
|
||||
*
|
||||
* A profile is a small object that includes the necessary info for talking
|
||||
* to a CloudAPI. E.g.:
|
||||
* {
|
||||
* "name": "east1",
|
||||
* "account": "billy.bob",
|
||||
* "keyId": "de:e7:73:9a:aa:91:bb:3e:72:8d:cc:62:ca:58:a2:ec",
|
||||
* "url": "https://us-east-1.api.joyent.com"
|
||||
* }
|
||||
*
|
||||
* Profiles are stored as separate JSON files in
|
||||
* "$configDir/profiles.d/$name.json". Typically `triton profiles ...` is
|
||||
* used to manage them. In addition there is the special "env" profile that
|
||||
* is constructed from the "SDC_*" environment variables.
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2014 Joyent Inc. All rights reserved.
|
||||
*/
|
||||
|
||||
var p = console.log;
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var fs = require('fs');
|
||||
var mkdirp = require('mkdirp');
|
||||
var path = require('path');
|
||||
var vasync = require('vasync');
|
||||
var sprintf = require('extsprintf').sprintf;
|
||||
|
||||
var common = require('./common');
|
||||
var errors = require('./errors');
|
||||
|
||||
|
||||
var CONFIG_PATH = path.resolve(process.env.HOME, '.triton', 'config.json');
|
||||
var DEFAULTS_PATH = path.resolve(__dirname, '..', 'etc', 'defaults.json');
|
||||
var OVERRIDE_NAMES = []; // config object keys to do a one-level deep override
|
||||
|
||||
// TODO: use this const to create the "Configuration" docs table.
|
||||
var CONFIG_VAR_NAMES = [
|
||||
'profile',
|
||||
// Intentionally exclude 'oldProfile' so that it isn't manually set.
|
||||
// 'oldProfile',
|
||||
'cacheDir'
|
||||
];
|
||||
|
||||
// TODO: use this to create a profile doc table?
|
||||
var PROFILE_FIELDS = {
|
||||
name: true,
|
||||
url: true,
|
||||
account: true,
|
||||
keyId: true,
|
||||
insecure: true,
|
||||
user: true,
|
||||
roles: true,
|
||||
actAsAccount: true
|
||||
};
|
||||
|
||||
|
||||
// --- internal support stuff
|
||||
|
||||
function configPathFromDir(configDir) {
|
||||
return path.resolve(configDir, 'config.json');
|
||||
}
|
||||
|
||||
|
||||
// --- Config
|
||||
var OVERRIDE_KEYS = []; // config object keys to do a one-level deep override
|
||||
|
||||
/**
|
||||
* Load the TritonApi config. This is a merge of the built-in "defaults" (at
|
||||
* etc/defaults.json) and the "user" config (at "$configDir/config.json",
|
||||
* typically "~/.triton/config.json", if it exists).
|
||||
* Load the Triton client config. This is a merge of the built-in "defaults" (at
|
||||
* etc/defaults.json) and the "user" config (at ~/.triton/config.json if it
|
||||
* exists).
|
||||
*
|
||||
* This includes some internal data on keys with a leading underscore:
|
||||
* _defaults the defaults.json object
|
||||
* _configDir the user config dir (if one is provided)
|
||||
* _user the "user" config.json object (if exists)
|
||||
*
|
||||
* @param opts.configDir {String} Optional. A base dir for TritonApi config.
|
||||
* @returns {Object} The loaded config.
|
||||
* This includes some internal data on keys with a leading underscore.
|
||||
*/
|
||||
function loadConfig(opts) {
|
||||
assert.object(opts, 'opts');
|
||||
assert.optionalString(opts.configDir, 'opts.configDir');
|
||||
|
||||
var configDir;
|
||||
var configPath;
|
||||
if (opts.configDir) {
|
||||
configDir = common.tildeSync(opts.configDir);
|
||||
configPath = configPathFromDir(configDir);
|
||||
}
|
||||
|
||||
function loadConfigSync() {
|
||||
var c = fs.readFileSync(DEFAULTS_PATH, 'utf8');
|
||||
var _defaults = JSON.parse(c);
|
||||
var config = JSON.parse(c);
|
||||
if (configPath && fs.existsSync(configPath)) {
|
||||
c = fs.readFileSync(configPath, 'utf8');
|
||||
try {
|
||||
var _user = JSON.parse(c);
|
||||
var userConfig = JSON.parse(c);
|
||||
} catch (userConfigParseErr) {
|
||||
if (fs.existsSync(CONFIG_PATH)) {
|
||||
c = fs.readFileSync(CONFIG_PATH, 'utf8');
|
||||
var _user = JSON.parse(c);
|
||||
var userConfig = JSON.parse(c);
|
||||
if (typeof(userConfig) !== 'object' || Array.isArray(userConfig)) {
|
||||
throw new errors.ConfigError(
|
||||
format('"%s" is invalid JSON', configPath));
|
||||
}
|
||||
if (typeof (userConfig) !== 'object' || Array.isArray(userConfig)) {
|
||||
throw new errors.ConfigError(
|
||||
format('"%s" is not an object', configPath));
|
||||
sprintf('"%s" is not an object', CONFIG_PATH));
|
||||
}
|
||||
// These special keys are merged into the key of the same name in the
|
||||
// base "defaults.json".
|
||||
Object.keys(userConfig).forEach(function (key) {
|
||||
if (~OVERRIDE_NAMES.indexOf(key) && config[key] !== undefined) {
|
||||
if (~OVERRIDE_KEYS.indexOf(key) && config[key] !== undefined) {
|
||||
Object.keys(userConfig[key]).forEach(function (subKey) {
|
||||
if (userConfig[key][subKey] === null) {
|
||||
delete config[key][subKey];
|
||||
@ -135,318 +55,40 @@ function loadConfig(opts) {
|
||||
config._user = _user;
|
||||
}
|
||||
config._defaults = _defaults;
|
||||
if (configDir) {
|
||||
config._configDir = configDir;
|
||||
|
||||
// Add 'env' profile.
|
||||
if (!config.profiles) {
|
||||
config.profiles = [];
|
||||
}
|
||||
//XXX Add TRITON_* envvars.
|
||||
config.profiles.push({
|
||||
name: 'env',
|
||||
account: process.env.SDC_USER || process.env.SDC_ACCOUNT,
|
||||
url: process.env.SDC_URL,
|
||||
keyId: process.env.SDC_KEY_ID,
|
||||
insecure: common.boolFromString(process.env.SDC_TESTING)
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
function setConfigVars(opts, cb) {
|
||||
assert.object(opts, 'opts');
|
||||
assert.string(opts.configDir, 'opts.configDir');
|
||||
assert.object(opts.vars, 'opts.vars');
|
||||
Object.keys(opts.vars).forEach(function (name) {
|
||||
assert.ok(name.indexOf('.') === -1,
|
||||
'dotted config name not yet supported');
|
||||
assert.ok(CONFIG_VAR_NAMES.indexOf(name) !== -1,
|
||||
'unknown config var name: ' + name);
|
||||
});
|
||||
|
||||
var configPath = configPathFromDir(opts.configDir);
|
||||
var config;
|
||||
|
||||
vasync.pipeline({funcs: [
|
||||
function loadExisting(_, next) {
|
||||
fs.exists(configPath, function (exists) {
|
||||
if (!exists) {
|
||||
config = {};
|
||||
return next();
|
||||
}
|
||||
fs.readFile(configPath, function (err, data) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
try {
|
||||
config = JSON.parse(data);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
next();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
function mkConfigDir(_, next) {
|
||||
fs.exists(opts.configDir, function (exists) {
|
||||
if (!exists) {
|
||||
mkdirp(opts.configDir, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
* To support `triton profile set -` to set profile to the *last*
|
||||
* one used, we special case the setting of the "profile" config var
|
||||
* to *also* then set "oldProfile" to the old value. (We are copying
|
||||
* the "OLDPWD" naming used by the shell for `cd -`.)
|
||||
*/
|
||||
function specialCaseOldProfile(_, next) {
|
||||
if (opts.vars.hasOwnProperty('profile') && config.profile) {
|
||||
opts.vars['oldProfile'] = config.profile;
|
||||
}
|
||||
next();
|
||||
},
|
||||
|
||||
function updateAndSave(_, next) {
|
||||
Object.keys(opts.vars).forEach(function (name) {
|
||||
config[name] = opts.vars[name];
|
||||
});
|
||||
fs.writeFile(configPath, JSON.stringify(config, null, 4), next);
|
||||
}
|
||||
]}, cb);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- Profiles
|
||||
|
||||
function validateProfile(profile, profilePath) {
|
||||
assert.object(profile, 'profile');
|
||||
assert.optionalString(profilePath, 'profilePath');
|
||||
|
||||
try {
|
||||
assert.string(profile.name, 'profile.name');
|
||||
assert.string(profile.url,
|
||||
profile.name === 'env' ? 'SC_URL' : 'profile.url');
|
||||
assert.string(profile.account,
|
||||
profile.name === 'env' ? 'SC_ACCOUNT'
|
||||
: 'profile.account');
|
||||
assert.string(profile.keyId,
|
||||
profile.name === 'env' ? 'SC_KEY_ID'
|
||||
: 'profile.keyId');
|
||||
assert.optionalBool(profile.insecure,
|
||||
profile.name === 'env' ? 'SC_INSECURE'
|
||||
: 'profile.insecure');
|
||||
assert.optionalString(profile.user,
|
||||
profile.name === 'env' ? 'SC_USER'
|
||||
: 'profile.user');
|
||||
assert.optionalString(profile.actAsAccount, 'profile.actAsAccount');
|
||||
assert.optionalArrayOfString(profile.roles, 'profile.roles');
|
||||
} catch (err) {
|
||||
var msg = format('invalid %sprofile%s: %s',
|
||||
profile.name ? '"' + profile.name + '" ' : '',
|
||||
profilePath ? ' from ' + profilePath: '',
|
||||
err.message);
|
||||
throw new errors.ConfigError(msg);
|
||||
}
|
||||
|
||||
var bogusFields = [];
|
||||
Object.keys(profile).forEach(function (field) {
|
||||
if (!PROFILE_FIELDS[field]) {
|
||||
bogusFields.push(field);
|
||||
}
|
||||
});
|
||||
if (bogusFields.length) {
|
||||
throw new errors.ConfigError(format(
|
||||
'extraneous fields in "%s" profile: %s%s', profile.name,
|
||||
(profilePath ? profilePath + ': ' : ''), bogusFields.join(', ')));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Load the special 'env' profile, which handles details of getting
|
||||
* values from envvars. Typically we'd piggyback on dashdash's env support
|
||||
* <https://github.com/trentm/node-dashdash#environment-variable-integration>.
|
||||
* However, per the "Environment variable integration" comment in cli.js, we
|
||||
* do that manually.
|
||||
* Apply the given key:value updates to the user config and save it out.
|
||||
*
|
||||
* @returns {Object} The 'env' profile. If no relevant envvars are set, then
|
||||
* this returns null.
|
||||
* @throws {errors.ConfigError} If the profile defined by the environment is
|
||||
* invalid.
|
||||
* @param config {Object} The loaded config, as from `loadConfigSync`.
|
||||
* @param updates {Object} key/value pairs to update.
|
||||
*/
|
||||
function _loadEnvProfile(profileOverrides) {
|
||||
var envProfile = {
|
||||
name: 'env'
|
||||
};
|
||||
|
||||
envProfile.account = process.env.SC_ACCOUNT;
|
||||
var user = process.env.SC_USER;
|
||||
if (user) {
|
||||
envProfile.user = user;
|
||||
}
|
||||
envProfile.url = process.env.SC_URL;
|
||||
envProfile.keyId = process.env.SC_KEY_ID;
|
||||
|
||||
if (process.env.SC_TLS_INSECURE) {
|
||||
envProfile.insecure = common.boolFromString(
|
||||
process.env.SC_TLS_INSECURE, undefined, 'SC_TLS_INSECURE');
|
||||
} else if (process.env.SC_TESTING) {
|
||||
// For compatibility with the legacy behavior of the smartdc
|
||||
// tools, *any* set value but the empty string is considered true.
|
||||
envProfile.insecure = true;
|
||||
}
|
||||
|
||||
for (var attr in profileOverrides) {
|
||||
envProfile[attr] = profileOverrides[attr];
|
||||
}
|
||||
|
||||
/*
|
||||
* If missing any of the required vars, then there is no env profile.
|
||||
*/
|
||||
if (!envProfile.account || !envProfile.url || !envProfile.keyId) {
|
||||
return null;
|
||||
}
|
||||
validateProfile(envProfile, 'environment variables');
|
||||
|
||||
return envProfile;
|
||||
}
|
||||
|
||||
function _profileFromPath(profilePath, name, profileOverrides) {
|
||||
if (! fs.existsSync(profilePath)) {
|
||||
throw new errors.ConfigError('no such profile: ' + name);
|
||||
}
|
||||
var profile;
|
||||
try {
|
||||
profile = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
|
||||
} catch (e) {
|
||||
throw new errors.ConfigError(e, format(
|
||||
'error in "%s" profile: %s: %s', name,
|
||||
profilePath, e.message));
|
||||
}
|
||||
if (profile.name) {
|
||||
throw new errors.ConfigError(format(
|
||||
'error in "%s" profile: %s: file must not include "name" field',
|
||||
name, profilePath));
|
||||
}
|
||||
profile.name = name;
|
||||
|
||||
for (var attr in profileOverrides) {
|
||||
profile[attr] = profileOverrides[attr];
|
||||
}
|
||||
validateProfile(profile, profilePath);
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
|
||||
function loadProfile(opts) {
|
||||
assert.string(opts.name, 'opts.name');
|
||||
assert.optionalString(opts.configDir, 'opts.configDir');
|
||||
assert.optionalObject(opts.profileOverrides, 'opts.profileOverrides');
|
||||
|
||||
if (opts.name === 'env') {
|
||||
var envProfile = _loadEnvProfile(opts.profileOverrides);
|
||||
if (!envProfile) {
|
||||
throw new errors.ConfigError('could not load "env" profile '
|
||||
+ '(missing SC_* environment variables)');
|
||||
}
|
||||
return envProfile;
|
||||
} else if (!opts.configDir) {
|
||||
throw new errors.ConfigError(
|
||||
'cannot load profiles (other than "env") without `opts.configDir`');
|
||||
} else {
|
||||
var profilePath = path.resolve(
|
||||
common.tildeSync(opts.configDir), 'profiles.d',
|
||||
opts.name + '.json');
|
||||
return _profileFromPath(profilePath, opts.name, opts.profileOverrides);
|
||||
}
|
||||
}
|
||||
|
||||
function loadAllProfiles(opts) {
|
||||
assert.string(opts.configDir, 'opts.configDir');
|
||||
assert.object(opts.log, 'opts.log');
|
||||
assert.optionalObject(opts.profileOverrides, 'opts.profileOverrides');
|
||||
|
||||
var profiles = [];
|
||||
|
||||
var envProfile = _loadEnvProfile(opts.profileOverrides);
|
||||
if (envProfile) {
|
||||
profiles.push(envProfile);
|
||||
}
|
||||
|
||||
var d = path.join(common.tildeSync(opts.configDir), 'profiles.d');
|
||||
if (fs.existsSync(d)) {
|
||||
var files = fs.readdirSync(d);
|
||||
files.forEach(function (file) {
|
||||
file = path.join(d, file);
|
||||
var ext = path.extname(file);
|
||||
if (ext !== '.json')
|
||||
return;
|
||||
|
||||
var name = path.basename(file).slice(
|
||||
0, - path.extname(file).length);
|
||||
if (name.toLowerCase() === 'env') {
|
||||
// Skip the special 'env'.
|
||||
opts.log.warn({profilePath: file},
|
||||
'invalid "env" profile; skipping');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
profiles.push(_profileFromPath(file, name));
|
||||
} catch (e) {
|
||||
opts.log.warn({err: e, profilePath: file},
|
||||
'error loading profile; skipping');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return profiles;
|
||||
}
|
||||
|
||||
function deleteProfile(opts) {
|
||||
assert.string(opts.configDir, 'opts.configDir');
|
||||
assert.string(opts.name, 'opts.name');
|
||||
|
||||
if (opts.name === 'env') {
|
||||
throw new Error('cannot delete "env" profile');
|
||||
}
|
||||
|
||||
var profilePath = path.resolve(opts.configDir, 'profiles.d',
|
||||
opts.name + '.json');
|
||||
fs.unlinkSync(profilePath);
|
||||
}
|
||||
|
||||
function saveProfileSync(opts) {
|
||||
assert.string(opts.configDir, 'opts.configDir');
|
||||
assert.object(opts.profile, 'opts.profile');
|
||||
|
||||
var name = opts.profile.name;
|
||||
if (name === 'env') {
|
||||
throw new Error('cannot save "env" profile');
|
||||
}
|
||||
|
||||
validateProfile(opts.profile);
|
||||
|
||||
var toSave = common.objCopy(opts.profile);
|
||||
delete toSave.name;
|
||||
|
||||
var profilePath = path.resolve(opts.configDir, 'profiles.d',
|
||||
name + '.json');
|
||||
if (!fs.existsSync(path.dirname(profilePath))) {
|
||||
mkdirp.sync(path.dirname(profilePath));
|
||||
}
|
||||
fs.writeFileSync(profilePath, JSON.stringify(toSave, null, 4), 'utf8');
|
||||
console.log('Saved profile "%s".', name);
|
||||
function updateUserConfigSync(config, updates) {
|
||||
XXX
|
||||
///XXX START HERE: to implement for 'sdc dcs add foo bar'
|
||||
}
|
||||
|
||||
|
||||
//---- exports
|
||||
|
||||
module.exports = {
|
||||
loadConfig: loadConfig,
|
||||
setConfigVars: setConfigVars,
|
||||
|
||||
validateProfile: validateProfile,
|
||||
loadProfile: loadProfile,
|
||||
loadAllProfiles: loadAllProfiles,
|
||||
deleteProfile: deleteProfile,
|
||||
saveProfileSync: saveProfileSync
|
||||
CONFIG_PATH: CONFIG_PATH,
|
||||
loadConfigSync: loadConfigSync
|
||||
};
|
||||
// vim: set softtabstop=4 shiftwidth=4:
|
||||
|
121
lib/constants.js
121
lib/constants.js
@ -1,121 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*/
|
||||
|
||||
/*
|
||||
* node-triton constants.
|
||||
*
|
||||
* CLI usage:
|
||||
* $ node lib/constants.js
|
||||
* ... emits all the constants as a JSON object ...
|
||||
* $ node lib/constants.js KEY
|
||||
* ... emits the value of KEY (in json-y form, i.e. quotes removed from a
|
||||
* string) ...
|
||||
*/
|
||||
|
||||
var mod_path = require('path');
|
||||
|
||||
|
||||
// ---- determining constants
|
||||
|
||||
/*
|
||||
* The `triton` CLI's config dir.
|
||||
*
|
||||
* For *testing* only, we allow override of this dir.
|
||||
*/
|
||||
var CLI_CONFIG_DIR;
|
||||
if (process.env.SCTEST_CLI_CONFIG_DIR) {
|
||||
CLI_CONFIG_DIR = process.env.SCTEST_CLI_CONFIG_DIR;
|
||||
} else if (process.platform === 'win32') {
|
||||
/*
|
||||
* For better or worse we are using APPDATA (i.e. the *Roaming* AppData
|
||||
* dir) over LOCALAPPDATA (non-roaming). The former is meant for "user"
|
||||
* data, the latter for "machine" data.
|
||||
*
|
||||
* TODO: We should likely separate out the *cache* subdir to
|
||||
* machine-specific data dir.
|
||||
*/
|
||||
CLI_CONFIG_DIR = mod_path.resolve(process.env.APPDATA, 'Spearhead', 'sc');
|
||||
} else {
|
||||
CLI_CONFIG_DIR = mod_path.resolve(process.env.HOME, '.spearhead');
|
||||
}
|
||||
|
||||
// <Network Object Key> -> <expected typeof>
|
||||
var NETWORK_OBJECT_FIELDS = {
|
||||
ipv4_uuid: 'string',
|
||||
ipv4_ips: 'string'
|
||||
};
|
||||
|
||||
|
||||
// ---- exports
|
||||
|
||||
module.exports = {
|
||||
CLI_CONFIG_DIR: CLI_CONFIG_DIR,
|
||||
NETWORK_OBJECT_FIELDS: NETWORK_OBJECT_FIELDS
|
||||
};
|
||||
|
||||
|
||||
// ---- mainline
|
||||
|
||||
function main(argv) {
|
||||
var assert = require('assert-plus');
|
||||
var dashdash = require('cmdln').dashdash;
|
||||
|
||||
assert.arrayOfString(argv, 'argv');
|
||||
|
||||
var options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Print this help and exit.'
|
||||
}
|
||||
];
|
||||
var parser = dashdash.createParser({options: options});
|
||||
try {
|
||||
var opts = parser.parse(argv);
|
||||
} catch (e) {
|
||||
console.error('lib/constants.js: error: %s', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (opts.help) {
|
||||
console.log([
|
||||
'usage: node .../lib/constants.js [OPTIONS] [KEY]',
|
||||
'options:',
|
||||
parser.help().trimRight()
|
||||
].join('\n'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
var key;
|
||||
if (opts._args.length === 1) {
|
||||
key = opts._args[0];
|
||||
} else if (opts._args.length === 0) {
|
||||
key = null;
|
||||
} else {
|
||||
console.error('lib/constants.js: error: too many args: %s',
|
||||
opts._args.join(' '));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (key) {
|
||||
var val = module.exports[key];
|
||||
if (typeof (val) === 'string') {
|
||||
console.log(val);
|
||||
} else {
|
||||
console.log(JSON.stringify(val, null, 4));
|
||||
}
|
||||
} else {
|
||||
console.log(JSON.stringify(module.exports, null, 4));
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main(process.argv);
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* A CLI distraction during a long process (e.g. waiting for
|
||||
* create).
|
||||
*
|
||||
* Usage:
|
||||
* var distractions = require('./distractions');
|
||||
* var distraction = distractions.createDistraction([num]);
|
||||
* setTimeout(function () {
|
||||
* distraction.destroy();
|
||||
* }, 5000);
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var bigspinner = require('bigspinner');
|
||||
|
||||
|
||||
function createDistraction(num) {
|
||||
assert.optionalNumber(num, 'num');
|
||||
|
||||
var height, width;
|
||||
if (num <= 2) {
|
||||
height = Math.min(5, process.stdout.rows - 2);
|
||||
width = Math.min(5*2, process.stdout.columns - 2);
|
||||
} else if (num === 3) {
|
||||
height = Math.min(10, process.stdout.rows - 2);
|
||||
width = Math.min(10*2, process.stdout.columns - 2);
|
||||
} else {
|
||||
var BORDER = 10;
|
||||
height = Math.max(2, process.stdout.rows - 2 - BORDER);
|
||||
width = Math.max(2, process.stdout.columns - 1 - BORDER);
|
||||
}
|
||||
return bigspinner.createSpinner({
|
||||
delay: 50,
|
||||
positions: 40,
|
||||
stream: process.stderr,
|
||||
height: height,
|
||||
width: width,
|
||||
hideCursor: true,
|
||||
//fontChar: '\u2588' // '\x1b[7m \x1b[m'
|
||||
fontChar: '#'
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createDistraction: createDistraction
|
||||
};
|
66
lib/do_account.js
Normal file
66
lib/do_account.js
Normal file
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2015 Joyent Inc.
|
||||
*
|
||||
* `triton account ...`
|
||||
*/
|
||||
|
||||
var common = require('./common');
|
||||
|
||||
function do_account(subcmd, opts, args, callback) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length !== 0) {
|
||||
callback(new Error('invalid args: ' + args));
|
||||
return;
|
||||
}
|
||||
|
||||
this.triton.cloudapi.getAccount(function (err, account) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(account));
|
||||
} else {
|
||||
// pretty print
|
||||
var dates = ['updated', 'created'];
|
||||
Object.keys(account).forEach(function (key) {
|
||||
var val = account[key];
|
||||
if (dates.indexOf(key) >= 0) {
|
||||
console.log('%s: %s (%s)', key, val,
|
||||
common.longAgo(new Date(val)));
|
||||
} else {
|
||||
console.log('%s: %s', key, val);
|
||||
}
|
||||
});
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
do_account.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON output.'
|
||||
}
|
||||
];
|
||||
do_account.help = (
|
||||
'Show account information\n'
|
||||
+ '\n'
|
||||
+ 'Usage:\n'
|
||||
+ ' {{name}} account\n'
|
||||
+ '\n'
|
||||
+ '{{options}}'
|
||||
);
|
||||
|
||||
do_account.aliases = ['whoami'];
|
||||
|
||||
module.exports = do_account;
|
@ -1,79 +0,0 @@
|
||||
/*
|
||||
* 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 account get ...`
|
||||
*/
|
||||
|
||||
var common = require('../common');
|
||||
|
||||
function do_get(subcmd, opts, args, callback) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length !== 0) {
|
||||
callback(new Error('invalid args: ' + args));
|
||||
return;
|
||||
}
|
||||
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
return;
|
||||
}
|
||||
tritonapi.cloudapi.getAccount(function (err, account) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(account));
|
||||
} else {
|
||||
// pretty print
|
||||
var dates = ['updated', 'created'];
|
||||
Object.keys(account).forEach(function (key) {
|
||||
var val = account[key];
|
||||
if (dates.indexOf(key) >= 0) {
|
||||
console.log('%s: %s (%s)', key, val,
|
||||
common.longAgo(new Date(val)));
|
||||
} else {
|
||||
console.log('%s: %s', key, val);
|
||||
}
|
||||
});
|
||||
}
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_get.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON output.'
|
||||
}
|
||||
];
|
||||
|
||||
do_get.synopses = ['{{name}} {{cmd}}'];
|
||||
|
||||
do_get.help = [
|
||||
'Show account information',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}'
|
||||
].join('\n');
|
||||
|
||||
module.exports = do_get;
|
@ -1,158 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton account update ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var fs = require('fs');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
var UPDATE_ACCOUNT_FIELDS
|
||||
= require('../cloudapi2').CloudApi.prototype.UPDATE_ACCOUNT_FIELDS;
|
||||
|
||||
|
||||
function do_update(subcmd, opts, args, callback) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
}
|
||||
|
||||
var log = this.log;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
|
||||
function gatherDataArgs(ctx, next) {
|
||||
if (opts.file) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ctx.data = common.objFromKeyValueArgs(args, {
|
||||
disableDotted: true,
|
||||
typeHintFromKey: UPDATE_ACCOUNT_FIELDS
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
},
|
||||
|
||||
function gatherDataFile(ctx, next) {
|
||||
if (!opts.file || opts.file === '-') {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
var input = fs.readFileSync(opts.file, 'utf8');
|
||||
try {
|
||||
ctx.data = JSON.parse(input);
|
||||
} catch (err) {
|
||||
next(new errors.TritonError(format(
|
||||
'invalid JSON for account update in "%s": %s',
|
||||
opts.file, err)));
|
||||
return;
|
||||
}
|
||||
next();
|
||||
},
|
||||
|
||||
function gatherDataStdin(ctx, next) {
|
||||
if (opts.file !== '-') {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
common.readStdin(function gotStdin(stdin) {
|
||||
try {
|
||||
ctx.data = JSON.parse(stdin);
|
||||
} catch (err) {
|
||||
log.trace({stdin: stdin},
|
||||
'invalid account update JSON on stdin');
|
||||
next(new errors.TritonError(format(
|
||||
'invalid JSON for account update on stdin: %s', err)));
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
function validateIt(ctx, next) {
|
||||
try {
|
||||
common.validateObject(ctx.data, UPDATE_ACCOUNT_FIELDS);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
|
||||
function updateAway(ctx, next) {
|
||||
var keys = Object.keys(ctx.data);
|
||||
|
||||
tritonapi.cloudapi.updateAccount(ctx.data, function (err) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
console.log('Updated account "%s" (fields: %s)',
|
||||
tritonapi.profile.account, keys.join(', '));
|
||||
next();
|
||||
});
|
||||
}
|
||||
]}, callback);
|
||||
}
|
||||
|
||||
do_update.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['file', 'f'],
|
||||
type: 'string',
|
||||
helpArg: 'FILE',
|
||||
help: 'A file holding a JSON file of updates, or "-" to read ' +
|
||||
'JSON from stdin.'
|
||||
}
|
||||
];
|
||||
|
||||
do_update.synopses = [
|
||||
'{{name}} {{cmd}} [FIELD=VALUE ...]',
|
||||
'{{name}} {{cmd}} -f JSON-FILE'
|
||||
];
|
||||
|
||||
do_update.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Update account information',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
|
||||
'Updateable fields:',
|
||||
' ' + Object.keys(UPDATE_ACCOUNT_FIELDS).sort().map(function (field) {
|
||||
return field + ' (' + UPDATE_ACCOUNT_FIELDS[field] + ')';
|
||||
}).join('\n '),
|
||||
|
||||
'',
|
||||
'Note that because of cross-data center replication of account information, ',
|
||||
'an update might not be immediately reflected in a get.'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_update.completionArgtypes = ['tritonupdateaccountfield'];
|
||||
|
||||
module.exports = do_update;
|
@ -1,50 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton account ...`
|
||||
*/
|
||||
|
||||
var Cmdln = require('cmdln').Cmdln;
|
||||
var util = require('util');
|
||||
|
||||
|
||||
|
||||
// ---- CLI class
|
||||
|
||||
function AccountCLI(top) {
|
||||
this.top = top;
|
||||
Cmdln.call(this, {
|
||||
name: top.name + ' account',
|
||||
/* BEGIN JSSTYLED */
|
||||
desc: [
|
||||
'Get and update your Spearhead account.'
|
||||
].join('\n'),
|
||||
/* END JSSTYLED */
|
||||
helpOpts: {
|
||||
minHelpCol: 24 /* line up with option help */
|
||||
},
|
||||
helpSubcmds: [
|
||||
'help',
|
||||
'get',
|
||||
'update'
|
||||
]
|
||||
});
|
||||
}
|
||||
util.inherits(AccountCLI, Cmdln);
|
||||
|
||||
AccountCLI.prototype.init = function init(opts, args, cb) {
|
||||
this.log = this.top.log;
|
||||
Cmdln.prototype.init.apply(this, arguments);
|
||||
};
|
||||
|
||||
AccountCLI.prototype.do_get = require('./do_get');
|
||||
AccountCLI.prototype.do_update = require('./do_update');
|
||||
|
||||
|
||||
module.exports = AccountCLI;
|
@ -1,11 +1,5 @@
|
||||
/*
|
||||
* 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.
|
||||
* Copyright (c) 2015 Joyent Inc. All rights reserved.
|
||||
*
|
||||
* `triton package ...`
|
||||
*/
|
||||
@ -18,7 +12,7 @@ var path = require('path');
|
||||
function do_badger(subcmd, opts, args, callback) {
|
||||
var callbackOnce = once(callback);
|
||||
var badger = path.resolve(__dirname, '../etc/badger');
|
||||
var input = fs.createReadStream(badger);
|
||||
var input = fs.createReadStream(badger)
|
||||
input.pipe(process.stdout);
|
||||
input.on('error', function (err) {
|
||||
callbackOnce(err);
|
||||
@ -26,10 +20,10 @@ function do_badger(subcmd, opts, args, callback) {
|
||||
input.on('end', function () {
|
||||
callbackOnce();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
do_badger.options = [];
|
||||
do_badger.help = 'Badger don\'t care.';
|
||||
do_badger.help = 'Rawr!';
|
||||
do_badger.hidden = true;
|
||||
|
||||
module.exports = do_badger;
|
||||
|
@ -1,97 +1,53 @@
|
||||
/*
|
||||
* 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.
|
||||
* Copyright 2015 Joyent Inc.
|
||||
*
|
||||
* `triton cloudapi ...`
|
||||
*/
|
||||
|
||||
var http = require('http');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('./common');
|
||||
var errors = require('./errors');
|
||||
|
||||
|
||||
function do_cloudapi(subcmd, opts, args, callback) {
|
||||
function do_cloudapi (subcmd, opts, args, callback) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length < 1) {
|
||||
} else if (args.length < 1 || args.length > 2) {
|
||||
callback(new Error('invalid arguments'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get `reqOpts` from given options.
|
||||
var method = opts.method;
|
||||
if (!method) {
|
||||
if (opts.data) {
|
||||
method = 'PUT';
|
||||
} else {
|
||||
method = 'GET';
|
||||
}
|
||||
var method = args[0];
|
||||
var path = args[1];
|
||||
if (path === undefined) {
|
||||
path = method;
|
||||
method = 'GET';
|
||||
}
|
||||
var reqOpts = {
|
||||
|
||||
var reqopts = {
|
||||
method: method.toLowerCase(),
|
||||
headers: {},
|
||||
path: args[0]
|
||||
path: path
|
||||
};
|
||||
if (opts.header) {
|
||||
for (var i = 0; i < opts.header.length; i++) {
|
||||
var raw = opts.header[i];
|
||||
var j = raw.indexOf(':');
|
||||
if (j < 0) {
|
||||
callback(new errors.TritonError(
|
||||
'failed to parse header: ' + raw));
|
||||
return;
|
||||
}
|
||||
var header = raw.substr(0, j);
|
||||
var value = raw.substr(j + 1).trimLeft();
|
||||
reqOpts.headers[header] = value;
|
||||
}
|
||||
}
|
||||
if (opts.data) {
|
||||
try {
|
||||
reqOpts.data = JSON.parse(opts.data);
|
||||
} catch (parseErr) {
|
||||
callback(new errors.TritonError(parseErr,
|
||||
'given DATA is not valid JSON: ' + parseErr.message));
|
||||
|
||||
this.triton.cloudapi._request(reqopts, function (err, req, res, body) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
vasync.pipeline({arg: {cli: this}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
|
||||
function callCloudapi(ctx, next) {
|
||||
var cloudapi = ctx.cli.tritonapi.cloudapi;
|
||||
cloudapi._request(reqOpts, function (err, req, res, body) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
if (opts.headers || reqOpts.method === 'head') {
|
||||
console.error('%s/%s %d %s',
|
||||
req.connection.encrypted ? 'HTTPS' : 'HTTP',
|
||||
res.httpVersion,
|
||||
res.statusCode,
|
||||
http.STATUS_CODES[res.statusCode]);
|
||||
Object.keys(res.headers).forEach(function (key) {
|
||||
console.error('%s: %s', key, res.headers[key]);
|
||||
});
|
||||
console.error();
|
||||
}
|
||||
|
||||
if (reqOpts.method !== 'head')
|
||||
console.log(JSON.stringify(body, null, 4));
|
||||
next();
|
||||
if (opts.headers || reqopts.method === 'head') {
|
||||
console.error('%s/%s %d %s',
|
||||
req.connection.encrypted ? 'HTTPS' : 'HTTP',
|
||||
res.httpVersion,
|
||||
res.statusCode,
|
||||
http.STATUS_CODES[res.statusCode]);
|
||||
Object.keys(res.headers).forEach(function (key) {
|
||||
console.error('%s: %s', key, res.headers[key]);
|
||||
});
|
||||
console.error();
|
||||
}
|
||||
]}, callback);
|
||||
|
||||
if (reqopts.method !== 'head')
|
||||
console.log(JSON.stringify(body, null, 4));
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
do_cloudapi.options = [
|
||||
@ -100,45 +56,20 @@ do_cloudapi.options = [
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['method', 'X'],
|
||||
type: 'string',
|
||||
helpArg: 'METHOD',
|
||||
help: 'Request method to use. Default is "GET".'
|
||||
},
|
||||
{
|
||||
names: ['header', 'H'],
|
||||
type: 'arrayOfString',
|
||||
helpArg: 'HEADER',
|
||||
help: 'Headers to send with request.'
|
||||
},
|
||||
{
|
||||
names: ['headers', 'i'],
|
||||
type: 'bool',
|
||||
help: 'Print response headers to stderr.'
|
||||
},
|
||||
{
|
||||
names: ['data', 'd'],
|
||||
type: 'string',
|
||||
helpArg: 'DATA',
|
||||
help: 'Add POST data. This must be valid JSON.'
|
||||
}
|
||||
];
|
||||
|
||||
do_cloudapi.synopses = [
|
||||
'{{name}} {{cmd}} [-X METHOD] [-H HEADER=VAL] [-d DATA] ENDPOINT'
|
||||
];
|
||||
|
||||
do_cloudapi.help = [
|
||||
'Raw cloudapi request.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Examples:',
|
||||
' {{name}} {{cmd}} /--ping',
|
||||
' {{name}} {{cmd}} /my/machines'
|
||||
].join('\n');
|
||||
do_cloudapi.help = (
|
||||
'Raw cloudapi request.\n'
|
||||
+ '\n'
|
||||
+ 'Usage:\n'
|
||||
+ ' {{name}} <method> <endpoint>\n'
|
||||
+ '\n'
|
||||
+ '{{options}}'
|
||||
);
|
||||
|
||||
do_cloudapi.hidden = true;
|
||||
|
||||
|
@ -1,84 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton completion ...`
|
||||
*/
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
var CloudApi = require('./cloudapi2').CloudApi;
|
||||
var UPDATE_ACCOUNT_FIELDS = CloudApi.prototype.UPDATE_ACCOUNT_FIELDS;
|
||||
var UPDATE_FWRULE_FIELDS = CloudApi.prototype.UPDATE_FWRULE_FIELDS;
|
||||
|
||||
|
||||
// Replace {{variable}} in `s` with the template data in `d`.
|
||||
function renderTemplate(s, d) {
|
||||
return s.replace(/{{([a-zA-Z_]+)}}/g, function (match, key) {
|
||||
return d.hasOwnProperty(key) ? d[key] : match;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function do_completion(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.raw) {
|
||||
console.log(this.bashCompletionSpec());
|
||||
} else {
|
||||
var specExtraIn = fs.readFileSync(
|
||||
path.join(__dirname, '../etc/triton-bash-completion-types.sh'),
|
||||
'utf8');
|
||||
var specExtra = renderTemplate(specExtraIn, {
|
||||
UPDATE_ACCOUNT_FIELDS: Object.keys(UPDATE_ACCOUNT_FIELDS).sort()
|
||||
.map(function (field) { return field + '='; }).join(' '),
|
||||
UPDATE_FWRULE_FIELDS: Object.keys(UPDATE_FWRULE_FIELDS).sort()
|
||||
.map(function (field) { return field + '='; }).join(' ')
|
||||
});
|
||||
console.log(this.bashCompletion({specExtra: specExtra}));
|
||||
}
|
||||
cb();
|
||||
}
|
||||
|
||||
do_completion.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['raw'],
|
||||
type: 'bool',
|
||||
hidden: true,
|
||||
help: 'Only output the Bash completion "spec". ' +
|
||||
'This is only useful for debugging.'
|
||||
}
|
||||
];
|
||||
do_completion.help = [
|
||||
'Emit bash completion. See help for installation.',
|
||||
'',
|
||||
'Installation (Mac):',
|
||||
' {{name}} completion > /usr/local/etc/bash_completion.d/{{name}} \\',
|
||||
' && source /usr/local/etc/bash_completion.d/{{name}}',
|
||||
'',
|
||||
'Installation (Linux):',
|
||||
' sudo {{name}} completion > /etc/bash_completion.d/{{name}} \\',
|
||||
' && source /etc/bash_completion.d/{{name}}',
|
||||
'',
|
||||
'Alternative installation:',
|
||||
' {{name}} completion > ~/.{{name}}.completion # or to whatever path',
|
||||
' echo "source ~/.{{name}}.completion" >> ~/.bashrc',
|
||||
'',
|
||||
'{{options}}'
|
||||
].join('\n');
|
||||
|
||||
module.exports = do_completion;
|
117
lib/do_create.js
117
lib/do_create.js
@ -1,29 +1,106 @@
|
||||
/*
|
||||
* 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 2017 Joyent, Inc.
|
||||
* Copyright (c) 2015 Joyent Inc. All rights reserved.
|
||||
*
|
||||
* `triton create ...` bwcompat shortcut for `triton instance create ...`.
|
||||
* `triton images ...`
|
||||
*/
|
||||
|
||||
var targ = require('./do_instance/do_create');
|
||||
var format = require('util').format;
|
||||
var tabula = require('tabula');
|
||||
|
||||
var errors = require('./errors');
|
||||
|
||||
|
||||
function do_create(subcmd, opts, args, callback) {
|
||||
this.handlerFromSubcmd('instance').dispatch({
|
||||
subcmd: 'create',
|
||||
opts: opts,
|
||||
args: args
|
||||
}, callback);
|
||||
}
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length > 1) {
|
||||
return callback(new Error('too many args: ' + args));
|
||||
}
|
||||
var triton = this.triton;
|
||||
|
||||
do_create.help = 'A shortcut for "spearhead instance create".\n' + targ.help;
|
||||
do_create.helpOpts = targ.helpOpts;
|
||||
do_create.synopses = targ.synopses;
|
||||
do_create.options = targ.options;
|
||||
do_create.completionArgtypes = targ.completionArgtypes;
|
||||
// XXX The smarts here should move to Triton class.
|
||||
|
||||
assert.string(opts.image, '--image <img>');
|
||||
assert.string(opts['package'], '--package <pkg>');
|
||||
assert.number(opts.count)
|
||||
|
||||
// XXX
|
||||
/*
|
||||
* Should all this move into sdc.createMachine? yes
|
||||
*
|
||||
* - lookup image, package, networks from args
|
||||
* - assign names
|
||||
* - start provisions (slight stagger, max N at a time)
|
||||
* - return immediately, or '-w|--wait'
|
||||
*/
|
||||
async.series([
|
||||
function lookups(next) {
|
||||
async.parallel([
|
||||
//XXX
|
||||
//sdc.lookup(image)
|
||||
])
|
||||
},
|
||||
function provisions(next) {
|
||||
|
||||
},
|
||||
function wait(next) {
|
||||
next();
|
||||
}
|
||||
], function (err) {
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
do_create.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['dc', 'd'],
|
||||
type: 'string',
|
||||
helpArg: '<dc>',
|
||||
help: 'The datacenter in which to provision. Required if the current'
|
||||
+ ' profile includes more than one datacenter. Use `sdc profile`'
|
||||
+ ' to list profiles and `sdc dcs` to list available datacenters.'
|
||||
},
|
||||
{
|
||||
names: ['image', 'i'],
|
||||
type: 'string',
|
||||
helpArg: '<img>',
|
||||
help: 'The machine image with which to provision. Required.'
|
||||
},
|
||||
{
|
||||
names: ['package', 'p'],
|
||||
type: 'string',
|
||||
helpArg: '<pkg>',
|
||||
help: 'The package or instance type for the new machine(s). Required.'
|
||||
},
|
||||
{
|
||||
names: ['name', 'n'],
|
||||
type: 'string',
|
||||
helpArg: '<name>',
|
||||
help: 'A name for the machine. If not specified, a short random name'
|
||||
+ ' will be generated.',
|
||||
// TODO: for count>1 support '%d' code in name: foo0, foo1, ...
|
||||
},
|
||||
{
|
||||
names: ['count', 'c'],
|
||||
type: 'positiveInteger',
|
||||
'default': 1,
|
||||
helpArg: '<n>',
|
||||
help: 'The number of machines to provision. Default is 1.'
|
||||
},
|
||||
];
|
||||
do_create.help = (
|
||||
'Create a new instance.\n'
|
||||
+ '\n'
|
||||
+ 'Usage:\n'
|
||||
+ ' {{name}} create <options>\n'
|
||||
+ '\n'
|
||||
+ '{{options}}'
|
||||
);
|
||||
do_create.aliases = ['create-inst'];
|
||||
|
||||
module.exports = do_create;
|
||||
|
227
lib/do_create_instance.js
Normal file
227
lib/do_create_instance.js
Normal file
@ -0,0 +1,227 @@
|
||||
/*
|
||||
* Copyright (c) 2015 Joyent Inc. All rights reserved.
|
||||
*
|
||||
* `triton create ...`
|
||||
*/
|
||||
|
||||
var bigspinner = require('bigspinner');
|
||||
var format = require('util').format;
|
||||
var path = require('path');
|
||||
var spawn = require('child_process').spawn;
|
||||
var tabula = require('tabula');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('./common');
|
||||
var errors = require('./errors');
|
||||
|
||||
|
||||
function do_create_instance(subcmd, opts, args, callback) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length < 1 || args.length > 2) {
|
||||
return callback(new errors.UsageError(format(
|
||||
'incorrect number of args (%d): %s', args.length, args.join(' '))));
|
||||
}
|
||||
|
||||
var log = this.triton.log;
|
||||
var cloudapi = this.triton.cloudapi;
|
||||
var cOpts = {};
|
||||
|
||||
vasync.pipeline({arg: {}, funcs: [
|
||||
function getImg(ctx, next) {
|
||||
// XXX don't get the image object if it is a UUID, waste of time
|
||||
self.triton.getImage(args[0], function (err, img) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
ctx.img = img;
|
||||
log.trace({img: img}, 'create-instance img');
|
||||
next();
|
||||
});
|
||||
},
|
||||
function getPkg(ctx, next) {
|
||||
if (args.length < 2) {
|
||||
return next();
|
||||
}
|
||||
// XXX don't get the package object if it is a UUID, waste of time
|
||||
self.triton.getPackage(args[1], function (err, pkg) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
log.trace({pkg: pkg}, 'create-instance pkg');
|
||||
ctx.pkg = pkg;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function getNets(ctx, next) {
|
||||
if (!opts.networks) {
|
||||
return next();
|
||||
}
|
||||
self.triton.getNetworks(opts.networks, function (err, nets) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
ctx.nets = nets;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function createInst(ctx, next) {
|
||||
var createOpts = {
|
||||
name: opts.name,
|
||||
image: ctx.img.id,
|
||||
'package': ctx.pkg && ctx.pkg.id,
|
||||
networks: ctx.nets && ctx.nets.map(
|
||||
function (net) { return net.id; })
|
||||
};
|
||||
log.trace({createOpts: createOpts}, 'create-instance createOpts');
|
||||
ctx.start = Date.now();
|
||||
cloudapi.createMachine(createOpts, function (err, inst) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
ctx.inst = inst;
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(inst));
|
||||
} else {
|
||||
console.log('Creating instance %s (%s, %s@%s, %s)',
|
||||
inst.name, inst.id, ctx.img.name, ctx.img.version,
|
||||
inst.package);
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
function maybeWait(ctx, next) {
|
||||
if (!opts.wait) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var spinner, child, killed;
|
||||
var died = 0;
|
||||
if (!opts.quiet && process.stderr.isTTY) {
|
||||
/*
|
||||
spinner = bigspinner.createSpinner({
|
||||
delay: 250,
|
||||
stream: process.stderr,
|
||||
height: process.stdout.rows - 2,
|
||||
width: process.stdout.columns - 1,
|
||||
hideCursor: true,
|
||||
fontChar: '#'
|
||||
});
|
||||
*/
|
||||
var game = path.join(__dirname, '../node_modules/.bin/snake-game');
|
||||
function makechild() {
|
||||
var _now = Date.now();
|
||||
child = spawn(game, {stdio: 'inherit'});
|
||||
child.on('close', function (code) {
|
||||
child = null;
|
||||
if (!killed) {
|
||||
var delta = Date.now() - _now;
|
||||
died++;
|
||||
console.error('[snake] survived %s',
|
||||
common.humanDurationFromMs(delta));
|
||||
makechild();
|
||||
} else {
|
||||
var ESC = '\u001b';
|
||||
var CSI = ESC + '[';
|
||||
process.stdout.write(CSI + '?25h');
|
||||
}
|
||||
});
|
||||
}
|
||||
makechild();
|
||||
}
|
||||
|
||||
cloudapi.waitForMachineStates({
|
||||
id: ctx.inst.id,
|
||||
states: ['running', 'failed']
|
||||
}, function (err, inst) {
|
||||
if (spinner) {
|
||||
spinner.destroy();
|
||||
}
|
||||
if (child) {
|
||||
killed = true;
|
||||
child.kill('SIGINT');
|
||||
console.error('[snake] died %d times!', died);
|
||||
}
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(inst));
|
||||
} else if (inst.state === 'running') {
|
||||
var dur = Date.now() - ctx.start;
|
||||
console.log('Created instance %s (%s) in %s',
|
||||
inst.name, inst.id, common.humanDurationFromMs(dur));
|
||||
}
|
||||
if (inst.state !== 'running') {
|
||||
next(new Error(format('failed to create instance %s (%s)',
|
||||
inst.name, inst.id)));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
]}, function (err) {
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
|
||||
do_create_instance.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
group: 'Create options'
|
||||
},
|
||||
{
|
||||
names: ['name', 'n'],
|
||||
type: 'string',
|
||||
help: 'Instance name. If not given, a random one will be created.'
|
||||
},
|
||||
// XXX arrayOfCommaSepString dashdash type
|
||||
//{
|
||||
// names: ['networks', 'nets'],
|
||||
// type: 'arrayOfCommaSepString',
|
||||
// help: 'One or more (comma-separated) networks IDs.'
|
||||
//},
|
||||
// XXX enable-firewall
|
||||
// XXX locality: near, far
|
||||
// XXX metadata, metadata-file
|
||||
// XXX script (user-script)
|
||||
// XXX tag
|
||||
{
|
||||
group: 'Other options'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Wait for the creation to complete.'
|
||||
},
|
||||
{
|
||||
names: ['quiet', 'q'],
|
||||
type: 'bool',
|
||||
help: 'No progress spinner while waiting.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
}
|
||||
];
|
||||
do_create_instance.help = (
|
||||
/* BEGIN JSSTYLED */
|
||||
'Create a new instance.\n' +
|
||||
'\n' +
|
||||
'Usage:\n' +
|
||||
' {{name}} create-instance [<options>] IMAGE [PACKAGE]\n' +
|
||||
'\n' +
|
||||
'{{options}}'
|
||||
/* END JSSTYLED */
|
||||
);
|
||||
|
||||
do_create_instance.aliases = ['create'];
|
||||
|
||||
module.exports = do_create_instance;
|
@ -1,97 +0,0 @@
|
||||
/*
|
||||
* 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 datacenters ...`
|
||||
*/
|
||||
|
||||
var tabula = require('tabula');
|
||||
|
||||
var common = require('./common');
|
||||
|
||||
// columns default without -o
|
||||
var columnsDefault = 'name,url';
|
||||
|
||||
// sort default with -s
|
||||
var sortDefault = 'name';
|
||||
|
||||
function do_datacenters(subcmd, opts, args, callback) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length !== 0) {
|
||||
callback(new Error('invalid args: ' + args));
|
||||
return;
|
||||
}
|
||||
|
||||
var columns = opts.o.split(',');
|
||||
var sort = opts.s.split(',');
|
||||
var tritonapi = this.tritonapi;
|
||||
|
||||
common.cliSetupTritonApi({cli: this}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
return;
|
||||
}
|
||||
tritonapi.cloudapi.listDatacenters(function (err, datacenters) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(datacenters));
|
||||
} else {
|
||||
/*
|
||||
* datacenters are returned in the form of:
|
||||
* {name: 'url', name2: 'url2', ...}
|
||||
* we "normalize" them for use by tabula by making them an array
|
||||
*/
|
||||
var dcs = [];
|
||||
Object.keys(datacenters).forEach(function (key) {
|
||||
dcs.push({
|
||||
name: key,
|
||||
url: datacenters[key]
|
||||
});
|
||||
});
|
||||
tabula(dcs, {
|
||||
skipHeader: opts.H,
|
||||
columns: columns,
|
||||
sort: sort,
|
||||
dottedLookup: true
|
||||
});
|
||||
}
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_datacenters.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
}
|
||||
].concat(common.getCliTableOptions({
|
||||
columnsDefault: columnsDefault,
|
||||
sortDefault: sortDefault
|
||||
}));
|
||||
|
||||
do_datacenters.synopses = ['{{name}} {{cmd}}'];
|
||||
|
||||
do_datacenters.help = [
|
||||
'Show datacenters in this cloud.',
|
||||
'A "cloud" is a set of related datacenters that share account',
|
||||
'information.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}'
|
||||
].join('\n');
|
||||
|
||||
module.exports = do_datacenters;
|
@ -1,30 +0,0 @@
|
||||
/*
|
||||
* 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 2017 Joyent, Inc.
|
||||
*
|
||||
* `triton delete ...` bwcompat shortcut for `triton instance delete ...`.
|
||||
*/
|
||||
|
||||
var targ = require('./do_instance/do_delete');
|
||||
|
||||
function do_delete(subcmd, opts, args, callback) {
|
||||
this.handlerFromSubcmd('instance').dispatch({
|
||||
subcmd: 'delete',
|
||||
opts: opts,
|
||||
args: args
|
||||
}, callback);
|
||||
}
|
||||
|
||||
do_delete.help = 'A shortcut for "spearhead instance delete".\n' + targ.help;
|
||||
do_delete.synopses = targ.synopses;
|
||||
do_delete.options = targ.options;
|
||||
do_delete.completionArgtypes = targ.completionArgtypes;
|
||||
|
||||
do_delete.aliases = ['rm'];
|
||||
|
||||
module.exports = do_delete;
|
98
lib/do_delete_instance.js
Normal file
98
lib/do_delete_instance.js
Normal file
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2015 Joyent Inc.
|
||||
*
|
||||
* `triton delete ...`
|
||||
*/
|
||||
|
||||
var common = require('./common');
|
||||
|
||||
function do_delete_instance(subcmd, opts, args, callback) {
|
||||
var self = this;
|
||||
|
||||
var now = Date.now();
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length !== 1) {
|
||||
callback(new Error('invalid args: ' + args));
|
||||
return;
|
||||
}
|
||||
|
||||
var arg = args[0];
|
||||
var uuid;
|
||||
|
||||
if (common.isUUID(arg)) {
|
||||
uuid = arg;
|
||||
go1();
|
||||
} else {
|
||||
self.triton.getMachineByAlias(arg, function (err, machine) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
uuid = machine.id;
|
||||
go1();
|
||||
});
|
||||
}
|
||||
|
||||
function go1() {
|
||||
// called when "uuid" is set
|
||||
self.triton.cloudapi.deleteMachine(uuid, function (err, body, res) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts.wait) {
|
||||
console.log('Deleted (async) instance %s (id %s, %s)',
|
||||
arg, uuid, common.humanDurationFromMs(Date.now() - now));
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
self.triton.cloudapi.waitForMachineStates({
|
||||
id: uuid,
|
||||
states: ['deleted']
|
||||
}, function (err, machine, res) {
|
||||
if (res && res.statusCode === 410) {
|
||||
// gone... success!
|
||||
console.log('Deleted instance %s (id %s, %s)',
|
||||
arg, uuid, common.humanDurationFromMs(Date.now() - now));
|
||||
callback();
|
||||
return;
|
||||
} else if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
callback(new Error('unknown state'));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
do_delete_instance.aliases = ['delete'];
|
||||
|
||||
do_delete_instance.help = [
|
||||
'delete a single instance.',
|
||||
'',
|
||||
'Usage:',
|
||||
' {{name}} delete <alias|id>',
|
||||
'',
|
||||
'{{options}}'
|
||||
].join('\n');
|
||||
do_delete_instance.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Wait for machine to be deleted.'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
module.exports = do_delete_instance;
|
237
lib/do_env.js
237
lib/do_env.js
@ -1,237 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Joyent Inc.
|
||||
*
|
||||
* `triton env ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var strsplit = require('strsplit');
|
||||
var sshpk = require('sshpk');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('./common');
|
||||
var errors = require('./errors');
|
||||
var mod_config = require('./config');
|
||||
|
||||
|
||||
|
||||
function do_env(subcmd, opts, args, cb) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
if (args.length > 1) {
|
||||
return cb(new errors.UsageError('too many arguments'));
|
||||
}
|
||||
|
||||
var profileName = args[0] || this.tritonapi.profile.name;
|
||||
var allClientTypes = ['triton', 'docker', 'smartdc'];
|
||||
var clientTypes = [];
|
||||
var explicit;
|
||||
var shortOpts = '';
|
||||
if (opts.triton) {
|
||||
shortOpts += 't';
|
||||
clientTypes.push('triton');
|
||||
}
|
||||
if (opts.docker) {
|
||||
shortOpts += 'd';
|
||||
clientTypes.push('docker');
|
||||
}
|
||||
if (opts.smartdc) {
|
||||
shortOpts += 's';
|
||||
clientTypes.push('smartdc');
|
||||
}
|
||||
if (opts.unset) {
|
||||
shortOpts += 'u';
|
||||
}
|
||||
if (clientTypes.length === 0) {
|
||||
explicit = false;
|
||||
clientTypes = allClientTypes;
|
||||
} else {
|
||||
explicit = true;
|
||||
}
|
||||
|
||||
try {
|
||||
var profile = mod_config.loadProfile({
|
||||
configDir: this.configDir,
|
||||
name: profileName
|
||||
});
|
||||
} catch (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (profile.name === this.tritonapi.profile.name) {
|
||||
this._applyProfileOverrides(profile);
|
||||
}
|
||||
|
||||
var p = console.log;
|
||||
clientTypes.forEach(function (clientType) {
|
||||
switch (clientType) {
|
||||
case 'triton':
|
||||
p('# triton');
|
||||
if (opts.unset) {
|
||||
[
|
||||
'SC_PROFILE',
|
||||
'SC_URL',
|
||||
'SC_ACCOUNT',
|
||||
'SC_USER',
|
||||
'SC_KEY_ID',
|
||||
'SC_TLS_INSECURE'
|
||||
].forEach(function (key) {
|
||||
p('unset %s', key);
|
||||
});
|
||||
} else {
|
||||
p('export SC_PROFILE="%s"', profile.name);
|
||||
}
|
||||
break;
|
||||
case 'docker':
|
||||
p('# docker');
|
||||
var setupJson = path.resolve(self.configDir, 'docker',
|
||||
common.profileSlug(profile), 'setup.json');
|
||||
if (fs.existsSync(setupJson)) {
|
||||
var setup;
|
||||
try {
|
||||
setup = JSON.parse(fs.readFileSync(setupJson));
|
||||
} catch (err) {
|
||||
cb(new errors.ConfigError(err, format(
|
||||
'error determining Docker environment from "%s": %s',
|
||||
setupJson, err)));
|
||||
return;
|
||||
}
|
||||
Object.keys(setup.env).forEach(function (key) {
|
||||
var val = setup.env[key];
|
||||
if (opts.unset || val === null) {
|
||||
p('unset %s', key);
|
||||
} else {
|
||||
p('export %s=%s', key, val);
|
||||
}
|
||||
});
|
||||
} else if (opts.unset) {
|
||||
[
|
||||
'DOCKER_HOST',
|
||||
'DOCKER_CERT_PATH',
|
||||
'DOCKER_TLS_VERIFY',
|
||||
'COMPOSE_HTTP_TIMEOUT'
|
||||
].forEach(function (key) {
|
||||
p('unset %s', key);
|
||||
});
|
||||
} else if (explicit) {
|
||||
cb(new errors.ConfigError(format('could not find Docker '
|
||||
+ 'environment setup for profile "%s":\n Run `triton '
|
||||
+ 'profile docker-setup %s` to setup.',
|
||||
profile.name, profile.name)));
|
||||
}
|
||||
break;
|
||||
case 'smartdc':
|
||||
p('# smartdc');
|
||||
if (opts.unset) {
|
||||
[
|
||||
'SDC_URL',
|
||||
'SDC_ACCOUNT',
|
||||
'SDC_USER',
|
||||
'SDC_KEY_ID',
|
||||
'SDC_TESTING'
|
||||
].forEach(function (key) {
|
||||
p('unset %s', key);
|
||||
});
|
||||
} else {
|
||||
p('export SDC_URL="%s"', profile.url);
|
||||
p('export SDC_ACCOUNT="%s"', profile.account);
|
||||
if (profile.user) {
|
||||
p('export SDC_USER="%s"', profile.user);
|
||||
} else {
|
||||
p('unset SDC_USER');
|
||||
}
|
||||
p('export SDC_KEY_ID="%s"', profile.keyId);
|
||||
if (profile.insecure) {
|
||||
p('export SDC_TESTING="%s"', profile.insecure);
|
||||
} else {
|
||||
p('unset SDC_TESTING');
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return cb(new errors.InternalError(
|
||||
'unknown clientType: ' + clientType));
|
||||
}
|
||||
});
|
||||
|
||||
p('# Run this command to configure your shell:');
|
||||
p('# eval "$(spearhead env%s%s)"',
|
||||
(shortOpts ? ' -'+shortOpts : ''),
|
||||
(profile.name === this.tritonapi.profile.name
|
||||
? '' : ' ' + profile.name));
|
||||
cb();
|
||||
}
|
||||
|
||||
do_env.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
group: ''
|
||||
},
|
||||
{
|
||||
names: ['triton', 't'],
|
||||
type: 'bool',
|
||||
help: 'Emit environment commands for node-triton itself (i.e. the ' +
|
||||
'"SC_PROFILE" variable).'
|
||||
},
|
||||
{
|
||||
names: ['docker', 'd'],
|
||||
type: 'bool',
|
||||
help: 'Emit environment commands for docker ("DOCKER_HOST" et al).'
|
||||
},
|
||||
{
|
||||
names: ['smartdc', 's'],
|
||||
type: 'bool',
|
||||
help: 'Emit environment for node-smartdc (i.e. the legacy ' +
|
||||
'"SDC_*" variables).'
|
||||
},
|
||||
{
|
||||
group: ''
|
||||
},
|
||||
{
|
||||
names: ['unset', 'u'],
|
||||
type: 'bool',
|
||||
help: 'Emit environment to *unset* the relevant environment variables.'
|
||||
}
|
||||
];
|
||||
|
||||
do_env.synopses = ['{{name}} {{cmd}} [PROFILE]'];
|
||||
|
||||
do_env.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Emit shell commands to setup environment.',
|
||||
'',
|
||||
'Supported "clients" here are: node-smartdc (i.e. the `sdc-*` tools),',
|
||||
'node-triton and spearhead-node. By default this emits the environment ',
|
||||
'for all supported tools. Use options to be specific.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'If no options are given, environment variables are emitted for all ',
|
||||
'clients. If PROFILE is not given, the current profile is used.',
|
||||
'',
|
||||
'The following Bash function can be added to one\'s "~/.bashrc" to quickly',
|
||||
'change between Spearhead profiles:',
|
||||
' triton-select () { eval "$(triton env $1)"; }',
|
||||
'for example:',
|
||||
' $ triton-select west1',
|
||||
' $ triton profile get | grep name',
|
||||
' name: west1',
|
||||
' $ triton-select east1',
|
||||
' $ triton profile get | grep name',
|
||||
' name: east1'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_env.completionArgtypes = ['tritonprofile', 'none'];
|
||||
|
||||
module.exports = do_env;
|
@ -1,120 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton fwrule create ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
function do_create(subcmd, opts, args, cb) {
|
||||
assert.optionalString(opts.description, 'opts.description');
|
||||
assert.optionalBool(opts.disabled, 'opts.disabled');
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
if (args.length === 0) {
|
||||
cb(new errors.UsageError('missing FWRULE argument'));
|
||||
return;
|
||||
} else if (args.length > 1) {
|
||||
cb(new errors.UsageError('incorrect number of arguments'));
|
||||
return;
|
||||
}
|
||||
|
||||
var createOpts = {
|
||||
rule: args[0]
|
||||
};
|
||||
if (!opts.disabled) {
|
||||
createOpts.enabled = true;
|
||||
}
|
||||
if (opts.description) {
|
||||
createOpts.description = opts.description;
|
||||
}
|
||||
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
tritonapi.cloudapi.createFirewallRule(
|
||||
createOpts, function (err, fwrule) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
console.log('Created firewall rule %s%s', fwrule.id,
|
||||
(!fwrule.enabled ? ' (disabled)' : ''));
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_create.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
},
|
||||
{
|
||||
names: ['disabled', 'd'],
|
||||
type: 'bool',
|
||||
help: 'Disable the created firewall rule. By default a created '
|
||||
+ 'firewall rule is enabled. Use "spearhead fwrule enable" '
|
||||
+ 'to enable it later.'
|
||||
},
|
||||
{
|
||||
names: ['description', 'D'],
|
||||
type: 'string',
|
||||
helpArg: 'DESC',
|
||||
help: 'Description of the firewall rule.'
|
||||
}
|
||||
];
|
||||
|
||||
do_create.synopses = ['{{name}} {{cmd}} [OPTIONS] RULE-TEXT'];
|
||||
|
||||
do_create.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Create a firewall rule.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Examples:',
|
||||
' # Allow SSH access from any IP to all instances in a datacenter.',
|
||||
' spearhead fwrule create -D "ssh" "FROM any TO all vms ALLOW tcp PORT 22"',
|
||||
'',
|
||||
' # Allow SSH access to a specific instance.',
|
||||
' spearhead fwrule create \\',
|
||||
' "FROM any TO vm ba2c95e9-1cdf-4295-8253-3fee371374d9 ALLOW tcp PORT 22"'
|
||||
// TODO: link to
|
||||
// https://github.com/joyent/sdc-fwrule/blob/master/docs/examples.md
|
||||
// or docs.jo Cloud Firewall examples? What link? Ditto in parent.
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_create.helpOpts = {
|
||||
helpCol: 25
|
||||
};
|
||||
|
||||
module.exports = do_create;
|
@ -1,116 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton snapshot delete ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
function do_delete(subcmd, opts, args, cb) {
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length < 1) {
|
||||
cb(new errors.UsageError('missing FWRULE argument(s)'));
|
||||
return;
|
||||
}
|
||||
|
||||
var tritonapi = this.top.tritonapi;
|
||||
var ruleIds = args;
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function confirm(_, next) {
|
||||
if (opts.force) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var msg;
|
||||
if (ruleIds.length === 1) {
|
||||
msg = 'Delete firewall rule "' + ruleIds[0] + '"? [y/n] ';
|
||||
} else {
|
||||
msg = format('Delete %d firewall rules (%s)? [y/n] ',
|
||||
ruleIds.length, ruleIds.join(', '));
|
||||
}
|
||||
|
||||
common.promptYesNo({msg: msg}, function (answer) {
|
||||
if (answer !== 'y') {
|
||||
console.error('Aborting');
|
||||
next(true); // early abort signal
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
function deleteThem(_, next) {
|
||||
vasync.forEachParallel({
|
||||
inputs: ruleIds,
|
||||
func: function deleteOne(id, nextId) {
|
||||
tritonapi.deleteFirewallRule({
|
||||
id: id
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
nextId(err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Deleted rule %s', id);
|
||||
nextId();
|
||||
});
|
||||
}
|
||||
}, next);
|
||||
}
|
||||
]}, function (err) {
|
||||
if (err === true) {
|
||||
err = null;
|
||||
}
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_delete.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['force', 'f'],
|
||||
type: 'bool',
|
||||
help: 'Skip confirmation of delete.'
|
||||
}
|
||||
];
|
||||
|
||||
do_delete.synopses = ['{{name}} {{cmd}} FWRULE [FWRULE ...]'];
|
||||
|
||||
do_delete.help = [
|
||||
'Remove a firewall rule.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where FWRULE is a firewall rule id (full UUID) or short id.'
|
||||
].join('\n');
|
||||
|
||||
do_delete.aliases = ['rm'];
|
||||
|
||||
do_delete.completionArgtypes = ['tritonfwrule'];
|
||||
|
||||
module.exports = do_delete;
|
@ -1,78 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton fwrule disable ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
function do_disable(subcmd, opts, args, cb) {
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
cb(new errors.UsageError('missing FWRULE argument(s)'));
|
||||
return;
|
||||
}
|
||||
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
vasync.forEachParallel({
|
||||
inputs: args,
|
||||
func: function disableOne(id, nextId) {
|
||||
tritonapi.disableFirewallRule({ id: id }, function (err) {
|
||||
if (err) {
|
||||
nextId(err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Disabled firewall rule %s', id);
|
||||
nextId();
|
||||
});
|
||||
}
|
||||
}, cb);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_disable.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
}
|
||||
];
|
||||
|
||||
do_disable.synopses = ['{{name}} {{cmd}} FWRULE [FWRULE ...]'];
|
||||
|
||||
do_disable.help = [
|
||||
'Disable a specific firewall rule.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where FWRULE is a firewall rule id (full UUID) or short id.'
|
||||
].join('\n');
|
||||
|
||||
do_disable.completionArgtypes = ['tritonfwrule'];
|
||||
|
||||
module.exports = do_disable;
|
@ -1,78 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton fwrule enable ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
function do_enable(subcmd, opts, args, cb) {
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
cb(new errors.UsageError('missing FWRULE argument(s)'));
|
||||
return;
|
||||
}
|
||||
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
vasync.forEachParallel({
|
||||
inputs: args,
|
||||
func: function enableOne(id, nextId) {
|
||||
tritonapi.enableFirewallRule({ id: id }, function (err) {
|
||||
if (err) {
|
||||
nextId(err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Enabled firewall rule %s', id);
|
||||
nextId();
|
||||
});
|
||||
}
|
||||
}, cb);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_enable.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
}
|
||||
];
|
||||
|
||||
do_enable.synopses = ['{{name}} {{cmd}} FWRULE [FWRULE ...]'];
|
||||
|
||||
do_enable.help = [
|
||||
'Enable a specific firewall rule.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where FWRULE is a firewall rule id (full UUID) or short id.'
|
||||
].join('\n');
|
||||
|
||||
do_enable.completionArgtypes = ['tritonfwrule'];
|
||||
|
||||
module.exports = do_enable;
|
@ -1,87 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton fwrule get ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
function do_get(subcmd, opts, args, cb) {
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
cb(new errors.UsageError('missing FWRULE argument'));
|
||||
return;
|
||||
} else if (args.length > 1) {
|
||||
cb(new errors.UsageError('incorrect number of arguments'));
|
||||
return;
|
||||
}
|
||||
|
||||
var id = args[0];
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
tritonapi.getFirewallRule(id, function onRule(err, fwrule) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(fwrule));
|
||||
} else {
|
||||
console.log(JSON.stringify(fwrule, null, 4));
|
||||
}
|
||||
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_get.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
}
|
||||
];
|
||||
|
||||
do_get.synopses = ['{{name}} {{cmd}} FWRULE'];
|
||||
|
||||
do_get.help = [
|
||||
'Show a specific firewall rule.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where FWRULE is a firewall rule id (full UUID) or short id.'
|
||||
].join('\n');
|
||||
|
||||
do_get.completionArgtypes = ['tritonfwrule', 'none'];
|
||||
|
||||
module.exports = do_get;
|
@ -1,179 +0,0 @@
|
||||
/*
|
||||
* 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 2018 Joyent, Inc.
|
||||
*
|
||||
* `triton fwrule instances ...`
|
||||
*/
|
||||
|
||||
var format = require('util').format;
|
||||
var tabula = require('tabula');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
var COLUMNS_DEFAULT = 'shortid,name,img,state,flags,age';
|
||||
var COLUMNS_LONG = 'id,name,img,brand,package,state,flags,primaryIp,created';
|
||||
var SORT_DEFAULT = 'created';
|
||||
|
||||
|
||||
function do_instances(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
cb(new errors.UsageError('missing FWRULE argument'));
|
||||
return;
|
||||
} else if (args.length > 1) {
|
||||
cb(new errors.UsageError('incorrect number of arguments'));
|
||||
return;
|
||||
}
|
||||
|
||||
var id = args[0];
|
||||
|
||||
var columns = COLUMNS_DEFAULT;
|
||||
if (opts.o) {
|
||||
columns = opts.o;
|
||||
} else if (opts.long) {
|
||||
columns = COLUMNS_LONG;
|
||||
}
|
||||
columns = columns.split(',');
|
||||
|
||||
var sort = opts.s.split(',');
|
||||
|
||||
var imgs;
|
||||
var insts;
|
||||
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
vasync.parallel({funcs: [
|
||||
function getTheImages(next) {
|
||||
tritonapi.listImages({
|
||||
useCache: true,
|
||||
state: 'all'
|
||||
}, function (err, _imgs) {
|
||||
if (err) {
|
||||
next(err);
|
||||
} else {
|
||||
imgs = _imgs;
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
function getTheMachines(next) {
|
||||
tritonapi.listFirewallRuleInstances({
|
||||
id: id
|
||||
}, function (err, _insts) {
|
||||
if (err) {
|
||||
next(err);
|
||||
} else {
|
||||
insts = _insts;
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
]}, function (err, results) {
|
||||
/*
|
||||
* Error handling: vasync.parallel's `err` is always a
|
||||
* MultiError. We want to prefer the `getTheMachines` err,
|
||||
* e.g. if both get a self-signed cert error.
|
||||
*/
|
||||
if (err) {
|
||||
err = results.operations[1].err || err;
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
// map "uuid" => "image_name"
|
||||
var imgmap = {};
|
||||
imgs.forEach(function (img) {
|
||||
imgmap[img.id] = format('%s@%s', img.name, img.version);
|
||||
});
|
||||
|
||||
// Add extra fields for nice output.
|
||||
var now = new Date();
|
||||
insts.forEach(function (inst) {
|
||||
var created = new Date(inst.created);
|
||||
inst.age = common.longAgo(created, now);
|
||||
inst.img = imgmap[inst.image] ||
|
||||
common.uuidToShortId(inst.image);
|
||||
inst.shortid = inst.id.split('-', 1)[0];
|
||||
var flags = [];
|
||||
if (inst.brand === 'bhyve') flags.push('B');
|
||||
if (inst.docker) flags.push('D');
|
||||
if (inst.firewall_enabled) flags.push('F');
|
||||
if (inst.brand === 'kvm') flags.push('K');
|
||||
if (inst.deletion_protection) flags.push('P');
|
||||
inst.flags = flags.length ? flags.join('') : undefined;
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
common.jsonStream(insts);
|
||||
} else {
|
||||
tabula(insts, {
|
||||
skipHeader: opts.H,
|
||||
columns: columns,
|
||||
sort: sort,
|
||||
dottedLookup: true
|
||||
});
|
||||
}
|
||||
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_instances.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
}
|
||||
].concat(common.getCliTableOptions({
|
||||
includeLong: true,
|
||||
sortDefault: SORT_DEFAULT
|
||||
}));
|
||||
|
||||
do_instances.synopses = ['{{name}} {{cmd}} [OPTIONS] FWRULE'];
|
||||
|
||||
do_instances.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'List instances to which a firewall rule applies',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where FWRULE is a firewall rule id (full UUID) or short id.',
|
||||
'',
|
||||
'Fields (most are self explanatory, "*" indicates a field added client-side',
|
||||
'for convenience):',
|
||||
' shortid* A short ID prefix.',
|
||||
' flags* Single letter flags summarizing some fields:',
|
||||
' "B" the brand is "bhyve"',
|
||||
' "D" docker instance',
|
||||
' "F" firewall is enabled',
|
||||
' "K" the brand is "kvm"',
|
||||
' "P" deletion protected',
|
||||
' age* Approximate time since created, e.g. 1y, 2w.',
|
||||
' img* The image "name@version", if available, else its',
|
||||
' "shortid".'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_instances.aliases = ['insts'];
|
||||
|
||||
do_instances.completionArgtypes = ['tritonfwrule', 'none'];
|
||||
|
||||
module.exports = do_instances;
|
@ -1,105 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton fwrule list ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var tabula = require('tabula');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
var COLUMNS_DEFAULT = 'shortid,enabled,global,rule';
|
||||
var COLUMNS_LONG = 'id,enabled,global,rule,description';
|
||||
var SORT_DEFAULT = 'rule';
|
||||
|
||||
|
||||
function do_list(subcmd, opts, args, cb) {
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length > 0) {
|
||||
cb(new errors.UsageError('incorrect number of arguments'));
|
||||
return;
|
||||
}
|
||||
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
tritonapi.cloudapi.listFirewallRules({}, function onRules(err, rules) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
common.jsonStream(rules);
|
||||
} else {
|
||||
var columns = COLUMNS_DEFAULT;
|
||||
|
||||
if (opts.o) {
|
||||
columns = opts.o;
|
||||
} else if (opts.long) {
|
||||
columns = COLUMNS_LONG;
|
||||
}
|
||||
|
||||
columns = columns.toLowerCase().split(',');
|
||||
var sort = opts.s.toLowerCase().split(',');
|
||||
|
||||
if (columns.indexOf('shortid') !== -1) {
|
||||
rules.forEach(function (rule) {
|
||||
rule.shortid = common.uuidToShortId(rule.id);
|
||||
});
|
||||
}
|
||||
|
||||
tabula(rules, {
|
||||
skipHeader: opts.H,
|
||||
columns: columns,
|
||||
sort: sort
|
||||
});
|
||||
}
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_list.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
}
|
||||
].concat(common.getCliTableOptions({
|
||||
includeLong: true,
|
||||
sortDefault: SORT_DEFAULT
|
||||
}));
|
||||
|
||||
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS]'];
|
||||
|
||||
do_list.help = [
|
||||
'Show all firewall rules.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}'
|
||||
].join('\n');
|
||||
|
||||
do_list.aliases = ['ls'];
|
||||
|
||||
module.exports = do_list;
|
@ -1,169 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton fwrule update ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var fs = require('fs');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
var UPDATE_FWRULE_FIELDS
|
||||
= require('../cloudapi2').CloudApi.prototype.UPDATE_FWRULE_FIELDS;
|
||||
|
||||
|
||||
function do_update(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
var log = this.log;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
if (args.length === 0) {
|
||||
cb(new errors.UsageError('missing FWRULE argument'));
|
||||
return;
|
||||
}
|
||||
|
||||
var id = args.shift();
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
|
||||
function gatherDataArgs(ctx, next) {
|
||||
if (opts.file) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ctx.data = common.objFromKeyValueArgs(args, {
|
||||
disableDotted: true,
|
||||
typeHintFromKey: UPDATE_FWRULE_FIELDS
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
|
||||
function gatherDataFile(ctx, next) {
|
||||
if (!opts.file || opts.file === '-') {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
var input = fs.readFileSync(opts.file, 'utf8');
|
||||
|
||||
try {
|
||||
ctx.data = JSON.parse(input);
|
||||
} catch (err) {
|
||||
next(new errors.TritonError(format(
|
||||
'invalid JSON for firewall rule update in "%s": %s',
|
||||
opts.file, err)));
|
||||
return;
|
||||
}
|
||||
next();
|
||||
},
|
||||
|
||||
function gatherDataStdin(ctx, next) {
|
||||
if (opts.file !== '-') {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
common.readStdin(function gotStdin(stdin) {
|
||||
try {
|
||||
ctx.data = JSON.parse(stdin);
|
||||
} catch (err) {
|
||||
log.trace({stdin: stdin},
|
||||
'invalid firewall rule update JSON on stdin');
|
||||
next(new errors.TritonError(format(
|
||||
'invalid JSON for firewall rule update on stdin: %s',
|
||||
err)));
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
function validateIt(ctx, next) {
|
||||
try {
|
||||
common.validateObject(ctx.data, UPDATE_FWRULE_FIELDS);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
|
||||
function updateAway(ctx, next) {
|
||||
var data = ctx.data;
|
||||
data.id = id;
|
||||
|
||||
tritonapi.updateFirewallRule(data, function (err) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
delete data.id;
|
||||
console.log('Updated firewall rule %s (fields: %s)', id,
|
||||
Object.keys(data).join(', '));
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
]}, cb);
|
||||
}
|
||||
|
||||
do_update.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['file', 'f'],
|
||||
type: 'string',
|
||||
helpArg: 'JSON-FILE',
|
||||
help: 'A file holding a JSON file of updates, or "-" to read ' +
|
||||
'JSON from stdin.'
|
||||
}
|
||||
];
|
||||
|
||||
do_update.synopses = [
|
||||
'{{name}} {{cmd}} FWRULE [FIELD=VALUE ...]',
|
||||
'{{name}} {{cmd}} -f JSON-FILE FWRULE'
|
||||
];
|
||||
do_update.help = [
|
||||
'Update a firewall rule',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
|
||||
'Updateable fields:',
|
||||
' ' + Object.keys(UPDATE_FWRULE_FIELDS).sort().map(function (f) {
|
||||
return f + ' (' + UPDATE_FWRULE_FIELDS[f] + ')';
|
||||
}).join('\n '),
|
||||
'',
|
||||
'Where FWRULE is a firewall rule id (full UUID) or short id.'
|
||||
].join('\n');
|
||||
|
||||
do_update.completionArgtypes = ['tritonfwrule', 'tritonupdatefwrulefield'];
|
||||
|
||||
module.exports = do_update;
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton fwrule ...`
|
||||
*/
|
||||
|
||||
var Cmdln = require('cmdln').Cmdln;
|
||||
var util = require('util');
|
||||
|
||||
|
||||
|
||||
// ---- CLI class
|
||||
|
||||
function FirewallRuleCLI(top) {
|
||||
this.top = top;
|
||||
|
||||
Cmdln.call(this, {
|
||||
name: top.name + ' fwrule',
|
||||
desc: 'List and manage Spearhead firewall rules.',
|
||||
helpSubcmds: [
|
||||
'help',
|
||||
'list',
|
||||
'get',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
{ group: '' },
|
||||
'enable',
|
||||
'disable',
|
||||
'instances'
|
||||
],
|
||||
helpOpts: {
|
||||
minHelpCol: 23
|
||||
}
|
||||
});
|
||||
}
|
||||
util.inherits(FirewallRuleCLI, Cmdln);
|
||||
|
||||
FirewallRuleCLI.prototype.init = function init(opts, args, cb) {
|
||||
this.log = this.top.log;
|
||||
Cmdln.prototype.init.apply(this, arguments);
|
||||
};
|
||||
|
||||
FirewallRuleCLI.prototype.do_list = require('./do_list');
|
||||
FirewallRuleCLI.prototype.do_create = require('./do_create');
|
||||
FirewallRuleCLI.prototype.do_get = require('./do_get');
|
||||
FirewallRuleCLI.prototype.do_update = require('./do_update');
|
||||
FirewallRuleCLI.prototype.do_delete = require('./do_delete');
|
||||
FirewallRuleCLI.prototype.do_enable = require('./do_enable');
|
||||
FirewallRuleCLI.prototype.do_disable = require('./do_disable');
|
||||
FirewallRuleCLI.prototype.do_instances = require('./do_instances');
|
||||
|
||||
module.exports = FirewallRuleCLI;
|
@ -1,30 +0,0 @@
|
||||
/*
|
||||
* 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 2017 Joyent, Inc.
|
||||
*
|
||||
* `triton fwrules ...` shortcut for `triton fwrule list ...`.
|
||||
*/
|
||||
|
||||
var targ = require('./do_fwrule/do_list');
|
||||
|
||||
function do_fwrules(subcmd, opts, args, callback) {
|
||||
this.handlerFromSubcmd('fwrule').dispatch({
|
||||
subcmd: 'list',
|
||||
opts: opts,
|
||||
args: args
|
||||
}, callback);
|
||||
}
|
||||
|
||||
do_fwrules.help = 'A shortcut for "spearhead fwrule list".\n' + targ.help;
|
||||
do_fwrules.synopses = targ.synopses;
|
||||
do_fwrules.options = targ.options;
|
||||
do_fwrules.completionArgtypes = targ.completionArgtypes;
|
||||
|
||||
do_fwrules.hidden = true;
|
||||
|
||||
module.exports = do_fwrules;
|
66
lib/do_image.js
Normal file
66
lib/do_image.js
Normal file
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) 2015 Joyent Inc. All rights reserved.
|
||||
*
|
||||
* `triton image ...`
|
||||
*/
|
||||
|
||||
var format = require('util').format;
|
||||
|
||||
var errors = require('./errors');
|
||||
|
||||
|
||||
function do_image(subcmd, opts, args, callback) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length !== 1) {
|
||||
return callback(new errors.UsageError(format(
|
||||
'incorrect number of args (%d): %s', args.length, args.join(' '))));
|
||||
}
|
||||
|
||||
this.triton.getImage(args[0], function onRes(err, img) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(img));
|
||||
} else {
|
||||
console.log(JSON.stringify(img, null, 4));
|
||||
}
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
do_image.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
}
|
||||
];
|
||||
do_image.help = (
|
||||
/* BEGIN JSSTYLED */
|
||||
'Get an image.\n' +
|
||||
'\n' +
|
||||
'Usage:\n' +
|
||||
' {{name}} image [<options>] ID|NAME\n' +
|
||||
'\n' +
|
||||
'{{options}}' +
|
||||
'\n' +
|
||||
'If there are more than one image with the given "NAME", the latest\n' +
|
||||
'image (by "published_at") is returned.\n' +
|
||||
'\n' +
|
||||
'Note: Currently this dumps prettified JSON by default. That might change\n' +
|
||||
'in the future. Use "-j" to explicitly get JSON output.\n'
|
||||
/* END JSSTYLED */
|
||||
);
|
||||
|
||||
do_image.aliases = ['img'];
|
||||
|
||||
module.exports = do_image;
|
@ -1,107 +0,0 @@
|
||||
/*
|
||||
* 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 (c) 2018, Joyent, Inc.
|
||||
*
|
||||
* `triton image clone ...`
|
||||
*/
|
||||
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
// ---- the command
|
||||
|
||||
function do_clone(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length !== 1) {
|
||||
cb(new errors.UsageError(
|
||||
'incorrect number of args: expected 1, got ' + args.length));
|
||||
return;
|
||||
}
|
||||
|
||||
var log = this.top.log;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function cloneImage(ctx, next) {
|
||||
log.trace({dryRun: opts.dry_run, account: ctx.account},
|
||||
'image clone account');
|
||||
|
||||
if (opts.dry_run) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
tritonapi.cloneImage({image: args[0]}, function _cloneCb(err, img) {
|
||||
if (err) {
|
||||
next(new errors.TritonError(err, 'error cloning image'));
|
||||
return;
|
||||
}
|
||||
|
||||
log.trace({img: img}, 'image clone result');
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(img));
|
||||
} else {
|
||||
console.log('Cloned image %s to %s',
|
||||
args[0], common.imageRepr(img));
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
]}, cb);
|
||||
}
|
||||
|
||||
do_clone.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
group: 'Other options'
|
||||
},
|
||||
{
|
||||
names: ['dry-run'],
|
||||
type: 'bool',
|
||||
help: 'Go through the motions without actually cloning.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
}
|
||||
];
|
||||
|
||||
do_clone.synopses = [
|
||||
'{{name}} {{cmd}} [OPTIONS] IMAGE'
|
||||
];
|
||||
|
||||
do_clone.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Clone a shared image.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "IMAGE" is an image id (a full UUID), an image name (selects the',
|
||||
'latest, by "published_at", image with that name), an image "name@version"',
|
||||
'(selects latest match by "published_at"), or an image short ID (ID prefix).',
|
||||
'',
|
||||
'Note: Only shared images can be cloned.'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_clone.completionArgtypes = ['tritonimage', 'none'];
|
||||
|
||||
module.exports = do_clone;
|
@ -1,119 +0,0 @@
|
||||
/*
|
||||
* 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 (c) 2018, Joyent, Inc.
|
||||
*
|
||||
* `triton image copy ...`
|
||||
*/
|
||||
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
// ---- the command
|
||||
|
||||
function do_copy(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length !== 2) {
|
||||
cb(new errors.UsageError(
|
||||
'incorrect number of args: expected 2, got ' + args.length));
|
||||
return;
|
||||
}
|
||||
|
||||
var log = this.top.log;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function copyImage(ctx, next) {
|
||||
log.trace({dryRun: opts.dry_run, account: ctx.account, args: args},
|
||||
'image copy');
|
||||
|
||||
if (opts.dry_run) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
tritonapi.copyImageToDatacenter(
|
||||
{image: args[0], datacenter: args[1]},
|
||||
function (err, img) {
|
||||
if (err) {
|
||||
next(new errors.TritonError(err, 'error copying image'));
|
||||
return;
|
||||
}
|
||||
|
||||
log.trace({img: img}, 'image copy result');
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(img));
|
||||
} else {
|
||||
console.log('Copied image %s to datacenter %s',
|
||||
common.imageRepr(img), args[1]);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
]}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
do_copy.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
group: 'Other options'
|
||||
},
|
||||
{
|
||||
names: ['dry-run'],
|
||||
type: 'bool',
|
||||
help: 'Go through the motions without actually copying.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
}
|
||||
];
|
||||
|
||||
do_copy.synopses = [
|
||||
'{{name}} {{cmd}} [OPTIONS] IMAGE DATACENTER'
|
||||
];
|
||||
|
||||
do_copy.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Copy image to another datacenter.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "IMAGE" is an image id (a full UUID), an image name (selects the',
|
||||
'latest, by "published_at", image with that name), an image "name@version"',
|
||||
'(selects latest match by "published_at"), or an image short ID (ID prefix).',
|
||||
'You must be the owner of the image to copy it. (You can use `triton image',
|
||||
'clone` to get your own image clone of an image shared to you.)',
|
||||
'',
|
||||
'"DATACENTER" is the name of the datacenter to which to copy your image.',
|
||||
'Use `triton datacenters` to show the available datacenter names.'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_copy.aliases = ['cp'];
|
||||
|
||||
// TODO: tritonimage should really be 'tritonownedimage' or something to
|
||||
// limit to images owned by this account
|
||||
// TODO: tritondatacenter bash completion
|
||||
do_copy.completionArgtypes = ['tritonimage', 'tritondatacenter', 'none'];
|
||||
|
||||
module.exports = do_copy;
|
@ -1,276 +0,0 @@
|
||||
/*
|
||||
* 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 image create ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var fs = require('fs');
|
||||
var strsplit = require('strsplit');
|
||||
var tabula = require('tabula');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var distractions = require('../distractions');
|
||||
var errors = require('../errors');
|
||||
var mat = require('../metadataandtags');
|
||||
|
||||
|
||||
// ---- the command
|
||||
|
||||
function do_create(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length !== 3) {
|
||||
cb(new errors.UsageError(
|
||||
'incorrect number of args: expect 3, got ' + args.length));
|
||||
return;
|
||||
}
|
||||
|
||||
var log = this.top.log;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function loadTags(ctx, next) {
|
||||
mat.tagsFromCreateOpts(opts, log, function (err, tags) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
if (tags) {
|
||||
log.trace({tags: tags}, 'tags loaded from opts');
|
||||
ctx.tags = tags;
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
function loadAcl(ctx, next) {
|
||||
if (!opts.acl) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < opts.acl.length; i++) {
|
||||
if (!common.isUUID(opts.acl[i])) {
|
||||
next(new errors.UsageError(format(
|
||||
'invalid --acl: "%s" is not a UUID', opts.acl[i])));
|
||||
return;
|
||||
}
|
||||
}
|
||||
ctx.acl = opts.acl;
|
||||
next();
|
||||
},
|
||||
function getInst(ctx, next) {
|
||||
var id = args[0];
|
||||
if (common.isUUID(id)) {
|
||||
ctx.inst = {id: id};
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
tritonapi.getInstance(id, function (err, inst) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
log.trace({inst: inst}, 'image create: inst');
|
||||
ctx.inst = inst;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function createImg(ctx, next) {
|
||||
var createOpts = {
|
||||
machine: ctx.inst.id,
|
||||
name: args[1],
|
||||
version: args[2],
|
||||
description: opts.description,
|
||||
homepage: opts.homepage,
|
||||
eula: opts.eula,
|
||||
acl: ctx.acl,
|
||||
tags: ctx.tags
|
||||
};
|
||||
|
||||
log.trace({dryRun: opts.dry_run, createOpts: createOpts},
|
||||
'image create createOpts');
|
||||
ctx.start = Date.now();
|
||||
if (opts.dry_run) {
|
||||
ctx.inst = {
|
||||
id: 'cafecafe-4c0e-11e5-86cd-a7fd38d2a50b',
|
||||
name: 'this-is-a-dry-run'
|
||||
};
|
||||
console.log('Creating image %s@%s from instance %s%s',
|
||||
createOpts.name, createOpts.version, ctx.inst.id,
|
||||
(ctx.inst.name ? ' ('+ctx.inst.name+')' : ''));
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
tritonapi.cloudapi.createImageFromMachine(
|
||||
createOpts, function (err, img) {
|
||||
if (err) {
|
||||
next(new errors.TritonError(err,
|
||||
'error creating image'));
|
||||
return;
|
||||
}
|
||||
ctx.img = img;
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(img));
|
||||
} else {
|
||||
console.log('Creating image %s@%s (%s)',
|
||||
img.name, img.version, img.id);
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
function maybeWait(ctx, next) {
|
||||
if (!opts.wait) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 1 'wait': no distraction.
|
||||
// >1 'wait': distraction, pass in the N.
|
||||
var distraction;
|
||||
if (process.stderr.isTTY && opts.wait.length > 1) {
|
||||
distraction = distractions.createDistraction(opts.wait.length);
|
||||
}
|
||||
|
||||
// Dry-run: fake wait for a few seconds.
|
||||
var waiter = (opts.dry_run ?
|
||||
function dryWait(waitOpts, waitCb) {
|
||||
setTimeout(function () {
|
||||
ctx.img.state = 'running';
|
||||
waitCb(null, ctx.img);
|
||||
}, 5000);
|
||||
} : tritonapi.cloudapi.waitForImageStates.bind(
|
||||
tritonapi.cloudapi));
|
||||
|
||||
waiter({
|
||||
id: ctx.img.id,
|
||||
states: ['active', 'failed']
|
||||
}, function (err, img) {
|
||||
if (distraction) {
|
||||
distraction.destroy();
|
||||
}
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(img));
|
||||
} else if (img.state === 'active') {
|
||||
var dur = Date.now() - ctx.start;
|
||||
console.log('Created image %s (%s@%s) in %s',
|
||||
img.id, img.name, img.version,
|
||||
common.humanDurationFromMs(dur));
|
||||
}
|
||||
if (img.state !== 'active') {
|
||||
next(new Error(format('failed to create image %s (%s@%s)%s',
|
||||
img.id, img.name, img.version,
|
||||
(img.error ? format(': (%s) %s',
|
||||
img.error.code, img.error.message): ''))));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
]}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
do_create.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
group: 'Create options'
|
||||
},
|
||||
{
|
||||
names: ['description', 'd'],
|
||||
type: 'string',
|
||||
helpArg: 'DESC',
|
||||
help: 'A short description of the image.'
|
||||
},
|
||||
{
|
||||
names: ['homepage'],
|
||||
type: 'string',
|
||||
helpArg: 'URL',
|
||||
help: 'A homepage URL for the image.'
|
||||
},
|
||||
{
|
||||
names: ['eula'],
|
||||
type: 'string',
|
||||
helpArg: 'DESC',
|
||||
help: 'A URL for an End User License Agreement (EULA) for the image.'
|
||||
},
|
||||
{
|
||||
names: ['acl'],
|
||||
type: 'arrayOfString',
|
||||
helpArg: 'ID',
|
||||
help: 'Access Control List. The ID of an account to which to give ' +
|
||||
'access to this private image. This option can be used multiple ' +
|
||||
'times to give access to multiple accounts.'
|
||||
},
|
||||
{
|
||||
names: ['tag', 't'],
|
||||
type: 'arrayOfString',
|
||||
helpArg: 'TAG',
|
||||
help: 'Add a tag when creating the image. Tags are ' +
|
||||
'key/value pairs available on the image API object as the ' +
|
||||
'"tags" field. TAG is one of: a "key=value" string (bool and ' +
|
||||
'numeric "value" are converted to that type), a JSON object ' +
|
||||
'(if first char is "{"), or a "@FILE" to have tags be ' +
|
||||
'loaded from FILE. This option can be used multiple times.'
|
||||
},
|
||||
{
|
||||
group: 'Other options'
|
||||
},
|
||||
{
|
||||
names: ['dry-run'],
|
||||
type: 'bool',
|
||||
help: 'Go through the motions without actually creating.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'arrayOfBool',
|
||||
help: 'Wait for the creation to complete. Use multiple times for a ' +
|
||||
'spinner.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
}
|
||||
];
|
||||
|
||||
do_create.synopses = [
|
||||
'{{name}} {{cmd}} [OPTIONS] INST IMAGE-NAME IMAGE-VERSION'
|
||||
];
|
||||
|
||||
do_create.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Create a new instance.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "INST" is an instance name, id, or short id.'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_create.helpOpts = {
|
||||
maxHelpCol: 20
|
||||
};
|
||||
|
||||
do_create.completionArgtypes = ['tritoninstance', 'file'];
|
||||
|
||||
module.exports = do_create;
|
@ -1,160 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton image delete ...`
|
||||
*/
|
||||
|
||||
var format = require('util').format;
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
function do_delete(subcmd, opts, args, cb) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
return this.do_help('help', {}, [subcmd], cb);
|
||||
} else if (args.length < 1) {
|
||||
return cb(new errors.UsageError('missing IMAGE arg(s)'));
|
||||
}
|
||||
var ids = args;
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
/*
|
||||
* Lookup images, if not given UUIDs: we'll need to do it anyway
|
||||
* for the DeleteImage call(s), and doing so explicitly here allows
|
||||
* us to emit better output.
|
||||
*/
|
||||
function getImgs(ctx, next) {
|
||||
ctx.imgFromId = {};
|
||||
ctx.missingIds = [];
|
||||
// TODO: this should have a concurrency
|
||||
vasync.forEachParallel({
|
||||
inputs: ids,
|
||||
func: function getImg(id, nextImg) {
|
||||
if (common.isUUID(id)) {
|
||||
// TODO: get info from cache if we have it
|
||||
ctx.imgFromId[id] = {
|
||||
id: id,
|
||||
_repr: id
|
||||
};
|
||||
nextImg();
|
||||
return;
|
||||
}
|
||||
// TODO: allow use of cache here
|
||||
self.top.tritonapi.getImage(id, function (err, img) {
|
||||
if (err) {
|
||||
if (err.statusCode === 404) {
|
||||
ctx.missingIds.push(id);
|
||||
nextImg();
|
||||
} else {
|
||||
nextImg(err);
|
||||
}
|
||||
} else {
|
||||
ctx.imgFromId[img.id] = img;
|
||||
img._repr = format('%s (%s@%s)', img.id,
|
||||
img.name, img.version);
|
||||
nextImg();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, next);
|
||||
},
|
||||
|
||||
function errOnMissingIds(ctx, next) {
|
||||
if (ctx.missingIds.length === 1) {
|
||||
next(new errors.TritonError('no such image: '
|
||||
+ ctx.missingIds[0]));
|
||||
} else if (ctx.missingIds.length > 1) {
|
||||
next(new errors.TritonError('no such images: '
|
||||
+ ctx.missingIds.join(', ')));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
|
||||
function confirm(ctx, next) {
|
||||
if (opts.force) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
var keys = Object.keys(ctx.imgFromId);
|
||||
var msg;
|
||||
if (keys.length === 1) {
|
||||
msg = format('Delete image %s? [y/n] ',
|
||||
ctx.imgFromId[keys[0]]._repr);
|
||||
} else {
|
||||
msg = format('Delete %d images? [y/n] ', keys.length);
|
||||
}
|
||||
|
||||
common.promptYesNo({msg: msg}, function (answer) {
|
||||
if (answer !== 'y') {
|
||||
console.error('Aborting');
|
||||
next(true); // early abort signal
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
function deleteThem(ctx, next) {
|
||||
// TODO: forEachParallel with concurrency
|
||||
vasync.forEachPipeline({
|
||||
inputs: Object.keys(ctx.imgFromId),
|
||||
func: function deleteOne(id, nextOne) {
|
||||
self.top.tritonapi.cloudapi.deleteImage(id, function (err) {
|
||||
if (!err) {
|
||||
console.log('Deleted image %s',
|
||||
ctx.imgFromId[id]._repr);
|
||||
}
|
||||
nextOne(err);
|
||||
});
|
||||
}
|
||||
}, next);
|
||||
}
|
||||
]}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
do_delete.synopses = ['{{name}} {{cmd}} [OPTIONS] IMAGE [IMAGE ...]'];
|
||||
|
||||
do_delete.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Delete one or more images.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "IMAGE" is an image id (a full UUID), an image name (selects the',
|
||||
'latest, by "published_at", image with that name), an image "name@version"',
|
||||
'(selects latest match by "published_at"), or an image short ID (ID prefix).'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_delete.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['force', 'f'],
|
||||
type: 'bool',
|
||||
help: 'Skip confirmation of delete.'
|
||||
}
|
||||
];
|
||||
|
||||
do_delete.completionArgtypes = ['tritonimage'];
|
||||
|
||||
do_delete.aliases = ['rm'];
|
||||
module.exports = do_delete;
|
@ -1,120 +0,0 @@
|
||||
/*
|
||||
* 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 2017 Joyent, Inc.
|
||||
*
|
||||
* `triton image export ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
// ---- the command
|
||||
|
||||
function do_export(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length !== 2) {
|
||||
cb(new errors.UsageError(
|
||||
'incorrect number of args: expect 2, got ' + args.length));
|
||||
return;
|
||||
}
|
||||
|
||||
var log = this.top.log;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function exportImage(ctx, next) {
|
||||
log.trace({dryRun: opts.dry_run, manta_path: ctx.manta_path},
|
||||
'image export path');
|
||||
|
||||
console.log('Exporting image %s to %s', args[0], args[1]);
|
||||
|
||||
if (opts.dry_run) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
tritonapi.exportImage({
|
||||
image: args[0],
|
||||
manta_path: args[1]
|
||||
}, function (err, exportInfo) {
|
||||
if (err) {
|
||||
next(new errors.TritonError(err,
|
||||
'error exporting image to manta'));
|
||||
return;
|
||||
}
|
||||
|
||||
log.trace({exportInfo: exportInfo}, 'image export: exportInfo');
|
||||
ctx.exportInfo = exportInfo;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function outputResults(ctx, next) {
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(ctx.exportInfo));
|
||||
} else {
|
||||
console.log(' Manta URL: %s', ctx.exportInfo.manta_url);
|
||||
console.log('Manifest path: %s', ctx.exportInfo.manifest_path);
|
||||
console.log(' Image path: %s', ctx.exportInfo.image_path);
|
||||
}
|
||||
next();
|
||||
}
|
||||
]}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
do_export.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
group: 'Other options'
|
||||
},
|
||||
{
|
||||
names: ['dry-run'],
|
||||
type: 'bool',
|
||||
help: 'Go through the motions without actually exporting.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
}
|
||||
];
|
||||
|
||||
do_export.synopses = [
|
||||
'{{name}} {{cmd}} [OPTIONS] IMAGE MANTA_PATH'
|
||||
];
|
||||
|
||||
do_export.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Export an image.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "IMAGE" is an image id (a full UUID), an image name (selects the',
|
||||
'latest, by "published_at", image with that name), an image "name@version"',
|
||||
'(selects latest match by "published_at"), or an image short ID (ID prefix).',
|
||||
'',
|
||||
'Note: Only images that are owned by the account can be exported.'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_export.completionArgtypes = ['tritonimage', 'none'];
|
||||
|
||||
module.exports = do_export;
|
@ -1,95 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton image get ...`
|
||||
*/
|
||||
|
||||
var format = require('util').format;
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
function do_get(subcmd, opts, args, callback) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length !== 1) {
|
||||
return callback(new errors.UsageError(format(
|
||||
'incorrect number of args (%d)', args.length)));
|
||||
}
|
||||
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
return;
|
||||
}
|
||||
var getOpts = {
|
||||
name: args[0],
|
||||
excludeInactive: !opts.all
|
||||
};
|
||||
tritonapi.getImage(getOpts, function onRes(err, img) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(img));
|
||||
} else {
|
||||
console.log(JSON.stringify(img, null, 4));
|
||||
}
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_get.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
},
|
||||
{
|
||||
group: 'Filtering options'
|
||||
},
|
||||
{
|
||||
names: ['all', 'a'],
|
||||
type: 'bool',
|
||||
help: 'Include all images when matching by name or short ID, not ' +
|
||||
'just "active" ones. By default only active images are included.'
|
||||
}
|
||||
];
|
||||
|
||||
do_get.synopses = ['{{name}} {{cmd}} [OPTIONS] IMAGE'];
|
||||
|
||||
do_get.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Get an image.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "IMAGE" is an image id (a full UUID), an image name (selects the',
|
||||
'latest, by "published_at", image with that name), an image "name@version"',
|
||||
'(selects latest match by "published_at"), or an image short ID (ID prefix).',
|
||||
'',
|
||||
'Note: Currently this dumps prettified JSON by default. That might change',
|
||||
'in the future. Use "-j" to explicitly get JSON output.'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_get.completionArgtypes = ['tritonimage', 'none'];
|
||||
|
||||
module.exports = do_get;
|
@ -1,218 +0,0 @@
|
||||
/*
|
||||
* 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 2018 Joyent, Inc.
|
||||
*
|
||||
* `triton image list ...`
|
||||
*/
|
||||
|
||||
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');
|
||||
|
||||
// filters to pass triton.listImages
|
||||
var validFilters = [
|
||||
'name',
|
||||
'os',
|
||||
'version',
|
||||
'public',
|
||||
'state',
|
||||
'owner',
|
||||
'type'
|
||||
];
|
||||
|
||||
// columns default without -o
|
||||
var columnsDefault = 'shortid,name,version,flags,os,type,pubdate';
|
||||
|
||||
// columns default with -l
|
||||
var columnsDefaultLong = 'id,name,version,state,flags,os,type,pubdate';
|
||||
|
||||
// sort default with -s
|
||||
var sortDefault = 'published_at';
|
||||
|
||||
function do_list(subcmd, opts, args, callback) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
}
|
||||
|
||||
var columns = columnsDefault;
|
||||
if (opts.o) {
|
||||
columns = opts.o;
|
||||
} else if (opts.long) {
|
||||
columns = columnsDefaultLong;
|
||||
}
|
||||
columns = columns.split(',');
|
||||
|
||||
var sort = opts.s.split(',');
|
||||
|
||||
var listOpts;
|
||||
try {
|
||||
listOpts = common.objFromKeyValueArgs(args, {
|
||||
disableDotted: true,
|
||||
validKeys: validFilters,
|
||||
disableTypeConversions: true
|
||||
});
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
if (opts.all) {
|
||||
listOpts.state = 'all';
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
vasync.pipeline({ arg: {}, funcs: [
|
||||
function setupTritonApi(_, next) {
|
||||
common.cliSetupTritonApi({cli: self.top}, next);
|
||||
},
|
||||
function getImages(ctx, next) {
|
||||
tritonapi.listImages(listOpts, function onRes(err, imgs, res) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
ctx.imgs = imgs;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function getUserAccount(ctx, next) {
|
||||
// If using json output, or when there are no images that use an ACL
|
||||
// - we don't need to fetch the account, as the account is only used
|
||||
// to check if the image is shared (i.e. the account is in the image
|
||||
// ACL) so it can output image flags in non-json mode.
|
||||
if (opts.json || ctx.imgs.every(function _checkAcl(img) {
|
||||
return !Array.isArray(img.acl) || img.acl.length === 0;
|
||||
})) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
tritonapi.cloudapi.getAccount(function _accountCb(err, account) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
ctx.account = account;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function formatImages(ctx, next) {
|
||||
var imgs = ctx.imgs;
|
||||
if (opts.json) {
|
||||
common.jsonStream(imgs);
|
||||
} else {
|
||||
// Add some convenience fields
|
||||
// Added fields taken from imgapi-cli.git.
|
||||
for (var i = 0; i < imgs.length; i++) {
|
||||
var img = imgs[i];
|
||||
img.shortid = img.id.split('-', 1)[0];
|
||||
if (img.published_at) {
|
||||
// Just the date.
|
||||
img.pubdate = img.published_at.slice(0, 10);
|
||||
// Normalize on no milliseconds.
|
||||
img.pub = img.published_at.replace(/\.\d+Z$/, 'Z');
|
||||
}
|
||||
if (img.files && img.files[0]) {
|
||||
img.size = img.files[0].size;
|
||||
}
|
||||
var flags = [];
|
||||
if (img.origin) flags.push('I');
|
||||
if (img['public']) flags.push('P');
|
||||
if (img.state !== 'active') flags.push('X');
|
||||
|
||||
// Add image sharing flags.
|
||||
if (Array.isArray(img.acl) && img.acl.length > 0) {
|
||||
assert.string(ctx.account.id, 'ctx.account.id');
|
||||
if (img.owner === ctx.account.id) {
|
||||
// This image has been shared with other accounts.
|
||||
flags.push('+');
|
||||
}
|
||||
if (img.acl.indexOf(ctx.account.id) !== -1) {
|
||||
// This image has been shared with this account.
|
||||
flags.push('S');
|
||||
}
|
||||
}
|
||||
|
||||
img.flags = flags.length ? flags.join('') : undefined;
|
||||
}
|
||||
|
||||
tabula(imgs, {
|
||||
skipHeader: opts.H,
|
||||
columns: columns,
|
||||
sort: sort
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
]}, callback);
|
||||
}
|
||||
|
||||
do_list.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
group: 'Filtering options'
|
||||
},
|
||||
{
|
||||
names: ['all', 'a'],
|
||||
type: 'bool',
|
||||
help: 'List all images, not just "active" ones. This ' +
|
||||
'is a shortcut for the "state=all" filter.'
|
||||
}
|
||||
].concat(common.getCliTableOptions({
|
||||
includeLong: true,
|
||||
sortDefault: sortDefault
|
||||
}));
|
||||
|
||||
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] [FILTERS]'];
|
||||
|
||||
do_list.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'List images.',
|
||||
'',
|
||||
'Note: Currently, *docker* images are not included in this endpoint\'s responses.',
|
||||
'You must use `docker images` against the Docker service for this data center.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Filters:',
|
||||
' FIELD=VALUE Equality filter. Supported fields: account, owner,',
|
||||
' state, name, os, and type.',
|
||||
' FIELD=true|false Boolean filter. Supported fields: public.',
|
||||
' FIELD=~SUBSTRING Substring filter. Supported fields: name',
|
||||
'',
|
||||
'Fields (most are self explanatory, "*" indicates a field added client-side',
|
||||
'for convenience):',
|
||||
' shortid* A short ID prefix.',
|
||||
' flags* Single letter flags summarizing some fields:',
|
||||
' "P" image is public',
|
||||
' "+" you are sharing this image with others',
|
||||
' "S" this image has been shared with you',
|
||||
' "I" an incremental image (i.e. has an origin)',
|
||||
' "X" has a state *other* than "active"',
|
||||
' pubdate* Short form of "published_at" with just the date',
|
||||
' pub* Short form of "published_at" elliding milliseconds.',
|
||||
' size* The number of bytes of the image file (files.0.size)',
|
||||
' type The image type. As of CloudAPI 8.0 this is defined by',
|
||||
' <https://images.joyent.com/docs/#manifest-type>. Before',
|
||||
' that it was one of "smartmachine" or "virtualmachine".'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_list.aliases = ['ls'];
|
||||
|
||||
module.exports = do_list;
|
@ -1,115 +0,0 @@
|
||||
/*
|
||||
* 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 (c) 2018, Joyent, Inc.
|
||||
*
|
||||
* `triton image share ...`
|
||||
*/
|
||||
|
||||
var format = require('util').format;
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
// ---- the command
|
||||
|
||||
function do_share(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length !== 2) {
|
||||
cb(new errors.UsageError(
|
||||
'incorrect number of args: expect 2, got ' + args.length));
|
||||
return;
|
||||
}
|
||||
|
||||
var log = this.top.log;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function shareImage(ctx, next) {
|
||||
log.trace({dryRun: opts.dry_run, account: ctx.account},
|
||||
'image share account');
|
||||
|
||||
if (opts.dry_run) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
tritonapi.shareImage({
|
||||
image: args[0],
|
||||
account: args[1]
|
||||
}, function (err, img) {
|
||||
if (err) {
|
||||
next(new errors.TritonError(err, 'error sharing image'));
|
||||
return;
|
||||
}
|
||||
|
||||
log.trace({img: img}, 'image share result');
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(img));
|
||||
} else {
|
||||
console.log('Shared image %s with account %s',
|
||||
args[0], args[1]);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
]}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
do_share.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
group: 'Other options'
|
||||
},
|
||||
{
|
||||
names: ['dry-run'],
|
||||
type: 'bool',
|
||||
help: 'Go through the motions without actually sharing.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
}
|
||||
];
|
||||
|
||||
do_share.synopses = [
|
||||
'{{name}} {{cmd}} [OPTIONS] IMAGE ACCOUNT'
|
||||
];
|
||||
|
||||
do_share.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Share an image with another account.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "IMAGE" is an image id (a full UUID), an image name (selects the',
|
||||
'latest, by "published_at", image with that name), an image "name@version"',
|
||||
'(selects latest match by "published_at"), or an image short ID (ID prefix).',
|
||||
'',
|
||||
'Where "ACCOUNT" is the full account UUID.',
|
||||
'',
|
||||
'Note: Only images that are owned by the account can be shared.'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_share.completionArgtypes = ['tritonimage', 'none'];
|
||||
|
||||
module.exports = do_share;
|
@ -1,115 +0,0 @@
|
||||
/*
|
||||
* 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 (c) 2018, Joyent, Inc.
|
||||
*
|
||||
* `triton image unshare ...`
|
||||
*/
|
||||
|
||||
var format = require('util').format;
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
// ---- the command
|
||||
|
||||
function do_unshare(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length !== 2) {
|
||||
cb(new errors.UsageError(
|
||||
'incorrect number of args: expect 2, got ' + args.length));
|
||||
return;
|
||||
}
|
||||
|
||||
var log = this.top.log;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function unshareImage(ctx, next) {
|
||||
log.trace({dryRun: opts.dry_run, account: ctx.account},
|
||||
'image unshare account');
|
||||
|
||||
if (opts.dry_run) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
tritonapi.unshareImage({
|
||||
image: args[0],
|
||||
account: args[1]
|
||||
}, function (err, img) {
|
||||
if (err) {
|
||||
next(new errors.TritonError(err, 'error unsharing image'));
|
||||
return;
|
||||
}
|
||||
|
||||
log.trace({img: img}, 'image unshare result');
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(img));
|
||||
} else {
|
||||
console.log('Unshared image %s with account %s',
|
||||
args[0], args[1]);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
]}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
do_unshare.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
group: 'Other options'
|
||||
},
|
||||
{
|
||||
names: ['dry-run'],
|
||||
type: 'bool',
|
||||
help: 'Go through the motions without actually sharing.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
}
|
||||
];
|
||||
|
||||
do_unshare.synopses = [
|
||||
'{{name}} {{cmd}} [OPTIONS] IMAGE ACCOUNT'
|
||||
];
|
||||
|
||||
do_unshare.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Unshare an image with another account.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "IMAGE" is an image id (a full UUID), an image name (selects the',
|
||||
'latest, by "published_at", image with that name), an image "name@version"',
|
||||
'(selects latest match by "published_at"), or an image short ID (ID prefix).',
|
||||
'',
|
||||
'Where "ACCOUNT" is the full account UUID.',
|
||||
'',
|
||||
'Note: Only images that are owned by the account can be unshared.'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_unshare.completionArgtypes = ['tritonimage', 'none'];
|
||||
|
||||
module.exports = do_unshare;
|
@ -1,148 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton image wait ...`
|
||||
*/
|
||||
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var distractions = require('../distractions');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
function do_wait(subcmd, opts, args, cb) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
return this.do_help('help', {}, [subcmd], cb);
|
||||
} else if (args.length < 1) {
|
||||
return cb(new errors.UsageError('missing IMAGE arg(s)'));
|
||||
}
|
||||
var ids = args;
|
||||
var states = [];
|
||||
opts.states.forEach(function (s) {
|
||||
/* JSSTYLED */
|
||||
states = states.concat(s.trim().split(/\s*,\s*/g));
|
||||
});
|
||||
|
||||
var distraction;
|
||||
var done = 0;
|
||||
var imgFromId = {};
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function getImgs(_, next) {
|
||||
vasync.forEachParallel({
|
||||
inputs: ids,
|
||||
func: function getImg(id, nextImg) {
|
||||
self.top.tritonapi.getImage(id, function (err, img) {
|
||||
if (err) {
|
||||
return nextImg(err);
|
||||
}
|
||||
if (states.indexOf(img.state) !== -1) {
|
||||
console.log('%d/%d: Image %s (%s@%s) already %s',
|
||||
++done, ids.length, img.id, img.name,
|
||||
img.version, img.state);
|
||||
} else {
|
||||
imgFromId[img.id] = img;
|
||||
}
|
||||
nextImg();
|
||||
});
|
||||
}
|
||||
}, next);
|
||||
},
|
||||
|
||||
function waitForImgs(_, next) {
|
||||
var idsToWaitFor = Object.keys(imgFromId);
|
||||
if (idsToWaitFor.length === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (idsToWaitFor.length === 1) {
|
||||
var img2 = imgFromId[idsToWaitFor[0]];
|
||||
console.log(
|
||||
'Waiting for image %s (%s@%s) to enter state (states: %s)',
|
||||
img2.id, img2.name, img2.version, states.join(', '));
|
||||
} else {
|
||||
console.log(
|
||||
'Waiting for %d images to enter state (states: %s)',
|
||||
idsToWaitFor.length, states.join(', '));
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: need BigSpinner.log first.
|
||||
* TODO: Also when adding a spinner, we need an equiv option to
|
||||
* `triton create -wwww` to trigger the spinner (and size). By
|
||||
* default: no spinner.
|
||||
*/
|
||||
if (false &&
|
||||
process.stderr.isTTY)
|
||||
{
|
||||
distraction = distractions.createDistraction();
|
||||
}
|
||||
|
||||
vasync.forEachParallel({
|
||||
inputs: idsToWaitFor,
|
||||
func: function waitForImg(id, nextImg) {
|
||||
self.top.tritonapi.cloudapi.waitForImageStates({
|
||||
id: id,
|
||||
states: states
|
||||
}, function (err, img, res) {
|
||||
if (err) {
|
||||
return nextImg(err);
|
||||
}
|
||||
console.log('%d/%d: Image %s (%s@%s) moved to state %s',
|
||||
++done, ids.length, img.id, img.name,
|
||||
img.version, img.state);
|
||||
nextImg();
|
||||
});
|
||||
}
|
||||
}, next);
|
||||
}
|
||||
|
||||
]}, function (err) {
|
||||
if (distraction) {
|
||||
distraction.destroy();
|
||||
}
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
do_wait.synopses = ['{{name}} {{cmd}} [-s STATES] IMAGE [IMAGE ...]'];
|
||||
|
||||
do_wait.help = [
|
||||
'Wait for images to change to a particular state.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "states" is a comma-separated list of target instance states,',
|
||||
'by default "active,failed". In other words, "spearhead img wait foo0" will',
|
||||
'wait for image "foo0" to complete creation.'
|
||||
].join('\n');
|
||||
|
||||
do_wait.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['states', 's'],
|
||||
type: 'arrayOfString',
|
||||
default: ['active', 'failed'],
|
||||
helpArg: 'STATES',
|
||||
help: 'Instance states on which to wait. Default is "active,failed". '
|
||||
+ 'Values can be comma-separated or multiple uses of the option.'
|
||||
}
|
||||
];
|
||||
|
||||
do_wait.completionArgtypes = ['tritonimage'];
|
||||
|
||||
module.exports = do_wait;
|
@ -1,68 +0,0 @@
|
||||
/*
|
||||
* 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 (c) 2018, Joyent, Inc.
|
||||
*
|
||||
* `triton image ...`
|
||||
*/
|
||||
|
||||
var Cmdln = require('cmdln').Cmdln;
|
||||
var util = require('util');
|
||||
|
||||
|
||||
|
||||
// ---- CLI class
|
||||
|
||||
function ImageCLI(top) {
|
||||
this.top = top;
|
||||
Cmdln.call(this, {
|
||||
name: top.name + ' image',
|
||||
/* BEGIN JSSTYLED */
|
||||
desc: [
|
||||
'List and manage Spearhead images.'
|
||||
].join('\n'),
|
||||
/* END JSSTYLED */
|
||||
helpOpts: {
|
||||
minHelpCol: 24 /* line up with option help */
|
||||
},
|
||||
helpSubcmds: [
|
||||
'help',
|
||||
'list',
|
||||
'get',
|
||||
'clone',
|
||||
'copy',
|
||||
'create',
|
||||
'delete',
|
||||
'export',
|
||||
'share',
|
||||
'unshare',
|
||||
'wait'
|
||||
]
|
||||
});
|
||||
}
|
||||
util.inherits(ImageCLI, Cmdln);
|
||||
|
||||
ImageCLI.prototype.init = function init(opts, args, cb) {
|
||||
this.log = this.top.log;
|
||||
Cmdln.prototype.init.apply(this, arguments);
|
||||
};
|
||||
|
||||
ImageCLI.prototype.do_list = require('./do_list');
|
||||
ImageCLI.prototype.do_get = require('./do_get');
|
||||
ImageCLI.prototype.do_clone = require('./do_clone');
|
||||
ImageCLI.prototype.do_copy = require('./do_copy');
|
||||
ImageCLI.prototype.do_create = require('./do_create');
|
||||
ImageCLI.prototype.do_delete = require('./do_delete');
|
||||
ImageCLI.prototype.do_export = require('./do_export');
|
||||
ImageCLI.prototype.do_share = require('./do_share');
|
||||
ImageCLI.prototype.do_unshare = require('./do_unshare');
|
||||
ImageCLI.prototype.do_wait = require('./do_wait');
|
||||
|
||||
|
||||
ImageCLI.aliases = ['img'];
|
||||
|
||||
module.exports = ImageCLI;
|
162
lib/do_images.js
162
lib/do_images.js
@ -1,31 +1,153 @@
|
||||
/*
|
||||
* 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 2017 Joyent, Inc.
|
||||
* Copyright (c) 2015 Joyent Inc. All rights reserved.
|
||||
*
|
||||
* `triton images ...` bwcompat shortcut for `triton image list ...`.
|
||||
* `triton images ...`
|
||||
*/
|
||||
|
||||
var targ = require('./do_image/do_list');
|
||||
var format = require('util').format;
|
||||
var tabula = require('tabula');
|
||||
|
||||
var common = require('./common');
|
||||
var errors = require('./errors');
|
||||
|
||||
|
||||
function do_images(subcmd, opts, args, callback) {
|
||||
this.handlerFromSubcmd('image').dispatch({
|
||||
subcmd: 'list',
|
||||
opts: opts,
|
||||
args: args
|
||||
}, callback);
|
||||
}
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
}
|
||||
|
||||
do_images.help = 'A shortcut for "spearhead image list".\n' + targ.help;
|
||||
do_images.synopses = targ.synopses;
|
||||
do_images.options = targ.options;
|
||||
do_images.completionArgtypes = targ.completionArgtypes;
|
||||
/* JSSTYLED */
|
||||
var columns = opts.o.trim().split(/\s*,\s*/g);
|
||||
/* JSSTYLED */
|
||||
var sort = opts.s.trim().split(/\s*,\s*/g);
|
||||
|
||||
var validFilters = [
|
||||
'name', 'os', 'version', 'public', 'state', 'owner', 'type'
|
||||
];
|
||||
var listOpts;
|
||||
try {
|
||||
listOpts = common.kvToObj(args, validFilters);
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
if (opts.all) {
|
||||
listOpts.state = 'all';
|
||||
}
|
||||
|
||||
this.triton.cloudapi.listImages(listOpts, function onRes(err, imgs, res) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
// XXX we should have a common method for all these:
|
||||
// XXX sorting
|
||||
// XXX if opts.o is given, then filter to just those fields?
|
||||
console.log(common.jsonStream(imgs));
|
||||
} else {
|
||||
// Add some convenience fields
|
||||
// Added fields taken from imgapi-cli.git.
|
||||
for (var i = 0; i < imgs.length; i++) {
|
||||
var img = imgs[i];
|
||||
img.shortid = img.id.split('-', 1)[0];
|
||||
if (img.published_at) {
|
||||
// Just the date.
|
||||
img.pubdate = img.published_at.slice(0, 10);
|
||||
// Normalize on no milliseconds.
|
||||
img.pub = img.published_at.replace(/\.\d+Z$/, 'Z');
|
||||
}
|
||||
if (img.files && img.files[0]) {
|
||||
img.size = img.files[0].size;
|
||||
}
|
||||
var flags = [];
|
||||
if (img.origin) flags.push('I');
|
||||
if (img['public']) flags.push('P');
|
||||
if (img.state !== 'active') flags.push('X');
|
||||
img.flags = flags.length ? flags.join('') : undefined;
|
||||
}
|
||||
|
||||
tabula(imgs, {
|
||||
skipHeader: opts.H,
|
||||
columns: columns,
|
||||
sort: sort
|
||||
});
|
||||
}
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
do_images.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
group: 'Filtering options'
|
||||
},
|
||||
{
|
||||
names: ['all', 'a'],
|
||||
type: 'bool',
|
||||
help: 'List all images, not just "active" ones. This ' +
|
||||
'is a shortcut for the "state=all" filter.'
|
||||
},
|
||||
{
|
||||
group: 'Output options'
|
||||
},
|
||||
{
|
||||
names: ['H'],
|
||||
type: 'bool',
|
||||
help: 'Omit table header row.'
|
||||
},
|
||||
{
|
||||
names: ['o'],
|
||||
type: 'string',
|
||||
default: 'id,name,version,state,flags,os,pubdate',
|
||||
help: 'Specify fields (columns) to output.',
|
||||
helpArg: 'field1,...'
|
||||
},
|
||||
{
|
||||
names: ['s'],
|
||||
type: 'string',
|
||||
default: 'published_at',
|
||||
help: 'Sort on the given fields. Default is "published_at".',
|
||||
helpArg: 'field1,...'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
}
|
||||
];
|
||||
do_images.help = (
|
||||
/* BEGIN JSSTYLED */
|
||||
'List images.\n' +
|
||||
'\n' +
|
||||
'Usage:\n' +
|
||||
' {{name}} images [<options>] [<filters>]\n' +
|
||||
'\n' +
|
||||
'Filters:\n' +
|
||||
' FIELD=VALUE Field equality filter. Supported fields: \n' +
|
||||
' account, owner, state, name, os, and type.\n' +
|
||||
' FIELD=true|false Field boolean filter. Supported fields: public.\n' +
|
||||
' FIELD=~SUBSTRING Field substring filter. Supported fields: name\n' +
|
||||
'\n' +
|
||||
'Fields (most are self explanatory, the client adds some for convenience):\n' +
|
||||
' flags This is a set of single letter flags\n' +
|
||||
' summarizing some fields. "P" indicates the\n' +
|
||||
' image is public. "I" indicates an incremental\n' +
|
||||
' image (i.e. has an origin). "X" indicates an\n' +
|
||||
' image with a state *other* than "active".\n' +
|
||||
' pubdate Short form of "published_at" with just the date\n' +
|
||||
' pub Short form of "published_at" elliding milliseconds.\n' +
|
||||
' size The number of bytes of the image file (files.0.size)\n' +
|
||||
'\n' +
|
||||
'{{options}}'
|
||||
/* END JSSTYLED */
|
||||
);
|
||||
|
||||
do_images.aliases = ['imgs'];
|
||||
do_images.hidden = true;
|
||||
|
||||
module.exports = do_images;
|
||||
|
113
lib/do_info.js
113
lib/do_info.js
@ -1,11 +1,5 @@
|
||||
/*
|
||||
* 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.
|
||||
* Copyright 2015 Joyent Inc.
|
||||
*
|
||||
* `triton info ...`
|
||||
*/
|
||||
@ -28,36 +22,33 @@ function do_info(subcmd, opts, args, callback) {
|
||||
|
||||
var out = {};
|
||||
var i = 0;
|
||||
var tritonapi = this.tritonapi;
|
||||
|
||||
common.cliSetupTritonApi({cli: this}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
this.triton.cloudapi.getAccount(cb.bind('account')); i++;
|
||||
this.triton.cloudapi.listMachines(cb.bind('machines')); i++;
|
||||
|
||||
function cb(err, data) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
tritonapi.cloudapi.getAccount(cb.bind('account')); i++;
|
||||
tritonapi.cloudapi.listMachines(cb.bind('machines')); i++;
|
||||
out[this.toString()] = data;
|
||||
if (--i === 0)
|
||||
done();
|
||||
}
|
||||
|
||||
function cb(err, data) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
out[this.toString()] = data;
|
||||
if (--i === 0)
|
||||
done();
|
||||
}
|
||||
|
||||
function done() {
|
||||
// parse name
|
||||
var name;
|
||||
if (out.account.firstName && out.account.lastName)
|
||||
name = format('%s %s', out.account.firstName,
|
||||
out.account.lastName);
|
||||
else if (out.account.firstName)
|
||||
name = out.account.firstName;
|
||||
|
||||
// parse machine states and accounting
|
||||
function done() {
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(out));
|
||||
} else {
|
||||
// pretty print
|
||||
console.log('%s - %s %s <%s>',
|
||||
out.account.login,
|
||||
out.account.firstName,
|
||||
out.account.lastName,
|
||||
out.account.email);
|
||||
console.log(self.triton.cloudapi.url);
|
||||
console.log();
|
||||
console.log('%d instance(s)', out.machines.length);
|
||||
var states = {};
|
||||
var disk = 0;
|
||||
var memory = 0;
|
||||
@ -68,36 +59,16 @@ function do_info(subcmd, opts, args, callback) {
|
||||
memory += machine.memory;
|
||||
disk += machine.disk;
|
||||
});
|
||||
disk *= 1000 * 1000;
|
||||
memory *= 1000 * 1000;
|
||||
|
||||
var data = {};
|
||||
data.login = out.account.login;
|
||||
if (name)
|
||||
data.name = name;
|
||||
data.email = out.account.email;
|
||||
data.url = self.tritonapi.cloudapi.url;
|
||||
data.totalDisk = disk;
|
||||
data.totalMemory = memory;
|
||||
|
||||
if (opts.json) {
|
||||
data.totalInstances = out.machines.length;
|
||||
data.instances = states;
|
||||
console.log(JSON.stringify(data));
|
||||
} else {
|
||||
data.totalDisk = common.humanSizeFromBytes(disk);
|
||||
data.totalMemory = common.humanSizeFromBytes(memory);
|
||||
Object.keys(data).forEach(function (key) {
|
||||
console.log('%s: %s', key, data[key]);
|
||||
});
|
||||
console.log('instances: %d', out.machines.length);
|
||||
Object.keys(states).forEach(function (key) {
|
||||
console.log(' %s: %d', key, states[key]);
|
||||
});
|
||||
}
|
||||
callback();
|
||||
Object.keys(states).forEach(function (state) {
|
||||
console.log('- %d %s', states[state], state);
|
||||
});
|
||||
console.log('- %s RAM Total',
|
||||
common.humanSizeFromBytes(memory * 1000 * 1000));
|
||||
console.log('- %s Disk Total',
|
||||
common.humanSizeFromBytes(disk * 1000 * 1000));
|
||||
}
|
||||
});
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
do_info.options = [
|
||||
@ -112,15 +83,13 @@ do_info.options = [
|
||||
help: 'JSON output.'
|
||||
}
|
||||
];
|
||||
|
||||
do_info.synopses = ['{{name}} {{cmd}}'];
|
||||
|
||||
do_info.help = [
|
||||
'Print an account summary.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}'
|
||||
].join('\n');
|
||||
do_info.help = (
|
||||
'Print an account summary.\n'
|
||||
+ '\n'
|
||||
+ 'Usage:\n'
|
||||
+ ' {{name}} account\n'
|
||||
+ '\n'
|
||||
+ '{{options}}'
|
||||
);
|
||||
|
||||
module.exports = do_info;
|
||||
|
64
lib/do_instance.js
Normal file
64
lib/do_instance.js
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2015 Joyent Inc.
|
||||
*
|
||||
* `triton instance ...`
|
||||
*/
|
||||
|
||||
var common = require('./common');
|
||||
|
||||
function do_instance(subcmd, opts, args, callback) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length !== 1) {
|
||||
callback(new Error('invalid args: ' + args));
|
||||
return;
|
||||
}
|
||||
|
||||
var id = args[0];
|
||||
|
||||
if (common.isUUID(id)) {
|
||||
this.triton.cloudapi.getMachine(id, cb);
|
||||
} else {
|
||||
this.triton.getMachineByAlias(id, cb);
|
||||
}
|
||||
|
||||
function cb(err, machine) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(machine));
|
||||
} else {
|
||||
console.log(JSON.stringify(machine, null, 4));
|
||||
}
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
do_instance.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON output.'
|
||||
}
|
||||
];
|
||||
do_instance.help = (
|
||||
'Show a single instance.\n'
|
||||
+ '\n'
|
||||
+ 'Usage:\n'
|
||||
+ ' {{name}} instance <alias|id>\n'
|
||||
+ '\n'
|
||||
+ '{{options}}'
|
||||
);
|
||||
|
||||
do_instance.aliases = ['inst'];
|
||||
|
||||
module.exports = do_instance;
|
@ -1,124 +0,0 @@
|
||||
/*
|
||||
* 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 instance audit ...`
|
||||
*/
|
||||
|
||||
var format = require('util').format;
|
||||
var tabula = require('tabula');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
// columns default without -o
|
||||
var columnsDefault = 'shortid,time,action,success';
|
||||
|
||||
// columns default with -l
|
||||
var columnsDefaultLong = 'id,time,action,success';
|
||||
|
||||
// sort default with -s
|
||||
var sortDefault = 'id,time';
|
||||
|
||||
|
||||
function do_audit(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length === 0) {
|
||||
cb(new errors.UsageError('missing INST argument'));
|
||||
return;
|
||||
} else if (args.length > 1) {
|
||||
cb(new errors.UsageError('too many arguments: ' + args));
|
||||
return;
|
||||
}
|
||||
|
||||
var columns = columnsDefault;
|
||||
if (opts.o) {
|
||||
columns = opts.o;
|
||||
} else if (opts.long) {
|
||||
columns = columnsDefaultLong;
|
||||
}
|
||||
columns = columns.split(',');
|
||||
|
||||
var sort = opts.s.split(',');
|
||||
|
||||
var arg = args[0];
|
||||
var uuid;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
if (common.isUUID(arg)) {
|
||||
uuid = arg;
|
||||
go1();
|
||||
} else {
|
||||
tritonapi.getInstance(arg, function (err, inst) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
uuid = inst.id;
|
||||
go1();
|
||||
});
|
||||
}});
|
||||
|
||||
function go1() {
|
||||
tritonapi.cloudapi.machineAudit(uuid, function (err, audit) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
audit.forEach(function (a) {
|
||||
a.id = uuid;
|
||||
a.shortid = common.uuidToShortId(uuid);
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
common.jsonStream(audit);
|
||||
} else {
|
||||
tabula(audit, {
|
||||
skipHeader: opts.H,
|
||||
columns: columns,
|
||||
sort: sort,
|
||||
dottedLookup: true
|
||||
});
|
||||
}
|
||||
cb();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
do_audit.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
}
|
||||
].concat(common.getCliTableOptions({
|
||||
includeLong: true,
|
||||
sortDefault: sortDefault
|
||||
}));
|
||||
|
||||
do_audit.synopses = ['{{name}} {{cmd}} [OPTIONS] INST'];
|
||||
do_audit.help = [
|
||||
'List instance actions.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "INST" is an instance name, id, or short id.'
|
||||
].join('\n');
|
||||
|
||||
do_audit.completionArgtypes = ['tritoninstance', 'none'];
|
||||
|
||||
module.exports = do_audit;
|
@ -1,553 +0,0 @@
|
||||
/*
|
||||
* 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 2019 Joyent, Inc.
|
||||
*
|
||||
* `triton instance create ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var tabula = require('tabula');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var distractions = require('../distractions');
|
||||
var errors = require('../errors');
|
||||
var mat = require('../metadataandtags');
|
||||
var NETWORK_OBJECT_FIELDS =
|
||||
require('../constants').NETWORK_OBJECT_FIELDS;
|
||||
|
||||
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 "<volume name>:<mount path>[:<mode>]", 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) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length !== 2) {
|
||||
return cb(new errors.UsageError('incorrect number of args'));
|
||||
} else if (opts.nic && opts.network) {
|
||||
return cb(new errors.UsageError(
|
||||
'--network and --nic cannot be specified together'));
|
||||
}
|
||||
|
||||
var log = this.top.log;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
|
||||
/*
|
||||
* 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();
|
||||
},
|
||||
|
||||
/*
|
||||
* Parse any nics given via `--nic`
|
||||
*/
|
||||
function parseNics(ctx, next) {
|
||||
if (!opts.nic) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.nics = [];
|
||||
var i;
|
||||
var networksSeen = {};
|
||||
var nic;
|
||||
var nics = opts.nic;
|
||||
|
||||
log.trace({nics: nics}, 'parsing nics');
|
||||
|
||||
for (i = 0; i < nics.length; i++) {
|
||||
nic = nics[i].split(',');
|
||||
|
||||
try {
|
||||
nic = common.parseNicStr(nic);
|
||||
if (networksSeen[nic.ipv4_uuid]) {
|
||||
throw new errors.UsageError(format(
|
||||
'only 1 ip on a network allowed '
|
||||
+ '(network %s specified multiple times)',
|
||||
nic.ipv4_uuid));
|
||||
}
|
||||
networksSeen[nic.ipv4_uuid] = true;
|
||||
ctx.nics.push(nic);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
log.trace({nics: ctx.nics}, 'parsed nics');
|
||||
|
||||
next();
|
||||
},
|
||||
|
||||
function loadMetadata(ctx, next) {
|
||||
mat.metadataFromOpts(opts, log, function (err, metadata) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
if (metadata) {
|
||||
log.trace({metadata: metadata},
|
||||
'metadata loaded from opts');
|
||||
ctx.metadata = metadata;
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
function loadTags(ctx, next) {
|
||||
mat.tagsFromCreateOpts(opts, log, function (err, tags) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
if (tags) {
|
||||
log.trace({tags: tags}, 'tags loaded from opts');
|
||||
ctx.tags = tags;
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
function getImg(ctx, next) {
|
||||
var _opts = {
|
||||
name: args[0],
|
||||
excludeInactive: true,
|
||||
useCache: true
|
||||
};
|
||||
tritonapi.getImage(_opts, function (err, img) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
ctx.img = img;
|
||||
log.trace({img: img}, 'create-instance img');
|
||||
next();
|
||||
});
|
||||
},
|
||||
function getPkg(ctx, next) {
|
||||
if (args.length < 2) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var id = args[1];
|
||||
if (common.isUUID(id)) {
|
||||
ctx.pkg = {id: id};
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
tritonapi.getPackage(id, function (err, pkg) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
log.trace({pkg: pkg}, 'create-instance pkg');
|
||||
ctx.pkg = pkg;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function getNets(ctx, next) {
|
||||
if (!opts.network) {
|
||||
return next();
|
||||
}
|
||||
// TODO: want an error or warning on no networks?
|
||||
ctx.nets = [];
|
||||
vasync.forEachPipeline({
|
||||
inputs: opts.network,
|
||||
func: function getOneNetwork(name, nextNet) {
|
||||
tritonapi.getNetwork(name, function (err, net) {
|
||||
if (err) {
|
||||
nextNet(err);
|
||||
} else {
|
||||
ctx.nets.push(net);
|
||||
nextNet();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, next);
|
||||
},
|
||||
|
||||
function createInst(ctx, next) {
|
||||
assert.optionalArrayOfObject(ctx.volMounts, 'ctx.volMounts');
|
||||
|
||||
var createOpts = {
|
||||
name: opts.name,
|
||||
image: ctx.img.id,
|
||||
'package': ctx.pkg && ctx.pkg.id
|
||||
};
|
||||
|
||||
if (ctx.nets) {
|
||||
createOpts.networks = ctx.nets.map(function (net) {
|
||||
return net.id;
|
||||
});
|
||||
} else if (ctx.nics) {
|
||||
createOpts.networks = ctx.nics;
|
||||
}
|
||||
|
||||
if (ctx.volMounts) {
|
||||
createOpts.volumes = ctx.volMounts;
|
||||
}
|
||||
if (opts.affinity) {
|
||||
createOpts.affinity = opts.affinity;
|
||||
}
|
||||
if (ctx.metadata) {
|
||||
Object.keys(ctx.metadata).forEach(function (key) {
|
||||
createOpts['metadata.'+key] = ctx.metadata[key];
|
||||
});
|
||||
}
|
||||
if (ctx.tags) {
|
||||
Object.keys(ctx.tags).forEach(function (key) {
|
||||
createOpts['tag.'+key] = ctx.tags[key];
|
||||
});
|
||||
}
|
||||
if (opts.allow_shared_images) {
|
||||
createOpts.allow_shared_images = true;
|
||||
}
|
||||
|
||||
for (var i = 0; i < opts._order.length; i++) {
|
||||
var opt = opts._order[i];
|
||||
if (opt.key === 'firewall') {
|
||||
createOpts.firewall_enabled = opt.value;
|
||||
} else if (opt.key === 'deletion_protection') {
|
||||
createOpts.deletion_protection = opt.value;
|
||||
}
|
||||
}
|
||||
|
||||
log.trace({dryRun: opts.dry_run, createOpts: createOpts},
|
||||
'create-instance createOpts');
|
||||
ctx.start = Date.now();
|
||||
if (opts.dry_run) {
|
||||
ctx.inst = {
|
||||
id: 'beefbeef-4c0e-11e5-86cd-a7fd38d2a50b',
|
||||
name: 'this-is-a-dry-run'
|
||||
};
|
||||
console.log('Creating instance %s (%s, %s@%s)',
|
||||
ctx.inst.name, ctx.inst.id,
|
||||
ctx.img.name, ctx.img.version);
|
||||
return next();
|
||||
}
|
||||
|
||||
tritonapi.cloudapi.createMachine(createOpts, function (err, inst) {
|
||||
if (err) {
|
||||
next(new errors.TritonError(err,
|
||||
'error creating instance'));
|
||||
return;
|
||||
}
|
||||
ctx.inst = inst;
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(inst));
|
||||
} else {
|
||||
console.log('Creating instance %s (%s, %s@%s%s)',
|
||||
inst.name, inst.id, ctx.img.name, ctx.img.version,
|
||||
inst.package ? format(', %s', inst.package) : '');
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
function maybeWait(ctx, next) {
|
||||
if (!opts.wait) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 1 'wait': no distraction.
|
||||
// >1 'wait': distraction, pass in the N.
|
||||
var distraction;
|
||||
if (process.stderr.isTTY && opts.wait.length > 1) {
|
||||
distraction = distractions.createDistraction(opts.wait.length);
|
||||
}
|
||||
|
||||
// Dry-run: fake wait for a few seconds.
|
||||
var waiter = (opts.dry_run ?
|
||||
function dryWait(waitOpts, waitCb) {
|
||||
setTimeout(function () {
|
||||
ctx.inst.state = 'running';
|
||||
waitCb(null, ctx.inst);
|
||||
}, 5000);
|
||||
} : tritonapi.cloudapi.waitForMachineStates.bind(
|
||||
tritonapi.cloudapi));
|
||||
|
||||
waiter({
|
||||
id: ctx.inst.id,
|
||||
states: ['running', 'failed']
|
||||
}, function (err, inst) {
|
||||
if (distraction) {
|
||||
distraction.destroy();
|
||||
}
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(inst));
|
||||
} else if (inst.state === 'running') {
|
||||
var dur = Date.now() - ctx.start;
|
||||
console.log('Created instance %s (%s) in %s',
|
||||
inst.name, inst.id, common.humanDurationFromMs(dur));
|
||||
}
|
||||
if (inst.state !== 'running') {
|
||||
next(new Error(format('failed to create instance %s (%s)',
|
||||
inst.name, inst.id)));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
]}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
do_create.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
group: 'Create options'
|
||||
},
|
||||
{
|
||||
names: ['name', 'n'],
|
||||
helpArg: 'NAME',
|
||||
type: 'string',
|
||||
help: 'Instance name. If not given, one will be generated server-side.'
|
||||
},
|
||||
{
|
||||
names: ['tag', 't'],
|
||||
type: 'arrayOfString',
|
||||
helpArg: 'TAG',
|
||||
help: 'Add a tag when creating the instance. Tags are ' +
|
||||
'key/value pairs available on the instance API object as the ' +
|
||||
'"tags" field. TAG is one of: a "key=value" string (bool and ' +
|
||||
'numeric "value" are converted to that type), a JSON object ' +
|
||||
'(if first char is "{"), or a "@FILE" to have tags be ' +
|
||||
'loaded from FILE. This option can be used multiple times.'
|
||||
},
|
||||
{
|
||||
names: ['affinity', 'a'],
|
||||
type: 'arrayOfString',
|
||||
helpArg: 'RULE',
|
||||
help: 'Affinity rules for selecting a server for this instance. ' +
|
||||
'Rules have one of the following forms: `instance==INST` (the ' +
|
||||
'new instance must be on the same server as INST), ' +
|
||||
'`instance!=INST` (new inst must *not* be on the same server as ' +
|
||||
'INST), `instance==~INST` (*attempt* to place on the same server ' +
|
||||
'as INST), or `instance!=~INST` (*attempt* to place on a server ' +
|
||||
'other than INST\'s). `INST` is an existing instance name or ' +
|
||||
'id. Use this option more than once for multiple rules.',
|
||||
completionType: 'tritonaffinityrule'
|
||||
},
|
||||
|
||||
{
|
||||
group: ''
|
||||
},
|
||||
{
|
||||
names: ['network', 'N'],
|
||||
type: 'arrayOfCommaSepString',
|
||||
helpArg: 'NETWORK',
|
||||
help: 'One or more comma-separated networks (ID, name or short id). ' +
|
||||
'This option can be used multiple times.',
|
||||
completionType: 'tritonnetwork'
|
||||
},
|
||||
{
|
||||
names: ['nic'],
|
||||
type: 'arrayOfString',
|
||||
helpArg: 'NICOPTS',
|
||||
help: 'A network interface object containing comma separated ' +
|
||||
'key=value pairs (Network object format). ' +
|
||||
'This option can be used multiple times for multiple NICs. ' +
|
||||
'Valid keys are: ' + Object.keys(NETWORK_OBJECT_FIELDS).join(', ')
|
||||
},
|
||||
{
|
||||
// TODO: add boolNegationPrefix:'no-' when that cmdln pull is in
|
||||
names: ['firewall'],
|
||||
type: 'bool',
|
||||
help: 'Enable Cloud Firewall on this instance. See ' +
|
||||
'<https://docs.spearhead.cloud/network/firewall>'
|
||||
},
|
||||
{
|
||||
names: ['deletion-protection'],
|
||||
type: 'bool',
|
||||
help: 'Enable Deletion Protection on this instance. Such an instance ' +
|
||||
'cannot be deleted until the protection is disabled. See ' +
|
||||
'<https://apidocs.joyent.com/cloudapi/#deletion-protection>'
|
||||
},
|
||||
{
|
||||
names: ['volume', 'v'],
|
||||
type: 'arrayOfString',
|
||||
help: 'Mount a volume into the instance (non-KVM only). VOLMOUNT is ' +
|
||||
'"<volume-name:/mount/point>[: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: ''
|
||||
},
|
||||
{
|
||||
names: ['metadata', 'm'],
|
||||
type: 'arrayOfString',
|
||||
helpArg: 'DATA',
|
||||
help: 'Add metadata when creating the instance. Metadata are ' +
|
||||
'key/value pairs available on the instance API object as the ' +
|
||||
'"metadata" field, and inside the instance via the "mdata-*" ' +
|
||||
'commands. DATA is one of: a "key=value" string (bool and ' +
|
||||
'numeric "value" are converted to that type), a JSON object ' +
|
||||
'(if first char is "{"), or a "@FILE" to have metadata be ' +
|
||||
'loaded from FILE. This option can be used multiple times.'
|
||||
},
|
||||
{
|
||||
names: ['metadata-file', 'M'],
|
||||
type: 'arrayOfString',
|
||||
helpArg: 'KEY=FILE',
|
||||
help: 'Set a metadata key KEY from the contents of FILE.'
|
||||
},
|
||||
{
|
||||
names: ['script'],
|
||||
type: 'arrayOfString',
|
||||
helpArg: 'FILE',
|
||||
help: 'Load a file to be used for the "user-script" metadata key. In ' +
|
||||
'Joyent-provided images, the user-script is run at every boot ' +
|
||||
'of the instance. This is a shortcut for `-M user-script=FILE`.'
|
||||
},
|
||||
{
|
||||
names: ['allow-shared-images'],
|
||||
type: 'bool',
|
||||
help: 'Allow instance creation to use a shared image.'
|
||||
},
|
||||
|
||||
{
|
||||
group: 'Other options'
|
||||
},
|
||||
{
|
||||
names: ['dry-run'],
|
||||
type: 'bool',
|
||||
help: 'Go through the motions without actually creating.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'arrayOfBool',
|
||||
help: 'Wait for the creation to complete. Use multiple times for a ' +
|
||||
'spinner.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
}
|
||||
];
|
||||
|
||||
do_create.synopses = ['{{name}} {{cmd}} [OPTIONS] IMAGE PACKAGE'];
|
||||
|
||||
do_create.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Create a new instance.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where IMAGE is an image name, name@version, id, or short id (from ',
|
||||
'`spearhead image list`) and PACKAGE is a package name, id, or short id',
|
||||
'(from `spearhead package list`).'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_create.helpOpts = {
|
||||
maxHelpCol: 16
|
||||
};
|
||||
|
||||
do_create.completionArgtypes = ['tritonimage', 'tritonpackage', 'none'];
|
||||
|
||||
module.exports = do_create;
|
@ -1,17 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton instance delete ...`
|
||||
*/
|
||||
|
||||
var gen_do_ACTION = require('./gen_do_ACTION');
|
||||
|
||||
|
||||
var do_delete = gen_do_ACTION({action: 'delete', aliases: ['rm']});
|
||||
module.exports = do_delete;
|
@ -1,125 +0,0 @@
|
||||
/*
|
||||
* 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 2018 Joyent, Inc.
|
||||
*
|
||||
* `triton instance disable-deletion-protection ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
function do_disable_deletion_protection(subcmd, opts, args, cb) {
|
||||
assert.object(opts, 'opts');
|
||||
assert.arrayOfString(args, 'args');
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
cb(new errors.UsageError('missing INST argument(s)'));
|
||||
return;
|
||||
}
|
||||
|
||||
var cli = this.top;
|
||||
|
||||
function wait(name, id, next) {
|
||||
assert.string(name, 'name');
|
||||
assert.uuid(id, 'id');
|
||||
assert.func(next, 'next');
|
||||
|
||||
cli.tritonapi.cloudapi.waitForDeletionProtectionEnabled({
|
||||
id: id,
|
||||
state: false
|
||||
}, function (err, inst) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.ok(!inst.deletion_protection, 'inst ' + id
|
||||
+ ' deletion_protection not in expected state after '
|
||||
+ 'waitForDeletionProtectionEnabled');
|
||||
|
||||
console.log('Disabled deletion protection for instance "%s"', name);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function disableOne(name, next) {
|
||||
assert.string(name, 'name');
|
||||
assert.func(next, 'next');
|
||||
|
||||
cli.tritonapi.disableInstanceDeletionProtection({
|
||||
id: name
|
||||
}, function disableProtectionCb(err, fauxInst) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Disabling deletion protection for instance "%s"',
|
||||
name);
|
||||
|
||||
if (opts.wait) {
|
||||
wait(name, fauxInst.id, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
|
||||
vasync.forEachParallel({
|
||||
inputs: args,
|
||||
func: disableOne
|
||||
}, function vasyncCb(err) {
|
||||
cb(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_disable_deletion_protection.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Wait for deletion protection to be removed.'
|
||||
}
|
||||
];
|
||||
do_disable_deletion_protection.synopses = [
|
||||
'{{name}} disable-deletion-protection [OPTIONS] INST [INST ...]'
|
||||
];
|
||||
do_disable_deletion_protection.help = [
|
||||
'Disable deletion protection on one or more instances.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "INST" is an instance name, id, or short id.'
|
||||
].join('\n');
|
||||
|
||||
do_disable_deletion_protection.completionArgtypes = ['tritoninstance'];
|
||||
|
||||
module.exports = do_disable_deletion_protection;
|
@ -1,112 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton instance disable-firewall ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
function do_disable_firewall(subcmd, opts, args, cb) {
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
cb(new errors.UsageError('missing INST argument(s)'));
|
||||
return;
|
||||
}
|
||||
|
||||
var cli = this.top;
|
||||
|
||||
function wait(name, id, next) {
|
||||
cli.tritonapi.cloudapi.waitForMachineFirewallEnabled({
|
||||
id: id,
|
||||
state: false
|
||||
}, function (err, inst) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
assert.ok(!inst.firewall_enabled, format(
|
||||
'inst %s firewall_enabled not in expected state after '
|
||||
+ 'waitForMachineFirewallEnabled', id));
|
||||
|
||||
console.log('Disabled firewall for instance "%s"', name);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
vasync.forEachParallel({
|
||||
inputs: args,
|
||||
func: function disableOne(name, nextInst) {
|
||||
cli.tritonapi.disableInstanceFirewall({
|
||||
id: name
|
||||
}, function (err, fauxInst) {
|
||||
if (err) {
|
||||
nextInst(err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Disabling firewall for instance "%s"', name);
|
||||
|
||||
if (opts.wait) {
|
||||
wait(name, fauxInst.id, nextInst);
|
||||
} else {
|
||||
nextInst();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_disable_firewall.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Wait for the firewall to be disabled.'
|
||||
}
|
||||
];
|
||||
do_disable_firewall.synopses = [
|
||||
'{{name}} disable-firewall [OPTIONS] INST [INST ...]'
|
||||
];
|
||||
do_disable_firewall.help = [
|
||||
'Disable the firewall of one or more instances.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "INST" is an instance name, id, or short id.'
|
||||
].join('\n');
|
||||
|
||||
do_disable_firewall.completionArgtypes = ['tritoninstance'];
|
||||
|
||||
module.exports = do_disable_firewall;
|
@ -1,125 +0,0 @@
|
||||
/*
|
||||
* 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 2018 Joyent, Inc.
|
||||
*
|
||||
* `triton instance enable-deletion-protection ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
function do_enable_deletion_protection(subcmd, opts, args, cb) {
|
||||
assert.object(opts, 'opts');
|
||||
assert.arrayOfString(args, 'args');
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
cb(new errors.UsageError('missing INST argument(s)'));
|
||||
return;
|
||||
}
|
||||
|
||||
var cli = this.top;
|
||||
|
||||
function wait(name, id, next) {
|
||||
assert.string(name, 'name');
|
||||
assert.uuid(id, 'id');
|
||||
assert.func(next, 'next');
|
||||
|
||||
cli.tritonapi.cloudapi.waitForDeletionProtectionEnabled({
|
||||
id: id,
|
||||
state: true
|
||||
}, function (err, inst) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.ok(inst.deletion_protection, 'inst ' + id
|
||||
+ ' deletion_protection not in expected state after '
|
||||
+ 'waitForDeletionProtectionEnabled');
|
||||
|
||||
console.log('Enabled deletion protection for instance "%s"', name);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function enableOne(name, next) {
|
||||
assert.string(name, 'name');
|
||||
assert.func(next, 'next');
|
||||
|
||||
cli.tritonapi.enableInstanceDeletionProtection({
|
||||
id: name
|
||||
}, function enableProtectionCb(err, fauxInst) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Enabling deletion protection for instance "%s"',
|
||||
name);
|
||||
|
||||
if (opts.wait) {
|
||||
wait(name, fauxInst.id, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
|
||||
vasync.forEachParallel({
|
||||
inputs: args,
|
||||
func: enableOne
|
||||
}, function vasyncCb(err) {
|
||||
cb(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_enable_deletion_protection.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Wait for deletion protection to be enabled.'
|
||||
}
|
||||
];
|
||||
do_enable_deletion_protection.synopses = [
|
||||
'{{name}} enable-deletion-protection [OPTIONS] INST [INST ...]'
|
||||
];
|
||||
do_enable_deletion_protection.help = [
|
||||
'Enable deletion protection for one or more instances.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "INST" is an instance name, id, or short id.'
|
||||
].join('\n');
|
||||
|
||||
do_enable_deletion_protection.completionArgtypes = ['tritoninstance'];
|
||||
|
||||
module.exports = do_enable_deletion_protection;
|
@ -1,112 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton instance enable-firewall ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
function do_enable_firewall(subcmd, opts, args, cb) {
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
cb(new errors.UsageError('missing INST argument(s)'));
|
||||
return;
|
||||
}
|
||||
|
||||
var cli = this.top;
|
||||
|
||||
function wait(name, id, next) {
|
||||
cli.tritonapi.cloudapi.waitForMachineFirewallEnabled({
|
||||
id: id,
|
||||
state: true
|
||||
}, function (err, inst) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
assert.ok(inst.firewall_enabled, format(
|
||||
'inst %s firewall_enabled not in expected state after '
|
||||
+ 'waitForMachineFirewallEnabled', id));
|
||||
|
||||
console.log('Enabled firewall for instance "%s"', name);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
vasync.forEachParallel({
|
||||
inputs: args,
|
||||
func: function enableOne(name, nextInst) {
|
||||
cli.tritonapi.enableInstanceFirewall({
|
||||
id: name
|
||||
}, function (err, fauxInst) {
|
||||
if (err) {
|
||||
nextInst(err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Enabling firewall for instance "%s"', name);
|
||||
|
||||
if (opts.wait) {
|
||||
wait(name, fauxInst.id, nextInst);
|
||||
} else {
|
||||
nextInst();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_enable_firewall.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Wait for the firewall to be enabled.'
|
||||
}
|
||||
];
|
||||
do_enable_firewall.synopses = [
|
||||
'{{name}} enable-firewall [OPTIONS] INST [INST ...]'
|
||||
];
|
||||
do_enable_firewall.help = [
|
||||
'Enable the firewall of one or more instances.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "INST" is an instance name, id, or short id.'
|
||||
].join('\n');
|
||||
|
||||
do_enable_firewall.completionArgtypes = ['tritoninstance'];
|
||||
|
||||
module.exports = do_enable_firewall;
|
@ -1,113 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton instance fwrules ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var tabula = require('tabula');
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
|
||||
|
||||
var COLUMNS_DEFAULT = 'shortid,enabled,global,rule';
|
||||
var COLUMNS_LONG = 'id,enabled,global,rule,description';
|
||||
var SORT_DEFAULT = 'rule';
|
||||
|
||||
|
||||
function do_list(subcmd, opts, args, cb) {
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
cb(new errors.UsageError('missing INST argument'));
|
||||
return;
|
||||
} else if (args.length > 1) {
|
||||
cb(new errors.UsageError('too many arguments: ' + args.join(' ')));
|
||||
return;
|
||||
}
|
||||
|
||||
var id = args[0];
|
||||
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
tritonapi.listInstanceFirewallRules({
|
||||
id: id
|
||||
}, function onRules(err, rules) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
common.jsonStream(rules);
|
||||
} else {
|
||||
var columns = COLUMNS_DEFAULT;
|
||||
|
||||
if (opts.o) {
|
||||
columns = opts.o;
|
||||
} else if (opts.long) {
|
||||
columns = COLUMNS_LONG;
|
||||
}
|
||||
|
||||
columns = columns.toLowerCase().split(',');
|
||||
var sort = opts.s.toLowerCase().split(',');
|
||||
|
||||
if (columns.indexOf('shortid') !== -1) {
|
||||
rules.forEach(function (rule) {
|
||||
rule.shortid = common.normShortId(rule.id);
|
||||
});
|
||||
}
|
||||
|
||||
tabula(rules, {
|
||||
skipHeader: opts.H,
|
||||
columns: columns,
|
||||
sort: sort
|
||||
});
|
||||
}
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_list.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
}
|
||||
].concat(common.getCliTableOptions({
|
||||
includeLong: true,
|
||||
sortDefault: SORT_DEFAULT
|
||||
}));
|
||||
|
||||
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] INST'];
|
||||
|
||||
do_list.help = [
|
||||
'Show firewall rules applied to an instance.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "INST" is an instance name, id, or short id.'
|
||||
].join('\n');
|
||||
|
||||
do_list.completionArgtypes = ['tritoninstance', 'none'];
|
||||
|
||||
module.exports = do_list;
|
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/*
|
||||
* `triton instance fwrule ...`
|
||||
*/
|
||||
|
||||
var Cmdln = require('cmdln').Cmdln;
|
||||
var util = require('util');
|
||||
|
||||
|
||||
// ---- CLI class
|
||||
|
||||
function InstanceFwruleCLI(parent) {
|
||||
this.top = parent.top;
|
||||
Cmdln.call(this, {
|
||||
name: parent.name + ' fwrule',
|
||||
desc: [
|
||||
'List fwrules on Spearhead instances.'
|
||||
].join('\n'),
|
||||
helpOpts: {
|
||||
minHelpCol: 24 /* line up with option help */
|
||||
},
|
||||
helpSubcmds: [
|
||||
'help',
|
||||
'list'
|
||||
]
|
||||
});
|
||||
}
|
||||
util.inherits(InstanceFwruleCLI, Cmdln);
|
||||
|
||||
// `triton instance fwrules` came first, so we'll hide this one.
|
||||
InstanceFwruleCLI.hidden = true;
|
||||
|
||||
InstanceFwruleCLI.prototype.init = function init(opts, args, cb) {
|
||||
this.log = this.top.log;
|
||||
Cmdln.prototype.init.apply(this, arguments);
|
||||
};
|
||||
|
||||
InstanceFwruleCLI.prototype.do_list = require('./do_list');
|
||||
|
||||
module.exports = InstanceFwruleCLI;
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/*
|
||||
* `triton instance fwrules ...` shortcut for
|
||||
* `triton instance fwrule list ...`.
|
||||
*/
|
||||
|
||||
function do_fwrules(subcmd, opts, args, callback) {
|
||||
this.handlerFromSubcmd('fwrule').dispatch({
|
||||
subcmd: 'list',
|
||||
opts: opts,
|
||||
args: args
|
||||
}, callback);
|
||||
}
|
||||
|
||||
var do_fwrule_list = require('./do_fwrule/do_list');
|
||||
do_fwrules.help = do_fwrule_list.help;
|
||||
do_fwrules.options = do_fwrule_list.options;
|
||||
do_fwrules.synopses = do_fwrule_list.synopses;
|
||||
do_fwrules.completionArgtypes = do_fwrule_list.completionArgtypes;
|
||||
|
||||
module.exports = do_fwrules;
|
@ -1,83 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton instance get ...`
|
||||
*/
|
||||
|
||||
var common = require('../common');
|
||||
|
||||
function do_get(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
return this.do_help('help', {}, [subcmd], cb);
|
||||
} else if (args.length !== 1) {
|
||||
return cb(new Error('invalid args: ' + args));
|
||||
}
|
||||
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
tritonapi.getInstance({
|
||||
id: args[0],
|
||||
credentials: opts.credentials
|
||||
}, function onInst(err, inst) {
|
||||
if (inst) {
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(inst));
|
||||
} else {
|
||||
console.log(JSON.stringify(inst, null, 4));
|
||||
}
|
||||
}
|
||||
cb(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_get.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['credentials'],
|
||||
type: 'bool',
|
||||
help: 'Include generated credentials, in the "metadata.credentials" ' +
|
||||
'field, if any. Typically used with "-j", though one can show ' +
|
||||
'values with "-o metadata.credentials".'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON output.'
|
||||
}
|
||||
];
|
||||
do_get.synopses = ['{{name}} {{cmd}} [OPTIONS] INST'];
|
||||
do_get.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Get an instance.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "INST" is an instance name, id, or short id.',
|
||||
'',
|
||||
'A *deleted* instance may still respond with the instance object. In that',
|
||||
'case a the instance will be print *and* an error will be raised.',
|
||||
'',
|
||||
'Currently this dumps prettified JSON by default. That might change',
|
||||
'in the future. Use "-j" to explicitly get JSON output.'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_get.completionArgtypes = ['tritoninstance', 'none'];
|
||||
|
||||
module.exports = do_get;
|
@ -1,80 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton instance ip ...`
|
||||
*/
|
||||
|
||||
var format = require('util').format;
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
function do_ip(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length === 0) {
|
||||
cb(new errors.UsageError('missing INST argument'));
|
||||
return;
|
||||
} else if (args.length > 1) {
|
||||
cb(new errors.UsageError('too many arguments: ' + args.join(' ')));
|
||||
return;
|
||||
}
|
||||
|
||||
var cli = this.top;
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
cli.tritonapi.getInstance(args[0], function (err, inst) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!inst.primaryIp) {
|
||||
cb(new errors.TritonError(format(
|
||||
'primaryIp not found for instance "%s"', args[0])));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(inst.primaryIp);
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_ip.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
}
|
||||
];
|
||||
|
||||
do_ip.synopses = ['{{name}} {{cmd}} INST'];
|
||||
|
||||
do_ip.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Print the primary IP of the given instance.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "INST" is an instance name, id, or short id.',
|
||||
'For example: ssh root@$(spearhead ip my-instance)'
|
||||
].join('\n');
|
||||
|
||||
|
||||
do_ip.completionArgtypes = ['tritoninstance', 'none'];
|
||||
|
||||
module.exports = do_ip;
|
@ -1,230 +0,0 @@
|
||||
/*
|
||||
* 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 (c) 2018, Joyent, Inc.
|
||||
*
|
||||
* `triton instance list ...`
|
||||
*/
|
||||
|
||||
var format = require('util').format;
|
||||
var tabula = require('tabula');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
|
||||
|
||||
/*
|
||||
* Filters to be passed as query string args to /my/machines.
|
||||
* See <https://apidocs.joyent.com/cloudapi/#ListMachines>.
|
||||
*/
|
||||
var validFilters = [
|
||||
'brand', // Added in CloudAPI 8.0.0
|
||||
'docker', // Added in CloudAPI 8.0.0
|
||||
'image',
|
||||
'memory',
|
||||
'name',
|
||||
'state',
|
||||
// jsl:ignore
|
||||
/^tag\./,
|
||||
// jsl:end
|
||||
'type'
|
||||
];
|
||||
|
||||
// columns default without -o
|
||||
var columnsDefault = 'shortid,name,img,state,flags,age';
|
||||
|
||||
// columns default with -l
|
||||
var columnsDefaultLong
|
||||
= 'id,name,img,brand,package,state,flags,primaryIp,created';
|
||||
|
||||
// sort default with -s
|
||||
var sortDefault = 'created';
|
||||
|
||||
function do_list(subcmd, opts, args, callback) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
}
|
||||
var log = self.top.log;
|
||||
|
||||
var columns = columnsDefault;
|
||||
if (opts.o) {
|
||||
columns = opts.o;
|
||||
} else if (opts.long) {
|
||||
columns = columnsDefaultLong;
|
||||
}
|
||||
columns = columns.split(',');
|
||||
|
||||
var sort = opts.s.split(',');
|
||||
|
||||
var listOpts;
|
||||
try {
|
||||
listOpts = common.objFromKeyValueArgs(args, {
|
||||
disableDotted: true,
|
||||
validKeys: validFilters,
|
||||
disableTypeConversions: true
|
||||
});
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
if (opts.credentials) {
|
||||
listOpts.credentials = true;
|
||||
}
|
||||
|
||||
var imgs = [];
|
||||
var insts;
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
return;
|
||||
}
|
||||
vasync.parallel({funcs: [
|
||||
function getTheImages(next) {
|
||||
self.top.tritonapi.listImages({
|
||||
state: 'all',
|
||||
useCache: true
|
||||
}, function (err, _imgs) {
|
||||
if (err) {
|
||||
if (err.statusCode === 403) {
|
||||
/*
|
||||
* This could be a authorization error due
|
||||
* to RBAC on a subuser. We don't want to
|
||||
* fail `triton inst ls` if the subuser
|
||||
* can ListMachines, but not ListImages.
|
||||
*/
|
||||
log.debug(
|
||||
err,
|
||||
'authz error listing images for insts info');
|
||||
next();
|
||||
} else {
|
||||
next(err);
|
||||
}
|
||||
} else {
|
||||
imgs = _imgs;
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
function getTheMachines(next) {
|
||||
self.top.tritonapi.cloudapi.listMachines(
|
||||
listOpts,
|
||||
function (err, _insts) {
|
||||
if (err) {
|
||||
next(err);
|
||||
} else {
|
||||
insts = _insts;
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
]}, function (err, results) {
|
||||
/*
|
||||
* Error handling: vasync.parallel's `err` is always a
|
||||
* MultiError. We want to prefer the `getTheMachines` err,
|
||||
* e.g. if both get a self-signed cert error.
|
||||
*/
|
||||
if (err) {
|
||||
err = results.operations[1].err || err;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// map "uuid" => "image_name"
|
||||
var imgmap = {};
|
||||
imgs.forEach(function (img) {
|
||||
imgmap[img.id] = format('%s@%s', img.name, img.version);
|
||||
});
|
||||
|
||||
// Add extra fields for nice output.
|
||||
var now = new Date();
|
||||
insts.forEach(function (inst) {
|
||||
var created = new Date(inst.created);
|
||||
inst.age = common.longAgo(created, now);
|
||||
inst.img = imgmap[inst.image] ||
|
||||
common.uuidToShortId(inst.image);
|
||||
inst.shortid = inst.id.split('-', 1)[0];
|
||||
var flags = [];
|
||||
if (inst.brand === 'bhyve') flags.push('B');
|
||||
if (inst.docker) flags.push('D');
|
||||
if (inst.firewall_enabled) flags.push('F');
|
||||
if (inst.brand === 'kvm') flags.push('K');
|
||||
if (inst.deletion_protection) flags.push('P');
|
||||
inst.flags = flags.length ? flags.join('') : undefined;
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
common.jsonStream(insts);
|
||||
} else {
|
||||
tabula(insts, {
|
||||
skipHeader: opts.H,
|
||||
columns: columns,
|
||||
sort: sort,
|
||||
dottedLookup: true
|
||||
});
|
||||
}
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_list.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['credentials'],
|
||||
type: 'bool',
|
||||
help: 'Include generated credentials, in the "metadata.credentials" ' +
|
||||
'keys, if any. Typically used with "-j", though one can show ' +
|
||||
'values with "-o metadata.credentials".'
|
||||
}
|
||||
].concat(common.getCliTableOptions({
|
||||
includeLong: true,
|
||||
sortDefault: sortDefault
|
||||
}));
|
||||
|
||||
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] [FILTERS...]'];
|
||||
|
||||
do_list.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'List instances.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Filters:',
|
||||
' FIELD=VALUE Equality filter. Supported fields: brand, image,',
|
||||
' memory, name, state, tag.TAGNAME, and type.',
|
||||
' FIELD=true|false Boolean filter. Supported fields: docker (added in',
|
||||
' CloudAPI 8.0.0).',
|
||||
'',
|
||||
'Fields (most are self explanatory, "*" indicates a field added client-side',
|
||||
'for convenience):',
|
||||
' shortid* A short ID prefix.',
|
||||
' flags* Single letter flags summarizing some fields:',
|
||||
' "B" the brand is "bhyve"',
|
||||
' "D" docker instance',
|
||||
' "F" firewall is enabled',
|
||||
' "K" the brand is "kvm"',
|
||||
' "P" deletion protected',
|
||||
' age* Approximate time since created, e.g. 1y, 2w.',
|
||||
' img* The image "name@version", if available, else its',
|
||||
' "shortid".',
|
||||
'',
|
||||
'Examples:',
|
||||
' {{name}} ls -Ho id state=running # IDs of running insts',
|
||||
' {{name}} ls docker=true tag.foo=bar # Docker insts w/ "foo=bar" tag'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_list.aliases = ['ls'];
|
||||
|
||||
module.exports = do_list;
|
@ -1,211 +0,0 @@
|
||||
/*
|
||||
* 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 2018 Joyent, Inc.
|
||||
*
|
||||
* `triton instance nic create ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
|
||||
function do_create(subcmd, opts, args, cb) {
|
||||
assert.optionalBool(opts.wait, 'opts.wait');
|
||||
assert.optionalBool(opts.json, 'opts.json');
|
||||
assert.optionalBool(opts.help, 'opts.help');
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length < 2) {
|
||||
cb(new errors.UsageError('missing INST and NETWORK or INST and' +
|
||||
' NICOPT=VALUE arguments'));
|
||||
return;
|
||||
}
|
||||
|
||||
var cli = this.top;
|
||||
|
||||
var netObj;
|
||||
var netObjArgs = [];
|
||||
var regularArgs = [];
|
||||
var createOpts = {};
|
||||
|
||||
args.forEach(function forEachArg(arg) {
|
||||
if (arg.indexOf('=') !== -1) {
|
||||
netObjArgs.push(arg);
|
||||
return;
|
||||
}
|
||||
regularArgs.push(arg);
|
||||
});
|
||||
|
||||
if (netObjArgs.length > 0) {
|
||||
if (regularArgs.length > 1) {
|
||||
cb(new errors.UsageError('cannot specify INST and NETWORK when'
|
||||
+ ' passing in ipv4 arguments'));
|
||||
return;
|
||||
}
|
||||
if (regularArgs.length !== 1) {
|
||||
cb(new errors.UsageError('missing INST argument'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
netObj = common.parseNicStr(netObjArgs);
|
||||
} catch (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (netObj) {
|
||||
assert.array(regularArgs, 'regularArgs');
|
||||
assert.equal(regularArgs.length, 1, 'instance uuid');
|
||||
|
||||
createOpts.id = regularArgs[0];
|
||||
createOpts.network = netObj;
|
||||
} else {
|
||||
assert.array(args, 'args');
|
||||
assert.equal(args.length, 2, 'INST and NETWORK');
|
||||
|
||||
createOpts.id = args[0];
|
||||
createOpts.network = args[1];
|
||||
}
|
||||
|
||||
function wait(instId, mac, next) {
|
||||
assert.string(instId, 'instId');
|
||||
assert.string(mac, 'mac');
|
||||
assert.func(next, 'next');
|
||||
|
||||
var waiter = cli.tritonapi.waitForNicStates.bind(cli.tritonapi);
|
||||
|
||||
/*
|
||||
* We request state running|stopped because net-agent is doing work to
|
||||
* keep a NICs state in sync with the VMs state. If a user adds a NIC
|
||||
* to a stopped instance the final state of the NIC should also be
|
||||
* stopped.
|
||||
*/
|
||||
waiter({
|
||||
id: instId,
|
||||
mac: mac,
|
||||
states: ['running', 'stopped']
|
||||
}, next);
|
||||
}
|
||||
|
||||
// same signature as wait(), but is a nop
|
||||
function waitNop(instId, mac, next) {
|
||||
assert.string(instId, 'instId');
|
||||
assert.string(mac, 'mac');
|
||||
assert.func(next, 'next');
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
|
||||
cli.tritonapi.addNic(createOpts, function onAddNic(err, nic) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// If a NIC exists on the network already we will receive a 302
|
||||
if (!nic) {
|
||||
var errMsg = 'Instance already has a NIC on that network';
|
||||
cb(new errors.TritonError(errMsg));
|
||||
return;
|
||||
}
|
||||
|
||||
// either wait or invoke a nop stub
|
||||
var func = opts.wait ? wait : waitNop;
|
||||
|
||||
if (opts.wait && !opts.json) {
|
||||
console.log('Creating NIC %s', nic.mac);
|
||||
}
|
||||
|
||||
func(createOpts.id, nic.mac, function onWait(err2, createdNic) {
|
||||
if (err2) {
|
||||
cb(err2);
|
||||
return;
|
||||
}
|
||||
|
||||
var nicInfo = createdNic || nic;
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(nicInfo));
|
||||
} else {
|
||||
console.log('Created NIC %s', nic.mac);
|
||||
}
|
||||
|
||||
cb();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_create.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Wait for the creation to complete.'
|
||||
}
|
||||
];
|
||||
|
||||
do_create.synopses = [
|
||||
'{{name}} {{cmd}} [OPTIONS] INST NETWORK',
|
||||
'{{name}} {{cmd}} [OPTIONS] INST NICOPT=VALUE [NICOPT=VALUE ...]'
|
||||
];
|
||||
|
||||
do_create.help = [
|
||||
'Create a NIC.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'INST is an instance id (full UUID), name, or short id,',
|
||||
'and NETWORK is a network id (full UUID), name, or short id.',
|
||||
'',
|
||||
'NICOPTs are NIC options. The following NIC options are supported:',
|
||||
'ipv4_uuid=<full network uuid> (required),' +
|
||||
' and ipv4_ips=<a single IP string>.',
|
||||
'',
|
||||
'Be aware that adding NICs to an instance will cause that instance to',
|
||||
'reboot.',
|
||||
'',
|
||||
'Example:',
|
||||
' triton instance nic create --wait 22b75576 ca8aefb9',
|
||||
' triton instance nic create 22b75576' +
|
||||
' ipv4_uuid=651446a8-dab0-439e-a2c4-2c841ab07c51' +
|
||||
' ipv4_ips=192.168.128.13'
|
||||
].join('\n');
|
||||
|
||||
do_create.helpOpts = {
|
||||
helpCol: 25
|
||||
};
|
||||
|
||||
do_create.completionArgtypes = ['tritoninstance', 'tritonnic', 'none'];
|
||||
|
||||
module.exports = do_create;
|
@ -1,126 +0,0 @@
|
||||
/*
|
||||
* 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 2018 Joyent, Inc.
|
||||
*
|
||||
* `triton instance nic delete ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
|
||||
|
||||
function do_delete(subcmd, opts, args, cb) {
|
||||
assert.object(opts, 'opts');
|
||||
assert.optionalBool(opts.force, 'opts.force');
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length < 2) {
|
||||
cb(new errors.UsageError('missing INST and MAC argument(s)'));
|
||||
return;
|
||||
} else if (args.length > 2) {
|
||||
cb(new errors.UsageError('incorrect number of arguments'));
|
||||
return;
|
||||
}
|
||||
|
||||
var inst = args[0];
|
||||
var mac = args[1];
|
||||
var cli = this.top;
|
||||
|
||||
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
|
||||
confirm({mac: mac, force: opts.force}, function onConfirm(confirmErr) {
|
||||
if (confirmErr) {
|
||||
console.error('Aborting');
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
cli.tritonapi.removeNic({
|
||||
id: inst,
|
||||
mac: mac
|
||||
}, function onRemove(err) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Deleted NIC %s', mac);
|
||||
cb();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Request confirmation before deleting, unless --force flag given.
|
||||
// If user declines, terminate early.
|
||||
function confirm(opts, cb) {
|
||||
assert.object(opts, 'opts');
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.force) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
common.promptYesNo({
|
||||
msg: 'Delete NIC "' + opts.mac + '"? [y/n] '
|
||||
}, function (answer) {
|
||||
if (answer !== 'y') {
|
||||
cb(new Error('Aborted NIC deletion'));
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_delete.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['force', 'f'],
|
||||
type: 'bool',
|
||||
help: 'Force removal.'
|
||||
}
|
||||
];
|
||||
|
||||
do_delete.synopses = ['{{name}} {{cmd}} INST MAC'];
|
||||
|
||||
do_delete.help = [
|
||||
'Remove a NIC from an instance.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where INST is an instance id (full UUID), name, or short id.',
|
||||
'',
|
||||
'Be aware that removing NICs from an instance will cause that instance to',
|
||||
'reboot.'
|
||||
].join('\n');
|
||||
|
||||
do_delete.aliases = ['rm'];
|
||||
|
||||
do_delete.completionArgtypes = ['tritoninstance', 'none'];
|
||||
|
||||
module.exports = do_delete;
|
@ -1,89 +0,0 @@
|
||||
/*
|
||||
* 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 2018 Joyent, Inc.
|
||||
*
|
||||
* `triton instance nic get ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
|
||||
|
||||
function do_get(subcmd, opts, args, cb) {
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length < 2) {
|
||||
cb(new errors.UsageError('missing INST and MAC arguments'));
|
||||
return;
|
||||
} else if (args.length > 2) {
|
||||
cb(new errors.UsageError('incorrect number of arguments'));
|
||||
return;
|
||||
}
|
||||
|
||||
var inst = args[0];
|
||||
var mac = args[1];
|
||||
var cli = this.top;
|
||||
|
||||
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
|
||||
cli.tritonapi.getNic({id: inst, mac: mac}, function onNic(err, nic) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(nic));
|
||||
} else {
|
||||
console.log(JSON.stringify(nic, null, 4));
|
||||
}
|
||||
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_get.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
}
|
||||
];
|
||||
|
||||
do_get.synopses = ['{{name}} {{cmd}} INST MAC'];
|
||||
|
||||
do_get.help = [
|
||||
'Show a specific NIC.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where INST is an instance id (full UUID), name, or short id.'
|
||||
].join('\n');
|
||||
|
||||
do_get.completionArgtypes = ['tritoninstance', 'none'];
|
||||
|
||||
module.exports = do_get;
|
@ -1,154 +0,0 @@
|
||||
/*
|
||||
* 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 2018 Joyent, Inc.
|
||||
*
|
||||
* `triton instance nic list ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var tabula = require('tabula');
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
|
||||
|
||||
var VALID_FILTERS = ['ip', 'mac', 'state', 'network', 'primary', 'gateway'];
|
||||
var COLUMNS_DEFAULT = 'ip,mac,state,network';
|
||||
var COLUMNS_DEFAULT_LONG = 'ip,mac,state,network,primary,gateway';
|
||||
var SORT_DEFAULT = 'ip';
|
||||
|
||||
|
||||
function do_list(subcmd, opts, args, cb) {
|
||||
assert.array(args, 'args');
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length < 1) {
|
||||
cb(new errors.UsageError('missing INST argument'));
|
||||
return;
|
||||
}
|
||||
|
||||
var inst = args.shift();
|
||||
|
||||
try {
|
||||
var filters = common.objFromKeyValueArgs(args, {
|
||||
validKeys: VALID_FILTERS,
|
||||
disableDotted: true
|
||||
});
|
||||
} catch (e) {
|
||||
cb(e);
|
||||
return;
|
||||
}
|
||||
|
||||
var cli = this.top;
|
||||
|
||||
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
|
||||
cli.tritonapi.listNics({id: inst}, function onNics(err, nics) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// do filtering
|
||||
Object.keys(filters).forEach(function filterByKey(key) {
|
||||
var val = filters[key];
|
||||
nics = nics.filter(function filterByNic(nic) {
|
||||
return nic[key] === val;
|
||||
});
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
common.jsonStream(nics);
|
||||
} else {
|
||||
nics.forEach(function onNic(nic) {
|
||||
nic.network = nic.network.split('-')[0];
|
||||
nic.ip = nic.ip + '/' + convertCidrSuffix(nic.netmask);
|
||||
});
|
||||
|
||||
var columns = COLUMNS_DEFAULT;
|
||||
|
||||
if (opts.o) {
|
||||
columns = opts.o;
|
||||
} else if (opts.long) {
|
||||
columns = COLUMNS_DEFAULT_LONG;
|
||||
}
|
||||
|
||||
columns = columns.split(',');
|
||||
var sort = opts.s.split(',');
|
||||
|
||||
tabula(nics, {
|
||||
skipHeader: opts.H,
|
||||
columns: columns,
|
||||
sort: sort
|
||||
});
|
||||
}
|
||||
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function convertCidrSuffix(netmask) {
|
||||
var bitmask = netmask.split('.').map(function (octet) {
|
||||
return (+octet).toString(2);
|
||||
}).join('');
|
||||
|
||||
var i = 0;
|
||||
for (i = 0; i < bitmask.length; i++) {
|
||||
if (bitmask[i] === '0')
|
||||
break;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
|
||||
do_list.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
}
|
||||
].concat(common.getCliTableOptions({
|
||||
includeLong: true,
|
||||
sortDefault: SORT_DEFAULT
|
||||
}));
|
||||
|
||||
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] [FILTERS]'];
|
||||
|
||||
do_list.help = [
|
||||
'Show all NICs on an instance.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'',
|
||||
'Where INST is an instance id (full UUID), name, or short id.',
|
||||
'',
|
||||
'Filters:',
|
||||
' FIELD=<string> String filter. Supported fields: ip, mac, state,',
|
||||
' network, netmask',
|
||||
'',
|
||||
'Filters are applied client-side (i.e. done by the triton command itself).'
|
||||
].join('\n');
|
||||
|
||||
do_list.completionArgtypes = ['tritoninstance', 'none'];
|
||||
|
||||
do_list.aliases = ['ls'];
|
||||
|
||||
module.exports = do_list;
|
@ -1,50 +0,0 @@
|
||||
/*
|
||||
* 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 2018 Joyent, Inc.
|
||||
*
|
||||
* `triton inst nic ...`
|
||||
*/
|
||||
|
||||
var Cmdln = require('cmdln').Cmdln;
|
||||
var util = require('util');
|
||||
|
||||
|
||||
|
||||
// ---- CLI class
|
||||
|
||||
function NicCLI(top) {
|
||||
this.top = top.top;
|
||||
|
||||
Cmdln.call(this, {
|
||||
name: top.name + ' nic',
|
||||
desc: 'List and manage instance NICs.',
|
||||
helpSubcmds: [
|
||||
'help',
|
||||
'list',
|
||||
'get',
|
||||
'create',
|
||||
'delete'
|
||||
],
|
||||
helpOpts: {
|
||||
minHelpCol: 23
|
||||
}
|
||||
});
|
||||
}
|
||||
util.inherits(NicCLI, Cmdln);
|
||||
|
||||
NicCLI.prototype.init = function init(opts, args, cb) {
|
||||
this.log = this.top.log;
|
||||
Cmdln.prototype.init.apply(this, arguments);
|
||||
};
|
||||
|
||||
NicCLI.prototype.do_list = require('./do_list');
|
||||
NicCLI.prototype.do_create = require('./do_create');
|
||||
NicCLI.prototype.do_get = require('./do_get');
|
||||
NicCLI.prototype.do_delete = require('./do_delete');
|
||||
|
||||
module.exports = NicCLI;
|
@ -1,105 +0,0 @@
|
||||
/*
|
||||
* 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 2017 Joyent, Inc.
|
||||
*
|
||||
* `triton instance reboot ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
function do_reboot(subcmd, opts, args, cb) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length < 1) {
|
||||
cb(new errors.UsageError('missing INST arg(s)'));
|
||||
return;
|
||||
}
|
||||
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
|
||||
var rebootErrs = [];
|
||||
|
||||
vasync.forEachParallel({
|
||||
inputs: args,
|
||||
func: function rebootOne(arg, nextInst) {
|
||||
console.log('Rebooting instance %s', arg);
|
||||
tritonapi.rebootInstance({
|
||||
id: arg,
|
||||
wait: opts.wait,
|
||||
waitTimeout: opts.wait_timeout * 1000
|
||||
}, function (rebootErr) {
|
||||
if (rebootErr) {
|
||||
rebootErrs.push(rebootErr);
|
||||
console.log('Error rebooting instance %s: %s', arg,
|
||||
rebootErr.message);
|
||||
} else if (opts.wait) {
|
||||
console.log('Rebooted instance %s', arg);
|
||||
}
|
||||
nextInst();
|
||||
});
|
||||
|
||||
}
|
||||
}, function doneReboots(err) {
|
||||
assert.ok(!err, '"err" should be impossible as written');
|
||||
if (rebootErrs.length === 1) {
|
||||
cb(rebootErrs[0]);
|
||||
} else if (rebootErrs.length > 1) {
|
||||
cb(new errors.MultiError(rebootErrs));
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_reboot.synopses = ['{{name}} reboot [OPTIONS] INST [INST ...]'];
|
||||
do_reboot.help = [
|
||||
'Reboot one or more instances.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "INST" is an instance name, id, or short id.'
|
||||
].join('\n');
|
||||
do_reboot.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Wait until the instance(s) have rebooted.'
|
||||
},
|
||||
{
|
||||
names: ['wait-timeout'],
|
||||
type: 'positiveInteger',
|
||||
default: 120,
|
||||
help: 'The number of seconds to wait before timing out with an error. '
|
||||
+ 'The default is 120 seconds.'
|
||||
}
|
||||
];
|
||||
|
||||
do_reboot.completionArgtypes = ['tritoninstance'];
|
||||
|
||||
|
||||
|
||||
module.exports = do_reboot;
|
@ -1,88 +0,0 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
function do_rename(subcmd, opts, args, callback) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length < 1) {
|
||||
callback(new errors.UsageError('missing INST arg'));
|
||||
return;
|
||||
} else if (args.length < 2) {
|
||||
callback(new errors.UsageError('missing NEWNAME arg'));
|
||||
return;
|
||||
}
|
||||
|
||||
var id = args[0];
|
||||
var name = args[1];
|
||||
console.log('Renaming instance %s to "%s"', id, name);
|
||||
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
return;
|
||||
}
|
||||
|
||||
tritonapi.renameInstance({
|
||||
id: id,
|
||||
name: name,
|
||||
wait: opts.wait,
|
||||
waitTimeout: opts.wait_timeout * 1000
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
console.log('Renamed instance %s to "%s"', id, name);
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_rename.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Block until renaming instance is complete.'
|
||||
},
|
||||
{
|
||||
names: ['wait-timeout'],
|
||||
type: 'positiveInteger',
|
||||
default: 120,
|
||||
help: 'The number of seconds to wait before timing out with an error. '
|
||||
+ 'The default is 120 seconds.'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
do_rename.synopses = ['{{name}} rename [OPTIONS] INST NEWNAME'];
|
||||
do_rename.help = [
|
||||
'Rename an instance.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "INST" is an instance name, id, or short id',
|
||||
'and "NEWNAME" is an instance name.',
|
||||
'',
|
||||
'Changing an instance name is asynchronous.',
|
||||
'Use "--wait" to not return until the changes are completed.'
|
||||
].join('\n');
|
||||
|
||||
do_rename.completionArgtypes = ['tritoninstance', 'none'];
|
||||
|
||||
|
||||
module.exports = do_rename;
|
@ -1,88 +0,0 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
function do_resize(subcmd, opts, args, callback) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length < 1) {
|
||||
callback(new errors.UsageError('missing INST arg'));
|
||||
return;
|
||||
} else if (args.length < 2) {
|
||||
callback(new errors.UsageError('missing PACKAGE arg'));
|
||||
return;
|
||||
}
|
||||
|
||||
var id = args[0];
|
||||
var pkg = args[1];
|
||||
console.log('Resizing instance %s to "%s"', id, pkg);
|
||||
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
return;
|
||||
}
|
||||
|
||||
tritonapi.resizeInstance({
|
||||
id: id,
|
||||
package: pkg,
|
||||
wait: opts.wait,
|
||||
waitTimeout: opts.wait_timeout * 1000
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
console.log('Resized instance %s to "%s"', id, pkg);
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_resize.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Block until resizing instance is complete.'
|
||||
},
|
||||
{
|
||||
names: ['wait-timeout'],
|
||||
type: 'positiveInteger',
|
||||
default: 120,
|
||||
help: 'The number of seconds to wait before timing out with an error. '
|
||||
+ 'The default is 120 seconds.'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
do_resize.synopses = ['{{name}} resize [OPTIONS] INST PACKAGE'];
|
||||
do_resize.help = [
|
||||
'Resize an instance.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "INST" is an instance name, id, or short id',
|
||||
'and "PACKAGE" is a package name, id, or short id.',
|
||||
'',
|
||||
'Changing an instance package is asynchronous.',
|
||||
'Use "--wait" to not return until the changes are completed.'
|
||||
].join('\n');
|
||||
|
||||
do_resize.completionArgtypes = ['tritoninstance', 'tritonpackage', 'none'];
|
||||
|
||||
|
||||
module.exports = do_resize;
|
@ -1,141 +0,0 @@
|
||||
/*
|
||||
* 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 2018 Joyent, Inc.
|
||||
*
|
||||
* `triton snapshot create ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
|
||||
|
||||
function do_create(subcmd, opts, args, cb) {
|
||||
assert.optionalString(opts.name, 'opts.name');
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
cb(new errors.UsageError('missing <inst> argument'));
|
||||
return;
|
||||
} else if (args.length > 1) {
|
||||
cb(new errors.UsageError('incorrect number of arguments'));
|
||||
return;
|
||||
}
|
||||
|
||||
var inst = args[0];
|
||||
var cli = this.top;
|
||||
|
||||
var createOpts = {
|
||||
userId: opts.userId,
|
||||
id: inst
|
||||
};
|
||||
|
||||
if (opts.name) {
|
||||
createOpts.name = opts.name;
|
||||
}
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function createSnapshot(ctx, next) {
|
||||
ctx.start = Date.now();
|
||||
|
||||
cli.tritonapi.createInstanceSnapshot(createOpts,
|
||||
function (err, snapshot, res) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Creating snapshot %s of instance %s',
|
||||
snapshot.name, createOpts.id);
|
||||
ctx.name = snapshot.name;
|
||||
ctx.instId = res.instId;
|
||||
|
||||
next();
|
||||
});
|
||||
},
|
||||
function maybeWait(ctx, next) {
|
||||
if (!opts.wait) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var cloudapi = cli.tritonapi.cloudapi;
|
||||
var waiter = cloudapi.waitForSnapshotStates.bind(cloudapi);
|
||||
|
||||
waiter({
|
||||
id: ctx.instId,
|
||||
name: ctx.name,
|
||||
states: ['created', 'failed']
|
||||
}, function (err, snap) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(snap));
|
||||
} else if (snap.state === 'created') {
|
||||
var duration = Date.now() - ctx.start;
|
||||
console.log('Created snapshot "%s" in %s', snap.name,
|
||||
common.humanDurationFromMs(duration));
|
||||
next();
|
||||
} else {
|
||||
next(new Error(format('Failed to create snapshot "%s"',
|
||||
snap.name)));
|
||||
}
|
||||
});
|
||||
}
|
||||
]}, cb);
|
||||
}
|
||||
|
||||
|
||||
do_create.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
},
|
||||
{
|
||||
names: ['name', 'n'],
|
||||
type: 'string',
|
||||
helpArg: 'SNAPNAME',
|
||||
help: 'An optional name for a snapshot.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Wait for the creation to complete.'
|
||||
}
|
||||
];
|
||||
|
||||
do_create.synopses = ['{{name}} {{cmd}} [OPTIONS] INST'];
|
||||
|
||||
do_create.help = [
|
||||
'Create a snapshot of an instance.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Snapshots do not work for instances of type "bhyve" or "kvm".'
|
||||
].join('\n');
|
||||
|
||||
do_create.completionArgtypes = ['tritoninstance', 'none'];
|
||||
|
||||
module.exports = do_create;
|
@ -1,160 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton snapshot delete ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
|
||||
|
||||
function do_delete(subcmd, opts, args, cb) {
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length < 2) {
|
||||
cb(new errors.UsageError('missing INST and SNAPNAME argument(s)'));
|
||||
return;
|
||||
}
|
||||
|
||||
var cli = this.top;
|
||||
var inst = args[0];
|
||||
var names = args.slice(1, args.length);
|
||||
|
||||
function wait(instId, name, startTime, next) {
|
||||
var cloudapi = cli.tritonapi.cloudapi;
|
||||
var waiter = cloudapi.waitForSnapshotStates.bind(cloudapi);
|
||||
|
||||
waiter({
|
||||
id: instId,
|
||||
name: name,
|
||||
states: ['deleted']
|
||||
}, function (err, snap) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (snap.state === 'deleted') {
|
||||
var duration = Date.now() - startTime;
|
||||
var durStr = common.humanDurationFromMs(duration);
|
||||
console.log('Deleted snapshot "%s" in %s', name, durStr);
|
||||
|
||||
next();
|
||||
} else {
|
||||
// shouldn't get here, but...
|
||||
next(new Error(format('Failed to delete snapshot "%s"', name)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function confirm(_, next) {
|
||||
if (opts.force) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var msg;
|
||||
if (names.length === 1) {
|
||||
msg = 'Delete snapshot "' + names[0] + '"? [y/n] ';
|
||||
} else {
|
||||
msg = format('Delete %d snapshots (%s)? [y/n] ',
|
||||
names.length, names.join(', '));
|
||||
}
|
||||
|
||||
common.promptYesNo({msg: msg}, function (answer) {
|
||||
if (answer !== 'y') {
|
||||
console.error('Aborting');
|
||||
next(true); // early abort signal
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
function deleteThem(_, next) {
|
||||
var startTime = Date.now();
|
||||
|
||||
vasync.forEachParallel({
|
||||
inputs: names,
|
||||
func: function deleteOne(name, nextName) {
|
||||
cli.tritonapi.deleteInstanceSnapshot({
|
||||
id: inst,
|
||||
name: name
|
||||
}, function (err, res) {
|
||||
if (err) {
|
||||
nextName(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var instId = res.instId;
|
||||
|
||||
var msg = 'Deleting snapshot "%s" of instance "%s"';
|
||||
console.log(msg, name, instId);
|
||||
|
||||
if (opts.wait) {
|
||||
wait(instId, name, startTime, nextName);
|
||||
} else {
|
||||
nextName();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, next);
|
||||
}
|
||||
]}, function (err) {
|
||||
if (err === true) {
|
||||
err = null;
|
||||
}
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_delete.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['force', 'f'],
|
||||
type: 'bool',
|
||||
help: 'Skip confirmation of delete.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Wait for the deletion to complete.'
|
||||
}
|
||||
];
|
||||
|
||||
do_delete.synopses = ['{{name}} {{cmd}} [OPTIONS] INST SNAPNAME [SNAPNAME...]'];
|
||||
|
||||
do_delete.help = [
|
||||
'Remove a snapshot from an instance.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}'
|
||||
].join('\n');
|
||||
|
||||
do_delete.aliases = ['rm'];
|
||||
|
||||
// TODO: When have 'tritonsnapshot' completion, then use this:
|
||||
// do_get.completionArgtypes = ['tritoninstance', 'tritonsnapshot'];
|
||||
do_delete.completionArgtypes = ['tritoninstance', 'none'];
|
||||
|
||||
module.exports = do_delete;
|
@ -1,91 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton snapshot get ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
|
||||
|
||||
function do_get(subcmd, opts, args, cb) {
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length < 2) {
|
||||
cb(new errors.UsageError('missing INST and/or SNAPNAME arguments'));
|
||||
return;
|
||||
} else if (args.length > 2) {
|
||||
cb(new errors.UsageError('incorrect number of arguments'));
|
||||
return;
|
||||
}
|
||||
|
||||
var id = args[0];
|
||||
var name = args[1];
|
||||
var cli = this.top;
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
cli.tritonapi.getInstanceSnapshot({
|
||||
id: id,
|
||||
name: name
|
||||
}, function onSnapshot(err, snapshot) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(snapshot));
|
||||
} else {
|
||||
console.log(JSON.stringify(snapshot, null, 4));
|
||||
}
|
||||
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_get.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON stream output.'
|
||||
}
|
||||
];
|
||||
|
||||
do_get.synopses = ['{{name}} {{cmd}} [OPTIONS] INST SNAPNAME'];
|
||||
|
||||
do_get.help = [
|
||||
'Show a specific snapshot of an instance.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}'
|
||||
].join('\n');
|
||||
|
||||
// TODO: When have 'tritonsnapshot' completion, then use this:
|
||||
// do_get.completionArgtypes = ['tritoninstance', 'tritonsnapshot', 'none'];
|
||||
do_get.completionArgtypes = ['tritoninstance', 'none'];
|
||||
|
||||
module.exports = do_get;
|
@ -1,107 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton snapshot list ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var tabula = require('tabula');
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
|
||||
|
||||
var COLUMNS_DEFAULT = 'name,state,created';
|
||||
var SORT_DEFAULT = 'name';
|
||||
|
||||
|
||||
function do_list(subcmd, opts, args, cb) {
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
cb(new errors.UsageError('missing INST argument'));
|
||||
return;
|
||||
} else if (args.length > 1) {
|
||||
cb(new errors.UsageError('incorrect number of arguments'));
|
||||
return;
|
||||
}
|
||||
|
||||
var cli = this.top;
|
||||
var machineId = args[0];
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
cli.tritonapi.listInstanceSnapshots({
|
||||
id: machineId
|
||||
}, function onSnapshots(err, snapshots) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
common.jsonStream(snapshots);
|
||||
} else {
|
||||
var columns = COLUMNS_DEFAULT;
|
||||
|
||||
if (opts.o) {
|
||||
columns = opts.o;
|
||||
} else if (opts.long) {
|
||||
columns = COLUMNS_DEFAULT;
|
||||
}
|
||||
|
||||
columns = columns.split(',');
|
||||
var sort = opts.s.split(',');
|
||||
|
||||
tabula(snapshots, {
|
||||
skipHeader: opts.H,
|
||||
columns: columns,
|
||||
sort: sort
|
||||
});
|
||||
}
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
do_list.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
}
|
||||
].concat(common.getCliTableOptions({
|
||||
includeLong: true,
|
||||
sortDefault: SORT_DEFAULT
|
||||
}));
|
||||
|
||||
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] INST'];
|
||||
|
||||
do_list.help = [
|
||||
'Show all of an instance\'s snapshots.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}'
|
||||
].join('\n');
|
||||
|
||||
do_list.completionArgtypes = ['tritoninstance', 'none'];
|
||||
|
||||
do_list.aliases = ['ls'];
|
||||
|
||||
module.exports = do_list;
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton snapshot ...`
|
||||
*/
|
||||
|
||||
var Cmdln = require('cmdln').Cmdln;
|
||||
var util = require('util');
|
||||
|
||||
|
||||
|
||||
// ---- CLI class
|
||||
|
||||
function SnapshotCLI(top) {
|
||||
this.top = top.top;
|
||||
|
||||
Cmdln.call(this, {
|
||||
name: top.name + ' snapshot',
|
||||
desc: 'List, get, create and delete Spearhead instance snapshots.',
|
||||
helpSubcmds: [
|
||||
'help',
|
||||
'create',
|
||||
'list',
|
||||
'get',
|
||||
'delete'
|
||||
],
|
||||
helpBody: 'Instances can be rolled back to a snapshot using\n' +
|
||||
'`spearhead instance start --snapshot=SNAPNAME`.'
|
||||
});
|
||||
}
|
||||
util.inherits(SnapshotCLI, Cmdln);
|
||||
|
||||
SnapshotCLI.prototype.init = function init(opts, args, cb) {
|
||||
this.log = this.top.log;
|
||||
Cmdln.prototype.init.apply(this, arguments);
|
||||
};
|
||||
|
||||
SnapshotCLI.prototype.do_create = require('./do_create');
|
||||
SnapshotCLI.prototype.do_get = require('./do_get');
|
||||
SnapshotCLI.prototype.do_list = require('./do_list');
|
||||
SnapshotCLI.prototype.do_delete = require('./do_delete');
|
||||
|
||||
module.exports = SnapshotCLI;
|
@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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 2017 Joyent, Inc.
|
||||
*
|
||||
* `triton instance snapshots ...` shortcut for
|
||||
* `triton instance snapshot list ...`.
|
||||
*/
|
||||
|
||||
var targ = require('./do_snapshot/do_list');
|
||||
|
||||
function do_snapshots(subcmd, opts, args, callback) {
|
||||
this.handlerFromSubcmd('snapshot').dispatch({
|
||||
subcmd: 'list',
|
||||
opts: opts,
|
||||
args: args
|
||||
}, callback);
|
||||
}
|
||||
|
||||
do_snapshots.help = 'A shortcut for "spearhead instance snapshot list".\n' +
|
||||
targ.help;
|
||||
do_snapshots.synopses = targ.synopses;
|
||||
do_snapshots.options = targ.options;
|
||||
do_snapshots.completionArgtypes = targ.completionArgtypes;
|
||||
|
||||
do_snapshots.hidden = true;
|
||||
|
||||
module.exports = do_snapshots;
|
@ -1,370 +0,0 @@
|
||||
/*
|
||||
* 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 (c) 2018, Joyent, Inc.
|
||||
*
|
||||
* `triton instance ssh ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var path = require('path');
|
||||
var spawn = require('child_process').spawn;
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
/*
|
||||
* The tag "tritoncli.ssh.ip" may be set to an IP address that belongs to the
|
||||
* instance but which is not the primary IP. If set, we will use that IP
|
||||
* address for the SSH connection instead of the primary IP.
|
||||
*/
|
||||
var TAG_SSH_IP = 'tritoncli.ssh.ip';
|
||||
|
||||
/*
|
||||
* The tag "tritoncli.ssh.proxy" may be set to either the name or the UUID of
|
||||
* another instance in this account. If set, we will use the "ProxyJump"
|
||||
* feature of SSH to tunnel through the SSH server on that host. This is
|
||||
* useful when exposing a single zone to the Internet while keeping the rest of
|
||||
* your infrastructure on a private fabric.
|
||||
*/
|
||||
var TAG_SSH_PROXY = 'tritoncli.ssh.proxy';
|
||||
|
||||
/*
|
||||
* The tag "tritoncli.ssh.proxyuser" may be set on the instance used as an SSH
|
||||
* proxy. If set, we will use this value when making the proxy connection
|
||||
* (i.e., it will be passed via the "ProxyJump" option). If not set, the
|
||||
* default user selection behaviour applies.
|
||||
*/
|
||||
var TAG_SSH_PROXY_USER = 'tritoncli.ssh.proxyuser';
|
||||
|
||||
|
||||
function do_ssh(subcmd, opts, args, callback) {
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length === 0) {
|
||||
callback(new errors.UsageError('missing INST arg'));
|
||||
return;
|
||||
}
|
||||
|
||||
var id = args.shift();
|
||||
|
||||
var user;
|
||||
var overrideUser = false;
|
||||
var i = id.indexOf('@');
|
||||
if (i >= 0) {
|
||||
user = id.substr(0, i);
|
||||
id = id.substr(i + 1);
|
||||
overrideUser = true;
|
||||
}
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
|
||||
function getInstanceIp(ctx, next) {
|
||||
ctx.cli.tritonapi.getInstance(id, function (err, inst) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.inst = inst;
|
||||
|
||||
if (inst.tags && inst.tags[TAG_SSH_IP]) {
|
||||
ctx.ip = inst.tags[TAG_SSH_IP];
|
||||
if (!inst.ips || inst.ips.indexOf(ctx.ip) === -1) {
|
||||
next(new Error('IP address ' + ctx.ip + ' not ' +
|
||||
'attached to the instance'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
ctx.ip = inst.primaryIp;
|
||||
}
|
||||
|
||||
if (!ctx.ip) {
|
||||
next(new Error('IP address not found for instance'));
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
function getInstanceBastionIp(ctx, next) {
|
||||
if (opts.no_proxy) {
|
||||
setImmediate(next);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.inst.tags || !ctx.inst.tags[TAG_SSH_PROXY]) {
|
||||
setImmediate(next);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.cli.tritonapi.getInstance(ctx.inst.tags[TAG_SSH_PROXY],
|
||||
function (err, proxy) {
|
||||
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (proxy.tags && proxy.tags[TAG_SSH_IP]) {
|
||||
ctx.proxyIp = proxy.tags[TAG_SSH_IP];
|
||||
if (!proxy.ips || proxy.ips.indexOf(ctx.proxyIp) === -1) {
|
||||
next(new Error('IP address ' + ctx.proxyIp + ' not ' +
|
||||
'attached to the instance'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
ctx.proxyIp = proxy.primaryIp;
|
||||
}
|
||||
|
||||
ctx.proxyImage = proxy.image;
|
||||
|
||||
/*
|
||||
* Selecting the right user to use for the proxy connection is
|
||||
* somewhat nuanced, in order to allow for various useful
|
||||
* configurations. We wish to enable the following cases:
|
||||
*
|
||||
* 1. The least sophisticated configuration; i.e., using two
|
||||
* instances (the target instance and the proxy instnace)
|
||||
* with the default "root" (or, e.g., "ubuntu") account
|
||||
* and smartlogin or authorized_keys metadata for SSH key
|
||||
* management.
|
||||
*
|
||||
* 2. The user has set up their own accounts (e.g., "roberta")
|
||||
* in all of their instances and does their own SSH key
|
||||
* management. They connect with:
|
||||
*
|
||||
* triton inst ssh roberta@instance
|
||||
*
|
||||
* In this case we will use "roberta" for both the proxy
|
||||
* and the target instance. This means a user provided on
|
||||
* the command line will override the per-image default
|
||||
* user (e.g., "root" or "ubuntu") -- if the user wants to
|
||||
* retain the default account for the proxy, they should
|
||||
* use case 3 below.
|
||||
*
|
||||
* 3. The user has set up their own accounts in the target
|
||||
* instance (e.g., "felicity"), but the proxy instance is
|
||||
* using a single specific account that should be used by
|
||||
* all users in the organisation (e.g., "partyline"). In
|
||||
* this case, we want the user to be able to specify the
|
||||
* global proxy account setting as a tag on the proxy
|
||||
* instance, so that for:
|
||||
*
|
||||
* triton inst ssh felicity@instance
|
||||
*
|
||||
* ... we will use "-o ProxyJump partyline@proxy" but
|
||||
* still use "felicity" for the target connection. This
|
||||
* last case requires the proxy user tag (if set) to
|
||||
* override a user provided on the command line.
|
||||
*/
|
||||
if (proxy.tags && proxy.tags[TAG_SSH_PROXY_USER]) {
|
||||
ctx.proxyUser = proxy.tags[TAG_SSH_PROXY_USER];
|
||||
}
|
||||
|
||||
if (!ctx.proxyIp) {
|
||||
next(new Error('IP address not found for proxy instance'));
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
function getUser(ctx, next) {
|
||||
if (overrideUser) {
|
||||
assert.string(user, 'user');
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.cli.tritonapi.getImage({
|
||||
name: ctx.inst.image,
|
||||
useCache: true
|
||||
}, function (getImageErr, image) {
|
||||
if (getImageErr) {
|
||||
next(getImageErr);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* This is a convention as seen on Joyent's "ubuntu-certified"
|
||||
* KVM images.
|
||||
*/
|
||||
if (image.tags && image.tags.default_user) {
|
||||
user = image.tags.default_user;
|
||||
} else {
|
||||
user = 'root';
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
function getBastionUser(ctx, next) {
|
||||
if (!ctx.proxyImage || ctx.proxyUser) {
|
||||
/*
|
||||
* If there is no image for the proxy host, or an override user
|
||||
* was already provided in the tags of the proxy instance
|
||||
* itself, we don't need to look up the default user.
|
||||
*/
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (overrideUser) {
|
||||
/*
|
||||
* A user was provided on the command line, but no user
|
||||
* override tag was present on the proxy instance. To enable
|
||||
* use case 2 (see comments above) we'll prefer this user over
|
||||
* the image default.
|
||||
*/
|
||||
assert.string(user, 'user');
|
||||
ctx.proxyUser = user;
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.cli.tritonapi.getImage({
|
||||
name: ctx.proxyImage,
|
||||
useCache: true
|
||||
}, function (getImageErr, image) {
|
||||
if (getImageErr) {
|
||||
next(getImageErr);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* This is a convention as seen on Joyent's "ubuntu-certified"
|
||||
* KVM images.
|
||||
*/
|
||||
assert.ok(!ctx.proxyUser, 'proxy user set twice');
|
||||
if (image.tags && image.tags.default_user) {
|
||||
ctx.proxyUser = image.tags.default_user;
|
||||
} else {
|
||||
ctx.proxyUser = 'root';
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
function doSsh(ctx, next) {
|
||||
args = ['-l', user, ctx.ip].concat(args);
|
||||
|
||||
if (ctx.proxyIp) {
|
||||
assert.string(ctx.proxyUser, 'ctx.proxyUser');
|
||||
args = [
|
||||
'-o', 'ProxyJump=' + ctx.proxyUser + '@' + ctx.proxyIp
|
||||
].concat(args);
|
||||
}
|
||||
|
||||
/*
|
||||
* By default we disable ControlMaster (aka mux, aka SSH
|
||||
* connection multiplexing) because of
|
||||
* https://github.com/joyent/node-triton/issues/52
|
||||
*/
|
||||
if (!opts.no_disable_mux) {
|
||||
/*
|
||||
* A simple `-o ControlMaster=no` doesn't work. With
|
||||
* just that option, a `ControlPath` option (from
|
||||
* ~/.ssh/config) will still be used if it exists. Our
|
||||
* hack is to set a ControlPath we know should not
|
||||
* exist. Using '/dev/null' wasn't a good alternative
|
||||
* because `ssh` tries "$ControlPath.$somerandomnum"
|
||||
* and also because Windows.
|
||||
*/
|
||||
var nullSshControlPath = path.resolve(
|
||||
ctx.cli.tritonapi.config._configDir, 'tmp',
|
||||
'nullSshControlPath');
|
||||
args = [
|
||||
'-o', 'ControlMaster=no',
|
||||
'-o', 'ControlPath='+nullSshControlPath
|
||||
].concat(args);
|
||||
}
|
||||
|
||||
ctx.cli.log.info({args: args}, 'forking ssh');
|
||||
var child = spawn('ssh', args, {stdio: 'inherit'});
|
||||
child.on('close', function (code) {
|
||||
/*
|
||||
* Once node 0.10 support is dropped we could instead:
|
||||
* process.exitCode = code;
|
||||
* callback();
|
||||
*/
|
||||
process.exit(code);
|
||||
/* process.exit does not return so no need to call next(). */
|
||||
});
|
||||
}
|
||||
]}, callback);
|
||||
}
|
||||
|
||||
do_ssh.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['no-proxy'],
|
||||
type: 'bool',
|
||||
help: 'Disable SSH proxy support (ignore "tritoncli.ssh.proxy" tag)'
|
||||
}
|
||||
];
|
||||
do_ssh.synopses = ['{{name}} ssh [-h] [USER@]INST [SSH-ARGUMENTS]'];
|
||||
do_ssh.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'SSH to the primary IP of an instance',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where INST is the name, id, or short id of an instance. Note that',
|
||||
'the INST argument must come before any `ssh` options or arguments.',
|
||||
'Where USER is the username to use on the instance. If not specified,',
|
||||
'the instance\'s image is inspected for the default_user tag.',
|
||||
'If USER is not specified and the default_user tag is not set, the user',
|
||||
'is assumed to be \"root\".',
|
||||
'',
|
||||
'The "tritoncli.ssh.proxy" tag on the target instance may be set to',
|
||||
'the name or the UUID of another instance through which to proxy this',
|
||||
'SSH connection. If set, the primary IP of the proxy instance will be',
|
||||
'loaded and passed to SSH via the ProxyJump option. The --no-proxy',
|
||||
'flag can be used to ignore the tag and force a direct connection.',
|
||||
'',
|
||||
'For example, to proxy connections to zone "narnia" through "wardrobe":',
|
||||
' triton instance tag set narnia tritoncli.ssh.proxy=wardrobe',
|
||||
'',
|
||||
'The "tritoncli.ssh.ip" tag on the target instance may be set to the',
|
||||
'IP address to use for SSH connections. This may be useful if the',
|
||||
'primary IP address is not available for SSH connections. This address',
|
||||
'must be set to one of the IP addresses attached to the instance.',
|
||||
'',
|
||||
'The "tritoncli.ssh.proxyuser" tag on the proxy instance may be set to',
|
||||
'the user account that should be used for the proxy connection (i.e., via',
|
||||
'the SSH ProxyJump option). This is useful when all users of the proxy',
|
||||
'instance should use a special common account, and will override the USER',
|
||||
'value (if one is provided) for the SSH connection to the target instance.',
|
||||
'',
|
||||
'There is a known issue with SSH connection multiplexing (a.k.a. ',
|
||||
'ControlMaster, mux) where stdout/stderr is lost. As a workaround, `ssh`',
|
||||
'is spawned with options disabling ControlMaster. See ',
|
||||
'<https://github.com/joyent/node-triton/issues/52> for details. If you ',
|
||||
'want to use ControlMaster, an alternative is:',
|
||||
' ssh root@$(spearhead ip INST)'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_ssh.interspersedOptions = false;
|
||||
|
||||
// Use 'file' to fallback to the default bash completion... even though 'file'
|
||||
// isn't quite right.
|
||||
do_ssh.completionArgtypes = ['tritoninstance', 'file'];
|
||||
|
||||
module.exports = do_ssh;
|
@ -1,17 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton instance start ...`
|
||||
*/
|
||||
|
||||
var gen_do_ACTION = require('./gen_do_ACTION');
|
||||
|
||||
|
||||
var do_start = gen_do_ACTION({action: 'start'});
|
||||
module.exports = do_start;
|
@ -1,17 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton instance stop ...`
|
||||
*/
|
||||
|
||||
var gen_do_ACTION = require('./gen_do_ACTION');
|
||||
|
||||
|
||||
var do_stop = gen_do_ACTION({action: 'stop'});
|
||||
module.exports = do_stop;
|
@ -1,122 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton instance tag delete ...`
|
||||
*/
|
||||
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
|
||||
|
||||
function do_delete(subcmd, opts, args, cb) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length < 1) {
|
||||
cb(new errors.UsageError('incorrect number of args'));
|
||||
return;
|
||||
} else if (args.length > 1 && opts.all) {
|
||||
cb(new errors.UsageError('cannot specify both tag names and --all'));
|
||||
return;
|
||||
}
|
||||
var waitTimeoutMs = opts.wait_timeout * 1000; /* seconds to ms */
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
if (opts.all) {
|
||||
self.top.tritonapi.deleteAllInstanceTags({
|
||||
id: args[0],
|
||||
wait: opts.wait,
|
||||
waitTimeout: waitTimeoutMs
|
||||
}, function (err) {
|
||||
console.log('Deleted all tags on instance %s', args[0]);
|
||||
cb(err);
|
||||
});
|
||||
} else {
|
||||
// Uniq'ify the given names.
|
||||
var names = {};
|
||||
args.slice(1).forEach(function (arg) { names[arg] = true; });
|
||||
names = Object.keys(names);
|
||||
|
||||
// TODO: Instead of waiting for each delete, let's delete
|
||||
// them all then wait for the set.
|
||||
vasync.forEachPipeline({
|
||||
inputs: names,
|
||||
func: function deleteOne(name, next) {
|
||||
self.top.tritonapi.deleteInstanceTag({
|
||||
id: args[0],
|
||||
tag: name,
|
||||
wait: opts.wait,
|
||||
waitTimeout: waitTimeoutMs
|
||||
}, function (err) {
|
||||
if (!err) {
|
||||
console.log('Deleted tag %s on instance %s',
|
||||
name, args[0]);
|
||||
}
|
||||
next(err);
|
||||
});
|
||||
}
|
||||
}, cb);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
do_delete.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['all', 'a'],
|
||||
type: 'bool',
|
||||
help: 'Remove all tags on this instance.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Wait for the tag changes to be applied.'
|
||||
},
|
||||
{
|
||||
names: ['wait-timeout'],
|
||||
type: 'positiveInteger',
|
||||
default: 120,
|
||||
help: 'The number of seconds to wait before timing out with an error. '
|
||||
+ 'The default is 120 seconds.'
|
||||
}
|
||||
];
|
||||
|
||||
do_delete.synopses = [
|
||||
'{{name}} {{cmd}} INST [NAME ...]',
|
||||
'{{name}} {{cmd}} --all INST # delete all tags'
|
||||
];
|
||||
|
||||
do_delete.help = [
|
||||
'Delete one or more instance tags.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where INST is an instance id, name, or shortid and NAME is a tag name.',
|
||||
'',
|
||||
'Changing instance tags is asynchronous. Use "--wait" to not return until',
|
||||
'the changes are completed.'
|
||||
].join('\n');
|
||||
|
||||
do_delete.aliases = ['rm'];
|
||||
|
||||
do_delete.completionArgtypes = ['tritoninstance', 'none'];
|
||||
|
||||
module.exports = do_delete;
|
@ -1,77 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton instance tag get ...`
|
||||
*/
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
|
||||
|
||||
function do_get(subcmd, opts, args, cb) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length !== 2) {
|
||||
cb(new errors.UsageError('incorrect number of args'));
|
||||
return;
|
||||
}
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
self.top.tritonapi.getInstanceTag({
|
||||
id: args[0],
|
||||
tag: args[1]
|
||||
}, function (err, value) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(value));
|
||||
} else {
|
||||
console.log(value);
|
||||
}
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_get.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON output.'
|
||||
}
|
||||
];
|
||||
|
||||
do_get.synopses = ['{{name}} {{cmd}} INST NAME'];
|
||||
|
||||
do_get.help = [
|
||||
'Get an instance tag.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where INST is an instance id, name, or shortid and NAME is a tag name.'
|
||||
].join('\n');
|
||||
|
||||
// TODO: When have 'tritoninstancetag' completion, add that in.
|
||||
do_get.completionArgtypes = ['tritoninstance', 'none'];
|
||||
|
||||
module.exports = do_get;
|
@ -1,78 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton instance tag list ...`
|
||||
*/
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
|
||||
function do_list(subcmd, opts, args, cb) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length !== 1) {
|
||||
cb(new errors.UsageError('incorrect number of args'));
|
||||
return;
|
||||
}
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
return;
|
||||
}
|
||||
self.top.tritonapi.listInstanceTags(
|
||||
{id: args[0]}, function (err, tags) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(tags));
|
||||
} else {
|
||||
console.log(JSON.stringify(tags, null, 4));
|
||||
}
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_list.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON output.'
|
||||
}
|
||||
];
|
||||
|
||||
do_list.synopses = ['{{name}} {{cmd}} INST'];
|
||||
|
||||
do_list.help = [
|
||||
'List instance tags.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where INST is an instance id, name, or shortid.',
|
||||
'',
|
||||
'Note: Currently this dumps prettified JSON by default. That might change',
|
||||
'in the future. Use "-j" to explicitly get JSON output.'
|
||||
].join('\n');
|
||||
|
||||
do_list.aliases = ['ls'];
|
||||
|
||||
do_list.completionArgtypes = ['tritoninstance', 'none'];
|
||||
|
||||
module.exports = do_list;
|
@ -1,137 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton instance tag replace-all ...`
|
||||
*/
|
||||
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
var mat = require('../../metadataandtags');
|
||||
|
||||
|
||||
function do_replace_all(subcmd, opts, args, cb) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length < 1) {
|
||||
cb(new errors.UsageError('incorrect number of args'));
|
||||
return;
|
||||
}
|
||||
var log = self.log;
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function gatherTags(ctx, next) {
|
||||
mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
log.trace({tags: tags || '<none>'},
|
||||
'tags loaded from opts and args');
|
||||
ctx.tags = tags;
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
function replaceAway(ctx, next) {
|
||||
if (!ctx.tags) {
|
||||
next(new errors.UsageError('no tags were provided'));
|
||||
return;
|
||||
}
|
||||
self.top.tritonapi.replaceAllInstanceTags({
|
||||
id: args[0],
|
||||
tags: ctx.tags,
|
||||
wait: opts.wait,
|
||||
waitTimeout: opts.wait_timeout * 1000 /* seconds to ms */
|
||||
}, function (err, updatedTags) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
if (!opts.quiet) {
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(updatedTags));
|
||||
} else {
|
||||
console.log(JSON.stringify(updatedTags, null, 4));
|
||||
}
|
||||
}
|
||||
cb();
|
||||
});
|
||||
}
|
||||
]}, cb);
|
||||
}
|
||||
|
||||
do_replace_all.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['file', 'f'],
|
||||
type: 'arrayOfString',
|
||||
helpArg: 'FILE',
|
||||
help: 'Load tag name/value pairs from the given file path. '
|
||||
+ 'The file may contain a JSON object or a file with "NAME=VALUE" '
|
||||
+ 'pairs, one per line. This option can be used multiple times.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Wait for the tag changes to be applied.'
|
||||
},
|
||||
{
|
||||
names: ['wait-timeout'],
|
||||
type: 'positiveInteger',
|
||||
default: 120,
|
||||
help: 'The number of seconds to wait before timing out with an error. '
|
||||
+ 'The default is 120 seconds.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON output.'
|
||||
},
|
||||
{
|
||||
names: ['quiet', 'q'],
|
||||
type: 'bool',
|
||||
help: 'Quieter output. Specifically do not dump the updated set of '
|
||||
+ 'tags on successful completion.'
|
||||
}
|
||||
];
|
||||
|
||||
do_replace_all.synopses = [
|
||||
'{{name}} {{cmd}} INST [NAME=VALUE ...]',
|
||||
'{{name}} {{cmd}} INST -f FILE # tags from file'
|
||||
];
|
||||
|
||||
do_replace_all.help = [
|
||||
'Replace all tags on the given instance.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where INST is an instance id, name, or shortid; NAME is a tag name;',
|
||||
'and VALUE is a tag value (bool and numeric "value" are converted to ',
|
||||
'that type).',
|
||||
'',
|
||||
'Currently this dumps prettified JSON by default. That might change in the',
|
||||
'future. Use "-j" to explicitly get JSON output.',
|
||||
'',
|
||||
'Changing instance tags is asynchronous. Use "--wait" to not return until',
|
||||
'the changes are completed.'
|
||||
].join('\n');
|
||||
|
||||
do_replace_all.completionArgtypes = ['tritoninstance', 'file'];
|
||||
|
||||
module.exports = do_replace_all;
|
@ -1,140 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton instance tag set ...`
|
||||
*/
|
||||
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
var mat = require('../../metadataandtags');
|
||||
|
||||
|
||||
function do_set(subcmd, opts, args, cb) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
} else if (args.length < 1) {
|
||||
cb(new errors.UsageError('incorrect number of args'));
|
||||
return;
|
||||
}
|
||||
var log = self.log;
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function gatherTags(ctx, next) {
|
||||
mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
log.trace({tags: tags || '<none>'},
|
||||
'tags loaded from opts and args');
|
||||
ctx.tags = tags;
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
function setMachineTags(ctx, next) {
|
||||
if (!ctx.tags) {
|
||||
log.trace('no tags to set');
|
||||
next();
|
||||
return;
|
||||
}
|
||||
self.top.tritonapi.setInstanceTags({
|
||||
id: args[0],
|
||||
tags: ctx.tags,
|
||||
wait: opts.wait,
|
||||
waitTimeout: opts.wait_timeout * 1000 /* seconds to ms */
|
||||
}, function (err, updatedTags) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
if (!opts.quiet) {
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(updatedTags));
|
||||
} else {
|
||||
console.log(JSON.stringify(updatedTags, null, 4));
|
||||
}
|
||||
}
|
||||
cb();
|
||||
});
|
||||
}
|
||||
]}, cb);
|
||||
}
|
||||
|
||||
do_set.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['file', 'f'],
|
||||
type: 'arrayOfString',
|
||||
helpArg: 'FILE',
|
||||
help: 'Load tag name/value pairs from the given file path. '
|
||||
+ 'The file may contain a JSON object or a file with "NAME=VALUE" '
|
||||
+ 'pairs, one per line. This option can be used multiple times.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Wait for the tag changes to be applied.'
|
||||
},
|
||||
{
|
||||
names: ['wait-timeout'],
|
||||
type: 'positiveInteger',
|
||||
default: 120,
|
||||
help: 'The number of seconds to wait before timing out with an error. '
|
||||
+ 'The default is 120 seconds.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON output.'
|
||||
},
|
||||
{
|
||||
names: ['quiet', 'q'],
|
||||
type: 'bool',
|
||||
help: 'Quieter output. Specifically do not dump the updated set of '
|
||||
+ 'tags on successful completion.'
|
||||
}
|
||||
];
|
||||
|
||||
do_set.synopses = [
|
||||
'{{name}} set INST [NAME=VALUE ...]',
|
||||
'{{name}} set INST -f FILE # tags from file'
|
||||
];
|
||||
|
||||
do_set.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Set one or more instance tags.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where INST is an instance id, name, or shortid; NAME is a tag name;',
|
||||
'and VALUE is a tag value (bool and numeric "value" are converted to ',
|
||||
'that type).',
|
||||
'',
|
||||
'Currently this dumps prettified JSON by default. That might change in the',
|
||||
'future. Use "-j" to explicitly get JSON output.',
|
||||
'',
|
||||
'Changing instance tags is asynchronous. Use "--wait" to not return until',
|
||||
'the changes are completed.'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
do_set.completionArgtypes = ['tritoninstance', 'file'];
|
||||
|
||||
module.exports = do_set;
|
@ -1,54 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton instance tag ...`
|
||||
*/
|
||||
|
||||
var Cmdln = require('cmdln').Cmdln;
|
||||
var util = require('util');
|
||||
|
||||
|
||||
// ---- CLI class
|
||||
|
||||
function InstanceTagCLI(parent) {
|
||||
this.top = parent.top;
|
||||
Cmdln.call(this, {
|
||||
name: parent.name + ' tag',
|
||||
/* BEGIN JSSTYLED */
|
||||
desc: [
|
||||
'List, get, set and delete tags on Spearhead instances.'
|
||||
].join('\n'),
|
||||
/* END JSSTYLED */
|
||||
helpOpts: {
|
||||
minHelpCol: 24 /* line up with option help */
|
||||
},
|
||||
helpSubcmds: [
|
||||
'help',
|
||||
'list',
|
||||
'get',
|
||||
'set',
|
||||
'replace-all',
|
||||
'delete'
|
||||
]
|
||||
});
|
||||
}
|
||||
util.inherits(InstanceTagCLI, Cmdln);
|
||||
|
||||
InstanceTagCLI.prototype.init = function init(opts, args, cb) {
|
||||
this.log = this.top.log;
|
||||
Cmdln.prototype.init.apply(this, arguments);
|
||||
};
|
||||
|
||||
InstanceTagCLI.prototype.do_list = require('./do_list');
|
||||
InstanceTagCLI.prototype.do_get = require('./do_get');
|
||||
InstanceTagCLI.prototype.do_set = require('./do_set');
|
||||
InstanceTagCLI.prototype.do_replace_all = require('./do_replace_all');
|
||||
InstanceTagCLI.prototype.do_delete = require('./do_delete');
|
||||
|
||||
module.exports = InstanceTagCLI;
|
@ -1,30 +0,0 @@
|
||||
/*
|
||||
* 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 2017 Joyent, Inc.
|
||||
*
|
||||
* `triton instance tags ...` shortcut for `triton instance tag list ...`.
|
||||
*/
|
||||
|
||||
var targ = require('./do_tag/do_list');
|
||||
|
||||
function do_tags(subcmd, opts, args, callback) {
|
||||
this.handlerFromSubcmd('tag').dispatch({
|
||||
subcmd: 'list',
|
||||
opts: opts,
|
||||
args: args
|
||||
}, callback);
|
||||
}
|
||||
|
||||
do_tags.help = 'A shortcut for "spearhead instance tag list".\n' + targ.help;
|
||||
do_tags.synopses = targ.synopses;
|
||||
do_tags.options = targ.options;
|
||||
do_tags.completionArgtypes = targ.completionArgtypes;
|
||||
|
||||
do_tags.hidden = true;
|
||||
|
||||
module.exports = do_tags;
|
@ -1,147 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* `triton instance wait ...`
|
||||
*/
|
||||
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var distractions = require('../distractions');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
function do_wait(subcmd, opts, args, cb) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
return this.do_help('help', {}, [subcmd], cb);
|
||||
} else if (args.length < 1) {
|
||||
return cb(new errors.UsageError('missing INST arg(s)'));
|
||||
}
|
||||
var ids = args;
|
||||
var states = [];
|
||||
opts.states.forEach(function (s) {
|
||||
/* JSSTYLED */
|
||||
states = states.concat(s.trim().split(/\s*,\s*/g));
|
||||
});
|
||||
|
||||
var distraction;
|
||||
var done = 0;
|
||||
var instFromId = {};
|
||||
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function getInsts(_, next) {
|
||||
vasync.forEachParallel({
|
||||
inputs: ids,
|
||||
func: function getInst(id, nextInst) {
|
||||
self.top.tritonapi.getInstance(id, function (err, inst) {
|
||||
if (err) {
|
||||
return nextInst(err);
|
||||
}
|
||||
if (states.indexOf(inst.state) !== -1) {
|
||||
console.log('%d/%d: Instance %s (%s) already %s',
|
||||
++done, ids.length, inst.name, id, inst.state);
|
||||
} else {
|
||||
instFromId[inst.id] = inst;
|
||||
}
|
||||
nextInst();
|
||||
});
|
||||
}
|
||||
}, next);
|
||||
},
|
||||
|
||||
function waitForInsts(_, next) {
|
||||
var idsToWaitFor = Object.keys(instFromId);
|
||||
if (idsToWaitFor.length === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (idsToWaitFor.length === 1) {
|
||||
var inst2 = instFromId[idsToWaitFor[0]];
|
||||
console.log(
|
||||
'Waiting for instance %s (%s) to enter state (states: %s)',
|
||||
inst2.name, inst2.id, states.join(', '));
|
||||
} else {
|
||||
console.log(
|
||||
'Waiting for %d instances to enter state (states: %s)',
|
||||
idsToWaitFor.length, states.join(', '));
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: need BigSpinner.log first.
|
||||
* TODO: Also when adding a spinner, we need an equiv option to
|
||||
* `triton create -wwww` to trigger the spinner (and size). By
|
||||
* default: no spinner.
|
||||
*/
|
||||
if (false &&
|
||||
process.stderr.isTTY)
|
||||
{
|
||||
distraction = distractions.createDistraction();
|
||||
}
|
||||
|
||||
vasync.forEachParallel({
|
||||
inputs: idsToWaitFor,
|
||||
func: function waitForInst(id, nextInst) {
|
||||
self.top.tritonapi.cloudapi.waitForMachineStates({
|
||||
id: id,
|
||||
states: states
|
||||
}, function (err, inst, res) {
|
||||
if (err) {
|
||||
return nextInst(err);
|
||||
}
|
||||
console.log('%d/%d: Instance %s (%s) moved to state %s',
|
||||
++done, ids.length, inst.name, inst.id, inst.state);
|
||||
nextInst();
|
||||
});
|
||||
}
|
||||
}, next);
|
||||
}
|
||||
|
||||
]}, function (err) {
|
||||
if (distraction) {
|
||||
distraction.destroy();
|
||||
}
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
do_wait.synopses = ['{{name}} {{cmd}} [-s STATES] INST [INST ...]'];
|
||||
do_wait.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Wait on instances changing state.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "INST" is an instance name, id, or short id; and "STATES" is a',
|
||||
'comma-separated list of target instance states, by default "running,failed".',
|
||||
'In other words, "spearhead inst wait foo0" will wait for instance "foo0" to',
|
||||
'complete provisioning.'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
do_wait.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['states', 's'],
|
||||
type: 'arrayOfString',
|
||||
default: ['running', 'failed'],
|
||||
helpArg: 'STATES',
|
||||
help: 'Instance states on which to wait. Default is "running,failed". '
|
||||
+ 'Values can be comma-separated or multiple uses of the option.'
|
||||
}
|
||||
];
|
||||
|
||||
do_wait.completionArgtypes = ['tritoninstance'];
|
||||
|
||||
module.exports = do_wait;
|
@ -1,204 +0,0 @@
|
||||
/*
|
||||
* 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 2016 Joyent, Inc.
|
||||
*
|
||||
* Shared support for:
|
||||
* `triton instance start ...`
|
||||
* `triton instance stop ...`
|
||||
* `triton instance delete ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
function perror(err) {
|
||||
console.error('error: %s', err.message);
|
||||
}
|
||||
|
||||
|
||||
function gen_do_ACTION(opts) {
|
||||
assert.object(opts, 'opts');
|
||||
assert.string(opts.action, 'opts.action');
|
||||
assert.optionalArrayOfString(opts.aliases, 'opts.aliases');
|
||||
var action = opts.action;
|
||||
|
||||
assert.ok(['start', 'stop', 'delete'].indexOf(action) >= 0,
|
||||
'invalid action');
|
||||
|
||||
function do_ACTION(subcmd, _opts, args, callback) {
|
||||
return _doTheAction.call(this, action, subcmd, _opts, args, callback);
|
||||
}
|
||||
do_ACTION.name = 'do_' + action;
|
||||
|
||||
if (opts.aliases) {
|
||||
do_ACTION.aliases = opts.aliases;
|
||||
}
|
||||
|
||||
do_ACTION.synopses = ['{{name}} ' + action + ' [OPTIONS] INST [INST ...]'];
|
||||
do_ACTION.help = [
|
||||
common.capitalize(action) + ' one or more instances.',
|
||||
'',
|
||||
'{{usage}}',
|
||||
'',
|
||||
'{{options}}',
|
||||
'Where "INST" is an instance name, id, or short id.'
|
||||
].join('\n');
|
||||
do_ACTION.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['wait', 'w'],
|
||||
type: 'bool',
|
||||
help: 'Block until instance state indicates the action is complete.'
|
||||
}
|
||||
];
|
||||
|
||||
do_ACTION.completionArgtypes = ['tritoninstance'];
|
||||
|
||||
if (action === 'start') {
|
||||
do_ACTION.options.push({
|
||||
names: ['snapshot'],
|
||||
type: 'string',
|
||||
help: 'Name of snapshot with which to start the instance.',
|
||||
helpArg: 'SNAPNAME'
|
||||
});
|
||||
}
|
||||
|
||||
return do_ACTION;
|
||||
}
|
||||
|
||||
function _doTheAction(action, subcmd, opts, args, callback) {
|
||||
var self = this;
|
||||
|
||||
var command, state;
|
||||
switch (action) {
|
||||
case 'start':
|
||||
command = opts.snapshot ? 'startMachineFromSnapshot' :
|
||||
'startMachine';
|
||||
state = 'running';
|
||||
break;
|
||||
case 'stop':
|
||||
command = 'stopMachine';
|
||||
state = 'stopped';
|
||||
break;
|
||||
case 'delete':
|
||||
command = 'deleteMachine';
|
||||
state = 'deleted';
|
||||
break;
|
||||
default:
|
||||
callback(new Error('unknown action: ' + action));
|
||||
break;
|
||||
}
|
||||
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length < 1) {
|
||||
callback(new errors.UsageError('missing INST arg(s)'));
|
||||
return;
|
||||
}
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
return;
|
||||
}
|
||||
_doOnEachInstance(self, action, command, state, args, opts, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function _doOnEachInstance(self, action, command, state, instances,
|
||||
opts, callback) {
|
||||
var now = Date.now();
|
||||
vasync.forEachParallel({
|
||||
func: function (arg, cb) {
|
||||
var alias, uuid;
|
||||
if (common.isUUID(arg)) {
|
||||
uuid = arg;
|
||||
done();
|
||||
} else {
|
||||
self.top.tritonapi.getInstance(arg, function (err, inst) {
|
||||
if (err) {
|
||||
perror(err);
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
alias = arg;
|
||||
uuid = inst.id;
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
// called when "uuid" is set
|
||||
function done() {
|
||||
var cOpts = uuid;
|
||||
if (command === 'startMachineFromSnapshot') {
|
||||
cOpts = { id: uuid, name: opts.snapshot };
|
||||
}
|
||||
|
||||
self.top.tritonapi.cloudapi[command](cOpts,
|
||||
function (err, body, res) {
|
||||
|
||||
if (err) {
|
||||
perror(err);
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts.wait) {
|
||||
if (alias)
|
||||
console.log('%s (async) instance %s (%s)',
|
||||
common.capitalize(action), alias, uuid);
|
||||
else
|
||||
console.log('%s (async) instance %s',
|
||||
common.capitalize(action), uuid);
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
self.top.tritonapi.cloudapi.waitForMachineStates({
|
||||
id: uuid,
|
||||
states: [state]
|
||||
}, function (err2, inst2, res2) {
|
||||
if (action === 'delete' &&
|
||||
res2 && res2.statusCode === 410) {
|
||||
// This is success, fall through to bottom.
|
||||
/* jsl:pass */
|
||||
} else if (err2) {
|
||||
perror(err2);
|
||||
cb(err2);
|
||||
return;
|
||||
}
|
||||
|
||||
var dur = common.humanDurationFromMs(Date.now() - now);
|
||||
if (alias)
|
||||
console.log('%s instance %s (%s, %s)',
|
||||
common.capitalize(action), alias, uuid, dur);
|
||||
else
|
||||
console.log('%s instance %s (%s)',
|
||||
common.capitalize(action), uuid, dur);
|
||||
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
inputs: instances
|
||||
}, function (err, results) {
|
||||
var e = err ? (new Error('command failure')) : null;
|
||||
callback(e);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = gen_do_ACTION;
|
@ -1,101 +0,0 @@
|
||||
/*
|
||||
* 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 2018 Joyent, Inc.
|
||||
*
|
||||
* `triton instance ...`
|
||||
*/
|
||||
|
||||
var Cmdln = require('cmdln').Cmdln;
|
||||
var util = require('util');
|
||||
|
||||
|
||||
// ---- CLI class
|
||||
|
||||
function InstanceCLI(top) {
|
||||
this.top = top;
|
||||
Cmdln.call(this, {
|
||||
name: top.name + ' instance',
|
||||
/* BEGIN JSSTYLED */
|
||||
desc: [
|
||||
'List and manage Spearhead instances.'
|
||||
].join('\n'),
|
||||
/* END JSSTYLED */
|
||||
helpOpts: {
|
||||
minHelpCol: 24 /* line up with option help */
|
||||
},
|
||||
helpSubcmds: [
|
||||
'help',
|
||||
'list',
|
||||
'get',
|
||||
'create',
|
||||
'delete',
|
||||
'resize',
|
||||
'rename',
|
||||
{ group: '' },
|
||||
'start',
|
||||
'stop',
|
||||
'reboot',
|
||||
{ group: '' },
|
||||
'fwrules',
|
||||
'enable-firewall',
|
||||
'disable-firewall',
|
||||
{ group: '' },
|
||||
'enable-deletion-protection',
|
||||
'disable-deletion-protection',
|
||||
{ group: '' },
|
||||
'ssh',
|
||||
'ip',
|
||||
'wait',
|
||||
'audit',
|
||||
'nic',
|
||||
'snapshot',
|
||||
'tag'
|
||||
]
|
||||
});
|
||||
}
|
||||
util.inherits(InstanceCLI, Cmdln);
|
||||
|
||||
InstanceCLI.prototype.init = function init(opts, args, cb) {
|
||||
this.log = this.top.log;
|
||||
Cmdln.prototype.init.apply(this, arguments);
|
||||
};
|
||||
|
||||
InstanceCLI.prototype.do_list = require('./do_list');
|
||||
InstanceCLI.prototype.do_get = require('./do_get');
|
||||
InstanceCLI.prototype.do_create = require('./do_create');
|
||||
InstanceCLI.prototype.do_delete = require('./do_delete');
|
||||
InstanceCLI.prototype.do_resize = require('./do_resize');
|
||||
InstanceCLI.prototype.do_rename = require('./do_rename');
|
||||
|
||||
InstanceCLI.prototype.do_start = require('./do_start');
|
||||
InstanceCLI.prototype.do_stop = require('./do_stop');
|
||||
InstanceCLI.prototype.do_reboot = require('./do_reboot');
|
||||
|
||||
InstanceCLI.prototype.do_fwrule = require('./do_fwrule');
|
||||
InstanceCLI.prototype.do_fwrules = require('./do_fwrules');
|
||||
InstanceCLI.prototype.do_enable_firewall = require('./do_enable_firewall');
|
||||
InstanceCLI.prototype.do_disable_firewall = require('./do_disable_firewall');
|
||||
|
||||
InstanceCLI.prototype.do_enable_deletion_protection =
|
||||
require('./do_enable_deletion_protection');
|
||||
InstanceCLI.prototype.do_disable_deletion_protection =
|
||||
require('./do_disable_deletion_protection');
|
||||
|
||||
InstanceCLI.prototype.do_ssh = require('./do_ssh');
|
||||
InstanceCLI.prototype.do_ip = require('./do_ip');
|
||||
InstanceCLI.prototype.do_wait = require('./do_wait');
|
||||
InstanceCLI.prototype.do_audit = require('./do_audit');
|
||||
InstanceCLI.prototype.do_nic = require('./do_nic');
|
||||
InstanceCLI.prototype.do_snapshot = require('./do_snapshot');
|
||||
InstanceCLI.prototype.do_snapshots = require('./do_snapshots');
|
||||
InstanceCLI.prototype.do_tag = require('./do_tag');
|
||||
InstanceCLI.prototype.do_tags = require('./do_tags');
|
||||
|
||||
InstanceCLI.aliases = ['inst'];
|
||||
|
||||
module.exports = InstanceCLI;
|
68
lib/do_instance_audit.js
Normal file
68
lib/do_instance_audit.js
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (c) 2015 Joyent Inc. All rights reserved.
|
||||
*
|
||||
* `triton instance-audit ...`
|
||||
*/
|
||||
|
||||
var format = require('util').format;
|
||||
var tabula = require('tabula');
|
||||
|
||||
var errors = require('./errors');
|
||||
|
||||
|
||||
function do_instance_audit(subcmd, opts, args, callback) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length > 1) {
|
||||
//XXX Support multiple machines.
|
||||
return callback(new Error('too many args: ' + args));
|
||||
}
|
||||
|
||||
var id = args[0];
|
||||
this.sdc.machineAudit({machine: id}, function (err, audit, dc) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
for (var i = 0; i < audit.length; i++) {
|
||||
audit[i].dc = dc;
|
||||
}
|
||||
if (opts.json) {
|
||||
p(JSON.stringify(audit, null, 4));
|
||||
} else {
|
||||
return callback(new error.InternalError("tabular output for audit NYI")); // XXX
|
||||
//common.tabulate(audit, {
|
||||
// columns: 'dc,id,name,state,created',
|
||||
// sort: 'created',
|
||||
// validFields: 'dc,id,name,type,state,image,package,memory,'
|
||||
// + 'disk,created,updated,compute_node,primaryIp'
|
||||
//});
|
||||
}
|
||||
callback();
|
||||
});
|
||||
};
|
||||
do_instance_audit.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON output.'
|
||||
}
|
||||
];
|
||||
do_instance_audit.help = (
|
||||
'List instance actions.\n'
|
||||
+ '\n'
|
||||
+ 'Usage:\n'
|
||||
+ ' {{name}} instance-audit <machine>\n'
|
||||
+ '\n'
|
||||
+ '{{options}}'
|
||||
);
|
||||
|
||||
|
||||
|
||||
module.exports = do_instance_audit;
|
@ -1,30 +1,128 @@
|
||||
/*
|
||||
* 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 2017 Joyent, Inc.
|
||||
* Copyright 2015 Joyent Inc.
|
||||
*
|
||||
* `triton instances ...` bwcompat shortcut for `triton instance list ...`.
|
||||
* `triton instances ...`
|
||||
*/
|
||||
|
||||
var targ = require('./do_instance/do_list');
|
||||
var tabula = require('tabula');
|
||||
|
||||
var common = require('./common');
|
||||
|
||||
// to be passed as query string args to /my/machines
|
||||
var validFilters = [
|
||||
'name',
|
||||
'image',
|
||||
'state',
|
||||
'memory',
|
||||
'tombstone',
|
||||
'credentials'
|
||||
];
|
||||
|
||||
// valid output fields to be printed
|
||||
var validFields = [
|
||||
'id',
|
||||
'name',
|
||||
'type',
|
||||
'state',
|
||||
'dataset',
|
||||
'memory',
|
||||
'disk',
|
||||
'ips',
|
||||
'metadata',
|
||||
'created',
|
||||
'updated',
|
||||
'package',
|
||||
'image',
|
||||
'ago'
|
||||
];
|
||||
|
||||
function do_instances(subcmd, opts, args, callback) {
|
||||
this.handlerFromSubcmd('instance').dispatch({
|
||||
subcmd: 'list',
|
||||
opts: opts,
|
||||
args: args
|
||||
}, callback);
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], callback);
|
||||
return;
|
||||
} else if (args.length > 1) {
|
||||
callback(new Error('too many args: ' + args));
|
||||
return;
|
||||
}
|
||||
|
||||
var columns = opts.o.trim().split(',');
|
||||
var sort = opts.s.trim().split(',');
|
||||
|
||||
var listOpts;
|
||||
try {
|
||||
listOpts = common.kvToObj(args, validFilters);
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
|
||||
this.triton.cloudapi.listMachines(listOpts, function (err, machines) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// add extra fields for nice output
|
||||
var now = new Date();
|
||||
machines.forEach(function (machine) {
|
||||
var created = new Date(machine.created);
|
||||
machine.ago = common.longAgo(created, now);
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(common.jsonStream(machines));
|
||||
} else {
|
||||
tabula(machines, {
|
||||
skipHeader: opts.H,
|
||||
columns: columns,
|
||||
sort: sort,
|
||||
validFields: validFields
|
||||
});
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
do_instances.help = 'A shortcut for "spearhead instance list".\n' + targ.help;
|
||||
do_instances.synopses = targ.synopses;
|
||||
do_instances.options = targ.options;
|
||||
do_instances.completionArgtypes = targ.completionArgtypes;
|
||||
do_instances.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['H'],
|
||||
type: 'bool',
|
||||
help: 'Omit table header row.'
|
||||
},
|
||||
{
|
||||
names: ['o'],
|
||||
type: 'string',
|
||||
default: 'id,name,state,type,image,memory,disk,ago',
|
||||
help: 'Specify fields (columns) to output.',
|
||||
helpArg: 'field1,...'
|
||||
},
|
||||
{
|
||||
names: ['s'],
|
||||
type: 'string',
|
||||
default: 'name',
|
||||
help: 'Sort on the given fields. Default is "name".',
|
||||
helpArg: 'field1,...'
|
||||
},
|
||||
{
|
||||
names: ['json', 'j'],
|
||||
type: 'bool',
|
||||
help: 'JSON output.'
|
||||
}
|
||||
];
|
||||
do_instances.help = (
|
||||
'List instances.\n'
|
||||
+ '\n'
|
||||
+ 'Usage:\n'
|
||||
+ ' {{name}} instances [<filters>...]\n'
|
||||
+ '\n'
|
||||
+ '{{options}}'
|
||||
);
|
||||
|
||||
do_instances.aliases = ['insts', 'ls'];
|
||||
do_instances.aliases = ['insts'];
|
||||
|
||||
module.exports = do_instances;
|
||||
|
28
lib/do_ip.js
28
lib/do_ip.js
@ -1,28 +0,0 @@
|
||||
/*
|
||||
* 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 2017 Joyent, Inc.
|
||||
*
|
||||
* `triton ip ...` shortcut for `triton instance ip ...`.
|
||||
*/
|
||||
|
||||
var targ = require('./do_instance/do_ip');
|
||||
|
||||
function do_ip(subcmd, opts, args, callback) {
|
||||
this.handlerFromSubcmd('instance').dispatch({
|
||||
subcmd: 'ip',
|
||||
opts: opts,
|
||||
args: args
|
||||
}, callback);
|
||||
}
|
||||
|
||||
do_ip.help = 'A shortcut for "spearhead instance ip".\n' + targ.help;
|
||||
do_ip.synopses = targ.synopses;
|
||||
do_ip.options = targ.options;
|
||||
do_ip.completionArgtypes = targ.completionArgtypes;
|
||||
|
||||
module.exports = do_ip;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user