Compare commits

..

6 Commits

Author SHA1 Message Date
Trent Mick 053d7354f2 PUBAPI-1233 Add firewalls to node-triton
PUBAPI-1234 Add snapshots to node-triton

CR updates.
- add 'triton fwrule update ID <TAB>' completion on updatable fields
- make ID arg first in ^^
- make 'triton fwrule create ...' have the rule enabled by default
- cosmetic help output tweaks
- allow 'triton fwrule update ...' to not *require* that the rule is
  updated. i.e you can update just the description
2016-03-01 16:46:06 -08:00
Marsell Kukuljevic f3f4f86f2f PUBAPI-1233/PUBAPI-1234 - code-review fixes. 2016-02-18 00:47:31 +11:00
Marsell Kukuljevic b7aa52dd0d Merge branch 'master' of https://github.com/joyent/node-triton into snapshot-fwrules 2016-02-12 23:50:09 +11:00
Marsell Kukuljevic 690c6e5198 PUBAPI-1233/PUBAPI-1234 - let fwrule and snapshot tests work even
when there isn't an existing machine to test with.
2016-02-08 00:50:47 +11:00
Marsell Kukuljevic e6334db9e1 PUBAPI-1233/PUBAPI-1234 - add support for --snapshot= flag when
starting a machine, and shortId support for fwrules.
2016-02-05 23:54:13 +11:00
Marsell Kukuljevic f3956df8ce PUBAPI-1233/PUBAPI-1234 - add support for `triton fwrules` and
`triton snapshots`; triton can work with machine snapshots and
firewall rules.
2016-02-05 00:39:50 +11:00
191 changed files with 3204 additions and 17197 deletions

3
.gitignore vendored
View File

@ -3,6 +3,3 @@
/test/*.json
/npm-debug.log
/triton-*.tgz
.DS_Store
.git
*.swp

View File

@ -1,546 +1,8 @@
# node-triton changelog
Known issues:
## 4.5.1 (not yet released)
- `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.
(nothing yet)
## 4.5.0

View File

@ -35,12 +35,6 @@ test-unit:
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
@ -51,27 +45,12 @@ check:: versioncheck
.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}'` ]]
[[ `cat package.json | json version` == `grep '^## ' CHANGES.md | head -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
[[ `git status | tail -n1` == "nothing to commit, working directory clean" ]]
./tools/cutarelease.py -p triton -f package.json
.PHONY: git-hooks
git-hooks:

357
README.md
View File

@ -1,38 +1,353 @@
![logo](https://code.spearhead.cloud/Spearhead/node-spearhead/raw/branch/master/tools/sphsp.png)
![logo](./tools/triton-text.png)
# node-spearhead
# node-triton
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).
`triton` is a CLI tool for working with the CloudAPI for Joyent's Triton [Public Cloud]
(https://docs.joyent.com/public-cloud) and [Private Cloud] (https://docs.joyent.com/private-cloud).
CloudAPI is a RESTful API for end users of the cloud to manage their accounts, instances,
networks, images, and to inquire other relevant details. CloudAPI provides a single view of
docker containers, infrastructure containers and hardware virtual machines available in the
Triton solution.
## Installation and configuration
There is currently another CLI tool known as [node-smartdc](https://github.com/joyent/node-smartdc)
for CloudAPI. `node-smartdc` CLI works off the 32-character object UUID to uniquely
identify object instances in API requests, and returns response payload in JSON format.
The CLI covers both basic and advanced usage of [CloudAPI](https://apidocs.joyent.com/cloudapi/).
### Get a Spearhead Cloud account
As a lightweight programmable interface for CloudAPI, the `triton` CLI supports both name or
UUID identification of object instances and the use of short ID, as well as the choice
between concise tabular responses and full JSON responses.
Create an account on the Spearhead Cloud and upload your SSH key. You can create an account
[here](https://spearhead.cloud/).
**The `triton` CLI is currently in beta and will be expanded over time to
support all CloudAPI commands, eventually replacing `node-smartdc` as both the
API client library for Triton cloud and the command line tool.**
## Setup
### User accounts, authentication, and security
Before you can use the CLI you'll need an account on the cloud to which you are connecting and
an SSH key uploaded. The SSH key is used to identify and secure SSH access to containers and
other resources in Triton.
If you do not already have an account on Joyent Public Cloud, sign up [here](https://www.joyent.com/public-cloud).
### Data-centers
### API endpoint
The list of available Spearhead Cloud data-centers is available
[here](https://spearhead.cloud/datacenters).
Each data center has a single CloudAPI endpoint. For Joyent Public Cloud, you can find the
list of data centers [here](https://docs.joyent.com/public-cloud/data-centers).
For private cloud implementations, please consult the private cloud operator for the correct URL.
Have the URL handy as you'll need it in the next step.
### Installation
Install [node.js](http://nodejs.org/), then:
npm install -g spearhead
1. Install [node.js](http://nodejs.org/).
2. `npm install -g triton`
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).
$ triton --version
Triton CLI 1.0.0
Configure the proper environmental variables that correspond to the API endpoint and account,
for example:
SDC_URL=https://us-east-3b.api.joyent.com
SDC_ACCOUNT=dave.eddy@joyent.com
SDC_KEY_ID=04:0c:22:25:c9:85:d8:e4:fa:27:0d:67:94:68:9e:e9
### Bash completion
Install Bash completion with
```bash
triton completion > /usr/local/etc/bash_completion.d/triton # Mac
triton completion > /etc/bash_completion.d/triton # Linux
```
Alternatively, if you don't have or don't want to use a "bash\_completion.d"
dir, then something like this would work:
```bash
triton completion > ~/.triton.completion
echo "source ~/.triton.completion" >> ~/.bashrc
```
Then open a new shell or manually `source FILE` that completion file, and
play with the bash completions:
triton <TAB>
## `triton` CLI Usage
### Create and view instances
$ triton instance list
SHORTID NAME IMG STATE PRIMARYIP AGO
We have no instances created yet, so let's create some. In order to create
an instance we need to specify two things: an image and a package. An image
represents what will be used as the root of the instances filesystem, and the
package represents the size of the instance, eg. ram, disk size, cpu shares,
etc. More information on images and packages below - for now we'll just use
SmartOS 64bit and a small 128M ram package which is a combo available on the
Joyent Public Cloud.
$ triton instance create base-64 t4-standard-128M
Without a name specified, the container created will have a generated ID. Now
to create a container-native Ubuntu 14.04 container with 2GB of ram with the
name "server-1"
$ triton instance create --name=server-1 ubuntu-14.04 t4-standard-2G
Now list your instances again
$ triton instance list
SHORTID NAME IMG STATE PRIMARYIP AGO
7db6c907 b851ba9 base-64@15.2.0 running 165.225.169.63 9m
9cf1f427 server-1 ubuntu-14.04@20150819 provisioning - 0s
Get a quick overview of your account
$ triton info
login: dave.eddy@joyent.com
name: Dave Eddy
email: dave.eddy@joyent.com
url: https://us-east-3b.api.joyent.com
totalDisk: 50.5 GiB
totalMemory: 2.0 MiB
instances: 2
running: 1
provisioning: 1
To obtain more detailed information of your instance
$ triton instance get server-1
{
"id": "9cf1f427-9a40-c188-ce87-fd0c4a5a2c2c",
"name": "251d4fd",
"type": "smartmachine",
"state": "running",
"image": "c8d68a9e-4682-11e5-9450-4f4fadd0936d",
"ips": [
"165.225.169.54",
"192.168.128.16"
],
"memory": 2048,
"disk": 51200,
"metadata": {
"root_authorized_keys": "(...ssh keys...)"
},
"tags": {},
"created": "2015-09-08T04:56:27.734Z",
"updated": "2015-09-08T04:56:43.000Z",
"networks": [
"feb7b2c5-0063-42f0-a4e6-b812917397f7",
"726379ac-358b-4fb4-bb7c-8bc4548bac1e"
],
"dataset": "c8d68a9e-4682-11e5-9450-4f4fadd0936d",
"primaryIp": "165.225.169.54",
"firewall_enabled": false,
"compute_node": "44454c4c-5400-1034-8053-b5c04f383432",
"package": "t4-standard-2G"
}
### SSH to an instance
Connect to an instance over SSH
$ triton ssh b851ba9
Last login: Wed Aug 26 17:59:35 2015 from 208.184.5.170
__ . .
_| |_ | .-. . . .-. :--. |-
|_ _| ;| || |(.-' | | |
|__| `--' `-' `;-| `-' ' ' `-'
/ ; Instance (base-64 15.2.0)
`-' https://docs.joyent.com/images/smartos/base
[root@7db6c907-2693-42bc-ea9b-f38678f2554b ~]# uptime
20:08pm up 2:27, 0 users, load average: 0.00, 0.00, 0.01
[root@7db6c907-2693-42bc-ea9b-f38678f2554b ~]# logout
Connection to 165.225.169.63 closed.
Or non-interactively
$ triton ssh b851ba9 uname -v
joyent_20150826T120743Z
### Manage an instance
Commonly used container operations are supported in the Triton CLI:
$ triton help instance
...
list (ls) List instances.
get Get an instance.
create Create a new instance.
delete (rm) Delete one or more instances.
start Start one or more instances.
stop Stop one or more instances.
reboot Reboot one or more instances.
ssh SSH to the primary IP of an instance
wait Wait on instances changing state.
audit List instance actions.
### View packages and images
Package definitions and images available vary between different data centers
and different Triton cloud implementations.
To see all the packages offered in the data center and specific package
information, use
$ triton package list
$ triton package get ID|NAME
Similarly, to find out the available images and their details, do
$ triton image list
$ triton images ID|NAME
Note that docker images are not shown in `triton images` as they are
maintained in Docker Hub and other third-party registries configured to be
used with Joyent's Triton clouds. **In general, docker containers should be
provisioned and managed with the regular
[`docker` CLI](https://docs.docker.com/installation/#installation)**
(Triton provides an endpoint that represents the _entire datacenter_
as a single `DOCKER_HOST`. See the [Triton Docker
documentation](https://apidocs.joyent.com/docker) for more information.)
## `TritonApi` Module Usage
Node-triton can also be used as a node module for your own node.js tooling.
A basic example:
var triton = require('triton');
// See `createClient` block comment for full usage details:
// https://github.com/joyent/node-triton/blob/master/lib/index.js
var client = triton.createClient({
profile: {
url: URL,
account: ACCOUNT,
keyId: KEY_ID
}
});
client.listImages(function (err, images) {
client.close(); // Remember to close the client to close TCP conn.
if (err) {
console.error('listImages err:', err);
} else {
console.log(JSON.stringify(images, null, 4));
}
});
## Configuration
This section defines all the vars in a TritonApi config. The baked in defaults
are in "etc/defaults.json" and can be overriden for the CLI in
"~/.triton/config.json" (on Windows: "%APPDATA%/Joyent/Triton/config.json").
| Name | Description |
| ---- | ----------- |
| profile | The name of the triton profile to use. The default with the CLI is "env", i.e. take config from `SDC_*` envvars. |
| cacheDir | The path (relative to the config dir, "~/.triton") where cache data is stored. The default is "cache", i.e. the `triton` CLI caches at "~/.triton/cache". |
## node-triton differences with node-smartdc
- There is a single `triton` command instead of a number of `sdc-*` commands.
- `TRITON_*` environment variables are preferred to the `SDC_*` environment
variables. However the `SDC_*` envvars are still supported.
- Node-smartdc still has more complete coverage of the Triton
[CloudAPI](https://apidocs.joyent.com/cloudapi/). However, `triton` is
catching up and is much more friendly to use.
## cloudapi2.js differences with node-smartdc/lib/cloudapi.js
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-triton) is a
re-write of the Cloud API lib with some backward incompatibilities. The
differences and backward incompatibilities are discussed here.
- 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.
## Development Hooks
Before commiting be sure to, at least:
make check # lint and style checks
make test-unit # run unit tests
A good way to do that is to install the stock pre-commit hook in your
clone via:
make git-hooks
Also please run the full (longer) test suite (`make test`). See the next
section.
## Test suite
node-triton has both unit tests (`make test-unit`) and integration tests (`make
test-integration`). Integration tests require a config file, by default at
"test/config.json". For example:
$ cat test/config.json
{
"profileName": "east3b",
"allowWriteActions": true,
"image": "minimal-64",
"package": "t4-standard-128M"
}
See "test/config.json.sample" for a description of all config vars. Minimally
just a "profileName" or "profile" is required.
*Warning:* Running the *integration* tests will create resources and could
incur costs if running against a public cloud.
Run all tests:
make test
You can use `TRITON_TEST_CONFIG` to override the test file, e.g.:
$ cat test/coal.json
{
"profileName": "coal",
"allowWriteActions": true
}
$ TRITON_TEST_CONFIG=test/coal.json make test
where "coal" here refers to a development Triton (a.k.a SDC) ["Cloud On A
Laptop"](https://github.com/joyent/sdc#getting-started) standup.
## License
MPL 2.0

View File

@ -1,14 +1,17 @@
- 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?
test suite:
- all the commands: test/integration/cli-*.test.js
- TritonApi testing: test/integration/api-*.test.js
- more test/unit/...
triton create affinity support for tag matching, globs, regex
sub-user support (profiles, `triton account`, env, auth)
note in README that full UUIDs is much faster in the API
*type*: cloudapi changes to clarify: LX, docker, smartos, kvm instances
bash completion: exclude '-J', better top/bottom comment boilerplate,
put the CLI's version in the top comment, ISO date format
# maybe next
PUBAPI-1117 triton create -c|--count N
@ -28,33 +31,3 @@ triton images
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"
}

View File

@ -1,5 +1,4 @@
# Functions for Bash completion of some 'triton' option/arg types.
function complete_tritonprofile {
local word="$1"
local candidates
@ -8,172 +7,6 @@ function complete_tritonprofile {
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

40
examples/example-get-account.js Executable file
View 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)
});

View File

@ -0,0 +1,46 @@
#!/usr/bin/env node
/**
* Example using cloudapi2.js to call cloudapi's ListMachines endpoint.
*
* Usage:
* ./example-list-images.js | bunyan
*/
var p = console.log;
var bunyan = require('bunyan');
var triton = require('../'); // typically `require('triton');`
var URL = process.env.SDC_URL || 'https://us-sw-1.api.joyent.com';
var ACCOUNT = 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 log = bunyan.createLogger({
name: 'test-list-instances',
level: process.env.LOG_LEVEL || 'trace'
});
/*
* More details on `createClient` options here:
* https://github.com/joyent/node-triton/blob/master/lib/index.js#L18-L61
* For example, if you want to use an existing `triton` CLI profile, you can
* pass that profile name in.
*/
var client = triton.createClient({
log: log,
profile: {
url: URL,
account: ACCOUNT,
keyId: KEY_ID
}
});
// TODO: Eventually the top-level TritonApi will have `.listInstances()` to use.
client.cloudapi.listMachines(function (err, insts) {
client.close(); // Remember to close the client to close TCP conn.
if (err) {
console.error('listInstances err:', err);
} else {
console.log(JSON.stringify(insts, null, 4));
}
});

View File

@ -0,0 +1,27 @@
*Caveat*: All `triton rbac ...` support is experimental.
This directly holds a super simple example Triton RBAC Profile for a mythical
"Simple Corp.", with `triton` CLI examples showing how to use it for RBAC.
Our Simple corporation will create an "rbactestsimple" Triton account and
use RBAC to manage its users, roles, etc. It has two users:
- emma: Should have full access, to everything.
- bert: Should only have read access, again to everything.
We want an RBAC config that allows appropriate access for all the employees
and tooling. Roughly we'll break that into roles as follows:
- Role `admin`. Complete access to the API. Only used by "emma" when, e.g.,
updating RBAC configuration itself.
- Role `ops`. Full access, except to RBAC configuration updates.
- Role `read`. Read-only access to compute resources.
See "rbac.json" where we encode all this.
The `triton rbac apply` command can work with a JSON config file (and
optionally separate user public ssh key files) to create and maintain a
Triton RBAC configuration. In our example this will be:
triton rbac apply # defaults to looking at "./rbac.json"

View File

@ -0,0 +1,43 @@
{
"users": [
{ "login": "emma", "email": "emma@simple.example.com" },
{ "login": "bert", "email": "bert@simple.example.com" }
],
"roles": [
{
"name": "admin",
"default_members": [],
"members": ["emma"],
"policies": ["policy-admin"]
},
{
"name": "ops",
"default_members": ["emma"],
"members": ["emma"],
"policies": ["policy-full"]
},
{
"name": "read",
"default_members": ["bert", "emma"],
"members": ["bert", "emma"],
"policies": ["policy-readonly"]
}
],
"policies": [
{
"name": "policy-admin",
"description": "full access",
"rules": ["CAN *"]
},
{
"name": "policy-full",
"description": "full access, except rbac",
"rules": ["CAN compute:*"]
},
{
"name": "policy-readonly",
"description": "read-only access",
"rules": ["CAN compute:Get*"]
}
]
}

View File

@ -96,18 +96,6 @@ SaferJsonClient.prototype.parse = function parse(req, callback) {
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');
@ -151,6 +139,9 @@ SaferJsonClient.prototype.parse = function parse(req, callback) {
}
// Special error handling.
if (resErr) {
resErr.message = body.toString('utf8');
}
if (res && res.statusCode >= 400) {
// Upcast error to a RestError (if we can)
// Be nice and handle errors like
@ -213,7 +204,7 @@ SaferJsonClient.prototype.parse = function parse(req, callback) {
res.on('data', function onData(chunk) {
if (contentMd5Hash) {
contentMd5Hash.update(chunk.toString('utf8'), 'binary');
contentMd5Hash.update(chunk.toString('utf8'));
}
if (gz) {

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright (c) 2017, Joyent, Inc.
* Copyright 2016 Joyent, Inc.
*
* The `triton` CLI class.
*/
@ -17,7 +17,6 @@ var child_process = require('child_process'),
exec = child_process.exec;
var cmdln = require('cmdln'),
Cmdln = cmdln.Cmdln;
var fs = require('fs');
var mkdirp = require('mkdirp');
var util = require('util'),
format = util.format;
@ -25,16 +24,30 @@ 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 tritonapi = require('./tritonapi');
//---- globals
var packageJson = require('../package.json');
var pkg = require('../package.json');
var CONFIG_DIR;
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.
*/
CONFIG_DIR = path.resolve(process.env.APPDATA, 'Joyent', 'Triton');
} else {
CONFIG_DIR = path.resolve(process.env.HOME, '.triton');
}
var OPTIONS = [
@ -58,9 +71,9 @@ var OPTIONS = [
names: ['profile', 'p'],
type: 'string',
completionType: 'tritonprofile',
env: 'SC_PROFILE',
env: 'TRITON_PROFILE',
helpArg: 'NAME',
help: 'Spearhead Cloud client profile to use.'
help: 'Triton client profile to use.'
},
{
@ -81,7 +94,8 @@ var OPTIONS = [
{
names: ['account', 'a'],
type: 'string',
help: 'Account (login name). Environment: SC_ACCOUNT=ACCOUNT ',
help: 'Account (login name). Environment: TRITON_ACCOUNT=ACCOUNT ' +
'or SDC_ACCOUNT=ACCOUNT.',
helpArg: 'ACCOUNT'
},
{
@ -89,48 +103,52 @@ var OPTIONS = [
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.',
'audited on the CloudAPI server side.',
helpArg: 'ACCOUNT',
hidden: true
},
{
names: ['user', 'u'],
type: 'string',
help: 'RBAC user (login name). Environment: SC_USER=USER',
help: 'RBAC user (login name). Environment: TRITON_USER=USER ' +
'or SDC_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,...'
},
// TODO: full rbac support
//{
// names: ['role'],
// type: 'arrayOfString',
// env: 'MANTA_ROLE',
// help: 'Assume a role. Use multiple times or once with a list',
// helpArg: 'ROLE,ROLE,...'
//},
{
names: ['keyId', 'k'],
type: 'string',
help: 'SSH key fingerprint. Environment: SC_KEY_ID=FINGERPRINT.',
help: 'SSH key fingerprint. Environment: TRITON_KEY_ID=FINGERPRINT ' +
'or SDC_KEY_ID=FINGERPRINT.',
helpArg: 'FP'
},
{
names: ['url', 'U'],
type: 'string',
help: 'Spearhead Cloud Datacenter URL. Environment: SC_URL=URL.',
help: 'CloudAPI URL. Environment: TRITON_URL=URL or SDC_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" ' +
help: 'Joyent Public Cloud (JPC) datacenter name. This is ' +
'a shortcut to the "https://$dc.api.joyent.com" ' +
'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).',
help: 'Do not validate the CloudAPI SSL certificate. Environment: ' +
'TRITON_TLS_INSECURE=1, SDC_TLS_INSECURE=1 (or the deprecated ' +
'SDC_TESTING=1).',
'default': false
},
{
@ -139,10 +157,10 @@ var OPTIONS = [
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 + '". ' +
'See `triton cloudapi /--ping` to list supported versions. ' +
'The default is "' + tritonapi.CLOUDAPI_ACCEPT_VERSION + '". ' +
'*This is intended for development use only. It could cause ' +
'`spearhead` processing of responses to break.*',
'`triton` processing of responses to break.*',
hidden: true
}
];
@ -179,8 +197,8 @@ cmdln.dashdash.addOptionType({
function CLI() {
Cmdln.call(this, {
name: 'spearhead',
desc: packageJson.description,
name: 'triton',
desc: pkg.description,
options: OPTIONS,
helpOpts: {
includeEnv: true,
@ -188,9 +206,8 @@ function CLI() {
},
helpSubcmds: [
'help',
'profile',
'env',
'completion',
'profile',
{ group: 'Instances (aka VMs/Machines/Containers)' },
'instance',
'instances',
@ -200,13 +217,11 @@ function CLI() {
'stop',
'reboot',
'ssh',
'ip',
{ group: 'Images, Packages, Networks, Firewall Rules' },
'image',
'package',
'network',
'fwrule',
'vlan',
{ group: 'Other Commands' },
'info',
'account',
@ -220,8 +235,8 @@ function CLI() {
' 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.'
' 3 "ResourceNotFound" error. Returned when an instance, image,',
' package, etc. with the given name or id is not found.'
/* END JSSTYLED */
].join('\n')
});
@ -245,8 +260,7 @@ CLI.prototype.init = function (opts, args, callback) {
}
if (opts.version) {
console.log('Spearhead CLI', packageJson.version);
console.log(packageJson.homepage);
console.log(this.name, pkg.version);
callback(false);
return;
}
@ -255,89 +269,37 @@ CLI.prototype.init = function (opts, args, callback) {
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);
opts.url = format('https://%s.api.joyent.com', opts.J);
}
this.configDir = constants.CLI_CONFIG_DIR;
this.configDir = CONFIG_DIR;
this.__defineGetter__('config', function getConfig() {
if (self._config === undefined) {
self._config = mod_config.loadConfig({
this.__defineGetter__('tritonapi', function () {
if (self._tritonapi === undefined) {
var config = mod_config.loadConfig({
configDir: self.configDir
});
self.log.trace({config: self._config}, 'loaded config');
}
return self._config;
});
self.log.trace({config: config}, 'loaded config');
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
var profileName = opts.profile || config.profile || 'env';
var profile = mod_config.loadProfile({
configDir: self.configDir,
name: profileName
});
self._applyProfileOverrides(profile);
self.log.trace({profile: profile}, 'loaded profile');
self._tritonapi = tritonapi.createClient({
log: self.log,
profile: profile,
config: 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);
};
@ -347,241 +309,7 @@ CLI.prototype.fini = function fini(subcmd, err, cb) {
this._tritonapi.close();
delete this._tritonapi;
}
cb();
};
/*
* 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);
});
cb(err, subcmd);
};
@ -589,26 +317,11 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
* 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() {
function _applyProfileOverrides(profile) {
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'},
@ -623,38 +336,6 @@ CLI.prototype._cliOptsAsProfile = function _cliOptsAsProfile() {
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
});
};
@ -676,7 +357,6 @@ 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');
@ -691,7 +371,6 @@ CLI.prototype.do_start = require('./do_start');
CLI.prototype.do_stop = require('./do_stop');
CLI.prototype.do_reboot = require('./do_reboot');
CLI.prototype.do_ssh = require('./do_ssh');
CLI.prototype.do_ip = require('./do_ip');
// Packages
CLI.prototype.do_packages = require('./do_packages');
@ -701,17 +380,11 @@ CLI.prototype.do_package = require('./do_package');
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
@ -722,71 +395,36 @@ function main(argv) {
}
var cli = new CLI();
cli.main(argv, function (err) {
cli.main(argv, function (err, subcmd) {
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),
} else if (err.message !== undefined) {
console.error('%s%s: error%s: %s',
cli.name,
(subcmd ? ' ' + subcmd : ''),
(code ? format(' (%s)', code) : ''),
(cli.showErrStack ? err.stack : errMessage),
bodyErrors);
}
(cli.showErrStack ? err.stack : err.message));
errHelp = cmdln.errHelpFromErr(err);
if (errHelp) {
console.error(errHelp);
// If this is a usage error, attempt to show some usage info.
if (['Usage', 'Option'].indexOf(code) !== -1 && subcmd) {
var help = cli.helpFromSubcmd(subcmd);
if (help && typeof (help) === 'string') {
// Would like a shorter synopsis. Attempt to
// parse it down, somewhat generally. Unfortunately this
// doesn't work for multi-level subcmds, like
// `triton rbac subcmd ...`.
var usageIdx = help.indexOf('\nUsage:');
if (usageIdx !== -1) {
help = help.slice(usageIdx);
}
console.error(help);
}
}
}
}
@ -801,10 +439,10 @@ function main(argv) {
});
}
//---- exports
module.exports = {
CONFIG_DIR: CONFIG_DIR,
CLI: CLI,
main: main
};

File diff suppressed because it is too large Load Diff

View File

@ -5,14 +5,13 @@
*/
/*
* Copyright (c) 2018, Joyent, Inc.
* Copyright 2015 Joyent, Inc.
*/
var assert = require('assert-plus');
var child_process = require('child_process');
var crypto = require('crypto');
var fs = require('fs');
var getpass = require('getpass');
var os = require('os');
var path = require('path');
var read = require('read');
@ -24,8 +23,7 @@ var wordwrap = require('wordwrap');
var errors = require('./errors'),
InternalError = errors.InternalError;
var NETWORK_OBJECT_FIELDS =
require('./constants').NETWORK_OBJECT_FIELDS;
// ---- support stuff
@ -92,7 +90,7 @@ function zeroPad(n, width) {
* raise TypeError trying.
*
* @param value {Boolean|String} The input value to convert.
* @param default_ {Boolean} The default value if `value` is undefined.
* @param default_ {Boolean} The default value is `value` is undefined.
* @param errName {String} The context to quote in the possibly
* raised TypeError.
*/
@ -127,138 +125,32 @@ function jsonStream(arr, stream) {
}
/**
* Parses the string "kv" of the form 'key=value' and returns an object that
* represents it with the form {'key': value}. If "key"" in the "kv" string is
* not included in the list "validKeys", it throws an error. It also throws an
* error if the string "kv" is malformed.
*
* By default, converts the values as if they were JSON representations of JS
* types, e.g the string 'false' is converted to the boolean primitive "false".
*
* @param {String} kv
* @param {Array} validKeys: Optional array of strings or regexes matching
* valid keys.
* @param {Object} options: Optional
* - @param disableTypeConversions {Boolean} Optional. If true, then no
* type conversion of values is performed, and all values are returned as
* strings.
* - @param typeHintFromKey {Object} Optional. Type hints for input keys.
* E.g. if parsing 'foo=false' and `typeHintFromKey={foo: 'string'}`,
* then we do NOT parse it to a boolean `false`.
* - @param failOnEmptyValue {Boolean} Optional - If true, throws an error
* if a given key's value is the empty string. Default is false.
*/
function _parseKeyValue(kv, validKeys, options) {
assert.string(kv, 'kv');
assert.optionalArray(validKeys, 'validKeys');
assert.optionalObject(options, 'options');
options = options || {};
assert.optionalBool(options.disableTypeConversions,
'options.disableTypeConversions');
assert.optionalObject(options.typeHintFromKey, 'options.typeHintFromKey');
assert.optionalBool(options.failOnEmptyValue, 'options.failOnEmptyValue');
var i;
var idx = kv.indexOf('=');
if (idx === -1) {
throw new errors.UsageError(format('invalid key=value: "%s"', kv));
}
var k = kv.slice(0, idx);
var typeHint;
var v = kv.slice(idx + 1);
var validKey;
if (validKeys) {
var foundMatch = false;
for (i = 0; i < validKeys.length; i++) {
validKey = validKeys[i];
if ((validKey instanceof RegExp && validKey.test(k)) ||
k === validKey) {
foundMatch = true;
break;
}
}
if (!foundMatch) {
throw new errors.UsageError(format(
'invalid key: "%s" (must match one of: %s)',
k, validKeys.join(', ')));
}
}
if (v === '' && options.failOnEmptyValue) {
throw new Error(format('key "%s" must have a value', k));
}
if (options.disableTypeConversions !== true) {
if (options.typeHintFromKey !== undefined) {
typeHint = options.typeHintFromKey[k];
}
if (typeHint === 'string') {
// Leave `v` a string.
/* jsl:pass */
} else if (v === '') {
v = null;
} else {
try {
v = JSON.parse(v);
} catch (e) {
/* pass */
}
}
}
return {
key: k,
value: v
};
}
/**
* given an array of key=value pairs, break them into a JSON predicate
* given an array of key=value pairs, break them into an object
*
* @param {Array} kvs - an array of key=value pairs
* @param {Array} validKeys: Optional array of strings or regexes matching
* valid keys.
* @param {String} compositionType - the way each key/value pair will be
* combined to form a JSON predicate. Valid values are 'or' and 'and'.
* @param {Array} valid (optional) - an array to validate pairs
*/
function jsonPredFromKv(kvs, validKeys, compositionType) {
function kvToObj(kvs, valid) {
assert.arrayOfString(kvs, 'kvs');
assert.string(compositionType, 'string');
assert.ok(compositionType === 'or' || compositionType === 'and',
'compositionType');
assert.optionalArrayOfString(valid, 'valid');
var keyName;
var predicate = {};
var parsedKeyValue;
var parsedKeyValues;
var parseOpts = {
disableDotted: true,
validKeys: validKeys,
failOnEmptyValue: true
};
if (kvs.length === 0) {
return predicate;
var o = {};
for (var i = 0; i < kvs.length; i++) {
var kv = kvs[i];
var idx = kv.indexOf('=');
if (idx === -1)
throw new errors.UsageError(format(
'invalid filter: "%s" (must be of the form "field=value")',
kv));
var k = kv.slice(0, idx);
var v = kv.slice(idx + 1);
if (valid && valid.indexOf(k) === -1)
throw new errors.UsageError(format(
'invalid filter name: "%s" (must be one of "%s")',
k, valid.join('", "')));
o[k] = v;
}
if (kvs.length === 1) {
parsedKeyValue = _parseKeyValue(kvs[0], validKeys, parseOpts);
predicate.eq = [parsedKeyValue.key, parsedKeyValue.value];
} else {
predicate[compositionType] = [];
parsedKeyValues = objFromKeyValueArgs(kvs, parseOpts);
for (keyName in parsedKeyValues) {
predicate[compositionType].push({
eq: [keyName, parsedKeyValues[keyName]]
});
}
}
return predicate;
return o;
}
/**
@ -618,9 +510,9 @@ function promptYesNo(opts_, cb) {
stdin.on('data', onData);
function postInput() {
stdout.write('\n');
stdin.setRawMode(false);
stdin.pause();
stdin.write('\n');
stdin.removeListener('data', onData);
}
@ -732,7 +624,7 @@ function promptEnter(prompt, cb) {
* string "cancelled".
*/
function promptField(field, cb) {
var wrap = wordwrap(Math.min(process.stdout.columns, 80));
var wrap = wordwrap(Math.min(process.stdout.columns, 78));
var validate = field.validate;
if (!validate && field.required) {
@ -778,119 +670,12 @@ function promptField(field, cb) {
}
if (field.desc) {
// Wrap, if no newlines.
var wrapped = field.desc;
if (field.desc.indexOf('\n') === -1) {
wrapped = wrap(field.desc);
}
// Bold up to the first period, or all of it, if no period.
var periodIdx = wrapped.indexOf('.');
if (periodIdx !== -1) {
console.log(
ansiStylize(wrapped.slice(0, periodIdx + 1), 'bold') +
wrapped.slice(periodIdx + 1));
} else {
console.log(ansiStylize(wrap(field.desc), 'bold'));
}
console.log(ansiStylize(wrap(field.desc), 'bold'));
}
attempt();
}
/**
* A utility method to unlock a private key on a TritonApi client instance,
* if necessary.
*
* If the client's key is locked, this will prompt for the passphrase on the
* TTY (via the `getpass` module) and attempt to unlock.
*
* @param opts {Object}
* - opts.tritonapi {Object} An `.init()`ialized TritonApi instance.
* @param cb {Function} `function (err)`
*/
function promptPassphraseUnlockKey(opts, cb) {
assert.object(opts.tritonapi, 'opts.tritonapi');
var kp = opts.tritonapi.keyPair;
if (!kp) {
cb(new errors.InternalError('TritonApi instance given to '
+ 'promptPassphraseUnlockKey is not initialized'));
return;
}
if (!kp.isLocked()) {
cb();
return;
}
var keyDesc;
if (kp.source !== undefined) {
keyDesc = kp.source;
} else if (kp.comment !== undefined && kp.comment.length > 1) {
keyDesc = kp.getPublicKey().type.toUpperCase() +
' key for ' + kp.comment;
} else {
keyDesc = kp.getPublicKey().type.toUpperCase() +
' key ' + kp.getKeyId();
}
var getpassOpts = {
prompt: 'Enter passphrase for ' + keyDesc
};
var tryPass = function (err, pass) {
if (err) {
cb(err);
return;
}
try {
kp.unlock(pass);
} catch (unlockErr) {
getpassOpts.prompt = 'Bad passphrase, try again for ' + keyDesc;
getpass.getPass(getpassOpts, tryPass);
return;
}
cb(null);
};
getpass.getPass(getpassOpts, tryPass);
}
/*
* A utility for the `triton` CLI subcommands to `init()`ialize a
* `tritonapi` instance and ensure that the profile's key is unlocked
* (prompting on a TTY if necessary). This is typically the CLI's
* `tritonapi` instance, but a `tritonapi` can also be passed in
* directly.
*
* @param opts.cli {Object}
* @param opts.tritonapi {Object}
* @param cb {Function} `function (err)`
*/
function cliSetupTritonApi(opts, cb) {
assert.optionalObject(opts.cli, 'opts.cli');
assert.optionalObject(opts.tritonapi, 'opts.tritonapi');
var tritonapi = opts.tritonapi || opts.cli.tritonapi;
assert.object(tritonapi, 'tritonapi');
tritonapi.init(function (initErr) {
if (initErr) {
cb(initErr);
return;
}
promptPassphraseUnlockKey({
tritonapi: tritonapi
}, function (keyErr) {
cb(keyErr);
});
});
}
/**
* Edit the given text in $EDITOR (defaulting to `vi`) and return the edited
* text.
@ -901,26 +686,18 @@ function cliSetupTritonApi(opts, cb) {
function editInEditor(opts, cb) {
assert.string(opts.text, 'opts.text');
assert.optionalString(opts.filename, 'opts.filename');
assert.optionalObject(opts.log, 'opts.log');
assert.func(cb, 'cb');
var tmpPath = path.resolve(os.tmpDir(),
format('triton-%s-edit-%s', process.pid, opts.filename || 'text'));
fs.writeFileSync(tmpPath, opts.text, 'utf8');
// TODO: want '-f' opt for vi? What about others?
var editor = process.env.EDITOR || '/usr/bin/vi';
var argv = argvFromLine(format('%s "%s"', editor, tmpPath));
if (opts.log) {
opts.log.trace({argv: argv}, 'editInEditor argv');
}
var kid = child_process.spawn(argv[0], argv.slice(1), {stdio: 'inherit'});
kid.on('exit', function (code, signal) {
if (code || signal) {
cb(new errors.TritonError(format(
'editor terminated abnormally: argv=%j, code=%j, signal=%j',
argv, code, signal)));
return;
var kid = child_process.spawn(editor, [tmpPath], {stdio: 'inherit'});
kid.on('exit', function (code) {
if (code) {
return (cb(code));
}
var afterText = fs.readFileSync(tmpPath, 'utf8');
fs.unlinkSync(tmpPath);
@ -964,18 +741,6 @@ function ansiStylize(str, color) {
}
/*
* Style the given string with ANSI style codes *if stdout is a TTY*.
*/
function ansiStylizeTty(str, color) {
if (!process.stdout.isTTY) {
return str;
} else {
return ansiStylize(str, color);
}
}
function indent(s, indentation) {
if (!indentation) {
indentation = ' ';
@ -1067,7 +832,8 @@ function execPlus(args, cb) {
+ '\tstdout:\n%s\n'
+ '\tstderr:\n%s',
cmd, err.code, stdout.trim(), stderr.trim());
cb(new errors.InternalError(err, msg), stdout, stderr);
cb(new errors.InternalError({message: msg, cause: err}),
stdout, stderr);
} else {
cb(null, stdout, stderr);
}
@ -1139,10 +905,6 @@ function tildeSync(s) {
* - @param typeHintFromKey {Object} Optional. Type hints for input keys.
* E.g. if parsing 'foo=false' and `typeHintFromKey={foo: 'string'}`,
* then we do NOT parse it to a boolean `false`.
* - @param {Array} validKeys: Optional array of strings or regexes
* matching valid keys. By default all keys are valid.
* - @param failOnEmptyValue {Boolean} Optional. If true, then a key with a
* value that is the empty string throws an error. Default is false.
*/
function objFromKeyValueArgs(args, opts)
{
@ -1150,30 +912,45 @@ function objFromKeyValueArgs(args, opts)
assert.optionalObject(opts, 'opts');
opts = opts || {};
assert.optionalBool(opts.disableDotted, 'opts.disableDotted');
assert.optionalBool(opts.disableTypeConversions,
'opts.disableTypeConversions');
assert.optionalObject(opts.typeHintFromKey, opts.typeHintFromKey);
assert.optionalBool(opts.failOnEmptyValue, 'opts.failOnEmptyValue');
var typeHintFromKey = opts.typeHintFromKey || {};
var obj = {};
args.forEach(function (arg) {
var parsedKeyValue = _parseKeyValue(arg, opts.validKeys, {
typeHintFromKey: opts.typeHintFromKey,
disableTypeConversions: opts.disableTypeConversions,
failOnEmptyValue: opts.failOnEmptyValue
});
var kv = strsplit(arg, '=', 2);
if (kv.length < 2) {
throw new TypeError(format('invalid key=value argument: "%s"',
arg));
}
var k = kv[0];
var t = typeHintFromKey[k];
var v = kv[1];
if (t === 'string') {
// Leave `v` a string.
/* jsl:pass */
} else if (v === '') {
v = null;
} else {
try {
v = JSON.parse(v);
} catch (e) {
/* pass */
}
}
if (opts.disableDotted) {
obj[parsedKeyValue.key] = parsedKeyValue.value;
obj[k] = v;
} else {
var dotted = strsplit(parsedKeyValue.key, '.', 2);
var dotted = strsplit(k, '.', 2);
if (dotted.length > 1) {
if (!obj[dotted[0]]) {
obj[dotted[0]] = {};
}
obj[dotted[0]][dotted[1]] = parsedKeyValue.value;
obj[dotted[0]][dotted[1]] = v;
} else {
obj[parsedKeyValue.key] = parsedKeyValue.value;
obj[k] = v;
}
}
});
@ -1181,298 +958,6 @@ function objFromKeyValueArgs(args, opts)
return obj;
}
/**
* Returns the time difference between the current time and the time
* represented by "relativeTo" in milliseconds. It doesn't use the built-in
* `Date` class internally, and instead uses a node facility that uses a
* monotonic clock. Thus, the time difference computed is not subject to time
* drifting due to e.g changes in the wall clock system time.
*
* @param {arrayOfNumber} relativeTo: an array representing the starting time as
* returned by `process.hrtime()` from which to compute the
* time difference.
*/
function monotonicTimeDiffMs(relativeTo) {
assert.arrayOfNumber(relativeTo, 'relativeTo');
var diff = process.hrtime(relativeTo);
var ms = (diff[0] * 1e3) + (diff[1] / 1e6); // in milliseconds
return ms;
}
/*
* Parse the given line into an argument vector, e.g. for use in sending to
* `child_process.spawn(argv[0], argv.slice(1), ...)`.
*
* Translated from the Python `line2argv` in https://github.com/trentm/cmdln
* See also the tests in "test/unit/argvFromLine.test.js".
*
* @throws {Error} if there are unbalanced quotes or some other parse failure.
*/
function argvFromLine(line) {
assert.string(line, 'line');
var trimmed = line.trim();
var argv = [];
var state = 'default';
var arg = null; // the current argument being parsed
var i = -1;
var WHITESPACE = {
' ': true,
'\t': true,
'\n': true,
'\r': true
// Other whitespace chars?
};
while (true) {
i += 1;
if (i >= trimmed.length) {
break;
}
var ch = trimmed[i];
// An escaped char always added to the arg.
if (ch == '\\' && i+1 < trimmed.length) {
if (arg === null) { arg = ''; }
/*
* Include the escaping backslash, unless it is escaping a quote
* inside a quoted string. E.g.:
* foo\Xbar => foo\Xbar
* 'foo\'bar' => foo'bar
* "foo\"bar" => foo"bar
*
* Note that cmdln.py's line2argv had a Windows-specific subtlety
* here (dating to cmdln commit 87430930160f) that we are skipping
* for now.
*/
if ((state === 'double-quoted' && trimmed[i+1] !== '"') ||
(state === 'single-quoted' && trimmed[i+1] !== '\'')) {
arg += ch;
}
i += 1;
arg += trimmed[i];
continue;
}
if (state === 'single-quoted') {
if (ch === '\'') {
state = 'default';
} else {
arg += ch;
}
} else if (state === 'double-quoted') {
if (ch === '"') {
state = 'default';
} else {
arg += ch;
}
} else if (state === 'default') {
if (ch === '"') {
if (arg === null) { arg = ''; }
state = 'double-quoted';
} else if (ch === '\'') {
if (arg === null) { arg = ''; }
state = 'single-quoted';
} else if (WHITESPACE.hasOwnProperty(ch)) {
if (arg !== null) {
argv.push(arg);
}
arg = null;
} else {
if (arg === null) { arg = ''; }
arg += ch;
}
}
}
if (arg !== null) {
argv.push(arg);
}
/*
* Note: cmdln.py's line2argv would not throw this error on Windows, i.e.
* allowing unclosed quoted-strings. This impl. is not following that lead.
*/
if (state !== 'default') {
throw new Error(format('unfinished %s segment in line: %j',
state, line));
}
return argv;
}
/*
* Read stdin in and callback with it as a string
*
* @param {Function} cb - callback in the form `function (str) {}`
*/
function readStdin(cb) {
assert.func(cb, 'cb');
var stdin = '';
process.stdin.setEncoding('utf8');
process.stdin.resume();
process.stdin.on('data', function stdinOnData(chunk) {
stdin += chunk;
});
process.stdin.on('end', function stdinOnEnd() {
cb(stdin);
});
}
/*
* Validate an object of values against an object of types.
*
* Example:
* var input = {
* foo: 'hello',
* bar: 42,
* baz: true
* };
* var valid = {
* foo: 'string',
* bar: 'number',
* baz: 'boolean'
* }
* validateObject(input, valid);
* // no error is thrown
*
* All keys in `input` are check for their matching counterparts in `valid`.
* If the key is not found in `valid`, or the type specified for the key in
* `valid` doesn't match the type of the value in `input` an error is thrown.
* Also an error is thrown (optionally, enabled by default) if the input object
* is empty. Note that any keys found in `valid` not found in `input` are not
* considered an error.
*
* @param {Object} input - Required. Input object of values.
* @param {Object} valid - Required. Validation object of types.
* @param {Object} opts: Optional
* - @param {Boolean} allowEmptyInput - don't consider an empty
* input object an error
* @throws {Error} if the input object contains a key not found in the
* validation object
*/
function validateObject(input, valid, opts) {
opts = opts || {};
assert.object(input, 'input');
assert.object(valid, 'valid');
assert.object(opts, 'opts');
assert.optionalBool(opts.allowEmptyInput, 'opts.allowEmptyInput');
var validFields = Object.keys(valid).sort().join(', ');
var i = 0;
Object.keys(input).forEach(function (key) {
var value = input[key];
var type = valid[key];
if (!type) {
throw new errors.UsageError(format('unknown or ' +
'unupdateable field: %s (updateable fields are: %s)',
key, validFields));
}
assert.string(type, 'type');
if (typeof (value) !== type) {
throw new errors.UsageError(format('field "%s" must be ' +
'of type "%s", but got a value of type "%s"',
key, type, typeof (value)));
}
i++;
});
if (i === 0 && !opts.allowEmptyInput) {
throw new errors.UsageError('Input object must not be empty');
}
}
/*
* Convert an IPv4 address (as a string) to a number
*/
function ipv4ToLong(ip) {
var l = 0;
var spl;
assert.string(ip, 'ip');
spl = ip.split('.');
assert.equal(spl.length, 4, 'ip octet length');
spl.forEach(function processIpOctet(octet) {
octet = parseInt(octet, 10);
assert.number(octet, 'octet');
assert(octet >= 0, 'octet >= 0');
assert(octet < 256, 'octet < 256');
l <<= 8;
l += octet;
});
return l;
}
/*
* Parse the input from the `--nics <nic>` CLI argument.
*
* @param a {Array} The array of strings formatted as key=value
* ex: ['ipv4_uuid=1234', 'ipv4_ips=1.2.3.4|5.6.7.8']
* @return {Object} A network object. From the example above:
* {
* "ipv4_uuid": 1234,
* "ipv4_ips": [
* "1.2.3.4",
* "5.6.7.8"
* ]
* }
* Note: "1234" is used as the UUID for this example, but would actually cause
* `parseNicStr` to throw as it is not a valid UUID.
*/
function parseNicStr(nic) {
assert.arrayOfString(nic);
var obj = objFromKeyValueArgs(nic, {
disableDotted: true,
typeHintFromKey: NETWORK_OBJECT_FIELDS,
validKeys: Object.keys(NETWORK_OBJECT_FIELDS)
});
if (!obj.ipv4_uuid) {
throw new errors.UsageError(
'ipv4_uuid must be specified in network object');
}
if (obj.ipv4_ips) {
obj.ipv4_ips = obj.ipv4_ips.split('|');
}
assert.uuid(obj.ipv4_uuid, 'obj.ipv4_uuid');
assert.optionalArrayOfString(obj.ipv4_ips, 'obj.ipv4_ips');
/*
* Only 1 IP address may be specified at this time. In the future, this
* limitation should be removed.
*/
if (obj.ipv4_ips && obj.ipv4_ips.length !== 1) {
throw new errors.UsageError('only 1 ipv4_ip may be specified');
}
return obj;
}
/*
* Return a short image string that represents the given image object.
*
* @param img {Object} The image object.
* @returns {String} A network object. E.g.
* 'a6cf222d-73f4-414c-a427-5c238ef8e1b7 (jillmin@1.0.0)'
*/
function imageRepr(img) {
assert.object(img);
return format('%s (%s@%s)', img.id, img.name, img.version);
}
//---- exports
@ -1484,6 +969,7 @@ module.exports = {
zeroPad: zeroPad,
boolFromString: boolFromString,
jsonStream: jsonStream,
kvToObj: kvToObj,
longAgo: longAgo,
isUUID: isUUID,
humanDurationFromMs: humanDurationFromMs,
@ -1497,25 +983,14 @@ module.exports = {
promptYesNo: promptYesNo,
promptEnter: promptEnter,
promptField: promptField,
promptPassphraseUnlockKey: promptPassphraseUnlockKey,
cliSetupTritonApi: cliSetupTritonApi,
editInEditor: editInEditor,
ansiStylize: ansiStylize,
ansiStylizeTty: ansiStylizeTty,
indent: indent,
chomp: chomp,
generatePassword: generatePassword,
execPlus: execPlus,
deepEqual: deepEqual,
tildeSync: tildeSync,
objFromKeyValueArgs: objFromKeyValueArgs,
argvFromLine: argvFromLine,
jsonPredFromKv: jsonPredFromKv,
monotonicTimeDiffMs: monotonicTimeDiffMs,
readStdin: readStdin,
validateObject: validateObject,
ipv4ToLong: ipv4ToLong,
parseNicStr: parseNicStr,
imageRepr: imageRepr
objFromKeyValueArgs: objFromKeyValueArgs
};
// vim: set softtabstop=4 shiftwidth=4:

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright 2016 Joyent, Inc.
*/
/*
@ -49,8 +49,6 @@ 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'
];
@ -61,9 +59,7 @@ var PROFILE_FIELDS = {
account: true,
keyId: true,
insecure: true,
user: true,
roles: true,
actAsAccount: true
user: true
};
@ -143,20 +139,19 @@ function loadConfig(opts) {
}
function setConfigVars(opts, cb) {
function setConfigVar(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);
});
assert.string(opts.name, 'opts.name');
assert.string(opts.value, 'opts.value');
assert.ok(opts.name.indexOf('.') === -1,
'dotted config name not yet supported');
assert.ok(CONFIG_VAR_NAMES.indexOf(opts.name) !== -1,
'unknown config var name: ' + opts.name);
var configPath = configPathFromDir(opts.configDir);
var config;
var config;
vasync.pipeline({funcs: [
function loadExisting(_, next) {
fs.exists(configPath, function (exists) {
@ -188,23 +183,8 @@ function setConfigVars(opts, cb) {
});
},
/*
* 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];
});
config[opts.name] = opts.value;
fs.writeFile(configPath, JSON.stringify(config, null, 4), next);
}
]}, cb);
@ -221,21 +201,19 @@ function validateProfile(profile, profilePath) {
try {
assert.string(profile.name, 'profile.name');
assert.string(profile.url,
profile.name === 'env' ? 'SC_URL' : 'profile.url');
profile.name === 'env' ? 'TRITON_URL or SDC_URL' : 'profile.url');
assert.string(profile.account,
profile.name === 'env' ? 'SC_ACCOUNT'
profile.name === 'env' ? 'TRITON_ACCOUNT or SDC_ACCOUNT'
: 'profile.account');
assert.string(profile.keyId,
profile.name === 'env' ? 'SC_KEY_ID'
profile.name === 'env' ? 'TRITON_KEY_ID or SDC_KEY_ID'
: 'profile.keyId');
assert.optionalBool(profile.insecure,
profile.name === 'env' ? 'SC_INSECURE'
profile.name === 'env' ? 'TRITON_INSECURE or SDC_INSECURE'
: 'profile.insecure');
assert.optionalString(profile.user,
profile.name === 'env' ? 'SC_USER'
profile.name === 'env' ? 'TRITON_USER or SDC_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 + '" ' : '',
@ -271,44 +249,44 @@ function validateProfile(profile, profilePath) {
* @throws {errors.ConfigError} If the profile defined by the environment is
* invalid.
*/
function _loadEnvProfile(profileOverrides) {
function _loadEnvProfile() {
var envProfile = {
name: 'env'
};
envProfile.account = process.env.SC_ACCOUNT;
var user = process.env.SC_USER;
envProfile.account = process.env.TRITON_ACCOUNT || process.env.SDC_ACCOUNT;
var user = process.env.TRITON_USER || process.env.SDC_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];
}
envProfile.url = process.env.TRITON_URL || process.env.SDC_URL;
envProfile.keyId = process.env.TRITON_KEY_ID || process.env.SDC_KEY_ID;
/*
* If missing any of the required vars, then there is no env profile.
* If none of the above envvars are defined, then there is no env profile.
*/
if (!envProfile.account || !envProfile.url || !envProfile.keyId) {
if (!envProfile.account && !envProfile.user && !envProfile.url &&
!envProfile.keyId)
{
return null;
}
if (process.env.TRITON_TLS_INSECURE) {
envProfile.insecure = common.boolFromString(
process.env.TRITON_TLS_INSECURE);
} else if (process.env.SDC_TLS_INSECURE) {
envProfile.insecure = common.boolFromString(
process.env.SDC_TLS_INSECURE);
} else if (process.env.SDC_TESTING) { // deprecated
envProfile.insecure = common.boolFromString(process.env.SDC_TESTING);
}
validateProfile(envProfile, 'environment variables');
return envProfile;
}
function _profileFromPath(profilePath, name, profileOverrides) {
function _profileFromPath(profilePath, name) {
if (! fs.existsSync(profilePath)) {
throw new errors.ConfigError('no such profile: ' + name);
}
@ -327,9 +305,6 @@ function _profileFromPath(profilePath, name, profileOverrides) {
}
profile.name = name;
for (var attr in profileOverrides) {
profile[attr] = profileOverrides[attr];
}
validateProfile(profile, profilePath);
return profile;
@ -339,13 +314,12 @@ function _profileFromPath(profilePath, name, profileOverrides) {
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);
var envProfile = _loadEnvProfile();
if (!envProfile) {
throw new errors.ConfigError('could not load "env" profile '
+ '(missing SC_* environment variables)');
+ '(missing TRITON_*, or SDC_*, environment variables)');
}
return envProfile;
} else if (!opts.configDir) {
@ -355,18 +329,17 @@ function loadProfile(opts) {
var profilePath = path.resolve(
common.tildeSync(opts.configDir), 'profiles.d',
opts.name + '.json');
return _profileFromPath(profilePath, opts.name, opts.profileOverrides);
return _profileFromPath(profilePath, opts.name);
}
}
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);
var envProfile = _loadEnvProfile();
if (envProfile) {
profiles.push(envProfile);
}
@ -433,7 +406,7 @@ function saveProfileSync(opts) {
mkdirp.sync(path.dirname(profilePath));
}
fs.writeFileSync(profilePath, JSON.stringify(toSave, null, 4), 'utf8');
console.log('Saved profile "%s".', name);
console.log('Saved profile "%s"', name);
}
@ -441,7 +414,7 @@ function saveProfileSync(opts) {
module.exports = {
loadConfig: loadConfig,
setConfigVars: setConfigVars,
setConfigVar: setConfigVar,
validateProfile: validateProfile,
loadProfile: loadProfile,

View File

@ -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);
}

View File

@ -21,35 +21,28 @@ function do_get(subcmd, opts, args, callback) {
return;
}
var tritonapi = this.top.tritonapi;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
callback(setupErr);
this.top.tritonapi.cloudapi.getAccount(function (err, account) {
if (err) {
callback(err);
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();
});
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();
});
}
@ -65,15 +58,13 @@ do_get.options = [
help: 'JSON output.'
}
];
do_get.synopses = ['{{name}} {{cmd}}'];
do_get.help = [
'Show account information',
'',
'{{usage}}',
'',
'{{options}}'
].join('\n');
do_get.help = (
'Show account information\n'
+ '\n'
+ 'Usage:\n'
+ ' {{name}} get\n'
+ '\n'
+ '{{options}}'
);
module.exports = do_get;

View File

@ -30,9 +30,7 @@ function do_update(subcmd, opts, args, callback) {
var log = this.log;
var tritonapi = this.top.tritonapi;
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
vasync.pipeline({arg: {}, funcs: [
function gatherDataArgs(ctx, next) {
if (opts.file) {
next();
@ -72,8 +70,12 @@ function do_update(subcmd, opts, args, callback) {
next();
return;
}
common.readStdin(function gotStdin(stdin) {
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
try {
ctx.data = JSON.parse(stdin);
} catch (err) {
@ -88,18 +90,36 @@ function do_update(subcmd, opts, args, callback) {
},
function validateIt(ctx, next) {
try {
common.validateObject(ctx.data, UPDATE_ACCOUNT_FIELDS);
} catch (e) {
next(e);
return;
}
var keys = Object.keys(ctx.data);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = ctx.data[key];
var type = UPDATE_ACCOUNT_FIELDS[key];
if (!type) {
next(new errors.UsageError(format('unknown or ' +
'unupdateable field: %s (updateable fields are: %s)',
key,
Object.keys(UPDATE_ACCOUNT_FIELDS).sort().join(', '))));
return;
}
if (typeof (value) !== type) {
next(new errors.UsageError(format('field "%s" must be ' +
'of type "%s", but got a value of type "%s"', key,
type, typeof (value))));
return;
}
}
next();
},
function updateAway(ctx, next) {
var keys = Object.keys(ctx.data);
if (keys.length === 0) {
console.log('No fields given for account update');
next();
return;
}
tritonapi.cloudapi.updateAccount(ctx.data, function (err) {
if (err) {
@ -128,17 +148,13 @@ do_update.options = [
'JSON from stdin.'
}
];
do_update.synopses = [
'{{name}} {{cmd}} [FIELD=VALUE ...]',
'{{name}} {{cmd}} -f JSON-FILE'
];
do_update.help = [
/* BEGIN JSSTYLED */
'Update account information',
'',
'{{usage}}',
'Usage:',
' {{name}} update [FIELD=VALUE ...]',
' {{name}} update -f JSON-FILE',
'',
'{{options}}',

View File

@ -23,7 +23,7 @@ function AccountCLI(top) {
name: top.name + ' account',
/* BEGIN JSSTYLED */
desc: [
'Get and update your Spearhead account.'
'Get and update your Triton account.'
].join('\n'),
/* END JSSTYLED */
helpOpts: {

View File

@ -11,9 +11,7 @@
*/
var http = require('http');
var vasync = require('vasync');
var common = require('./common');
var errors = require('./errors');
@ -59,39 +57,32 @@ function do_cloudapi(subcmd, opts, args, callback) {
reqOpts.data = JSON.parse(opts.data);
} catch (parseErr) {
callback(new errors.TritonError(parseErr,
'given DATA is not valid JSON: ' + parseErr.message));
'given <data> is not valid JSON: ' + parseErr.message));
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();
});
this.tritonapi.cloudapi._request(reqOpts, function (err, req, res, body) {
if (err) {
callback(err);
return;
}
]}, callback);
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));
callback();
});
}
do_cloudapi.options = [
@ -103,13 +94,13 @@ do_cloudapi.options = [
{
names: ['method', 'X'],
type: 'string',
helpArg: 'METHOD',
helpArg: '<method>',
help: 'Request method to use. Default is "GET".'
},
{
names: ['header', 'H'],
type: 'arrayOfString',
helpArg: 'HEADER',
helpArg: '<header>',
help: 'Headers to send with request.'
},
{
@ -120,25 +111,19 @@ do_cloudapi.options = [
{
names: ['data', 'd'],
type: 'string',
helpArg: 'DATA',
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}} cloudapi [-X <method>] [-H <header=value>] \\\n'
+ ' [-d <data>] <endpoint>\n'
+ '\n'
+ '{{options}}'
);
do_cloudapi.hidden = true;

View File

@ -66,16 +66,12 @@ do_completion.options = [
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}}',
'Installation:',
' {{name}} completion > /usr/local/etc/bash_completion.d/{{name}} # Mac',
' sudo {{name}} completion > /etc/bash_completion.d/{{name}} # Linux',
'',
'Alternative installation:',
' {{name}} completion > ~/.{{name}}.completion # or to whatever path',
' {{name}} completion > ~/.{{name}}.completion',
' echo "source ~/.{{name}}.completion" >> ~/.bashrc',
'',
'{{options}}'

View File

@ -5,13 +5,11 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright 2016 Joyent, Inc.
*
* `triton create ...` bwcompat shortcut for `triton instance create ...`.
*/
var targ = require('./do_instance/do_create');
function do_create(subcmd, opts, args, callback) {
this.handlerFromSubcmd('instance').dispatch({
subcmd: 'create',
@ -20,10 +18,7 @@ function do_create(subcmd, opts, args, callback) {
}, callback);
}
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;
do_create.help = 'A shortcut for "triton instance create".';
do_create.options = require('./do_instance/do_create').options;
module.exports = do_create;

View File

@ -31,43 +31,36 @@ function do_datacenters(subcmd, opts, args, callback) {
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);
this.tritonapi.cloudapi.listDatacenters(function (err, datacenters) {
if (err) {
callback(err);
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]
});
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();
});
});
tabula(dcs, {
skipHeader: opts.H,
columns: columns,
sort: sort,
dottedLookup: true
});
}
callback();
});
}
@ -82,16 +75,15 @@ do_datacenters.options = [
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');
do_datacenters.help = (
'Show datacenters in this cloud.\n'
+ 'A "cloud" is a set of related datacenters that share account\n'
+ 'information.\n'
+ '\n'
+ 'Usage:\n'
+ ' {{name}} datacenters\n'
+ '\n'
+ '{{options}}'
);
module.exports = do_datacenters;

View File

@ -5,13 +5,11 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright 2016 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',
@ -20,11 +18,8 @@ function do_delete(subcmd, opts, args, callback) {
}, 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.help = 'A shortcut for "triton instance delete".';
do_delete.aliases = ['rm'];
do_delete.options = require('./do_instance/do_delete').options;
module.exports = do_delete;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2016 Joyent Inc.
* Copyright (c) 2015 Joyent Inc.
*
* `triton env ...`
*/
@ -7,7 +7,6 @@
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');
@ -19,7 +18,6 @@ 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;
@ -29,30 +27,16 @@ function do_env(subcmd, opts, args, cb) {
}
var profileName = args[0] || this.tritonapi.profile.name;
var allClientTypes = ['triton', 'docker', 'smartdc'];
var allClientTypes = ['smartdc', 'triton'];
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 (opts.triton) {
clientTypes.push('triton');
}
if (clientTypes.length === 0) {
explicit = false;
clientTypes = allClientTypes;
} else {
explicit = true;
}
try {
@ -68,89 +52,27 @@ function do_env(subcmd, opts, args, cb) {
}
var p = console.log;
var shortOpts = '';
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)));
}
shortOpts += 't';
p('export TRITON_PROFILE="%s"', 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);
});
shortOpts += 's';
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('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');
}
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:
@ -160,11 +82,8 @@ function do_env(subcmd, opts, args, cb) {
});
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();
p('# eval "$(triton env%s %s)"',
(shortOpts ? ' -'+shortOpts : ''), profile.name);
}
do_env.options = [
@ -174,64 +93,35 @@ do_env.options = [
help: 'Show this help.'
},
{
group: ''
names: ['smartdc', 's'],
type: 'bool',
help: 'Emit environment for node-smartdc (i.e. the "SDC_*" variables).'
},
{
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.'
'"TRITON_PROFILE" variable).'
}
];
do_env.synopses = ['{{name}} {{cmd}} [PROFILE]'];
// TODO: support env for docker usage.
do_env.help = [
/* BEGIN JSSTYLED */
'Emit shell commands to setup environment.',
'Emit shell environment commands to setup clients for a particular CLI profile.',
'',
'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.',
'and node-triton itself. By default this emits the environment for all',
'supported tools. Use options to be specific.',
'',
'{{usage}}',
'Usage:',
' {{name}} env [PROFILE]',
'',
'{{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'
'{{options}}'
/* END JSSTYLED */
].join('\n');
do_env.completionArgtypes = ['tritonprofile', 'none'];
do_env.hidden = true;
module.exports = do_env;

View File

@ -28,7 +28,7 @@ function do_create(subcmd, opts, args, cb) {
return;
}
if (args.length === 0) {
cb(new errors.UsageError('missing FWRULE argument'));
cb(new errors.UsageError('missing <fwrule> argument'));
return;
} else if (args.length > 1) {
cb(new errors.UsageError('incorrect number of arguments'));
@ -45,22 +45,15 @@ function do_create(subcmd, opts, args, cb) {
createOpts.description = opts.description;
}
var tritonapi = this.top.tritonapi;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
this.top.tritonapi.cloudapi.createFirewallRule(createOpts,
function (err, fwrule) {
if (err) {
cb(err);
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();
});
console.log('Created firewall rule %s%s', fwrule.id,
(!fwrule.enabled ? ' (disabled)' : ''));
cb();
});
}
@ -80,37 +73,23 @@ do_create.options = [
names: ['disabled', 'd'],
type: 'bool',
help: 'Disable the created firewall rule. By default a created '
+ 'firewall rule is enabled. Use "spearhead fwrule enable" '
+ 'firewall rule is enabled. Use "triton fwrule enable" '
+ 'to enable it later.'
},
{
names: ['description', 'D'],
type: 'string',
helpArg: 'DESC',
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}}',
'Usage:',
' {{name}} create [<options>] <fwrule>',
'',
'{{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 */
'{{options}}'
].join('\n');
do_create.helpOpts = {

View File

@ -27,15 +27,14 @@ function do_delete(subcmd, opts, args, cb) {
}
if (args.length < 1) {
cb(new errors.UsageError('missing FWRULE argument(s)'));
cb(new errors.UsageError('missing <fwrule-id> argument(s)'));
return;
}
var tritonapi = this.top.tritonapi;
var cli = this.top;
var ruleIds = args;
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
vasync.pipeline({funcs: [
function confirm(_, next) {
if (opts.force) {
return next();
@ -62,8 +61,8 @@ function do_delete(subcmd, opts, args, cb) {
vasync.forEachParallel({
inputs: ruleIds,
func: function deleteOne(id, nextId) {
tritonapi.deleteFirewallRule({
id: id
cli.tritonapi.deleteFirewallRule({
id: id
}, function (err) {
if (err) {
nextId(err);
@ -97,20 +96,15 @@ do_delete.options = [
help: 'Skip confirmation of delete.'
}
];
do_delete.synopses = ['{{name}} {{cmd}} FWRULE [FWRULE ...]'];
do_delete.help = [
'Remove a firewall rule.',
'',
'{{usage}}',
'Usage:',
' {{name}} delete [<options>] <fwrule-id> [<fwrule-id>...]',
'',
'{{options}}',
'Where FWRULE is a firewall rule id (full UUID) or short id.'
'{{options}}'
].join('\n');
do_delete.aliases = ['rm'];
do_delete.completionArgtypes = ['tritonfwrule'];
module.exports = do_delete;

View File

@ -26,31 +26,26 @@ function do_disable(subcmd, opts, args, cb) {
}
if (args.length === 0) {
cb(new errors.UsageError('missing FWRULE argument(s)'));
cb(new errors.UsageError('Missing <fwrule-id> 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;
}
var cli = this.top;
console.log('Disabled firewall rule %s', id);
nextId();
});
}
}, cb);
});
vasync.forEachParallel({
inputs: args,
func: function disableOne(id, nextId) {
cli.tritonapi.disableFirewallRule({ id: id }, function (err) {
if (err) {
nextId(err);
return;
}
console.log('Disabled firewall rule %s', id);
nextId();
});
}
}, cb);
}
@ -61,18 +56,13 @@ do_disable.options = [
help: 'Show this help.'
}
];
do_disable.synopses = ['{{name}} {{cmd}} FWRULE [FWRULE ...]'];
do_disable.help = [
'Disable a specific firewall rule.',
'',
'{{usage}}',
'Usage:',
' {{name}} disable <fwrule-id> [<fwrule-id>...]',
'',
'{{options}}',
'Where FWRULE is a firewall rule id (full UUID) or short id.'
'{{options}}'
].join('\n');
do_disable.completionArgtypes = ['tritonfwrule'];
module.exports = do_disable;

View File

@ -26,31 +26,26 @@ function do_enable(subcmd, opts, args, cb) {
}
if (args.length === 0) {
cb(new errors.UsageError('missing FWRULE argument(s)'));
cb(new errors.UsageError('Missing <fwrule-id> 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;
}
var cli = this.top;
console.log('Enabled firewall rule %s', id);
nextId();
});
}
}, cb);
});
vasync.forEachParallel({
inputs: args,
func: function enableOne(id, nextId) {
cli.tritonapi.enableFirewallRule({ id: id }, function (err) {
if (err) {
nextId(err);
return;
}
console.log('Enabled firewall rule %s', id);
nextId();
});
}
}, cb);
}
@ -61,18 +56,13 @@ do_enable.options = [
help: 'Show this help.'
}
];
do_enable.synopses = ['{{name}} {{cmd}} FWRULE [FWRULE ...]'];
do_enable.help = [
'Enable a specific firewall rule.',
'',
'{{usage}}',
'Usage:',
' {{name}} enable <fwrule-id> [<fwrule-id>...]',
'',
'{{options}}',
'Where FWRULE is a firewall rule id (full UUID) or short id.'
'{{options}}'
].join('\n');
do_enable.completionArgtypes = ['tritonfwrule'];
module.exports = do_enable;

View File

@ -25,7 +25,7 @@ function do_get(subcmd, opts, args, cb) {
}
if (args.length === 0) {
cb(new errors.UsageError('missing FWRULE argument'));
cb(new errors.UsageError('missing <fwrule-id> argument'));
return;
} else if (args.length > 1) {
cb(new errors.UsageError('incorrect number of arguments'));
@ -33,27 +33,21 @@ function do_get(subcmd, opts, args, cb) {
}
var id = args[0];
var tritonapi = this.top.tritonapi;
var cli = this.top;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
cli.tritonapi.getFirewallRule(id, function onRule(err, fwrule) {
if (err) {
cb(err);
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));
}
if (opts.json) {
console.log(JSON.stringify(fwrule));
} else {
console.log(JSON.stringify(fwrule, null, 4));
}
cb();
});
cb();
});
}
@ -70,18 +64,13 @@ do_get.options = [
help: 'JSON stream output.'
}
];
do_get.synopses = ['{{name}} {{cmd}} FWRULE'];
do_get.help = [
'Show a specific firewall rule.',
'',
'{{usage}}',
'Usage:',
' {{name}} get <fwrule-id>',
'',
'{{options}}',
'Where FWRULE is a firewall rule id (full UUID) or short id.'
'{{options}}'
].join('\n');
do_get.completionArgtypes = ['tritonfwrule', 'none'];
module.exports = do_get;

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2018 Joyent, Inc.
* Copyright 2016 Joyent, Inc.
*
* `triton fwrule instances ...`
*/
@ -30,7 +30,7 @@ function do_instances(subcmd, opts, args, cb) {
}
if (args.length === 0) {
cb(new errors.UsageError('missing FWRULE argument'));
cb(new errors.UsageError('missing <fwrule-id> argument'));
return;
} else if (args.length > 1) {
cb(new errors.UsageError('incorrect number of arguments'));
@ -54,84 +54,73 @@ function do_instances(subcmd, opts, args, cb) {
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},
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);
}
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();
// 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.docker) flags.push('D');
if (inst.firewall_enabled) flags.push('F');
if (inst.brand === 'kvm') flags.push('K');
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();
});
}
@ -146,26 +135,22 @@ do_instances.options = [
sortDefault: SORT_DEFAULT
}));
do_instances.synopses = ['{{name}} {{cmd}} [OPTIONS] FWRULE'];
do_instances.help = [
/* BEGIN JSSTYLED */
'List instances to which a firewall rule applies',
'List instances a firewall rule is applied to.',
'',
'{{usage}}',
'Usage:',
' {{name}} instances [<options>] <fwrule-id>',
'',
'{{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".'
@ -174,6 +159,4 @@ do_instances.help = [
do_instances.aliases = ['insts'];
do_instances.completionArgtypes = ['tritonfwrule', 'none'];
module.exports = do_instances;

View File

@ -35,46 +35,40 @@ function do_list(subcmd, opts, args, cb) {
return;
}
var tritonapi = this.top.tritonapi;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
var cli = this.top;
cli.tritonapi.cloudapi.listFirewallRules({}, function onRules(err, rules) {
if (err) {
cb(err);
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;
}
if (opts.json) {
common.jsonStream(rules);
} else {
var columns = COLUMNS_DEFAULT;
columns = columns.toLowerCase().split(',');
var sort = opts.s.toLowerCase().split(',');
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
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();
});
cb();
});
}
@ -90,12 +84,11 @@ do_list.options = [
sortDefault: SORT_DEFAULT
}));
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS]'];
do_list.help = [
'Show all firewall rules.',
'',
'{{usage}}',
'Usage:',
' {{name}} list [<options>]',
'',
'{{options}}'
].join('\n');

View File

@ -31,15 +31,13 @@ function do_update(subcmd, opts, args, cb) {
var tritonapi = this.top.tritonapi;
if (args.length === 0) {
cb(new errors.UsageError('missing FWRULE argument'));
cb(new errors.UsageError('missing <fwrule-id> argument'));
return;
}
var id = args.shift();
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
vasync.pipeline({arg: {}, funcs: [
function gatherDataArgs(ctx, next) {
if (opts.file) {
next();
@ -84,7 +82,14 @@ function do_update(subcmd, opts, args, cb) {
return;
}
common.readStdin(function gotStdin(stdin) {
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
try {
ctx.data = JSON.parse(stdin);
} catch (err) {
@ -100,13 +105,33 @@ function do_update(subcmd, opts, args, cb) {
},
function validateIt(ctx, next) {
try {
common.validateObject(ctx.data, UPDATE_FWRULE_FIELDS);
} catch (e) {
next(e);
var keys = Object.keys(ctx.data);
if (keys.length === 0) {
console.log('No fields given for firewall rule update');
next();
return;
}
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = ctx.data[key];
var type = UPDATE_FWRULE_FIELDS[key];
if (!type) {
next(new errors.UsageError(format('unknown or ' +
'unupdateable field: %s (updateable fields are: %s)',
key,
Object.keys(UPDATE_FWRULE_FIELDS).sort().join(', '))));
return;
}
if (typeof (value) !== type) {
next(new errors.UsageError(format('field "%s" must be ' +
'of type "%s", but got a value of type "%s"', key,
type, typeof (value))));
return;
}
}
next();
},
@ -139,20 +164,17 @@ do_update.options = [
{
names: ['file', 'f'],
type: 'string',
helpArg: 'JSON-FILE',
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}}',
'Usage:',
' {{name}} update <fwrule-id> [FIELD=VALUE ...]',
' {{name}} update -f <json-file> <fwrule-id>',
'',
'{{options}}',
@ -160,8 +182,7 @@ do_update.help = [
' ' + 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'];

View File

@ -22,7 +22,7 @@ function FirewallRuleCLI(top) {
Cmdln.call(this, {
name: top.name + ' fwrule',
desc: 'List and manage Spearhead firewall rules.',
desc: 'List and manage Triton firewall rules.',
helpSubcmds: [
'help',
'list',

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -26,6 +26,7 @@ var mat = require('../metadataandtags');
// ---- the command
function do_create(subcmd, opts, args, cb) {
var self = this;
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
@ -36,12 +37,11 @@ function do_create(subcmd, opts, args, cb) {
}
var log = this.top.log;
var tritonapi = this.top.tritonapi;
var cloudapi = this.top.tritonapi.cloudapi;
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
vasync.pipeline({arg: {}, funcs: [
function loadTags(ctx, next) {
mat.tagsFromCreateOpts(opts, log, function (err, tags) {
mat.tagsFromOpts(opts, log, function (err, tags) {
if (err) {
next(err);
return;
@ -76,7 +76,7 @@ function do_create(subcmd, opts, args, cb) {
return;
}
tritonapi.getInstance(id, function (err, inst) {
self.top.tritonapi.getInstance(id, function (err, inst) {
if (err) {
next(err);
return;
@ -113,22 +113,20 @@ function do_create(subcmd, opts, args, cb) {
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();
});
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) {
@ -149,8 +147,8 @@ function do_create(subcmd, opts, args, cb) {
ctx.img.state = 'running';
waitCb(null, ctx.img);
}, 5000);
} : tritonapi.cloudapi.waitForImageStates.bind(
tritonapi.cloudapi));
}
: cloudapi.waitForImageStates.bind(cloudapi));
waiter({
id: ctx.img.id,
@ -252,25 +250,20 @@ do_create.options = [
}
];
do_create.synopses = [
'{{name}} {{cmd}} [OPTIONS] INST IMAGE-NAME IMAGE-VERSION'
];
do_create.help = [
do_create.help = (
/* BEGIN JSSTYLED */
'Create a new instance.',
'',
'{{usage}}',
'',
'{{options}}',
'Where "INST" is an instance name, id, or short id.'
'Create a new instance.\n' +
'\n' +
'Usage:\n' +
' {{name}} create [<options>] INSTANCE IMAGE-NAME IMAGE-VERSION\n' +
'\n' +
'{{options}}'
/* END JSSTYLED */
].join('\n');
);
do_create.helpOpts = {
maxHelpCol: 20
};
do_create.completionArgtypes = ['tritoninstance', 'file'];
module.exports = do_create;

View File

@ -26,8 +26,7 @@ function do_delete(subcmd, opts, args, cb) {
}
var ids = args;
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
vasync.pipeline({arg: {}, funcs: [
/*
* Lookup images, if not given UUIDs: we'll need to do it anyway
* for the DeleteImage call(s), and doing so explicitly here allows
@ -126,21 +125,19 @@ function do_delete(subcmd, opts, args, cb) {
});
}
do_delete.synopses = ['{{name}} {{cmd}} [OPTIONS] IMAGE [IMAGE ...]'];
do_delete.help = [
/* BEGIN JSSTYLED */
'Delete one or more images.',
'',
'{{usage}}',
'Usage:',
' {{name}} delete IMAGE [IMAGE...]',
'',
'{{options}}',
'Where "IMAGE" is an image id (a full UUID), an image name (selects the',
'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'],
@ -154,7 +151,5 @@ do_delete.options = [
}
];
do_delete.completionArgtypes = ['tritonimage'];
do_delete.aliases = ['rm'];
module.exports = do_delete;

View File

@ -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;

View File

@ -12,7 +12,6 @@
var format = require('util').format;
var common = require('../common');
var errors = require('../errors');
@ -25,28 +24,17 @@ function do_get(subcmd, opts, args, callback) {
'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;
this.top.tritonapi.getImage(args[0], function onRes(err, img) {
if (err) {
return callback(err);
}
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();
});
if (opts.json) {
console.log(JSON.stringify(img));
} else {
console.log(JSON.stringify(img, null, 4));
}
callback();
});
}
@ -60,36 +48,23 @@ do_get.options = [
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 = [
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.'
'Get an image.\n' +
'\n' +
'Usage:\n' +
' {{name}} get [<options>] ID|NAME\n' +
'\n' +
'{{options}}' +
'\n' +
'If there is 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 */
].join('\n');
do_get.completionArgtypes = ['tritonimage', 'none'];
);
module.exports = do_get;

View File

@ -5,15 +5,13 @@
*/
/*
* Copyright 2018 Joyent, Inc.
* Copyright 2016 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');
@ -56,11 +54,7 @@ function do_list(subcmd, opts, args, callback) {
var listOpts;
try {
listOpts = common.objFromKeyValueArgs(args, {
disableDotted: true,
validKeys: validFilters,
disableTypeConversions: true
});
listOpts = common.kvToObj(args, validFilters);
} catch (e) {
callback(e);
return;
@ -69,92 +63,43 @@ function do_list(subcmd, opts, args, callback) {
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();
this.top.tritonapi.listImages(listOpts, function onRes(err, imgs, res) {
if (err) {
return callback(err);
}
]}, callback);
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');
img.flags = flags.length ? flags.join('') : undefined;
}
tabula(imgs, {
skipHeader: opts.H,
columns: columns,
sort: sort
});
}
callback();
});
}
do_list.options = [
@ -177,16 +122,16 @@ do_list.options = [
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.',
'See <https://apidocs.joyent.com/docker>.',
'',
'{{usage}}',
'Usage:',
' {{name}} list [<options>] [<filters>]',
'',
'{{options}}',
'Filters:',
@ -200,8 +145,6 @@ do_list.help = [
' 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',

View File

@ -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;

View File

@ -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;

View File

@ -12,7 +12,6 @@
var vasync = require('vasync');
var common = require('../common');
var distractions = require('../distractions');
var errors = require('../errors');
@ -35,8 +34,7 @@ function do_wait(subcmd, opts, args, cb) {
var done = 0;
var imgFromId = {};
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
vasync.pipeline({funcs: [
function getImgs(_, next) {
vasync.forEachParallel({
inputs: ids,
@ -114,19 +112,17 @@ function do_wait(subcmd, opts, args, cb) {
});
}
do_wait.synopses = ['{{name}} {{cmd}} [-s STATES] IMAGE [IMAGE ...]'];
do_wait.help = [
'Wait for images to change to a particular state.',
'',
'{{usage}}',
'Usage:',
' {{name}} wait [-s STATES] IMAGE [IMAGE ...]',
'',
'{{options}}',
'Where "states" is a comma-separated list of target instance states,',
'by default "active,failed". In other words, "spearhead img wait foo0" will',
'by default "active,failed". In other words, "triton img wait foo0" will',
'wait for image "foo0" to complete creation.'
].join('\n');
do_wait.options = [
{
names: ['help', 'h'],
@ -143,6 +139,4 @@ do_wait.options = [
}
];
do_wait.completionArgtypes = ['tritonimage'];
module.exports = do_wait;

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright (c) 2018, Joyent, Inc.
* Copyright 2015 Joyent, Inc.
*
* `triton image ...`
*/
@ -23,7 +23,7 @@ function ImageCLI(top) {
name: top.name + ' image',
/* BEGIN JSSTYLED */
desc: [
'List and manage Spearhead images.'
'List and manage Triton images.'
].join('\n'),
/* END JSSTYLED */
helpOpts: {
@ -33,13 +33,8 @@ function ImageCLI(top) {
'help',
'list',
'get',
'clone',
'copy',
'create',
'delete',
'export',
'share',
'unshare',
'wait'
]
});
@ -53,13 +48,8 @@ ImageCLI.prototype.init = function init(opts, args, cb) {
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');

View File

@ -5,13 +5,11 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright 2016 Joyent, Inc.
*
* `triton images ...` bwcompat shortcut for `triton image list ...`.
*/
var targ = require('./do_image/do_list');
function do_images(subcmd, opts, args, callback) {
this.handlerFromSubcmd('image').dispatch({
subcmd: 'list',
@ -20,12 +18,9 @@ function do_images(subcmd, opts, args, callback) {
}, callback);
}
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;
do_images.help = 'A shortcut for "triton image list".';
do_images.aliases = ['imgs'];
do_images.hidden = true;
do_images.options = require('./do_image/do_list').options;
module.exports = do_images;

View File

@ -28,76 +28,69 @@ 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.tritonapi.cloudapi.getAccount(cb.bind('account')); i++;
this.tritonapi.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;
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
var states = {};
var disk = 0;
var memory = 0;
out.machines.forEach(function (machine) {
var state = machine.state;
states[state] = states[state] || 0;
states[state]++;
memory += machine.memory;
disk += machine.disk;
});
disk *= 1000 * 1000;
memory *= 1000 * 1000;
// parse machine states and accounting
var states = {};
var disk = 0;
var memory = 0;
out.machines.forEach(function (machine) {
var state = machine.state;
states[state] = states[state] || 0;
states[state]++;
memory += machine.memory;
disk += machine.disk;
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]);
});
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();
}
});
callback();
}
}
do_info.options = [
@ -112,15 +105,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}} info\n'
+ '\n'
+ '{{options}}'
);
module.exports = do_info;

View File

@ -27,15 +27,14 @@ var sortDefault = 'id,time';
function do_audit(subcmd, opts, args, cb) {
var self = this;
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;
} else if (args.length !== 1) {
//XXX Support multiple machines.
return cb(new Error('incorrect args: ' + args));
}
var columns = columnsDefault;
@ -50,29 +49,23 @@ function do_audit(subcmd, opts, args, cb) {
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;
if (common.isUUID(arg)) {
uuid = arg;
go1();
} else {
self.top.tritonapi.getInstance(arg, function (err, inst) {
if (err) {
cb(err);
return;
}
uuid = inst.id;
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) {
self.top.tritonapi.cloudapi.machineAudit(uuid, function (err, audit) {
if (err) {
cb(err);
return;
@ -109,16 +102,13 @@ do_audit.options = [
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'];
do_audit.help = (
'List instance actions.\n'
+ '\n'
+ 'Usage:\n'
+ ' {{name}} audit <alias|id>\n'
+ '\n'
+ '{{options}}'
);
module.exports = do_audit;

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2019 Joyent, Inc.
* Copyright 2015 Joyent, Inc.
*
* `triton instance create ...`
*/
@ -19,160 +19,21 @@ 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) {
var self = this;
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();
},
var cloudapi = this.top.tritonapi.cloudapi;
vasync.pipeline({arg: {}, funcs: [
function loadMetadata(ctx, next) {
mat.metadataFromOpts(opts, log, function (err, metadata) {
if (err) {
@ -203,10 +64,9 @@ function do_create(subcmd, opts, args, cb) {
function getImg(ctx, next) {
var _opts = {
name: args[0],
excludeInactive: true,
useCache: true
};
tritonapi.getImage(_opts, function (err, img) {
self.top.tritonapi.getImage(_opts, function (err, img) {
if (err) {
return next(err);
}
@ -227,7 +87,7 @@ function do_create(subcmd, opts, args, cb) {
return;
}
tritonapi.getPackage(id, function (err, pkg) {
self.top.tritonapi.getPackage(id, function (err, pkg) {
if (err) {
return next(err);
}
@ -245,7 +105,7 @@ function do_create(subcmd, opts, args, cb) {
vasync.forEachPipeline({
inputs: opts.network,
func: function getOneNetwork(name, nextNet) {
tritonapi.getNetwork(name, function (err, net) {
self.top.tritonapi.getNetwork(name, function (err, net) {
if (err) {
nextNet(err);
} else {
@ -256,30 +116,14 @@ function do_create(subcmd, opts, args, cb) {
}
}, 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
'package': ctx.pkg && ctx.pkg.id,
networks: ctx.nets && ctx.nets.map(
function (net) { return net.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];
@ -290,16 +134,11 @@ function do_create(subcmd, opts, args, cb) {
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;
}
}
@ -317,7 +156,7 @@ function do_create(subcmd, opts, args, cb) {
return next();
}
tritonapi.cloudapi.createMachine(createOpts, function (err, inst) {
cloudapi.createMachine(createOpts, function (err, inst) {
if (err) {
next(new errors.TritonError(err,
'error creating instance'));
@ -353,8 +192,8 @@ function do_create(subcmd, opts, args, cb) {
ctx.inst.state = 'running';
waitCb(null, ctx.inst);
}, 5000);
} : tritonapi.cloudapi.waitForMachineStates.bind(
tritonapi.cloudapi));
}
: cloudapi.waitForMachineStates.bind(cloudapi));
waiter({
id: ctx.inst.id,
@ -401,80 +240,12 @@ do_create.options = [
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: ''
'<https://docs.joyent.com/public-cloud/network/firewall>'
},
{
names: ['metadata', 'm'],
@ -503,10 +274,25 @@ do_create.options = [
'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.'
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: ['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.'
},
// XXX locality: near, far
{
group: 'Other options'
@ -529,25 +315,20 @@ do_create.options = [
}
];
do_create.synopses = ['{{name}} {{cmd}} [OPTIONS] IMAGE PACKAGE'];
do_create.help = [
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`).'
'Create a new instance.\n' +
'\n' +
'Usage:\n' +
' {{name}} create [<options>] IMAGE PACKAGE\n' +
'\n' +
'{{options}}'
/* END JSSTYLED */
].join('\n');
);
do_create.helpOpts = {
maxHelpCol: 16
maxHelpCol: 18
};
do_create.completionArgtypes = ['tritonimage', 'tritonpackage', 'none'];
module.exports = do_create;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -5,22 +5,99 @@
*/
/*
* `triton instance fwrules ...` shortcut for
* `triton instance fwrule list ...`.
* Copyright 2016 Joyent, Inc.
*
* `triton instance fwrules ...`
*/
function do_fwrules(subcmd, opts, args, callback) {
this.handlerFromSubcmd('fwrule').dispatch({
subcmd: 'list',
opts: opts,
args: args
}, callback);
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_fwrules(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 id = args[0];
var cli = this.top;
cli.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();
});
}
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;
do_fwrules.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
}
].concat(common.getCliTableOptions({
includeLong: true,
sortDefault: SORT_DEFAULT
}));
do_fwrules.help = [
'Show firewall rules applied to an instance.',
'',
'Usage:',
' {{name}} fwrules [<options>] <inst>',
'',
'{{options}}'
].join('\n');
module.exports = do_fwrules;

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2016 Joyent, Inc.
* Copyright 2015 Joyent, Inc.
*
* `triton instance get ...`
*/
@ -19,25 +19,17 @@ function do_get(subcmd, opts, args, cb) {
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;
this.top.tritonapi.getInstance(args[0], function (err, inst) {
if (err) {
return cb(err);
}
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);
});
if (opts.json) {
console.log(JSON.stringify(inst));
} else {
console.log(JSON.stringify(inst, null, 4));
}
cb();
});
}
@ -47,37 +39,24 @@ do_get.options = [
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 = [
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.'
'Get an instance.\n'
+ '\n'
+ 'Usage:\n'
+ ' {{name}} get <alias|id>\n'
+ '\n'
+ '{{options}}'
+ '\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 */
].join('\n');
do_get.completionArgtypes = ['tritoninstance', 'none'];
);
module.exports = do_get;

View File

@ -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;

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright (c) 2018, Joyent, Inc.
* Copyright 2016 Joyent, Inc.
*
* `triton instance list ...`
*/
@ -22,16 +22,13 @@ var common = require('../common');
* 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',
'type',
'brand', // Added in CloudAPI 8.0.0
'name',
'image',
'state',
// jsl:ignore
/^tag\./,
// jsl:end
'type'
'memory',
'docker' // Added in CloudAPI 8.0.0
];
// columns default without -o
@ -50,7 +47,6 @@ function do_list(subcmd, opts, args, callback) {
this.do_help('help', {}, [subcmd], callback);
return;
}
var log = self.top.log;
var columns = columnsDefault;
if (opts.o) {
@ -64,11 +60,7 @@ function do_list(subcmd, opts, args, callback) {
var listOpts;
try {
listOpts = common.objFromKeyValueArgs(args, {
disableDotted: true,
validKeys: validFilters,
disableTypeConversions: true
});
listOpts = common.kvToObj(args, validFilters);
} catch (e) {
callback(e);
return;
@ -77,99 +69,75 @@ function do_list(subcmd, opts, args, callback) {
listOpts.credentials = true;
}
var imgs = [];
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,
vasync.parallel({funcs: [
function getTheImages(next) {
self.top.tritonapi.listImages({useCache: true},
function (err, _imgs) {
if (err) {
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);
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);
}
// 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();
// 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.docker) flags.push('D');
if (inst.firewall_enabled) flags.push('F');
if (inst.brand === 'kvm') flags.push('K');
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();
});
}
@ -191,37 +159,30 @@ do_list.options = [
sortDefault: sortDefault
}));
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] [FILTERS...]'];
do_list.help = [
/* BEGIN JSSTYLED */
'List instances.',
'',
'{{usage}}',
'Usage:',
' {{name}} list [<filters>...]',
'',
'{{options}}',
'Filters:',
' FIELD=VALUE Equality filter. Supported fields: brand, image,',
' memory, name, state, tag.TAGNAME, and type.',
' FIELD=VALUE Equality filter. Supported fields: type, brand, name,',
' image, state, and memory',
' FIELD=true|false Boolean filter. Supported fields: docker (added in',
' CloudAPI 8.0.0).',
' 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'
' "shortid".'
/* END JSSTYLED */
].join('\n');

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -5,101 +5,13 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright 2016 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'];
var gen_do_ACTION = require('./gen_do_ACTION');
var do_reboot = gen_do_ACTION({action: 'reboot'});
module.exports = do_reboot;

View File

@ -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;

View File

@ -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;

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2018 Joyent, Inc.
* Copyright 2016 Joyent, Inc.
*
* `triton snapshot create ...`
*/
@ -15,6 +15,7 @@ var format = require('util').format;
var vasync = require('vasync');
var common = require('../../common');
var distractions = require('../../distractions');
var errors = require('../../errors');
@ -47,8 +48,7 @@ function do_create(subcmd, opts, args, cb) {
createOpts.name = opts.name;
}
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
vasync.pipeline({arg: {}, funcs: [
function createSnapshot(ctx, next) {
ctx.start = Date.now();
@ -72,6 +72,13 @@ function do_create(subcmd, opts, args, cb) {
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);
}
var cloudapi = cli.tritonapi.cloudapi;
var waiter = cloudapi.waitForSnapshotStates.bind(cloudapi);
@ -80,6 +87,10 @@ function do_create(subcmd, opts, args, cb) {
name: ctx.name,
states: ['created', 'failed']
}, function (err, snap) {
if (distraction) {
distraction.destroy();
}
if (err) {
return next(err);
}
@ -115,27 +126,24 @@ do_create.options = [
{
names: ['name', 'n'],
type: 'string',
helpArg: 'SNAPNAME',
helpArg: '<snapname>',
help: 'An optional name for a snapshot.'
},
{
names: ['wait', 'w'],
type: 'bool',
help: 'Wait for the creation to complete.'
type: 'arrayOfBool',
help: 'Wait for the creation to complete. Use multiple times for a ' +
'spinner.'
}
];
do_create.synopses = ['{{name}} {{cmd}} [OPTIONS] INST'];
do_create.help = [
'Create a snapshot of an instance.',
'',
'{{usage}}',
'Usage:',
' {{name}} create [<options>] <inst>',
'',
'{{options}}',
'Snapshots do not work for instances of type "bhyve" or "kvm".'
'Snapshot do not work for instances of type "kvm".'
].join('\n');
do_create.completionArgtypes = ['tritoninstance', 'none'];
module.exports = do_create;

View File

@ -15,6 +15,7 @@ var format = require('util').format;
var vasync = require('vasync');
var common = require('../../common');
var distractions = require('../../distractions');
var errors = require('../../errors');
@ -27,7 +28,7 @@ function do_delete(subcmd, opts, args, cb) {
}
if (args.length < 2) {
cb(new errors.UsageError('missing INST and SNAPNAME argument(s)'));
cb(new errors.UsageError('missing <inst> and <snapname> argument(s)'));
return;
}
@ -36,6 +37,13 @@ function do_delete(subcmd, opts, args, cb) {
var names = args.slice(1, args.length);
function wait(instId, name, startTime, 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);
}
var cloudapi = cli.tritonapi.cloudapi;
var waiter = cloudapi.waitForSnapshotStates.bind(cloudapi);
@ -44,10 +52,12 @@ function do_delete(subcmd, opts, args, cb) {
name: name,
states: ['deleted']
}, function (err, snap) {
if (distraction) {
distraction.destroy();
}
if (err) {
return next(err);
}
if (snap.state === 'deleted') {
var duration = Date.now() - startTime;
var durStr = common.humanDurationFromMs(duration);
@ -61,8 +71,7 @@ function do_delete(subcmd, opts, args, cb) {
});
}
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
vasync.pipeline({funcs: [
function confirm(_, next) {
if (opts.force) {
return next();
@ -136,25 +145,20 @@ do_delete.options = [
},
{
names: ['wait', 'w'],
type: 'bool',
help: 'Wait for the deletion to complete.'
type: 'arrayOfBool',
help: 'Wait for the deletion to complete. Use multiple times for a ' +
'spinner.'
}
];
do_delete.synopses = ['{{name}} {{cmd}} [OPTIONS] INST SNAPNAME [SNAPNAME...]'];
do_delete.help = [
'Remove a snapshot from an instance.',
'',
'{{usage}}',
'Usage:',
' {{name}} delete [<options>] <inst> <snapname> [<snapname>...]',
'',
'{{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;

View File

@ -25,7 +25,7 @@ function do_get(subcmd, opts, args, cb) {
}
if (args.length < 2) {
cb(new errors.UsageError('missing INST and/or SNAPNAME arguments'));
cb(new errors.UsageError('missing <inst> and/or <snapname> arguments'));
return;
} else if (args.length > 2) {
cb(new errors.UsageError('incorrect number of arguments'));
@ -36,31 +36,26 @@ function do_get(subcmd, opts, args, cb) {
var name = args[1];
var cli = this.top;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
cli.tritonapi.getInstanceSnapshot({
id: id,
name: name
}, function onSnapshot(err, snapshot) {
if (err) {
cb(err);
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));
}
if (opts.json) {
console.log(JSON.stringify(snapshot));
} else {
console.log(JSON.stringify(snapshot, null, 4));
}
cb();
});
cb();
});
}
do_get.options = [
{
names: ['help', 'h'],
@ -73,19 +68,13 @@ do_get.options = [
help: 'JSON stream output.'
}
];
do_get.synopses = ['{{name}} {{cmd}} [OPTIONS] INST SNAPNAME'];
do_get.help = [
'Show a specific snapshot of an instance.',
'',
'{{usage}}',
'Usage:',
' {{name}} get <inst> <snapname>',
'',
'{{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;

View File

@ -30,7 +30,7 @@ function do_list(subcmd, opts, args, cb) {
}
if (args.length === 0) {
cb(new errors.UsageError('missing INST argument'));
cb(new errors.UsageError('missing <inst> argument'));
return;
} else if (args.length > 1) {
cb(new errors.UsageError('incorrect number of arguments'));
@ -40,41 +40,35 @@ function do_list(subcmd, opts, args, cb) {
var cli = this.top;
var machineId = args[0];
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
cli.tritonapi.listInstanceSnapshots({
id: machineId
}, function onSnapshots(err, snapshots) {
if (err) {
cb(err);
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;
}
if (opts.json) {
common.jsonStream(snapshots);
} else {
var columns = COLUMNS_DEFAULT;
columns = columns.split(',');
var sort = opts.s.split(',');
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();
});
tabula(snapshots, {
skipHeader: opts.H,
columns: columns,
sort: sort
});
}
cb();
});
}
@ -90,18 +84,15 @@ do_list.options = [
sortDefault: SORT_DEFAULT
}));
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] INST'];
do_list.help = [
'Show all of an instance\'s snapshots.',
'',
'{{usage}}',
'Usage:',
' {{name}} list [<options>] <inst>',
'',
'{{options}}'
].join('\n');
do_list.completionArgtypes = ['tritoninstance', 'none'];
do_list.aliases = ['ls'];
module.exports = do_list;

View File

@ -22,7 +22,7 @@ function SnapshotCLI(top) {
Cmdln.call(this, {
name: top.name + ' snapshot',
desc: 'List, get, create and delete Spearhead instance snapshots.',
desc: 'List, get, create and delete Triton instance snapshots.',
helpSubcmds: [
'help',
'create',
@ -31,7 +31,7 @@ function SnapshotCLI(top) {
'delete'
],
helpBody: 'Instances can be rolled back to a snapshot using\n' +
'`spearhead instance start --snapshot=SNAPNAME`.'
'`triton instance start --snapshot=<snapname>`.'
});
}
util.inherits(SnapshotCLI, Cmdln);

View File

@ -5,14 +5,12 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright 2016 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',
@ -21,12 +19,8 @@ function do_snapshots(subcmd, opts, args, callback) {
}, 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.help = 'A shortcut for "triton instance snapshot list".';
do_snapshots.options = require('./do_snapshot/do_list').options;
do_snapshots.hidden = true;
module.exports = do_snapshots;

View File

@ -5,304 +5,56 @@
*/
/*
* Copyright (c) 2018, Joyent, Inc.
* Copyright 2016 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) {
var self = this;
if (opts.help) {
this.do_help('help', {}, [subcmd], callback);
return;
} else if (args.length === 0) {
callback(new errors.UsageError('missing INST arg'));
callback(new Error('invalid args: ' + args));
return;
}
var id = args.shift();
var user;
var overrideUser = false;
var user = 'root';
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(). */
});
this.top.tritonapi.getInstance(id, function (err, inst) {
if (err) {
callback(err);
return;
}
]}, callback);
var ip = inst.primaryIp;
if (!ip) {
callback(new Error('primaryIp not found for instance'));
return;
}
args = ['-l', user].concat(ip).concat(args);
self.top.log.info({args: args}, 'forking ssh');
var child = spawn('ssh', args, {stdio: 'inherit'});
child.on('close', function (code) {
process.exit(code);
});
});
}
do_ssh.options = [
@ -310,61 +62,17 @@ 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.help = (
'SSH to the primary IP of an instance\n'
+ '\n'
+ 'Usage:\n'
+ ' {{name}} ssh <alias|id> [arguments]\n'
+ '\n'
+ '{{options}}'
);
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;

View File

@ -12,7 +12,6 @@
var vasync = require('vasync');
var common = require('../../common');
var errors = require('../../errors');
@ -30,47 +29,41 @@ function do_delete(subcmd, opts, args, cb) {
}
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);
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);
}
});
// 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 = [
@ -98,25 +91,22 @@ do_delete.options = [
}
];
do_delete.synopses = [
'{{name}} {{cmd}} INST [NAME ...]',
'{{name}} {{cmd}} --all INST # delete all tags'
];
do_delete.help = [
/* BEGIN JSSTYLED */
'Delete one or more instance tags.',
'',
'{{usage}}',
'Usage:',
' {{name}} delete <inst> [<name> ...]',
' {{name}} delete --all <inst> # delete all tags',
'',
'{{options}}',
'Where INST is an instance id, name, or shortid and NAME is a tag name.',
'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.'
/* END JSSTYLED */
].join('\n');
do_delete.aliases = ['rm'];
do_delete.completionArgtypes = ['tritoninstance', 'none'];
module.exports = do_delete;

View File

@ -10,7 +10,6 @@
* `triton instance tag get ...`
*/
var common = require('../../common');
var errors = require('../../errors');
@ -24,26 +23,20 @@ function do_get(subcmd, opts, args, cb) {
return;
}
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
self.top.tritonapi.getInstanceTag({
id: args[0],
tag: args[1]
}, function (err, value) {
if (err) {
cb(err);
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();
});
if (opts.json) {
console.log(JSON.stringify(value));
} else {
console.log(value);
}
cb();
});
}
@ -60,18 +53,16 @@ do_get.options = [
}
];
do_get.synopses = ['{{name}} {{cmd}} INST NAME'];
do_get.help = [
/* BEGIN JSSTYLED */
'Get an instance tag.',
'',
'{{usage}}',
'Usage:',
' {{name}} get <inst> <name>',
'',
'{{options}}',
'Where INST is an instance id, name, or shortid and NAME is a tag name.'
'Where <inst> is an instance id, name, or shortid and <name> is a tag name.'
/* END JSSTYLED */
].join('\n');
// TODO: When have 'tritoninstancetag' completion, add that in.
do_get.completionArgtypes = ['tritoninstance', 'none'];
module.exports = do_get;

View File

@ -10,7 +10,6 @@
* `triton instance tag list ...`
*/
var common = require('../../common');
var errors = require('../../errors');
function do_list(subcmd, opts, args, cb) {
@ -23,24 +22,17 @@ function do_list(subcmd, opts, args, cb) {
return;
}
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
self.top.tritonapi.listInstanceTags({id: args[0]}, function (err, tags) {
if (err) {
cb(err);
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();
});
if (opts.json) {
console.log(JSON.stringify(tags));
} else {
console.log(JSON.stringify(tags, null, 4));
}
cb();
});
}
@ -57,22 +49,21 @@ do_list.options = [
}
];
do_list.synopses = ['{{name}} {{cmd}} INST'];
do_list.help = [
/* BEGIN JSSTYLED */
'List instance tags.',
'',
'{{usage}}',
'Usage:',
' {{name}} list <inst>',
'',
'{{options}}',
'Where INST is an instance id, name, or shortid.',
'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.'
/* END JSSTYLED */
].join('\n');
do_list.aliases = ['ls'];
do_list.completionArgtypes = ['tritoninstance', 'none'];
module.exports = do_list;

View File

@ -12,7 +12,6 @@
var vasync = require('vasync');
var common = require('../../common');
var errors = require('../../errors');
var mat = require('../../metadataandtags');
@ -28,8 +27,7 @@ function do_replace_all(subcmd, opts, args, cb) {
}
var log = self.log;
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
vasync.pipeline({arg: {}, funcs: [
function gatherTags(ctx, next) {
mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) {
if (err) {
@ -110,19 +108,17 @@ do_replace_all.options = [
}
];
do_replace_all.synopses = [
'{{name}} {{cmd}} INST [NAME=VALUE ...]',
'{{name}} {{cmd}} INST -f FILE # tags from file'
];
do_replace_all.help = [
/* BEGIN JSSTYLED */
'Replace all tags on the given instance.',
'',
'{{usage}}',
'Usage:',
' {{name}} replace-all <inst> [<name>=<value> ...]',
' {{name}} replace-all <inst> -f <file> # tags from file',
'',
'{{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 ',
'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',
@ -130,8 +126,7 @@ do_replace_all.help = [
'',
'Changing instance tags is asynchronous. Use "--wait" to not return until',
'the changes are completed.'
/* END JSSTYLED */
].join('\n');
do_replace_all.completionArgtypes = ['tritoninstance', 'file'];
module.exports = do_replace_all;

View File

@ -12,7 +12,6 @@
var vasync = require('vasync');
var common = require('../../common');
var errors = require('../../errors');
var mat = require('../../metadataandtags');
@ -28,8 +27,7 @@ function do_set(subcmd, opts, args, cb) {
}
var log = self.log;
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
vasync.pipeline({arg: {}, funcs: [
function gatherTags(ctx, next) {
mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) {
if (err) {
@ -111,20 +109,17 @@ do_set.options = [
}
];
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}}',
'Usage:',
' {{name}} set <inst> [<name>=<value> ...]',
' {{name}} set <inst> -f <file> # tags from file',
'',
'{{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 ',
'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',
@ -135,6 +130,4 @@ do_set.help = [
/* END JSSTYLED */
].join('\n');
do_set.completionArgtypes = ['tritoninstance', 'file'];
module.exports = do_set;

View File

@ -22,7 +22,7 @@ function InstanceTagCLI(parent) {
name: parent.name + ' tag',
/* BEGIN JSSTYLED */
desc: [
'List, get, set and delete tags on Spearhead instances.'
'List, get, set and delete tags on Triton instances.'
].join('\n'),
/* END JSSTYLED */
helpOpts: {

View File

@ -5,13 +5,11 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright 2016 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',
@ -20,11 +18,8 @@ function do_tags(subcmd, opts, args, callback) {
}, 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.help = 'A shortcut for "triton instance tag list".';
do_tags.options = require('./do_tag/do_list').options;
do_tags.hidden = true;
module.exports = do_tags;

View File

@ -12,7 +12,6 @@
var vasync = require('vasync');
var common = require('../common');
var distractions = require('../distractions');
var errors = require('../errors');
@ -22,7 +21,7 @@ function do_wait(subcmd, opts, args, cb) {
if (opts.help) {
return this.do_help('help', {}, [subcmd], cb);
} else if (args.length < 1) {
return cb(new errors.UsageError('missing INST arg(s)'));
return cb(new errors.UsageError('missing INSTANCE arg(s)'));
}
var ids = args;
var states = [];
@ -35,8 +34,7 @@ function do_wait(subcmd, opts, args, cb) {
var done = 0;
var instFromId = {};
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
vasync.pipeline({funcs: [
function getInsts(_, next) {
vasync.forEachParallel({
inputs: ids,
@ -112,19 +110,16 @@ function do_wait(subcmd, opts, args, cb) {
});
}
do_wait.synopses = ['{{name}} {{cmd}} [-s STATES] INST [INST ...]'];
do_wait.help = [
/* BEGIN JSSTYLED */
'Wait on instances changing state.',
'',
'{{usage}}',
'Usage:',
' {{name}} wait [-s STATES] INSTANCE [INSTANCE ...]',
'',
'{{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 */
'Where "states" is a comma-separated list of target instance states,',
'by default "running,failed". In other words, "triton inst wait foo0" will',
'wait for instance "foo0" to complete provisioning.'
].join('\n');
do_wait.options = [
{
@ -142,6 +137,4 @@ do_wait.options = [
}
];
do_wait.completionArgtypes = ['tritoninstance'];
module.exports = do_wait;

View File

@ -5,11 +5,12 @@
*/
/*
* Copyright 2016 Joyent, Inc.
* Copyright 2015 Joyent, Inc.
*
* Shared support for:
* `triton instance start ...`
* `triton instance stop ...`
* `triton instance reboot ...`
* `triton instance delete ...`
*/
@ -17,7 +18,9 @@ var assert = require('assert-plus');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
var f = require('util').format;
function perror(err) {
@ -31,26 +34,24 @@ function gen_do_ACTION(opts) {
assert.optionalArrayOfString(opts.aliases, 'opts.aliases');
var action = opts.action;
assert.ok(['start', 'stop', 'delete'].indexOf(action) >= 0,
assert.ok(['start', 'stop', 'reboot', '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.'
f('%s one or more instances.', common.capitalize(action)),
f(''),
f('Usage:'),
f(' {{name}} %s <alias|id> ...', action),
f(''),
f('{{options}}')
].join('\n');
do_ACTION.options = [
{
@ -65,14 +66,11 @@ function gen_do_ACTION(opts) {
}
];
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'
help: 'Name of snapshot to start machine with.'
});
}
@ -82,6 +80,8 @@ function gen_do_ACTION(opts) {
function _doTheAction(action, subcmd, opts, args, callback) {
var self = this;
var now = Date.now();
var command, state;
switch (action) {
case 'start':
@ -93,6 +93,10 @@ function _doTheAction(action, subcmd, opts, args, callback) {
command = 'stopMachine';
state = 'stopped';
break;
case 'reboot':
command = 'rebootMachine';
state = 'running';
break;
case 'delete':
command = 'deleteMachine';
state = 'deleted';
@ -106,21 +110,10 @@ function _doTheAction(action, subcmd, opts, args, callback) {
this.do_help('help', {}, [subcmd], callback);
return;
} else if (args.length < 1) {
callback(new errors.UsageError('missing INST arg(s)'));
callback(new Error('invalid args: ' + args));
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;
@ -194,7 +187,7 @@ function _doOnEachInstance(self, action, command, state, instances,
});
}
},
inputs: instances
inputs: args
}, function (err, results) {
var e = err ? (new Error('command failure')) : null;
callback(e);

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright 2018 Joyent, Inc.
* Copyright 2015 Joyent, Inc.
*
* `triton instance ...`
*/
@ -22,7 +22,7 @@ function InstanceCLI(top) {
name: top.name + ' instance',
/* BEGIN JSSTYLED */
desc: [
'List and manage Spearhead instances.'
'List and manage Triton instances.'
].join('\n'),
/* END JSSTYLED */
helpOpts: {
@ -34,25 +34,15 @@ function InstanceCLI(top) {
'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',
'fwrules',
'snapshot',
'tag'
]
@ -69,28 +59,15 @@ 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_fwrules = require('./do_fwrules');
InstanceCLI.prototype.do_snapshot = require('./do_snapshot');
InstanceCLI.prototype.do_snapshots = require('./do_snapshots');
InstanceCLI.prototype.do_tag = require('./do_tag');

View File

@ -5,13 +5,11 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright 2016 Joyent, Inc.
*
* `triton instances ...` bwcompat shortcut for `triton instance list ...`.
*/
var targ = require('./do_instance/do_list');
function do_instances(subcmd, opts, args, callback) {
this.handlerFromSubcmd('instance').dispatch({
subcmd: 'list',
@ -20,11 +18,8 @@ function do_instances(subcmd, opts, args, callback) {
}, 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.help = 'A shortcut for "triton instance list".';
do_instances.aliases = ['insts', 'ls'];
do_instances.options = require('./do_instance/do_list').options;
module.exports = do_instances;

View File

@ -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;

View File

@ -40,14 +40,19 @@ function do_add(subcmd, opts, args, cb) {
var filePath = args[0];
var cli = this.top;
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
vasync.pipeline({arg: {}, funcs: [
function gatherDataStdin(ctx, next) {
if (filePath !== '-') {
return next();
}
common.readStdin(function gotStdin(stdin) {
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
ctx.data = stdin;
ctx.from = '<stdin>';
next();
@ -120,13 +125,11 @@ do_add.options = [
help: 'An optional name for an added key.'
}
];
do_add.synopses = ['{{name}} {{cmd}} [OPTIONS] FILE'];
do_add.help = [
'Add an SSH key to an account.',
'',
'{{usage}}',
'Usage:',
' {{name}} add [<options>] FILE',
'',
'{{options}}',
'Where "FILE" must be a file path to an SSH public key, ',

View File

@ -35,8 +35,7 @@ function do_delete(subcmd, opts, args, cb) {
var cli = this.top;
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
vasync.pipeline({funcs: [
function confirm(_, next) {
if (opts.yes) {
return next();
@ -105,13 +104,11 @@ do_delete.options = [
help: 'Answer yes to confirmation to delete.'
}
];
do_delete.synopses = ['{{name}} {{cmd}} [OPTIONS] KEY [KEY ...]'];
do_delete.help = [
'Remove an SSH key from an account.',
'',
'{{usage}}',
'Usage:',
' {{name}} delete [<options>] KEY [KEY...]',
'',
'{{options}}',
'Where "KEY" is an SSH key "name" or "fingerprint".'
@ -119,6 +116,4 @@ do_delete.help = [
do_delete.aliases = ['rm'];
do_delete.completionArgtypes = ['tritonkey'];
module.exports = do_delete;

View File

@ -35,28 +35,22 @@ function do_get(subcmd, opts, args, cb) {
var id = args[0];
var cli = this.top;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
cli.tritonapi.cloudapi.getKey({
// Currently `cloudapi.getUserKey` isn't picky about the `name` being
// passed in as the `opts.fingerprint` arg.
fingerprint: id
}, function onKey(err, key) {
if (err) {
cb(err);
return;
}
cli.tritonapi.cloudapi.getKey({
// Currently `cloudapi.getUserKey` isn't picky about the
// `name` being passed in as the `opts.fingerprint` arg.
fingerprint: id
}, function onKey(err, key) {
if (err) {
cb(err);
return;
}
if (opts.json) {
console.log(JSON.stringify(key));
} else {
console.log(common.chomp(key.key));
}
cb();
});
if (opts.json) {
console.log(JSON.stringify(key));
} else {
console.log(common.chomp(key.key));
}
cb();
});
}
@ -73,18 +67,14 @@ do_get.options = [
help: 'JSON stream output.'
}
];
do_get.synopses = ['{{name}} {{cmd}} KEY'];
do_get.help = [
'Show a specific SSH key in an account.',
'',
'{{usage}}',
'Usage:',
' {{name}} get KEY',
'',
'{{options}}',
'Where "KEY" is an SSH key "name" or "fingerprint".'
].join('\n');
do_get.completionArgtypes = ['tritonkey', 'none'];
module.exports = do_get;

View File

@ -37,43 +37,37 @@ function do_list(subcmd, opts, args, cb) {
var cli = this.top;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
cli.tritonapi.cloudapi.listKeys({}, function onKeys(err, keys) {
if (err) {
cb(err);
return;
}
cli.tritonapi.cloudapi.listKeys({}, function onKeys(err, keys) {
if (err) {
cb(err);
return;
if (opts.json) {
common.jsonStream(keys);
} else if (opts.authorized_keys) {
keys.forEach(function (key) {
console.log(common.chomp(key.key));
});
} else {
var columns = COLUMNS_DEFAULT;
if (opts.o) {
columns = opts.o;
} else if (opts.long) {
columns = COLUMNS_LONG;
}
if (opts.json) {
common.jsonStream(keys);
} else if (opts.authorized_keys) {
keys.forEach(function (key) {
console.log(common.chomp(key.key));
});
} else {
var columns = COLUMNS_DEFAULT;
columns = columns.split(',');
var sort = opts.s.split(',');
if (opts.o) {
columns = opts.o;
} else if (opts.long) {
columns = COLUMNS_LONG;
}
columns = columns.split(',');
var sort = opts.s.split(',');
tabula(keys, {
skipHeader: false,
columns: columns,
sort: sort
});
}
cb();
});
tabula(keys, {
skipHeader: false,
columns: columns,
sort: sort
});
}
cb();
});
}
@ -96,11 +90,11 @@ do_list.options = [
}
]);
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS]'];
do_list.help = [
'Show all of an account\'s SSH keys.',
'',
'{{usage}}',
'Usage:',
' {{name}} list [<options>]',
'',
'{{options}}'
].join('\n');

View File

@ -5,13 +5,11 @@
*/
/*
* Copyright 2017 Joyent, Inc.
* Copyright 2016 Joyent, Inc.
*
* `triton keys ...` bwcompat shortcut for `triton key list ...`.
*/
var targ = require('./do_key/do_list');
function do_keys(subcmd, opts, args, callback) {
this.handlerFromSubcmd('key').dispatch({
subcmd: 'list',
@ -20,11 +18,8 @@ function do_keys(subcmd, opts, args, callback) {
}, callback);
}
do_keys.help = 'A shortcut for "spearhead key list".\n' + targ.help;
do_keys.synopses = targ.synopses;
do_keys.options = targ.options;
do_keys.completionArgtypes = targ.completionArgtypes;
do_keys.help = 'A shortcut for "triton key list".';
do_keys.hidden = true;
do_keys.options = require('./do_key/do_list').options;
module.exports = do_keys;

View File

@ -1,273 +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 network create ...`
*/
var assert = require('assert-plus');
var format = require('util').format;
var jsprim = require('jsprim');
var common = require('../common');
var errors = require('../errors');
function do_create(subcmd, opts, args, cb) {
assert.optionalString(opts.name, 'opts.name');
assert.optionalString(opts.subnet, 'opts.subnet');
assert.optionalString(opts.start_ip, 'opts.start_ip');
assert.optionalString(opts.end_ip, 'opts.end_ip');
assert.optionalString(opts.description, 'opts.description');
assert.optionalString(opts.gateway, 'opts.gateway');
assert.optionalArrayOfString(opts.resolver, 'opts.resolver');
assert.optionalArrayOfString(opts.route, 'opts.route');
assert.optionalBool(opts.no_nat, 'opts.no_nat');
assert.optionalBool(opts.json, 'opts.json');
assert.optionalBool(opts.help, 'opts.help');
assert.func(cb, 'cb');
var i;
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length === 0) {
cb(new errors.UsageError('missing VLAN argument'));
return;
} else if (args.length > 1) {
cb(new errors.UsageError('incorrect number of arguments'));
return;
}
var vlanId = jsprim.parseInteger(args[0], { allowSign: false });
if (typeof (vlanId) !== 'number') {
cb(new errors.UsageError('VLAN must be an integer'));
return;
}
if (!opts.subnet) {
cb(new errors.UsageError('must specify --subnet (-s) option'));
return;
}
if (!opts.name) {
cb(new errors.UsageError('must specify --name (-n) option'));
return;
}
if (!opts.start_ip) {
cb(new errors.UsageError('must specify --start-ip (-S) option'));
return;
}
if (!opts.end_ip) {
cb(new errors.UsageError('must specify --end-ip (-E) option'));
return;
}
var createOpts = {
vlan_id: vlanId,
name: opts.name,
subnet: opts.subnet,
provision_start_ip: opts.start_ip,
provision_end_ip: opts.end_ip,
resolvers: [],
routes: {}
};
if (opts.resolver) {
for (i = 0; i < opts.resolver.length; i++) {
if (createOpts.resolvers.indexOf(opts.resolver[i]) === -1) {
createOpts.resolvers.push(opts.resolver[i]);
}
}
}
if (opts.route) {
for (i = 0; i < opts.route.length; i++) {
var m = opts.route[i].match(new RegExp('^([^=]+)=([^=]+)$'));
if (m === null) {
cb(new errors.UsageError('invalid route: ' + opts.route[i]));
return;
}
createOpts.routes[m[1]] = m[2];
}
}
if (opts.no_nat) {
createOpts.internet_nat = false;
}
if (opts.gateway) {
createOpts.gateway = opts.gateway;
} else {
if (!opts.no_nat) {
cb(new errors.UsageError('without a --gateway (-g), you must ' +
'specify --no-nat (-x)'));
return;
}
}
if (opts.description) {
createOpts.description = opts.description;
}
var cli = this.top;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
var cloudapi = cli.tritonapi.cloudapi;
cloudapi.createFabricNetwork(createOpts, function onCreate(err, net) {
if (err) {
cb(err);
return;
}
if (opts.json) {
console.log(JSON.stringify(net));
} else {
console.log('Created network %s (%s)', net.name, net.id);
}
cb();
});
});
}
do_create.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
group: 'Create options'
},
{
names: ['name', 'n'],
type: 'string',
helpArg: 'NAME',
help: 'Name of the NETWORK.'
},
{
names: ['description', 'D'],
type: 'string',
helpArg: 'DESC',
help: 'Description of the NETWORK.'
},
{
group: ''
},
{
names: ['subnet', 's'],
type: 'string',
helpArg: 'SUBNET',
help: 'A CIDR string describing the NETWORK.'
},
{
names: ['start-ip', 'S', 'start_ip'],
type: 'string',
helpArg: 'START_IP',
help: 'First assignable IP address on NETWORK.'
},
{
names: ['end-ip', 'E', 'end_ip'],
type: 'string',
helpArg: 'END_IP',
help: 'Last assignable IP address on NETWORK.'
},
{
group: ''
},
{
names: ['gateway', 'g'],
type: 'string',
helpArg: 'IP',
help: 'Default gateway IP address.'
},
{
names: ['resolver', 'r'],
type: 'arrayOfString',
helpArg: 'RESOLVER',
help: 'DNS resolver IP address. Specify multiple -r options for ' +
'multiple resolvers.'
},
{
names: ['route', 'R'],
type: 'arrayOfString',
helpArg: 'SUBNET=IP',
help: [ 'Static route for network. Each route must include the',
'subnet (IP address with CIDR prefix length) and the router',
'address. Specify multiple -R options for multiple static',
'routes.' ].join(' ')
},
{
group: ''
},
{
names: ['no-nat', 'x', 'no_nat'],
type: 'bool',
helpArg: 'NO_NAT',
help: 'Disable creation of an Internet NAT zone on GATEWAY.'
},
{
group: 'Other options'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
do_create.synopses = ['{{name}} {{cmd}} [OPTIONS] VLAN'];
do_create.help = [
'Create a network on a VLAN.',
'',
'{{usage}}',
'',
'{{options}}',
'',
'Examples:',
' Create the "accounting" network on VLAN 1000:',
' triton network create -n accounting --subnet 192.168.0.0/24 \\',
' --start-ip 192.168.0.1 --end-ip 192.168.0.254 --no-nat \\',
' 1000',
'',
' Create the "eng" network on VLAN 1001 with a pair of static routes:',
' triton network create -n eng -s 192.168.1.0/24 \\',
' -S 192.168.1.1 -E 192.168.1.249 --no-nat \\',
' --route 10.1.1.0/24=192.168.1.50 \\',
' --route 10.1.2.0/24=192.168.1.100 \\',
' 1001',
'',
' Create the "ops" network on VLAN 1002 with DNS resolvers and NAT:',
' triton network create -n ops -s 192.168.2.0/24 \\',
' -S 192.168.2.10 -E 192.168.2.249 \\',
' --resolver 8.8.8.8 --resolver 8.4.4.4 \\',
' --gateway 192.168.2.1 \\',
' 1002'
].join('\n');
do_create.helpOpts = {
helpCol: 16
};
module.exports = do_create;

View File

@ -1,85 +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 network 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 NETWORK argument(s)'));
return;
}
var cli = this.top;
var networks = args;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
vasync.forEachParallel({
inputs: networks,
func: function deleteOne(id, next) {
cli.tritonapi.deleteFabricNetwork({ id: id },
function onDelete(err) {
if (err) {
next(err);
return;
}
console.log('Deleted network %s', id);
next();
});
}
}, cb);
});
}
do_delete.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
}
];
do_delete.synopses = ['{{name}} {{cmd}} NETWORK [NETWORK ...]'];
do_delete.help = [
'Remove a fabric network.',
'',
'{{usage}}',
'',
'{{options}}',
'Where NETWORK is a network id (full UUID), name, or short id.'
].join('\n');
do_delete.aliases = ['rm'];
do_delete.completionArgtypes = ['tritonnetwork'];
module.exports = do_delete;

View File

@ -25,25 +25,17 @@ function do_get(subcmd, opts, args, cb) {
'incorrect number of args (%d)', args.length)));
}
var tritonapi = this.top.tritonapi;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
this.top.tritonapi.getNetwork(args[0], function (err, net) {
if (err) {
return cb(err);
}
tritonapi.getNetwork(args[0], function (err, net) {
if (err) {
return cb(err);
}
if (opts.json) {
console.log(JSON.stringify(net));
} else {
console.log(JSON.stringify(net, null, 4));
}
cb();
});
if (opts.json) {
console.log(JSON.stringify(net));
} else {
console.log(JSON.stringify(net, null, 4));
}
cb();
});
}
@ -59,18 +51,13 @@ do_get.options = [
help: 'JSON output.'
}
];
do_get.synopses = ['{{name}} {{cmd}} NETWORK'];
do_get.help = [
'Show a network.',
'',
'{{usage}}',
'',
'{{options}}',
'Where NETWORK is a network id (full UUID), name, or short id.'
].join('\n');
do_get.completionArgtypes = ['tritonnetwork', 'none'];
do_get.help = (
'Show a network.\n'
+ '\n'
+ 'Usage:\n'
+ ' {{name}} get <id|name>\n'
+ '\n'
+ '{{options}}'
);
module.exports = do_get;

View File

@ -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 (c) 2018, Joyent, Inc.
*
* `triton network get-default ...`
*/
var assert = require('assert-plus');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
function do_get_default(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 cli = this.top;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
cli.tritonapi.cloudapi.getConfig({}, function getConf(err, conf) {
if (err) {
cb(err);
return;
}
var defaultNetwork = conf.default_network;
if (!defaultNetwork) {
cb(new Error('account has no default network configured'));
return;
}
cli.handlerFromSubcmd('network').dispatch({
subcmd: 'get',
opts: opts,
args: [defaultNetwork]
}, cb);
});
});
}
do_get_default.options = require('./do_get').options;
do_get_default.synopses = ['{{name}} {{cmd}}'];
do_get_default.help = [
'Get default network.',
'',
'{{usage}}',
'',
'{{options}}'
].join('\n');
do_get_default.completionArgtypes = ['tritonnetwork'];
module.exports = do_get_default;

View File

@ -1,81 +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 network ip get ...`
*/
var format = require('util').format;
var common = require('../../common');
var errors = require('../../errors');
function do_get(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length !== 2) {
return cb(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) {
cb(setupErr);
return;
}
var getIpOpts = {
id: args[0],
ip: args[1]
};
tritonapi.getNetworkIp(getIpOpts, function (err, ip, res) {
if (err) {
return cb(err);
}
if (opts.json) {
console.log(JSON.stringify(ip));
} else {
console.log(JSON.stringify(ip, null, 4));
}
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}} NETWORK IP'];
do_get.help = [
'Show a network ip.',
'',
'{{usage}}',
'',
'{{options}}',
'Where NETWORK is a network id, and IP is the ip address you want to get.'
].join('\n');
do_get.completionArgtypes = ['tritonnetwork', 'tritonnetworkip', 'none'];
module.exports = do_get;

View File

@ -1,129 +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 network ip list ...`
*/
var format = require('util').format;
var assert = require('assert-plus');
var tabula = require('tabula');
var vasync = require('vasync');
var common = require('../../common');
var errors = require('../../errors');
// columns default without -o
var columnsDefault = 'ip,managed,reserved,owner_uuid,belongs_to_uuid';
// sort default with -s
var sortDefault = 'ip';
function do_list(subcmd, opts, args, callback) {
var self = this;
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 columns = columnsDefault;
if (opts.o) {
columns = opts.o;
}
columns = columns.split(',');
var sort = opts.s.split(',').map(function mapSort(field) {
var so = {};
field = field.trim();
assert.ok(field, 'non-empty field');
if (field[0] === '-') {
so.field = field.slice(1);
so.reverse = true;
} else {
so.field = field;
}
switch (so.field) {
case 'ip':
so.keyFunc = common.ipv4ToLong;
break;
default:
break;
}
return so;
});
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function listIps(arg, next) {
self.top.tritonapi.listNetworkIps(args[0],
function (err, ips, res) {
if (err) {
next(err);
return;
}
arg.ips = ips;
next();
});
},
function doneIps(arg, next) {
var ips = arg.ips;
if (opts.json) {
common.jsonStream(ips);
} else {
tabula(ips, {
skipHeader: opts.H,
columns: columns,
sort: sort
});
}
next();
}
]}, callback);
}
do_list.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
}
].concat(common.getCliTableOptions({
includeLong: true,
sortDefault: sortDefault
}));
do_list.synopses = ['{{name}} {{cmd}} NETWORK'];
do_list.help = [
'List network IPs.',
'',
'{{usage}}',
'',
'{{options}}',
'Fields (most are self explanatory, the significant ones are as follows):',
' managed IP is manged by Spearhead and cannot be modified directly.',
'',
'See https://apidocs.joyent.com/cloudapi/#ListNetworkIPs for a full' +
' listing.'
].join('\n');
do_list.aliases = ['ls'];
do_list.completionArgtypes = ['tritonnetwork', 'none'];
module.exports = do_list;

View File

@ -1,193 +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 network ip update ...`
*/
var format = require('util').format;
var fs = require('fs');
var vasync = require('vasync');
var common = require('../../common');
var errors = require('../../errors');
var UPDATE_NETWORK_IP_FIELDS
= require('../../cloudapi2').CloudApi.prototype.UPDATE_NETWORK_IP_FIELDS;
function do_update(subcmd, opts, args, callback) {
if (opts.help) {
this.do_help('help', {}, [subcmd], callback);
return;
} else if (args.length < 2) {
callback(new errors.UsageError(format(
'incorrect number of args (%d)', args.length)));
return;
}
var log = this.log;
var tritonapi = this.top.tritonapi;
var updateIpOpts = {
id: args.shift(),
ip: args.shift()
};
if (args.length === 0 && !opts.file) {
callback(new errors.UsageError(
'FIELD=VALUE arguments or "-f FILE" must be specified'));
return;
}
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_NETWORK_IP_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 network IP 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 network IP update JSON on stdin');
next(new errors.TritonError(format(
'invalid JSON for network IP update on stdin: %s',
err)));
return;
}
next();
});
},
function validateIt(ctx, next) {
try {
common.validateObject(ctx.data, UPDATE_NETWORK_IP_FIELDS);
} catch (e) {
next(e);
return;
}
next();
},
function updateNetworkIP(ctx, next) {
Object.keys(ctx.data).forEach(function (key) {
updateIpOpts[key] = ctx.data[key];
});
tritonapi.updateNetworkIp(updateIpOpts, function (err, body, res) {
if (err) {
next(err);
return;
}
if (opts.json) {
console.log(JSON.stringify(body));
next();
return;
}
console.log('Updated network %s IP %s (fields: %s)',
updateIpOpts.id, updateIpOpts.ip,
Object.keys(ctx.data).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.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON output.'
}
];
do_update.synopses = [
'{{name}} {{cmd}} NETWORK IP [FIELD=VALUE ...]',
'{{name}} {{cmd}} NETWORK IP -f JSON-FILE'
];
do_update.help = [
/* BEGIN JSSTYLED */
'Update a network ip.',
'',
'{{usage}}',
'',
'{{options}}',
'Where NETWORK is a network id, and IP is the ip address you want to update.',
'',
'Updateable fields:',
' ' + Object.keys(UPDATE_NETWORK_IP_FIELDS).sort().map(function (field) {
return field + ' (' + UPDATE_NETWORK_IP_FIELDS[field] + ')';
}).join('\n '),
''
/* END JSSTYLED */
].join('\n');
do_update.completionArgtypes = [
'tritonnetwork',
'tritonnetworkip',
'tritonupdatenetworkipfield'
];
module.exports = do_update;

Some files were not shown because too many files have changed in this diff Show More