Compare commits
190 Commits
snapshot-f
...
master
Author | SHA1 | Date | |
---|---|---|---|
caa6da7821 | |||
|
5212530b37 | ||
|
cc07717008 | ||
|
4921fd2e36 | ||
|
05f1bae869 | ||
66114eb940 | |||
161f879723 | |||
2d92219fef | |||
e980829ca9 | |||
75ec73a31c | |||
|
aea9b2b7b3 | ||
|
aa58982e2a | ||
4684efe22a | |||
|
c86804cfe4 | ||
|
dc5dc12052 | ||
|
264f69dc54 | ||
|
3584c82e05 | ||
|
0bc11c1e33 | ||
|
f100c4dbb5 | ||
ef91454769 | |||
180560dc1e | |||
5438723d06 | |||
|
5734123e75 | ||
|
6015cf2145 | ||
|
6417595ba6 | ||
|
d3d3216a38 | ||
|
91b4c23a52 | ||
|
96a5be8ce7 | ||
|
06812c9cd4 | ||
|
8e6cf27121 | ||
|
002171ea06 | ||
|
bf64899685 | ||
|
39635cd0a2 | ||
be74f307e0 | |||
|
26b97b5bed | ||
|
3f243f8c8f | ||
a3adf3f475 | |||
ed6cf329be | |||
c623e7cad5 | |||
6b4b764b1e | |||
4279ba591e | |||
897af1951b | |||
3bc3dc00d4 | |||
1e1e7a65bd | |||
fd510140ab | |||
6f9cd272b7 | |||
6f4b5deb5e | |||
71f5d9568e | |||
d589694608 | |||
f7f86ae7d5 | |||
1caffe0000 | |||
10e3b9df19 | |||
f9ef30c090 | |||
5d72d087f6 | |||
658e1b951d | |||
a9addf752c | |||
dcd2491410 | |||
23ea5b27df | |||
|
e7c02436df | ||
|
68a889b8b7 | ||
|
225410b4fc | ||
|
ab564177b5 | ||
|
da5f3bade8 | ||
|
12f2794c39 | ||
|
69a4598458 | ||
|
e0b6c7e0bc | ||
|
04420b5088 | ||
|
45ed8883ef | ||
|
8438f446cc | ||
|
057a784dc3 | ||
|
3f24e06cf6 | ||
|
7728c02afe | ||
|
c7bb6b01e1 | ||
|
9d8681a21b | ||
|
c56d0a25da | ||
|
a5ab0f686b | ||
|
c0ad6bb378 | ||
|
546abee318 | ||
|
dc1b6c75b7 | ||
|
87b196ac31 | ||
|
9f17d668c9 | ||
|
8493c52be3 | ||
|
b69832c227 | ||
|
17669f35ea | ||
|
33ff58c3d3 | ||
|
a8f4b57ba3 | ||
|
f27d502d92 | ||
|
a3071585aa | ||
|
d14ac041f4 | ||
|
b66939751d | ||
|
d91e0326c2 | ||
|
2bad27fcbb | ||
|
46ded211f3 | ||
|
dba8915d4f | ||
|
86c689e809 | ||
|
98ba032b5d | ||
|
4ee67a5725 | ||
|
ec9b6cc5aa | ||
|
3f4eff598c | ||
|
3dd59ab109 | ||
|
1503088abd | ||
|
75cb032f3a | ||
|
68e5b68583 | ||
|
c7c91aea83 | ||
|
41b5c506ce | ||
|
d99110553d | ||
|
da80542222 | ||
|
5dab69c002 | ||
|
586b1924a0 | ||
|
16cff8d60a | ||
|
37d475ffd9 | ||
|
afe1972b69 | ||
|
76759e547e | ||
|
04cc61ee1d | ||
|
3a369e4bb8 | ||
|
2453bc12e8 | ||
|
c7140609f0 | ||
|
1486fa48f6 | ||
|
f7bbe5fcff | ||
|
ad7d608011 | ||
|
696439f1ae | ||
|
1e986ea733 | ||
|
e721ebe865 | ||
|
152d12767c | ||
|
a2c83aab56 | ||
|
6c7944f2c2 | ||
|
84338a44b2 | ||
|
e7c321f3db | ||
|
246810e342 | ||
|
0d4850405b | ||
|
bb9412c2bc | ||
|
d6e05e7a26 | ||
|
b8b753a94d | ||
|
21f2d29c86 | ||
|
c7f7f86126 | ||
|
11cd220c3c | ||
|
3cd11c941e | ||
|
48fa8fa03c | ||
|
0932365d46 | ||
|
fabe0a0841 | ||
|
42d5382c7e | ||
|
9d4fb8d45f | ||
|
ad20360306 | ||
|
9b099a91e9 | ||
|
85375517cb | ||
|
8d8d9ed39f | ||
|
0a3c48d2ea | ||
|
a48d7629dd | ||
|
de391bf013 | ||
|
0c605c95c5 | ||
|
3b8215a8bb | ||
|
e068c7abcf | ||
|
aeed6d9192 | ||
|
25e74f5c4e | ||
|
e6819a2fe7 | ||
|
f178d9abce | ||
|
d2cc677e46 | ||
|
8cd4dd80eb | ||
|
5929632e08 | ||
|
461320de8b | ||
|
9a6937eba8 | ||
|
2c9c749efc | ||
|
e1c977e623 | ||
|
1e2b19c0e6 | ||
|
4f778f696e | ||
|
3824623404 | ||
|
c227e15ae3 | ||
|
fb20159053 | ||
|
e8d1fb578b | ||
|
1dc156d87c | ||
|
9ff74ed54b | ||
|
e3b5e6b016 | ||
|
7d635fc81c | ||
|
ba16f0d9ef | ||
|
f50797b4a4 | ||
|
2e09059945 | ||
|
88c52d1610 | ||
|
1d0bafcc5e | ||
|
634350018a | ||
|
a1df6e0fe7 | ||
|
31ef581980 | ||
|
7a7c8c3044 | ||
|
99a083e232 | ||
|
bcf55f0a63 | ||
|
314a05b082 | ||
|
6703363934 | ||
|
6397c8bade | ||
|
c8f6c05bf4 | ||
|
3bc04b4c77 | ||
|
70c7bbd434 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,3 +3,6 @@
|
|||||||
/test/*.json
|
/test/*.json
|
||||||
/npm-debug.log
|
/npm-debug.log
|
||||||
/triton-*.tgz
|
/triton-*.tgz
|
||||||
|
.DS_Store
|
||||||
|
.git
|
||||||
|
*.swp
|
||||||
|
542
CHANGES.md
542
CHANGES.md
@ -1,8 +1,546 @@
|
|||||||
# node-triton changelog
|
# node-triton changelog
|
||||||
|
|
||||||
## 4.5.1 (not yet released)
|
Known issues:
|
||||||
|
|
||||||
(nothing yet)
|
- `triton ssh ...` disables ssh ControlMaster to avoid issue #52.
|
||||||
|
|
||||||
|
## not yet released
|
||||||
|
|
||||||
|
(nothing)
|
||||||
|
|
||||||
|
## 7.0.0
|
||||||
|
|
||||||
|
- [Backward incompatible.] `triton image get NAME|SHORTID` will now *exclude*
|
||||||
|
inactive images by default. Before this change inactive images (e.g. those
|
||||||
|
with a state of "creating" or "unactivated" or "disabled") would be
|
||||||
|
included. Use the new `-a,--all` option to include inactive images. This
|
||||||
|
matches the behavior of `triton image list [-a,--all] ...`.
|
||||||
|
|
||||||
|
- [joyent/node-triton#258] `triton instance create IMAGE ...` will now exclude
|
||||||
|
inactive images when looking for an image with the given name.
|
||||||
|
|
||||||
|
## 6.3.0
|
||||||
|
|
||||||
|
- [joyent/node-triton#259] Added basic support for use of SSH bastion hosts
|
||||||
|
to access zones on private fabrics. If the `tritoncli.ssh.proxy` tag is set
|
||||||
|
on an instance, `triton ssh` will look up the name or UUID of the proxy
|
||||||
|
instance and use `ssh -o ProxyJump` to tunnel the connection to the target.
|
||||||
|
If the `tritoncli.ssh.ip` tag is set on an instance, `triton ssh` will use
|
||||||
|
that IP address instead of the `primaryIp` when making its connection.
|
||||||
|
|
||||||
|
## 6.2.0
|
||||||
|
|
||||||
|
- [joyent/node-triton#255, joyent/node-triton#257] Improved the interface
|
||||||
|
and documentation of `triton network create` and `triton vlan create`. In
|
||||||
|
particular, it is now possible to specify static routes and DNS resolvers.
|
||||||
|
|
||||||
|
## 6.1.2
|
||||||
|
|
||||||
|
- [joyent/node-triton#249] Error when creating or deleting profiles when
|
||||||
|
using node v10.
|
||||||
|
|
||||||
|
## 6.1.1
|
||||||
|
|
||||||
|
- [TRITON-598] Fix error handling for `triton network get-default` when
|
||||||
|
no default network is set on the account.
|
||||||
|
|
||||||
|
## 6.1.0
|
||||||
|
|
||||||
|
- [joyent/node-triton#250] Avoid an error from `triton profile list` if
|
||||||
|
only *some* of the minimal `TRITON_` or `SDC_` envvars are defined.
|
||||||
|
- [TRITON-401] Add `triton network` and `triton vlan` commands, for
|
||||||
|
creating/changing/removing network fabrics and VLANs.
|
||||||
|
- [TRITON-524] Add `triton inst get --credentials ...` option to match
|
||||||
|
`triton inst list --credentials ...` for including generated credentials
|
||||||
|
in instance metadata.
|
||||||
|
- [joyent/node-triton#245] `triton profile` now generates fresh new keys during
|
||||||
|
Docker setup and signs them with an account key, rather than copying (and
|
||||||
|
decrypting) the account key itself. This makes using Docker simpler with keys
|
||||||
|
in an SSH Agent.
|
||||||
|
- [TRITON-53] x-account image clone. A user can make a copy of a shared image
|
||||||
|
using the `triton image clone` command.
|
||||||
|
- [TRITON-53] A shared image (i.e. when the user is on the image.acl) is no
|
||||||
|
longer provisionable by default - you will need to explicitly add the
|
||||||
|
--allow-shared-images cli option when calling `triton create` command to
|
||||||
|
provision from a shared image (or clone the image then provision from the
|
||||||
|
clone).
|
||||||
|
- [TRITON-52] x-DC image copy. A user can copy an image that they own into
|
||||||
|
another datacenter within the same cloud using the `triton image copy` cli
|
||||||
|
command. Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
triton -p us-east-1 image cp my-custom-image us-sw-1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6.0.0
|
||||||
|
|
||||||
|
This release containes some breaking changes with the --affinity flag to
|
||||||
|
`triton instance create`. It also does not work with cloudapi endpoints older
|
||||||
|
than 8.0.0 (mid 2016); for an older cloudapi endpoint, use node-triton 5.9.0.
|
||||||
|
|
||||||
|
- [TRITON-167, TRITON-168] update support for
|
||||||
|
`triton instance create --affinity=...`. It now fully supports regular
|
||||||
|
expressions, tags and globs, and works across a wider variety of situations.
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```
|
||||||
|
# regular expressions
|
||||||
|
triton instance create --affinity='instance!=/^production-db/' ...
|
||||||
|
|
||||||
|
# globs
|
||||||
|
triton instance create --affinity='instance!=production-db*' ...
|
||||||
|
|
||||||
|
# tags
|
||||||
|
triton instance create --affinity='role!=db'
|
||||||
|
```
|
||||||
|
|
||||||
|
See <https://apidocs.joyent.com/cloudapi/#affinity-rules> for more details
|
||||||
|
how affinities work.
|
||||||
|
|
||||||
|
However:
|
||||||
|
- Use of regular expressions requires a cloudapi version of 8.8.0 or later.
|
||||||
|
- 'inst' as a affinity shorthand no longer works. Use 'instance' instead.
|
||||||
|
E.g.: --affinity='instance==db1' instead of --affinity='inst==db1'
|
||||||
|
- The shorthand --affinity=<INST> no longer works. Use
|
||||||
|
--affinity='instance===<INST>' instead.
|
||||||
|
|
||||||
|
## 5.10.0
|
||||||
|
|
||||||
|
- [TRITON-19] add support for deletion protection on instances. An instance with
|
||||||
|
the deletion protection flag set true cannot be destroyed until the flag is
|
||||||
|
set false. It is exposed through
|
||||||
|
`triton instance create --deletion-protection ...`,
|
||||||
|
`triton instance enable-deletion-protection ...`, and
|
||||||
|
`triton instance disable-deletion-protection ...`. This flag is only supported
|
||||||
|
on cloudapi versions 8.7.0 or above.
|
||||||
|
- [TRITON-59] node-triton should support nic operations
|
||||||
|
`triton instance nic get ...`
|
||||||
|
`triton instance nic create ...`
|
||||||
|
`triton instance nic list ...`
|
||||||
|
`triton instance nic delete ...`
|
||||||
|
- [TRITON-42] node-triton should support nics when creating an instance, e.g.
|
||||||
|
`triton instance create --nic <Network Object> IMAGE PACKAGE`
|
||||||
|
|
||||||
|
## 5.9.0
|
||||||
|
|
||||||
|
- [TRITON-190] remove support for `triton instance create --brand=bhyve ...`.
|
||||||
|
The rest of bhyve support will remain, but selection of bhyve brand will
|
||||||
|
happen via images or packages that are bhyve-specific.
|
||||||
|
|
||||||
|
## 5.8.0
|
||||||
|
|
||||||
|
- [TRITON-124] add node-triton support for bhyve. This adds a `triton instance
|
||||||
|
create --brand=bhyve ...` option that can be used for zvol images that support
|
||||||
|
it. Note that bhyve support is alpha in TritonDC -- most datacenters won't yet
|
||||||
|
support this option.
|
||||||
|
|
||||||
|
## 5.7.0
|
||||||
|
|
||||||
|
- [TRITON-116] node-triton image sharing. Adds `triton image share` and
|
||||||
|
`triton image unshare` commands.
|
||||||
|
|
||||||
|
## 5.6.1
|
||||||
|
|
||||||
|
- [PUBAPI-1470] volume objects should expose their creation timestamp in a
|
||||||
|
property named "created" instead of "create_timestamp".
|
||||||
|
|
||||||
|
## 5.6.0
|
||||||
|
|
||||||
|
- [TRITON-30] Add UpdateNetworkIP to node-triton, e.g.
|
||||||
|
`triton network ip update`
|
||||||
|
- [TRITON-24] node-triton ListNetworkIPs has unordered results, e.g.
|
||||||
|
`triton network ip list NETWORK`
|
||||||
|
- [TRITON-88] node-triton "env" doesn't call its callback
|
||||||
|
|
||||||
|
## 5.5.0
|
||||||
|
|
||||||
|
- [PUBAPI-1452] Add ip subcommand to network, e.g.
|
||||||
|
`triton network ip`.
|
||||||
|
|
||||||
|
## 5.4.0
|
||||||
|
|
||||||
|
- [joyent/node-triton#74, TOOLS-1872] Filter instance list by tag, e.g.
|
||||||
|
`triton instance list tag.foo=bar`.
|
||||||
|
|
||||||
|
## 5.3.2
|
||||||
|
|
||||||
|
- [joyent/node-triton#187] DTraceProviderBindings errors on FreeBSD.
|
||||||
|
- [joyent/node-triton#226] added new `triton volume sizes` subcommand.
|
||||||
|
- [PUBAPI-1420] added support for mounting volumes in LX and SmartOS instances.
|
||||||
|
E.g., `triton instance create --volume VOLUME ...`.
|
||||||
|
|
||||||
|
## 5.3.1
|
||||||
|
|
||||||
|
- [joyent/node-triton#222] Fix the matching environment variable for the
|
||||||
|
`triton -r,--role ROLE ...` option to be `TRITON_ROLE` instead of
|
||||||
|
`MANTA_ROLE`.
|
||||||
|
- [joyent/node-triton#201] Fix `triton -r,--role ROLE ...` option for taking up
|
||||||
|
an RBAC role. This was introduced in v4.12.0 and was accidentally broken
|
||||||
|
in v5.0.0.
|
||||||
|
- [joyent/node-triton#217] `triton volume ls -l` should output a `RESOURCE`
|
||||||
|
column.
|
||||||
|
|
||||||
|
## 5.3.0
|
||||||
|
|
||||||
|
- [joyent/node-triton#173], [joyent/node-triton#174] and
|
||||||
|
[joyent/node-triton#175] Add support for creating and managing NFS shared
|
||||||
|
volumes. New `triton volume` commands are available:
|
||||||
|
|
||||||
|
* `triton volume create` to create NFS shared volumes
|
||||||
|
* `triton volume list` to list existing volumes
|
||||||
|
* `triton volume get` to get information about a given volume
|
||||||
|
* `triton volume delete` to delete one or more volumes
|
||||||
|
|
||||||
|
Use `triton volume --help` to get help on all of these commands.
|
||||||
|
|
||||||
|
Note that these commands are hidden for now. They will be made visible by
|
||||||
|
default once the server-side support for volumes is shipped in Triton.
|
||||||
|
|
||||||
|
## 5.2.1
|
||||||
|
|
||||||
|
- [joyent/node-triton#193] Fix possible CLI crash with `triton ssh ...` if the
|
||||||
|
instance's image doesn't have any tags.
|
||||||
|
- [joyent/node-triton#213] commands fail unhelpfully when `cliSetupTritonApi`
|
||||||
|
returns error (this includes e.g. supplying an incorrect key fingerprint,
|
||||||
|
which no longer results in a cryptic stack trace and crash)
|
||||||
|
|
||||||
|
## 5.2.0
|
||||||
|
|
||||||
|
- [joyent/node-triton#197] Create triton image export command
|
||||||
|
|
||||||
|
## 5.1.1
|
||||||
|
|
||||||
|
- [joyent/node-triton#190] Fix `triton profile create|docker-setup` breakage
|
||||||
|
with latest "17.03.\*" versions of `docker` installed.
|
||||||
|
- [joyent/node-triton#148] Fix `triton profile edit ...` to work with an
|
||||||
|
"EDITOR" environment variable with quotes and spaces.
|
||||||
|
- [joyent/node-triton#183] `triton profile create` will no longer use ANSI
|
||||||
|
codes for styling if stdout isn't a TTY.
|
||||||
|
|
||||||
|
## 5.1.0
|
||||||
|
|
||||||
|
- [joyent/node-triton#182] Add `-y, --yes` options to `triton profile create`
|
||||||
|
and `triton profile docker-setup` to allow non-interactive setup.
|
||||||
|
|
||||||
|
## 5.0.0
|
||||||
|
|
||||||
|
- [joyent/node-triton#108] Support for passphrase-protected private keys.
|
||||||
|
Before this work, an encrypted private SSH key (i.e. protected by a
|
||||||
|
passphrase) would have to be loaded in an ssh-agent for the `triton`
|
||||||
|
CLI to use it. Now `triton` will prompt for the passphrase to unlock
|
||||||
|
the private key (in memory), if needed. For example:
|
||||||
|
|
||||||
|
$ triton package list
|
||||||
|
Enter passphrase for id_rsa: <passphrase entered interactively here>
|
||||||
|
SHORTID NAME MEMORY SWAP DISK VCPUS
|
||||||
|
14ad9d54 g4-highcpu-128M 128M 512M 3G -
|
||||||
|
14ae2634 g4-highcpu-256M 256M 1G 5G -
|
||||||
|
...
|
||||||
|
|
||||||
|
- **BREAKING CHANGE for module usage of node-triton.**
|
||||||
|
To implement joyent/node-triton#108, the way a TritonApi client is
|
||||||
|
setup for use has changed from being (unrealistically) sync to async.
|
||||||
|
|
||||||
|
Client preparation is now a multi-step process:
|
||||||
|
|
||||||
|
1. create the client object;
|
||||||
|
2. initialize it (mainly involves finding the SSH key identified by the
|
||||||
|
`keyId`); and,
|
||||||
|
3. optionally unlock the SSH key (if it is passphrase-protected and not in
|
||||||
|
an ssh-agent).
|
||||||
|
|
||||||
|
`createClient` has changed to take a callback argument. It will create and
|
||||||
|
init the client (steps 1 and 2) and takes an optional `unlockKeyFn` parameter
|
||||||
|
to handle step 3. A new `mod_triton.promptPassphraseUnlockKey` export can be
|
||||||
|
used for `unlockKeyFn` for command-line tools to handle prompting for a
|
||||||
|
passphrase on stdin, if required. Therefore what used to be:
|
||||||
|
|
||||||
|
var mod_triton = require('triton');
|
||||||
|
try {
|
||||||
|
var client = mod_triton.createClient({ # No longer works.
|
||||||
|
profileName: 'env'
|
||||||
|
});
|
||||||
|
} catch (initErr) {
|
||||||
|
// handle err
|
||||||
|
}
|
||||||
|
|
||||||
|
// use `client`
|
||||||
|
|
||||||
|
is now:
|
||||||
|
|
||||||
|
var mod_triton = require('triton');
|
||||||
|
mod_triton.createClient({
|
||||||
|
profileName: 'env',
|
||||||
|
unlockKeyFn: mod_triton.promptPassphraseUnlockKey
|
||||||
|
}, function (err, client) {
|
||||||
|
if (err) {
|
||||||
|
// handle err
|
||||||
|
}
|
||||||
|
|
||||||
|
// use `client`
|
||||||
|
});
|
||||||
|
|
||||||
|
See [the examples/ directory](examples/) for more complete examples.
|
||||||
|
|
||||||
|
Low-level/raw handling of the three steps above is possible as follows
|
||||||
|
(error handling is elided):
|
||||||
|
|
||||||
|
var mod_bunyan = require('bunyan');
|
||||||
|
var mod_triton = require('triton');
|
||||||
|
|
||||||
|
// 1. create
|
||||||
|
var client = mod_triton.createTritonApiClient({
|
||||||
|
log: mod_bunyan.createLogger({name: 'my-tool'}),
|
||||||
|
config: {},
|
||||||
|
profile: mod_triton.loadProfile('env')
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. init
|
||||||
|
client.init(function (initErr) {
|
||||||
|
// 3. unlock key
|
||||||
|
// See top-comment in "lib/tritonapi.js".
|
||||||
|
});
|
||||||
|
|
||||||
|
- [joyent/node-triton#143] Fix duplicate output from 'triton rbac key ...'.
|
||||||
|
|
||||||
|
- [joyent/node-triton#157] Add `triton instance resize ...` command and
|
||||||
|
`TritonApi.resizeInstance` method.
|
||||||
|
|
||||||
|
- [joyent/node-triton#129] Fix `triton reboot --wait` to properly wait. Before
|
||||||
|
it would often return immediately, before the instance started rebooting.
|
||||||
|
Add `--wait-timeout N` option to `triton reboot`.
|
||||||
|
Also add `TritonApi#rebootInstance()` api method.
|
||||||
|
|
||||||
|
- [joyent/node-triton#166] Update sshpk to fix issue with the TLS client cert
|
||||||
|
created by `triton profile docker-setup` so that it doesn't create a cert that
|
||||||
|
Go's TLS library doesn't like.
|
||||||
|
|
||||||
|
- [joyent/node-triton#156] Providing all required profile options as command
|
||||||
|
line flags (account, url, keyId) no longer produces an incomplete profile
|
||||||
|
error.
|
||||||
|
|
||||||
|
- PUBAPI-1171/PUBAPI-1205/PUBAPI-1351 The handling of legacy `SDC_*`
|
||||||
|
environment variables has been cleaned up. These environment
|
||||||
|
variables are used for compatibility with the node-smartdc toolset.
|
||||||
|
* `SDC_TESTING` is now evaluated the same way as node-smartdc. Any
|
||||||
|
set value but the empty string is true.
|
||||||
|
* Errors on boolean environment variables will now identify the
|
||||||
|
variable at fault.
|
||||||
|
* `triton env` will emit additional comments grouping variables.
|
||||||
|
|
||||||
|
- [joyent/node-triton#80] Add `triton network list public=true|false`
|
||||||
|
filtering. Note that this filtering is client-side.
|
||||||
|
|
||||||
|
- [joyent/node-triton#146] Add `--wait` flag to `triton instance rename`.
|
||||||
|
|
||||||
|
- [joyent/node-triton#133] Add `triton inst fwrule list` and `triton fwrules`
|
||||||
|
shortcuts for the existing `triton inst fwrules` and `triton fwrule list`,
|
||||||
|
respectively.
|
||||||
|
|
||||||
|
- [joyent/node-triton#3] triton ssh command not aware of "ubuntu" login for
|
||||||
|
ubuntu-certified images.
|
||||||
|
|
||||||
|
- [joyent/node-triton#137] Improve the handling for the getting started case
|
||||||
|
when a user may not have envvars or a profile setup.
|
||||||
|
|
||||||
|
- [joyent/node-triton#158] tritonapi image cache never expires
|
||||||
|
|
||||||
|
- [joyent/node-triton#153] Bump restify-clients dep. Thanks, github.com/tomgco.
|
||||||
|
|
||||||
|
|
||||||
|
## 4.15.0
|
||||||
|
|
||||||
|
- [joyent/node-triton#64] Support 'triton instance rename ...' (by
|
||||||
|
github.com/YangYong3).
|
||||||
|
- [trentm/node-dashdash#30, joyent/node-triton#144] Change the output used by
|
||||||
|
Bash completion support to indicate "there are no completions for this
|
||||||
|
argument" to cope with different sorting rules on different Bash/platforms.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
$ triton -p test2 package get <TAB> # before
|
||||||
|
##-no -tritonpackage- completions-##
|
||||||
|
|
||||||
|
$ triton -p test2 package get <TAB> # after
|
||||||
|
##-no-completion- -results-##
|
||||||
|
|
||||||
|
## 4.14.2
|
||||||
|
|
||||||
|
- TOOLS-1592 First workaround for a possible BadDigestError when using
|
||||||
|
node v6.
|
||||||
|
|
||||||
|
## 4.14.1
|
||||||
|
|
||||||
|
- TOOLS-1587 'triton profile docker-setup' fails when path to 'docker' has
|
||||||
|
spaces. This can help on Windows where Docker Toolbox installs docker.exe
|
||||||
|
to "C:\Program Files\Docker Toolbox".
|
||||||
|
- [#136] bash completion for `triton profile create --copy <TAB>`
|
||||||
|
|
||||||
|
## 4.14.0
|
||||||
|
|
||||||
|
- [#130] Include disabled images when using an image cache (e.g. for filling in
|
||||||
|
image name and version details in `triton ls` output.
|
||||||
|
|
||||||
|
|
||||||
|
## 4.13.0
|
||||||
|
|
||||||
|
- [#120] Don't fail `triton instance list` if the retrieval of *image* info
|
||||||
|
(retrieved to get image name and version, as a bonus) fails with an
|
||||||
|
authorization error -- in case it is an RBAC failure where a subuser can
|
||||||
|
ListMachines, but not ListImages.
|
||||||
|
|
||||||
|
- [#113] *Usage* errors now some "error help", including option or command
|
||||||
|
synopses. Some examples (the new thing is marked with `>`):
|
||||||
|
|
||||||
|
- Command synopses when argument errors:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ triton create
|
||||||
|
triton instance create: error (Usage): incorrect number of args
|
||||||
|
> usage: triton instance create [OPTIONS] IMAGE PACKAGE
|
||||||
|
```
|
||||||
|
|
||||||
|
- Option synopsis with option errors:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ triton image ls --bogus
|
||||||
|
triton image ls: error (Option): unknown option: "--bogus"
|
||||||
|
> usage: triton image ls [ --help | -h ] [ --all | -a ] [ -H ] [ -o field1,... ]
|
||||||
|
> [ --long | -l ] [ -s field1,... ] [ --json | -j ] ...
|
||||||
|
```
|
||||||
|
|
||||||
|
- Suggested command name misspellings:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ triton in
|
||||||
|
triton: error (UnknownCommand): unknown command: "in"
|
||||||
|
> Did you mean this?
|
||||||
|
> info
|
||||||
|
> inst
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 4.12.0
|
||||||
|
|
||||||
|
- [#120] `triton -r,--role ROLE ...` option to take up an RBAC role(s).
|
||||||
|
|
||||||
|
|
||||||
|
## 4.11.0
|
||||||
|
|
||||||
|
- [#112] Fix `triton completion`, broke a while back.
|
||||||
|
- [#111] `triton env --unset,-u` option to emit environment commands to *unset*
|
||||||
|
relevant envvars.
|
||||||
|
- Unhide `triton env` from `triton --help` output.
|
||||||
|
|
||||||
|
|
||||||
|
## 4.10.0
|
||||||
|
|
||||||
|
- [#82] Affinity (a.k.a. locality hints) support for instance creation, e.g.:
|
||||||
|
|
||||||
|
# Use same server as instance 'db0':
|
||||||
|
triton create -a instance==db0 ...
|
||||||
|
triton create -a db0 ... # shortcut for same thing
|
||||||
|
|
||||||
|
# Use different server than instance 'db0':
|
||||||
|
triton create -a 'instance!=db0' ...
|
||||||
|
|
||||||
|
# *Attempt* to use same server as instance 'db0', but don't fail
|
||||||
|
# if cannot. This is called a "non-strict" or "soft" rule.
|
||||||
|
triton create -a instance==~db0 ...
|
||||||
|
|
||||||
|
# *Attempt* to use a different server than instance 'db0':
|
||||||
|
triton create -a 'instance!=~db0' ...
|
||||||
|
|
||||||
|
"Affinity" here refers to providing rules for deciding on which server
|
||||||
|
a new instance should by provisioned. Rules are in terms of existing
|
||||||
|
instances. As a shortcut, 'inst' can be used in place of 'instance'
|
||||||
|
above (e.g. `triton create -a 'inst!=db0' ...`).
|
||||||
|
|
||||||
|
## 4.9.0
|
||||||
|
|
||||||
|
- [#46] Initial support for `triton` helping setup and manage configuration for
|
||||||
|
using `docker` against a Triton datacenter. Triton datacenters can provide
|
||||||
|
a Docker Remote API endpoint against which you can run the normal `docker`
|
||||||
|
client. See <https://www.joyent.com/triton> for and overview and
|
||||||
|
<https://github.com/joyent/sdc-docker> for developer details.
|
||||||
|
|
||||||
|
- `triton profile create` will now setup the profile for running Docker,
|
||||||
|
if the Triton datacenter provides a docker endpoint. The typical flow
|
||||||
|
would be:
|
||||||
|
|
||||||
|
$ triton profile create
|
||||||
|
name: foo
|
||||||
|
...
|
||||||
|
$ triton profile set foo # make foo my default profile
|
||||||
|
$ eval "$(triton env --docker)" # set 'DOCKER_' envvars
|
||||||
|
$ docker info
|
||||||
|
|
||||||
|
This profile setup for Docker requires making requests to the
|
||||||
|
CloudAPI endpoint which can fail (e.g. if CloudAPI is down, credentials
|
||||||
|
are invalid, etc.). You can use the `--no-docker` option to skip
|
||||||
|
the Docker setup part of profile creation.
|
||||||
|
|
||||||
|
- For existing Triton CLI profiles, there is a new `triton profile
|
||||||
|
docker-setup [PROFILE]`.
|
||||||
|
|
||||||
|
$ triton profile docker-setup
|
||||||
|
$ eval "$(triton env --docker)"
|
||||||
|
$ docker info
|
||||||
|
|
||||||
|
- `triton env` will now emit commands to setup `DOCKER_` envvars. That
|
||||||
|
can be limited to just the Docker-relevant env via `triton env --docker`.
|
||||||
|
|
||||||
|
|
||||||
|
## 4.8.0
|
||||||
|
|
||||||
|
- #103 `triton ip <inst>` to output the instance's primaryIp
|
||||||
|
- #52 Workaround for `triton ssh ...`. In version 4.6.0, `triton ssh ...`
|
||||||
|
interactive sessions were broken. This version reverts that change and adds
|
||||||
|
a workaround for #52 (by disabling ControlMaster when spawning `ssh`).
|
||||||
|
See <https://github.com/joyent/node-triton/issues/52> for details.
|
||||||
|
- #97 `triton profile set -` to set the *last* profile as current.
|
||||||
|
- PUBAPI-1266 Added `instance enable-firewall` and `instance disable-firewall`
|
||||||
|
|
||||||
|
|
||||||
|
## 4.7.0
|
||||||
|
|
||||||
|
**Known issue: `triton ssh` interactive sessions are broken.
|
||||||
|
Upgrade to v4.7.1.**
|
||||||
|
|
||||||
|
- #101 Bash completion for server-side data: instances, images, etc.
|
||||||
|
Bash completion on TAB should now work for things like the following:
|
||||||
|
`triton create <TAB to complete images> <TAB to complete packages`,
|
||||||
|
`triton inst tag ls <TAB to complete instances>`. Cached (with a 5 minute
|
||||||
|
TTL) completions for the following data are supported: instances, images,
|
||||||
|
packages, networks, fwrules, account keys.
|
||||||
|
See `triton completion --help` for adding/updating Bash completion.
|
||||||
|
- #99 `triton profile set ...` alias for `set-current`
|
||||||
|
|
||||||
|
|
||||||
|
## 4.6.0
|
||||||
|
|
||||||
|
**Known issue: `triton ssh` interactive sessions are broken.
|
||||||
|
Upgrade to v4.7.1.**
|
||||||
|
|
||||||
|
- #98 `triton inst get ID` for a deleted instance will now emit the instance
|
||||||
|
object and error less obtusely. This adds a new `InstanceDeleted` error code
|
||||||
|
from `TritonApi`.
|
||||||
|
- PUBAPI-1233 firewalls: `triton fwrule ...`
|
||||||
|
- PUBAPI-1234 instance snapshots: `triton inst snapshot ...`
|
||||||
|
- #52 Fix 'triton ssh ...' stdout/stderr to fully flush with node >= 4.x.
|
||||||
|
|
||||||
|
|
||||||
|
## 4.5.2
|
||||||
|
|
||||||
|
- #95 Fix breakage of `triton image create` in v4.5.0. (By Kris Shannon.)
|
||||||
|
- #94, #93 `triton inst create ...` is broken if "images.json" cache file
|
||||||
|
is missing. (By Kris Shannon.)
|
||||||
|
|
||||||
|
|
||||||
|
## 4.5.1
|
||||||
|
|
||||||
|
- #92 `triton` CLI should summarize `err.body.errors` from CloudAPI
|
||||||
|
Per <https://github.com/joyent/eng/blob/master/docs/index.md#error-handling>,
|
||||||
|
CloudAPI error response will sometimes have extra error details to show.
|
||||||
|
|
||||||
|
|
||||||
## 4.5.0
|
## 4.5.0
|
||||||
|
27
Makefile
27
Makefile
@ -35,6 +35,12 @@ test-unit:
|
|||||||
test-integration:
|
test-integration:
|
||||||
NODE_NDEBUG= ./node_modules/.bin/tape test/integration/*.test.js
|
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
|
.PHONY: clean
|
||||||
clean::
|
clean::
|
||||||
rm -f triton-*.tgz
|
rm -f triton-*.tgz
|
||||||
@ -45,12 +51,27 @@ check:: versioncheck
|
|||||||
.PHONY: versioncheck
|
.PHONY: versioncheck
|
||||||
versioncheck:
|
versioncheck:
|
||||||
@echo version is: $(shell cat package.json | json version)
|
@echo version is: $(shell cat package.json | json version)
|
||||||
[[ `cat package.json | json version` == `grep '^## ' CHANGES.md | head -1 | awk '{print $$2}'` ]]
|
[[ `cat package.json | json version` == `grep '^## ' CHANGES.md | head -2 | tail -1 | awk '{print $$2}'` ]]
|
||||||
|
|
||||||
.PHONY: cutarelease
|
.PHONY: cutarelease
|
||||||
cutarelease: versioncheck
|
cutarelease: versioncheck
|
||||||
[[ `git status | tail -n1` == "nothing to commit, working directory clean" ]]
|
[[ -z `git status --short` ]] # If this fails, the working dir is dirty.
|
||||||
./tools/cutarelease.py -p triton -f package.json
|
@which json 2>/dev/null 1>/dev/null && \
|
||||||
|
ver=$(shell json -f package.json version) && \
|
||||||
|
name=$(shell json -f package.json name) && \
|
||||||
|
publishedVer=$(shell npm view -j $(shell json -f package.json name)@$(shell json -f package.json version) version 2>/dev/null) && \
|
||||||
|
if [[ -n "$$publishedVer" ]]; then \
|
||||||
|
echo "error: $$name@$$ver is already published to npm"; \
|
||||||
|
exit 1; \
|
||||||
|
fi && \
|
||||||
|
echo "** Are you sure you want to tag and publish $$name@$$ver to npm?" && \
|
||||||
|
echo "** Enter to continue, Ctrl+C to abort." && \
|
||||||
|
read
|
||||||
|
ver=$(shell cat package.json | json version) && \
|
||||||
|
date=$(shell date -u "+%Y-%m-%d") && \
|
||||||
|
git tag -a "$$ver" -m "version $$ver ($$date)" && \
|
||||||
|
git push --tags origin && \
|
||||||
|
npm publish
|
||||||
|
|
||||||
.PHONY: git-hooks
|
.PHONY: git-hooks
|
||||||
git-hooks:
|
git-hooks:
|
||||||
|
357
README.md
357
README.md
@ -1,353 +1,38 @@
|
|||||||
![logo](./tools/triton-text.png)
|
![logo](https://code.spearhead.cloud/Spearhead/node-spearhead/raw/branch/master/tools/sphsp.png)
|
||||||
|
|
||||||
# node-triton
|
# node-spearhead
|
||||||
|
|
||||||
`triton` is a CLI tool for working with the CloudAPI for Joyent's Triton [Public Cloud]
|
This repository holds the node-spearhead CLI tool to work with the Spearhead
|
||||||
(https://docs.joyent.com/public-cloud) and [Private Cloud] (https://docs.joyent.com/private-cloud).
|
Cloud. It is a fork of [node-triton](https://github.com/joyent/node-triton).
|
||||||
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.
|
|
||||||
|
|
||||||
There is currently another CLI tool known as [node-smartdc](https://github.com/joyent/node-smartdc)
|
## Installation and configuration
|
||||||
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/).
|
|
||||||
|
|
||||||
As a lightweight programmable interface for CloudAPI, the `triton` CLI supports both name or
|
### Get a Spearhead Cloud account
|
||||||
UUID identification of object instances and the use of short ID, as well as the choice
|
|
||||||
between concise tabular responses and full JSON responses.
|
|
||||||
|
|
||||||
**The `triton` CLI is currently in beta and will be expanded over time to
|
Create an account on the Spearhead Cloud and upload your SSH key. You can create an account
|
||||||
support all CloudAPI commands, eventually replacing `node-smartdc` as both the
|
[here](https://spearhead.cloud/).
|
||||||
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).
|
|
||||||
|
|
||||||
|
|
||||||
### API endpoint
|
### Data-centers
|
||||||
|
|
||||||
Each data center has a single CloudAPI endpoint. For Joyent Public Cloud, you can find the
|
The list of available Spearhead Cloud data-centers is available
|
||||||
list of data centers [here](https://docs.joyent.com/public-cloud/data-centers).
|
[here](https://spearhead.cloud/datacenters).
|
||||||
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
|
### Installation
|
||||||
|
|
||||||
1. Install [node.js](http://nodejs.org/).
|
Install [node.js](http://nodejs.org/), then:
|
||||||
2. `npm install -g triton`
|
|
||||||
|
npm install -g spearhead
|
||||||
|
|
||||||
Verify that it is installed and on your PATH:
|
Verify that it is installed and on your PATH:
|
||||||
|
$ spearhead --version
|
||||||
$ triton --version
|
Spearhead CLI 6.1.4
|
||||||
Triton CLI 1.0.0
|
https://code.spearhead.cloud/Spearhead/node-spearhead
|
||||||
|
|
||||||
Configure the proper environmental variables that correspond to the API endpoint and account,
|
Now you ca use `spearhead` to interact with our Public Cloud. More details
|
||||||
for example:
|
about installation and configuration are available
|
||||||
|
[here](https://docs.spearhead.cloud).
|
||||||
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
|
## License
|
||||||
|
|
||||||
MPL 2.0
|
MPL 2.0
|
||||||
|
43
TODO.txt
43
TODO.txt
@ -1,17 +1,14 @@
|
|||||||
test suite:
|
- The 'shortcut' commands use `handlerFromSubcmd(...).dispatch`. That
|
||||||
- all the commands: test/integration/cli-*.test.js
|
doesn't run the subcmd class's `.init()` method. node-cmdln should provide
|
||||||
- TritonApi testing: test/integration/api-*.test.js
|
a way to do this. ... basically want to call the *main()* but with preparsed
|
||||||
- more test/unit/...
|
options. Perhaps the init/fini should move into dispatch?
|
||||||
|
|
||||||
sub-user support (profiles, `triton account`, env, auth)
|
triton create affinity support for tag matching, globs, regex
|
||||||
|
|
||||||
note in README that full UUIDs is much faster in the API
|
note in README that full UUIDs is much faster in the API
|
||||||
|
|
||||||
*type*: cloudapi changes to clarify: LX, docker, smartos, kvm instances
|
*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
|
# maybe next
|
||||||
|
|
||||||
PUBAPI-1117 triton create -c|--count N
|
PUBAPI-1117 triton create -c|--count N
|
||||||
@ -31,3 +28,33 @@ triton images
|
|||||||
triton config get|set|list # see 'npm config'
|
triton config get|set|list # see 'npm config'
|
||||||
|
|
||||||
triton --shell # or whatever, repl
|
triton --shell # or whatever, repl
|
||||||
|
|
||||||
|
$ triton shell
|
||||||
|
$profile> cd inst
|
||||||
|
$profile inst> ls
|
||||||
|
...
|
||||||
|
$profile inst> cd vm0
|
||||||
|
$profile inst/vm0> get
|
||||||
|
...
|
||||||
|
$profile inst/vm0> cd snapshot
|
||||||
|
$profile inst/vm0/snapshot> ls
|
||||||
|
...
|
||||||
|
|
||||||
|
extensible triton commands:
|
||||||
|
~/.triton/plugins.d/$plugin.json
|
||||||
|
|
||||||
|
Would be nice to not have to read/parse all these files for every run,
|
||||||
|
i.e. lazily. Is that a problem for `triton` showing commands list?
|
||||||
|
|
||||||
|
How do plugins in other node projects work? Is there an npm special thing
|
||||||
|
that would work?
|
||||||
|
|
||||||
|
Should plugins just be commands? Perhaps for starters, but don't hardcode
|
||||||
|
that.
|
||||||
|
|
||||||
|
$ cat build.json
|
||||||
|
{
|
||||||
|
"cmd": "build",
|
||||||
|
"desc": "Build a Triton image from a Tritonfile"
|
||||||
|
"require": "triton-plugin-build"
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
# Functions for Bash completion of some 'triton' option/arg types.
|
# Functions for Bash completion of some 'triton' option/arg types.
|
||||||
|
|
||||||
function complete_tritonprofile {
|
function complete_tritonprofile {
|
||||||
local word="$1"
|
local word="$1"
|
||||||
local candidates
|
local candidates
|
||||||
@ -7,6 +8,172 @@ function complete_tritonprofile {
|
|||||||
compgen $compgen_opts -W "$candidates" -- "$word"
|
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 {
|
function complete_tritonupdateaccountfield {
|
||||||
local word="$1"
|
local word="$1"
|
||||||
local candidates
|
local candidates
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* Example using cloudapi2.js to call cloudapi's GetAccount endpoint.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* ./example-get-account.js | bunyan
|
|
||||||
*/
|
|
||||||
|
|
||||||
var p = console.log;
|
|
||||||
var auth = require('smartdc-auth');
|
|
||||||
var bunyan = require('bunyan');
|
|
||||||
var cloudapi = require('../lib/cloudapi2');
|
|
||||||
|
|
||||||
var log = bunyan.createLogger({
|
|
||||||
name: 'test-get-account',
|
|
||||||
level: 'trace'
|
|
||||||
})
|
|
||||||
|
|
||||||
var USER = process.env.SDC_USER || process.env.SDC_ACCOUNT || 'bob';
|
|
||||||
var KEY_ID = process.env.SDC_KEY_ID || 'b4:f0:b4:6c:18:3b:44:63:b4:4e:58:22:74:43:d4:bc';
|
|
||||||
|
|
||||||
var sign = auth.cliSigner({
|
|
||||||
keyId: KEY_ID,
|
|
||||||
user: USER,
|
|
||||||
log: log
|
|
||||||
});
|
|
||||||
var client = cloudapi.createClient({
|
|
||||||
url: 'https://us-sw-1.api.joyent.com',
|
|
||||||
user: USER,
|
|
||||||
version: '*',
|
|
||||||
sign: sign,
|
|
||||||
agent: false, // don't want KeepAlive
|
|
||||||
log: log
|
|
||||||
});
|
|
||||||
|
|
||||||
log.info('start')
|
|
||||||
client.getAccount(function (err, account) {
|
|
||||||
p('getAccount: err', err)
|
|
||||||
p('getAccount: account', account)
|
|
||||||
});
|
|
@ -1,46 +0,0 @@
|
|||||||
#!/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));
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,27 +0,0 @@
|
|||||||
*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"
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
|||||||
{
|
|
||||||
"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*"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -96,6 +96,18 @@ SaferJsonClient.prototype.parse = function parse(req, callback) {
|
|||||||
|
|
||||||
function finish() {
|
function finish() {
|
||||||
var body = Buffer.concat(chunks, len);
|
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()) {
|
if (res.log.trace()) {
|
||||||
res.log.trace({body: body.toString(), len: len},
|
res.log.trace({body: body.toString(), len: len},
|
||||||
'body received');
|
'body received');
|
||||||
@ -139,9 +151,6 @@ SaferJsonClient.prototype.parse = function parse(req, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Special error handling.
|
// Special error handling.
|
||||||
if (resErr) {
|
|
||||||
resErr.message = body.toString('utf8');
|
|
||||||
}
|
|
||||||
if (res && res.statusCode >= 400) {
|
if (res && res.statusCode >= 400) {
|
||||||
// Upcast error to a RestError (if we can)
|
// Upcast error to a RestError (if we can)
|
||||||
// Be nice and handle errors like
|
// Be nice and handle errors like
|
||||||
@ -204,7 +213,7 @@ SaferJsonClient.prototype.parse = function parse(req, callback) {
|
|||||||
|
|
||||||
res.on('data', function onData(chunk) {
|
res.on('data', function onData(chunk) {
|
||||||
if (contentMd5Hash) {
|
if (contentMd5Hash) {
|
||||||
contentMd5Hash.update(chunk.toString('utf8'));
|
contentMd5Hash.update(chunk.toString('utf8'), 'binary');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gz) {
|
if (gz) {
|
||||||
|
546
lib/cli.js
546
lib/cli.js
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* Copyright (c) 2017, Joyent, Inc.
|
||||||
*
|
*
|
||||||
* The `triton` CLI class.
|
* The `triton` CLI class.
|
||||||
*/
|
*/
|
||||||
@ -17,6 +17,7 @@ var child_process = require('child_process'),
|
|||||||
exec = child_process.exec;
|
exec = child_process.exec;
|
||||||
var cmdln = require('cmdln'),
|
var cmdln = require('cmdln'),
|
||||||
Cmdln = cmdln.Cmdln;
|
Cmdln = cmdln.Cmdln;
|
||||||
|
var fs = require('fs');
|
||||||
var mkdirp = require('mkdirp');
|
var mkdirp = require('mkdirp');
|
||||||
var util = require('util'),
|
var util = require('util'),
|
||||||
format = util.format;
|
format = util.format;
|
||||||
@ -24,30 +25,16 @@ var path = require('path');
|
|||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
|
|
||||||
var common = require('./common');
|
var common = require('./common');
|
||||||
|
var constants = require('./constants');
|
||||||
var mod_config = require('./config');
|
var mod_config = require('./config');
|
||||||
var errors = require('./errors');
|
var errors = require('./errors');
|
||||||
var tritonapi = require('./tritonapi');
|
var lib_tritonapi = require('./tritonapi');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//---- globals
|
//---- globals
|
||||||
|
|
||||||
var pkg = require('../package.json');
|
var packageJson = 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 = [
|
var OPTIONS = [
|
||||||
@ -71,9 +58,9 @@ var OPTIONS = [
|
|||||||
names: ['profile', 'p'],
|
names: ['profile', 'p'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
completionType: 'tritonprofile',
|
completionType: 'tritonprofile',
|
||||||
env: 'TRITON_PROFILE',
|
env: 'SC_PROFILE',
|
||||||
helpArg: 'NAME',
|
helpArg: 'NAME',
|
||||||
help: 'Triton client profile to use.'
|
help: 'Spearhead Cloud client profile to use.'
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -94,8 +81,7 @@ var OPTIONS = [
|
|||||||
{
|
{
|
||||||
names: ['account', 'a'],
|
names: ['account', 'a'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
help: 'Account (login name). Environment: TRITON_ACCOUNT=ACCOUNT ' +
|
help: 'Account (login name). Environment: SC_ACCOUNT=ACCOUNT ',
|
||||||
'or SDC_ACCOUNT=ACCOUNT.',
|
|
||||||
helpArg: 'ACCOUNT'
|
helpArg: 'ACCOUNT'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -103,52 +89,48 @@ var OPTIONS = [
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
help: 'Masquerade as the given account login name. This can only ' +
|
help: 'Masquerade as the given account login name. This can only ' +
|
||||||
'succeed for operator accounts. Note that accesses like these ' +
|
'succeed for operator accounts. Note that accesses like these ' +
|
||||||
'audited on the CloudAPI server side.',
|
'are audited on the CloudAPI server side.',
|
||||||
helpArg: 'ACCOUNT',
|
helpArg: 'ACCOUNT',
|
||||||
hidden: true
|
hidden: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
names: ['user', 'u'],
|
names: ['user', 'u'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
help: 'RBAC user (login name). Environment: TRITON_USER=USER ' +
|
help: 'RBAC user (login name). Environment: SC_USER=USER',
|
||||||
'or SDC_USER=USER.',
|
|
||||||
helpArg: 'USER'
|
helpArg: 'USER'
|
||||||
},
|
},
|
||||||
// TODO: full rbac support
|
{
|
||||||
//{
|
names: ['role', 'r'],
|
||||||
// names: ['role'],
|
type: 'arrayOfCommaSepString',
|
||||||
// type: 'arrayOfString',
|
env: 'SC_ROLE',
|
||||||
// env: 'MANTA_ROLE',
|
help: 'Assume an RBAC role. Use multiple times or once with a list',
|
||||||
// help: 'Assume a role. Use multiple times or once with a list',
|
helpArg: 'ROLE,...'
|
||||||
// helpArg: 'ROLE,ROLE,...'
|
},
|
||||||
//},
|
|
||||||
{
|
{
|
||||||
names: ['keyId', 'k'],
|
names: ['keyId', 'k'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
help: 'SSH key fingerprint. Environment: TRITON_KEY_ID=FINGERPRINT ' +
|
help: 'SSH key fingerprint. Environment: SC_KEY_ID=FINGERPRINT.',
|
||||||
'or SDC_KEY_ID=FINGERPRINT.',
|
|
||||||
helpArg: 'FP'
|
helpArg: 'FP'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
names: ['url', 'U'],
|
names: ['url', 'U'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
help: 'CloudAPI URL. Environment: TRITON_URL=URL or SDC_URL=URL.',
|
help: 'Spearhead Cloud Datacenter URL. Environment: SC_URL=URL.',
|
||||||
helpArg: 'URL'
|
helpArg: 'URL'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
names: ['J'],
|
names: ['J'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
hidden: true,
|
hidden: true,
|
||||||
help: 'Joyent Public Cloud (JPC) datacenter name. This is ' +
|
help: 'Spearhead Cloud (SC) datacenter name. This is ' +
|
||||||
'a shortcut to the "https://$dc.api.joyent.com" ' +
|
'a shortcut to the "https://$dc.api.spearhead.cloud" ' +
|
||||||
'cloudapi URL.'
|
'cloudapi URL.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
names: ['insecure', 'i'],
|
names: ['insecure', 'i'],
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
help: 'Do not validate the CloudAPI SSL certificate. Environment: ' +
|
help: 'Do not validate the SSL certificate. Environment: ' +
|
||||||
'TRITON_TLS_INSECURE=1, SDC_TLS_INSECURE=1 (or the deprecated ' +
|
'SC_TLS_INSECURE=1 (or the deprecated SC_TESTING=1).',
|
||||||
'SDC_TESTING=1).',
|
|
||||||
'default': false
|
'default': false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -157,10 +139,10 @@ var OPTIONS = [
|
|||||||
helpArg: 'VER',
|
helpArg: 'VER',
|
||||||
help: 'A cloudapi API version, or semver range, to attempt to use. ' +
|
help: 'A cloudapi API version, or semver range, to attempt to use. ' +
|
||||||
'This is passed in the "Accept-Version" header. ' +
|
'This is passed in the "Accept-Version" header. ' +
|
||||||
'See `triton cloudapi /--ping` to list supported versions. ' +
|
'See `spearhead cloudapi /--ping` to list supported versions. ' +
|
||||||
'The default is "' + tritonapi.CLOUDAPI_ACCEPT_VERSION + '". ' +
|
'The default is "' + lib_tritonapi.CLOUDAPI_ACCEPT_VERSION + '". ' +
|
||||||
'*This is intended for development use only. It could cause ' +
|
'*This is intended for development use only. It could cause ' +
|
||||||
'`triton` processing of responses to break.*',
|
'`spearhead` processing of responses to break.*',
|
||||||
hidden: true
|
hidden: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -197,8 +179,8 @@ cmdln.dashdash.addOptionType({
|
|||||||
|
|
||||||
function CLI() {
|
function CLI() {
|
||||||
Cmdln.call(this, {
|
Cmdln.call(this, {
|
||||||
name: 'triton',
|
name: 'spearhead',
|
||||||
desc: pkg.description,
|
desc: packageJson.description,
|
||||||
options: OPTIONS,
|
options: OPTIONS,
|
||||||
helpOpts: {
|
helpOpts: {
|
||||||
includeEnv: true,
|
includeEnv: true,
|
||||||
@ -206,8 +188,9 @@ function CLI() {
|
|||||||
},
|
},
|
||||||
helpSubcmds: [
|
helpSubcmds: [
|
||||||
'help',
|
'help',
|
||||||
'completion',
|
|
||||||
'profile',
|
'profile',
|
||||||
|
'env',
|
||||||
|
'completion',
|
||||||
{ group: 'Instances (aka VMs/Machines/Containers)' },
|
{ group: 'Instances (aka VMs/Machines/Containers)' },
|
||||||
'instance',
|
'instance',
|
||||||
'instances',
|
'instances',
|
||||||
@ -217,11 +200,13 @@ function CLI() {
|
|||||||
'stop',
|
'stop',
|
||||||
'reboot',
|
'reboot',
|
||||||
'ssh',
|
'ssh',
|
||||||
|
'ip',
|
||||||
{ group: 'Images, Packages, Networks, Firewall Rules' },
|
{ group: 'Images, Packages, Networks, Firewall Rules' },
|
||||||
'image',
|
'image',
|
||||||
'package',
|
'package',
|
||||||
'network',
|
'network',
|
||||||
'fwrule',
|
'fwrule',
|
||||||
|
'vlan',
|
||||||
{ group: 'Other Commands' },
|
{ group: 'Other Commands' },
|
||||||
'info',
|
'info',
|
||||||
'account',
|
'account',
|
||||||
@ -235,8 +220,8 @@ function CLI() {
|
|||||||
' 0 Successful completion.',
|
' 0 Successful completion.',
|
||||||
' 1 An error occurred.',
|
' 1 An error occurred.',
|
||||||
' 2 Usage error.',
|
' 2 Usage error.',
|
||||||
' 3 "ResourceNotFound" error. Returned when an instance, image,',
|
' 3 "ResourceNotFound" error (when an instance, image, etc. with',
|
||||||
' package, etc. with the given name or id is not found.'
|
' the given name or id is not found) or "InstanceDeleted" error.'
|
||||||
/* END JSSTYLED */
|
/* END JSSTYLED */
|
||||||
].join('\n')
|
].join('\n')
|
||||||
});
|
});
|
||||||
@ -260,7 +245,8 @@ CLI.prototype.init = function (opts, args, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (opts.version) {
|
if (opts.version) {
|
||||||
console.log(this.name, pkg.version);
|
console.log('Spearhead CLI', packageJson.version);
|
||||||
|
console.log(packageJson.homepage);
|
||||||
callback(false);
|
callback(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -269,37 +255,89 @@ CLI.prototype.init = function (opts, args, callback) {
|
|||||||
callback(new errors.UsageError(
|
callback(new errors.UsageError(
|
||||||
'cannot use both "--url" and "-J" options'));
|
'cannot use both "--url" and "-J" options'));
|
||||||
} else if (opts.J) {
|
} else if (opts.J) {
|
||||||
opts.url = format('https://%s.api.joyent.com', opts.J);
|
opts.url = format('https://%s.api.spearhead.cloud', opts.J);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.configDir = CONFIG_DIR;
|
this.configDir = constants.CLI_CONFIG_DIR;
|
||||||
|
|
||||||
this.__defineGetter__('tritonapi', function () {
|
this.__defineGetter__('config', function getConfig() {
|
||||||
if (self._tritonapi === undefined) {
|
if (self._config === undefined) {
|
||||||
var config = mod_config.loadConfig({
|
self._config = mod_config.loadConfig({
|
||||||
configDir: self.configDir
|
configDir: self.configDir
|
||||||
});
|
});
|
||||||
self.log.trace({config: config}, 'loaded config');
|
self.log.trace({config: self._config}, 'loaded config');
|
||||||
|
}
|
||||||
|
return self._config;
|
||||||
|
});
|
||||||
|
|
||||||
var profileName = opts.profile || config.profile || 'env';
|
this.__defineGetter__('profileName', function getProfileName() {
|
||||||
var profile = mod_config.loadProfile({
|
return (opts.profile || self.config.profile || 'env');
|
||||||
configDir: self.configDir,
|
});
|
||||||
name: profileName
|
|
||||||
});
|
|
||||||
self._applyProfileOverrides(profile);
|
|
||||||
self.log.trace({profile: profile}, 'loaded profile');
|
|
||||||
|
|
||||||
self._tritonapi = tritonapi.createClient({
|
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,
|
log: self.log,
|
||||||
profile: profile,
|
profile: self.profile,
|
||||||
config: config
|
config: self.config
|
||||||
});
|
});
|
||||||
|
self.log.trace('created tritonapi');
|
||||||
}
|
}
|
||||||
return self._tritonapi;
|
return self._tritonapi;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cmdln class handles `opts.help`.
|
if (process.env.SC_COMPLETE) {
|
||||||
Cmdln.prototype.init.apply(this, arguments);
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -309,7 +347,241 @@ CLI.prototype.fini = function fini(subcmd, err, cb) {
|
|||||||
this._tritonapi.close();
|
this._tritonapi.close();
|
||||||
delete this._tritonapi;
|
delete this._tritonapi;
|
||||||
}
|
}
|
||||||
cb(err, subcmd);
|
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);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -317,11 +589,26 @@ CLI.prototype.fini = function fini(subcmd, err, cb) {
|
|||||||
* Apply overrides from CLI options to the given profile object *in place*.
|
* Apply overrides from CLI options to the given profile object *in place*.
|
||||||
*/
|
*/
|
||||||
CLI.prototype._applyProfileOverrides =
|
CLI.prototype._applyProfileOverrides =
|
||||||
function _applyProfileOverrides(profile) {
|
function _applyProfileOverrides(profile) {
|
||||||
|
var optProfile = this._cliOptsAsProfile();
|
||||||
|
for (var attr in optProfile) {
|
||||||
|
profile[attr] = optProfile[attr];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create a profile dict from any cli override options specified.
|
||||||
|
* Unless all profile flags are specified on the cli, this profile
|
||||||
|
* will be incomplete and will need to be combined with another
|
||||||
|
* configuration source.
|
||||||
|
*/
|
||||||
|
CLI.prototype._cliOptsAsProfile = function _cliOptsAsProfile() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
var profile = {};
|
||||||
[
|
[
|
||||||
{oname: 'account', pname: 'account'},
|
{oname: 'account', pname: 'account'},
|
||||||
{oname: 'user', pname: 'user'},
|
{oname: 'user', pname: 'user'},
|
||||||
|
{oname: 'role', pname: 'roles'},
|
||||||
{oname: 'url', pname: 'url'},
|
{oname: 'url', pname: 'url'},
|
||||||
{oname: 'keyId', pname: 'keyId'},
|
{oname: 'keyId', pname: 'keyId'},
|
||||||
{oname: 'insecure', pname: 'insecure'},
|
{oname: 'insecure', pname: 'insecure'},
|
||||||
@ -336,6 +623,38 @@ CLI.prototype._applyProfileOverrides =
|
|||||||
profile[field.pname] = self.opts[field.oname];
|
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
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -357,6 +676,7 @@ CLI.prototype.do_keys = require('./do_keys');
|
|||||||
|
|
||||||
// Firewall rules
|
// Firewall rules
|
||||||
CLI.prototype.do_fwrule = require('./do_fwrule');
|
CLI.prototype.do_fwrule = require('./do_fwrule');
|
||||||
|
CLI.prototype.do_fwrules = require('./do_fwrules');
|
||||||
|
|
||||||
// Images
|
// Images
|
||||||
CLI.prototype.do_images = require('./do_images');
|
CLI.prototype.do_images = require('./do_images');
|
||||||
@ -371,6 +691,7 @@ CLI.prototype.do_start = require('./do_start');
|
|||||||
CLI.prototype.do_stop = require('./do_stop');
|
CLI.prototype.do_stop = require('./do_stop');
|
||||||
CLI.prototype.do_reboot = require('./do_reboot');
|
CLI.prototype.do_reboot = require('./do_reboot');
|
||||||
CLI.prototype.do_ssh = require('./do_ssh');
|
CLI.prototype.do_ssh = require('./do_ssh');
|
||||||
|
CLI.prototype.do_ip = require('./do_ip');
|
||||||
|
|
||||||
// Packages
|
// Packages
|
||||||
CLI.prototype.do_packages = require('./do_packages');
|
CLI.prototype.do_packages = require('./do_packages');
|
||||||
@ -380,11 +701,17 @@ CLI.prototype.do_package = require('./do_package');
|
|||||||
CLI.prototype.do_networks = require('./do_networks');
|
CLI.prototype.do_networks = require('./do_networks');
|
||||||
CLI.prototype.do_network = require('./do_network');
|
CLI.prototype.do_network = require('./do_network');
|
||||||
|
|
||||||
|
// VLANs
|
||||||
|
CLI.prototype.do_vlan = require('./do_vlan');
|
||||||
|
|
||||||
// Hidden commands
|
// Hidden commands
|
||||||
CLI.prototype.do_cloudapi = require('./do_cloudapi');
|
CLI.prototype.do_cloudapi = require('./do_cloudapi');
|
||||||
CLI.prototype.do_badger = require('./do_badger');
|
CLI.prototype.do_badger = require('./do_badger');
|
||||||
CLI.prototype.do_rbac = require('./do_rbac');
|
CLI.prototype.do_rbac = require('./do_rbac');
|
||||||
|
|
||||||
|
// Volumes
|
||||||
|
CLI.prototype.do_volumes = require('./do_volumes');
|
||||||
|
CLI.prototype.do_volume = require('./do_volume');
|
||||||
|
|
||||||
|
|
||||||
//---- mainline
|
//---- mainline
|
||||||
@ -395,36 +722,71 @@ function main(argv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var cli = new CLI();
|
var cli = new CLI();
|
||||||
cli.main(argv, function (err, subcmd) {
|
cli.main(argv, function (err) {
|
||||||
var exitStatus = (err ? err.exitStatus || 1 : 0);
|
var exitStatus = (err ? err.exitStatus || 1 : 0);
|
||||||
var showErr = (cli.showErr !== undefined ? cli.showErr : true);
|
var showErr = (cli.showErr !== undefined ? cli.showErr : true);
|
||||||
|
var errHelp;
|
||||||
|
var errMessage;
|
||||||
|
|
||||||
if (err && showErr) {
|
if (err && showErr) {
|
||||||
var code = (err.body ? err.body.code : err.code) || err.restCode;
|
var code = (err.body ? err.body.code : err.code) || err.restCode;
|
||||||
if (code === 'NoCommand') {
|
if (code === 'NoCommand') {
|
||||||
/* jsl:pass */
|
/* jsl:pass */
|
||||||
} else if (err.message !== undefined) {
|
} else if (err.name === 'InternalServerError') {
|
||||||
console.error('%s%s: error%s: %s',
|
/*
|
||||||
cli.name,
|
* Internal server error, we want to provide a useful error
|
||||||
(subcmd ? ' ' + subcmd : ''),
|
* message without exposing internals.
|
||||||
(code ? format(' (%s)', code) : ''),
|
*/
|
||||||
(cli.showErrStack ? err.stack : err.message));
|
console.error('%s: internal error. Please try again later, ' +
|
||||||
|
'and contact support in case the error persists.',
|
||||||
// If this is a usage error, attempt to show some usage info.
|
cmdln.nameFromErr(err));
|
||||||
if (['Usage', 'Option'].indexOf(code) !== -1 && subcmd) {
|
} else {
|
||||||
var help = cli.helpFromSubcmd(subcmd);
|
/*
|
||||||
if (help && typeof (help) === 'string') {
|
* If the err has `body.errors`, as some Triton/SDC APIs do per
|
||||||
// Would like a shorter synopsis. Attempt to
|
* // JSSTYLED
|
||||||
// parse it down, somewhat generally. Unfortunately this
|
* https://github.com/joyent/eng/blob/master/docs/index.md#error-handling
|
||||||
// doesn't work for multi-level subcmds, like
|
* then append a one-line summary for each error object.
|
||||||
// `triton rbac subcmd ...`.
|
*/
|
||||||
var usageIdx = help.indexOf('\nUsage:');
|
var bodyErrors = '';
|
||||||
if (usageIdx !== -1) {
|
if (err.body && err.body.errors) {
|
||||||
help = help.slice(usageIdx);
|
err.body.errors.forEach(function (e) {
|
||||||
|
bodyErrors += format('\n %s: %s', e.field, e.code);
|
||||||
|
if (e.message) {
|
||||||
|
bodyErrors += ': ' + e.message;
|
||||||
}
|
}
|
||||||
console.error(help);
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Try to find the most descriptive message to output.
|
||||||
|
*
|
||||||
|
* 1. If there's a message property on the error object, we
|
||||||
|
* assume this is suitable to output to the user.
|
||||||
|
*
|
||||||
|
* 2. Otherwise, if there's an "orignalBody" property, we output
|
||||||
|
* its content per joyent/node-triton#30.
|
||||||
|
*
|
||||||
|
* 3. We fall back to using the error's name as the error
|
||||||
|
* message.
|
||||||
|
*/
|
||||||
|
if (typeof (err.message) === 'string' && err.message !== '') {
|
||||||
|
errMessage = err.message;
|
||||||
|
} else if (err.originalBody !== undefined) {
|
||||||
|
errMessage = err.originalBody.toString();
|
||||||
|
} else {
|
||||||
|
errMessage = err.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('%s: error%s: %s%s',
|
||||||
|
cmdln.nameFromErr(err),
|
||||||
|
(code ? format(' (%s)', code) : ''),
|
||||||
|
(cli.showErrStack ? err.stack : errMessage),
|
||||||
|
bodyErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
errHelp = cmdln.errHelpFromErr(err);
|
||||||
|
if (errHelp) {
|
||||||
|
console.error(errHelp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,10 +801,10 @@ function main(argv) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//---- exports
|
//---- exports
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
CONFIG_DIR: CONFIG_DIR,
|
|
||||||
CLI: CLI,
|
CLI: CLI,
|
||||||
main: main
|
main: main
|
||||||
};
|
};
|
||||||
|
1061
lib/cloudapi2.js
1061
lib/cloudapi2.js
File diff suppressed because it is too large
Load Diff
649
lib/common.js
649
lib/common.js
@ -5,13 +5,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2015 Joyent, Inc.
|
* Copyright (c) 2018, Joyent, Inc.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var assert = require('assert-plus');
|
var assert = require('assert-plus');
|
||||||
var child_process = require('child_process');
|
var child_process = require('child_process');
|
||||||
var crypto = require('crypto');
|
var crypto = require('crypto');
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
|
var getpass = require('getpass');
|
||||||
var os = require('os');
|
var os = require('os');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var read = require('read');
|
var read = require('read');
|
||||||
@ -23,7 +24,8 @@ var wordwrap = require('wordwrap');
|
|||||||
|
|
||||||
var errors = require('./errors'),
|
var errors = require('./errors'),
|
||||||
InternalError = errors.InternalError;
|
InternalError = errors.InternalError;
|
||||||
|
var NETWORK_OBJECT_FIELDS =
|
||||||
|
require('./constants').NETWORK_OBJECT_FIELDS;
|
||||||
|
|
||||||
|
|
||||||
// ---- support stuff
|
// ---- support stuff
|
||||||
@ -90,7 +92,7 @@ function zeroPad(n, width) {
|
|||||||
* raise TypeError trying.
|
* raise TypeError trying.
|
||||||
*
|
*
|
||||||
* @param value {Boolean|String} The input value to convert.
|
* @param value {Boolean|String} The input value to convert.
|
||||||
* @param default_ {Boolean} The default value is `value` is undefined.
|
* @param default_ {Boolean} The default value if `value` is undefined.
|
||||||
* @param errName {String} The context to quote in the possibly
|
* @param errName {String} The context to quote in the possibly
|
||||||
* raised TypeError.
|
* raised TypeError.
|
||||||
*/
|
*/
|
||||||
@ -125,32 +127,138 @@ function jsonStream(arr, stream) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* given an array of key=value pairs, break them into an object
|
* 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
|
||||||
*
|
*
|
||||||
* @param {Array} kvs - an array of key=value pairs
|
* @param {Array} kvs - an array of key=value pairs
|
||||||
* @param {Array} valid (optional) - an array to validate 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'.
|
||||||
*/
|
*/
|
||||||
function kvToObj(kvs, valid) {
|
function jsonPredFromKv(kvs, validKeys, compositionType) {
|
||||||
assert.arrayOfString(kvs, 'kvs');
|
assert.arrayOfString(kvs, 'kvs');
|
||||||
assert.optionalArrayOfString(valid, 'valid');
|
assert.string(compositionType, 'string');
|
||||||
|
assert.ok(compositionType === 'or' || compositionType === 'and',
|
||||||
|
'compositionType');
|
||||||
|
|
||||||
var o = {};
|
var keyName;
|
||||||
for (var i = 0; i < kvs.length; i++) {
|
var predicate = {};
|
||||||
var kv = kvs[i];
|
var parsedKeyValue;
|
||||||
var idx = kv.indexOf('=');
|
var parsedKeyValues;
|
||||||
if (idx === -1)
|
var parseOpts = {
|
||||||
throw new errors.UsageError(format(
|
disableDotted: true,
|
||||||
'invalid filter: "%s" (must be of the form "field=value")',
|
validKeys: validKeys,
|
||||||
kv));
|
failOnEmptyValue: true
|
||||||
var k = kv.slice(0, idx);
|
};
|
||||||
var v = kv.slice(idx + 1);
|
|
||||||
if (valid && valid.indexOf(k) === -1)
|
if (kvs.length === 0) {
|
||||||
throw new errors.UsageError(format(
|
return predicate;
|
||||||
'invalid filter name: "%s" (must be one of "%s")',
|
|
||||||
k, valid.join('", "')));
|
|
||||||
o[k] = v;
|
|
||||||
}
|
}
|
||||||
return o;
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -510,9 +618,9 @@ function promptYesNo(opts_, cb) {
|
|||||||
stdin.on('data', onData);
|
stdin.on('data', onData);
|
||||||
|
|
||||||
function postInput() {
|
function postInput() {
|
||||||
|
stdout.write('\n');
|
||||||
stdin.setRawMode(false);
|
stdin.setRawMode(false);
|
||||||
stdin.pause();
|
stdin.pause();
|
||||||
stdin.write('\n');
|
|
||||||
stdin.removeListener('data', onData);
|
stdin.removeListener('data', onData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -624,7 +732,7 @@ function promptEnter(prompt, cb) {
|
|||||||
* string "cancelled".
|
* string "cancelled".
|
||||||
*/
|
*/
|
||||||
function promptField(field, cb) {
|
function promptField(field, cb) {
|
||||||
var wrap = wordwrap(Math.min(process.stdout.columns, 78));
|
var wrap = wordwrap(Math.min(process.stdout.columns, 80));
|
||||||
|
|
||||||
var validate = field.validate;
|
var validate = field.validate;
|
||||||
if (!validate && field.required) {
|
if (!validate && field.required) {
|
||||||
@ -670,12 +778,119 @@ function promptField(field, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (field.desc) {
|
if (field.desc) {
|
||||||
console.log(ansiStylize(wrap(field.desc), 'bold'));
|
// 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'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
attempt();
|
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
|
* Edit the given text in $EDITOR (defaulting to `vi`) and return the edited
|
||||||
* text.
|
* text.
|
||||||
@ -686,18 +901,26 @@ function promptField(field, cb) {
|
|||||||
function editInEditor(opts, cb) {
|
function editInEditor(opts, cb) {
|
||||||
assert.string(opts.text, 'opts.text');
|
assert.string(opts.text, 'opts.text');
|
||||||
assert.optionalString(opts.filename, 'opts.filename');
|
assert.optionalString(opts.filename, 'opts.filename');
|
||||||
|
assert.optionalObject(opts.log, 'opts.log');
|
||||||
assert.func(cb, 'cb');
|
assert.func(cb, 'cb');
|
||||||
|
|
||||||
var tmpPath = path.resolve(os.tmpDir(),
|
var tmpPath = path.resolve(os.tmpDir(),
|
||||||
format('triton-%s-edit-%s', process.pid, opts.filename || 'text'));
|
format('triton-%s-edit-%s', process.pid, opts.filename || 'text'));
|
||||||
fs.writeFileSync(tmpPath, opts.text, 'utf8');
|
fs.writeFileSync(tmpPath, opts.text, 'utf8');
|
||||||
|
|
||||||
// TODO: want '-f' opt for vi? What about others?
|
|
||||||
var editor = process.env.EDITOR || '/usr/bin/vi';
|
var editor = process.env.EDITOR || '/usr/bin/vi';
|
||||||
var kid = child_process.spawn(editor, [tmpPath], {stdio: 'inherit'});
|
var argv = argvFromLine(format('%s "%s"', editor, tmpPath));
|
||||||
kid.on('exit', function (code) {
|
if (opts.log) {
|
||||||
if (code) {
|
opts.log.trace({argv: argv}, 'editInEditor argv');
|
||||||
return (cb(code));
|
}
|
||||||
|
|
||||||
|
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 afterText = fs.readFileSync(tmpPath, 'utf8');
|
var afterText = fs.readFileSync(tmpPath, 'utf8');
|
||||||
fs.unlinkSync(tmpPath);
|
fs.unlinkSync(tmpPath);
|
||||||
@ -741,6 +964,18 @@ 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) {
|
function indent(s, indentation) {
|
||||||
if (!indentation) {
|
if (!indentation) {
|
||||||
indentation = ' ';
|
indentation = ' ';
|
||||||
@ -832,8 +1067,7 @@ function execPlus(args, cb) {
|
|||||||
+ '\tstdout:\n%s\n'
|
+ '\tstdout:\n%s\n'
|
||||||
+ '\tstderr:\n%s',
|
+ '\tstderr:\n%s',
|
||||||
cmd, err.code, stdout.trim(), stderr.trim());
|
cmd, err.code, stdout.trim(), stderr.trim());
|
||||||
cb(new errors.InternalError({message: msg, cause: err}),
|
cb(new errors.InternalError(err, msg), stdout, stderr);
|
||||||
stdout, stderr);
|
|
||||||
} else {
|
} else {
|
||||||
cb(null, stdout, stderr);
|
cb(null, stdout, stderr);
|
||||||
}
|
}
|
||||||
@ -905,6 +1139,10 @@ function tildeSync(s) {
|
|||||||
* - @param typeHintFromKey {Object} Optional. Type hints for input keys.
|
* - @param typeHintFromKey {Object} Optional. Type hints for input keys.
|
||||||
* E.g. if parsing 'foo=false' and `typeHintFromKey={foo: 'string'}`,
|
* E.g. if parsing 'foo=false' and `typeHintFromKey={foo: 'string'}`,
|
||||||
* then we do NOT parse it to a boolean `false`.
|
* 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)
|
function objFromKeyValueArgs(args, opts)
|
||||||
{
|
{
|
||||||
@ -912,45 +1150,30 @@ function objFromKeyValueArgs(args, opts)
|
|||||||
assert.optionalObject(opts, 'opts');
|
assert.optionalObject(opts, 'opts');
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
assert.optionalBool(opts.disableDotted, 'opts.disableDotted');
|
assert.optionalBool(opts.disableDotted, 'opts.disableDotted');
|
||||||
|
assert.optionalBool(opts.disableTypeConversions,
|
||||||
|
'opts.disableTypeConversions');
|
||||||
assert.optionalObject(opts.typeHintFromKey, opts.typeHintFromKey);
|
assert.optionalObject(opts.typeHintFromKey, opts.typeHintFromKey);
|
||||||
var typeHintFromKey = opts.typeHintFromKey || {};
|
assert.optionalBool(opts.failOnEmptyValue, 'opts.failOnEmptyValue');
|
||||||
|
|
||||||
var obj = {};
|
var obj = {};
|
||||||
args.forEach(function (arg) {
|
args.forEach(function (arg) {
|
||||||
var kv = strsplit(arg, '=', 2);
|
var parsedKeyValue = _parseKeyValue(arg, opts.validKeys, {
|
||||||
if (kv.length < 2) {
|
typeHintFromKey: opts.typeHintFromKey,
|
||||||
throw new TypeError(format('invalid key=value argument: "%s"',
|
disableTypeConversions: opts.disableTypeConversions,
|
||||||
arg));
|
failOnEmptyValue: opts.failOnEmptyValue
|
||||||
}
|
});
|
||||||
|
|
||||||
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) {
|
if (opts.disableDotted) {
|
||||||
obj[k] = v;
|
obj[parsedKeyValue.key] = parsedKeyValue.value;
|
||||||
} else {
|
} else {
|
||||||
var dotted = strsplit(k, '.', 2);
|
var dotted = strsplit(parsedKeyValue.key, '.', 2);
|
||||||
if (dotted.length > 1) {
|
if (dotted.length > 1) {
|
||||||
if (!obj[dotted[0]]) {
|
if (!obj[dotted[0]]) {
|
||||||
obj[dotted[0]] = {};
|
obj[dotted[0]] = {};
|
||||||
}
|
}
|
||||||
obj[dotted[0]][dotted[1]] = v;
|
obj[dotted[0]][dotted[1]] = parsedKeyValue.value;
|
||||||
} else {
|
} else {
|
||||||
obj[k] = v;
|
obj[parsedKeyValue.key] = parsedKeyValue.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -958,6 +1181,298 @@ function objFromKeyValueArgs(args, opts)
|
|||||||
return obj;
|
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
|
//---- exports
|
||||||
@ -969,7 +1484,6 @@ module.exports = {
|
|||||||
zeroPad: zeroPad,
|
zeroPad: zeroPad,
|
||||||
boolFromString: boolFromString,
|
boolFromString: boolFromString,
|
||||||
jsonStream: jsonStream,
|
jsonStream: jsonStream,
|
||||||
kvToObj: kvToObj,
|
|
||||||
longAgo: longAgo,
|
longAgo: longAgo,
|
||||||
isUUID: isUUID,
|
isUUID: isUUID,
|
||||||
humanDurationFromMs: humanDurationFromMs,
|
humanDurationFromMs: humanDurationFromMs,
|
||||||
@ -983,14 +1497,25 @@ module.exports = {
|
|||||||
promptYesNo: promptYesNo,
|
promptYesNo: promptYesNo,
|
||||||
promptEnter: promptEnter,
|
promptEnter: promptEnter,
|
||||||
promptField: promptField,
|
promptField: promptField,
|
||||||
|
promptPassphraseUnlockKey: promptPassphraseUnlockKey,
|
||||||
|
cliSetupTritonApi: cliSetupTritonApi,
|
||||||
editInEditor: editInEditor,
|
editInEditor: editInEditor,
|
||||||
ansiStylize: ansiStylize,
|
ansiStylize: ansiStylize,
|
||||||
|
ansiStylizeTty: ansiStylizeTty,
|
||||||
indent: indent,
|
indent: indent,
|
||||||
chomp: chomp,
|
chomp: chomp,
|
||||||
generatePassword: generatePassword,
|
generatePassword: generatePassword,
|
||||||
execPlus: execPlus,
|
execPlus: execPlus,
|
||||||
deepEqual: deepEqual,
|
deepEqual: deepEqual,
|
||||||
tildeSync: tildeSync,
|
tildeSync: tildeSync,
|
||||||
objFromKeyValueArgs: objFromKeyValueArgs
|
objFromKeyValueArgs: objFromKeyValueArgs,
|
||||||
|
argvFromLine: argvFromLine,
|
||||||
|
jsonPredFromKv: jsonPredFromKv,
|
||||||
|
monotonicTimeDiffMs: monotonicTimeDiffMs,
|
||||||
|
readStdin: readStdin,
|
||||||
|
validateObject: validateObject,
|
||||||
|
ipv4ToLong: ipv4ToLong,
|
||||||
|
parseNicStr: parseNicStr,
|
||||||
|
imageRepr: imageRepr
|
||||||
};
|
};
|
||||||
// vim: set softtabstop=4 shiftwidth=4:
|
// vim: set softtabstop=4 shiftwidth=4:
|
||||||
|
113
lib/config.js
113
lib/config.js
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* Copyright 2017 Joyent, Inc.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -49,6 +49,8 @@ var OVERRIDE_NAMES = []; // config object keys to do a one-level deep override
|
|||||||
// TODO: use this const to create the "Configuration" docs table.
|
// TODO: use this const to create the "Configuration" docs table.
|
||||||
var CONFIG_VAR_NAMES = [
|
var CONFIG_VAR_NAMES = [
|
||||||
'profile',
|
'profile',
|
||||||
|
// Intentionally exclude 'oldProfile' so that it isn't manually set.
|
||||||
|
// 'oldProfile',
|
||||||
'cacheDir'
|
'cacheDir'
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -59,7 +61,9 @@ var PROFILE_FIELDS = {
|
|||||||
account: true,
|
account: true,
|
||||||
keyId: true,
|
keyId: true,
|
||||||
insecure: true,
|
insecure: true,
|
||||||
user: true
|
user: true,
|
||||||
|
roles: true,
|
||||||
|
actAsAccount: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -139,19 +143,20 @@ function loadConfig(opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function setConfigVar(opts, cb) {
|
function setConfigVars(opts, cb) {
|
||||||
assert.object(opts, 'opts');
|
assert.object(opts, 'opts');
|
||||||
assert.string(opts.configDir, 'opts.configDir');
|
assert.string(opts.configDir, 'opts.configDir');
|
||||||
assert.string(opts.name, 'opts.name');
|
assert.object(opts.vars, 'opts.vars');
|
||||||
assert.string(opts.value, 'opts.value');
|
Object.keys(opts.vars).forEach(function (name) {
|
||||||
assert.ok(opts.name.indexOf('.') === -1,
|
assert.ok(name.indexOf('.') === -1,
|
||||||
'dotted config name not yet supported');
|
'dotted config name not yet supported');
|
||||||
assert.ok(CONFIG_VAR_NAMES.indexOf(opts.name) !== -1,
|
assert.ok(CONFIG_VAR_NAMES.indexOf(name) !== -1,
|
||||||
'unknown config var name: ' + opts.name);
|
'unknown config var name: ' + name);
|
||||||
|
});
|
||||||
|
|
||||||
var configPath = configPathFromDir(opts.configDir);
|
var configPath = configPathFromDir(opts.configDir);
|
||||||
|
|
||||||
var config;
|
var config;
|
||||||
|
|
||||||
vasync.pipeline({funcs: [
|
vasync.pipeline({funcs: [
|
||||||
function loadExisting(_, next) {
|
function loadExisting(_, next) {
|
||||||
fs.exists(configPath, function (exists) {
|
fs.exists(configPath, function (exists) {
|
||||||
@ -183,8 +188,23 @@ function setConfigVar(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) {
|
function updateAndSave(_, next) {
|
||||||
config[opts.name] = opts.value;
|
Object.keys(opts.vars).forEach(function (name) {
|
||||||
|
config[name] = opts.vars[name];
|
||||||
|
});
|
||||||
fs.writeFile(configPath, JSON.stringify(config, null, 4), next);
|
fs.writeFile(configPath, JSON.stringify(config, null, 4), next);
|
||||||
}
|
}
|
||||||
]}, cb);
|
]}, cb);
|
||||||
@ -201,19 +221,21 @@ function validateProfile(profile, profilePath) {
|
|||||||
try {
|
try {
|
||||||
assert.string(profile.name, 'profile.name');
|
assert.string(profile.name, 'profile.name');
|
||||||
assert.string(profile.url,
|
assert.string(profile.url,
|
||||||
profile.name === 'env' ? 'TRITON_URL or SDC_URL' : 'profile.url');
|
profile.name === 'env' ? 'SC_URL' : 'profile.url');
|
||||||
assert.string(profile.account,
|
assert.string(profile.account,
|
||||||
profile.name === 'env' ? 'TRITON_ACCOUNT or SDC_ACCOUNT'
|
profile.name === 'env' ? 'SC_ACCOUNT'
|
||||||
: 'profile.account');
|
: 'profile.account');
|
||||||
assert.string(profile.keyId,
|
assert.string(profile.keyId,
|
||||||
profile.name === 'env' ? 'TRITON_KEY_ID or SDC_KEY_ID'
|
profile.name === 'env' ? 'SC_KEY_ID'
|
||||||
: 'profile.keyId');
|
: 'profile.keyId');
|
||||||
assert.optionalBool(profile.insecure,
|
assert.optionalBool(profile.insecure,
|
||||||
profile.name === 'env' ? 'TRITON_INSECURE or SDC_INSECURE'
|
profile.name === 'env' ? 'SC_INSECURE'
|
||||||
: 'profile.insecure');
|
: 'profile.insecure');
|
||||||
assert.optionalString(profile.user,
|
assert.optionalString(profile.user,
|
||||||
profile.name === 'env' ? 'TRITON_USER or SDC_USER'
|
profile.name === 'env' ? 'SC_USER'
|
||||||
: 'profile.user');
|
: 'profile.user');
|
||||||
|
assert.optionalString(profile.actAsAccount, 'profile.actAsAccount');
|
||||||
|
assert.optionalArrayOfString(profile.roles, 'profile.roles');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
var msg = format('invalid %sprofile%s: %s',
|
var msg = format('invalid %sprofile%s: %s',
|
||||||
profile.name ? '"' + profile.name + '" ' : '',
|
profile.name ? '"' + profile.name + '" ' : '',
|
||||||
@ -249,44 +271,44 @@ function validateProfile(profile, profilePath) {
|
|||||||
* @throws {errors.ConfigError} If the profile defined by the environment is
|
* @throws {errors.ConfigError} If the profile defined by the environment is
|
||||||
* invalid.
|
* invalid.
|
||||||
*/
|
*/
|
||||||
function _loadEnvProfile() {
|
function _loadEnvProfile(profileOverrides) {
|
||||||
var envProfile = {
|
var envProfile = {
|
||||||
name: 'env'
|
name: 'env'
|
||||||
};
|
};
|
||||||
|
|
||||||
envProfile.account = process.env.TRITON_ACCOUNT || process.env.SDC_ACCOUNT;
|
envProfile.account = process.env.SC_ACCOUNT;
|
||||||
var user = process.env.TRITON_USER || process.env.SDC_USER;
|
var user = process.env.SC_USER;
|
||||||
if (user) {
|
if (user) {
|
||||||
envProfile.user = user;
|
envProfile.user = user;
|
||||||
}
|
}
|
||||||
envProfile.url = process.env.TRITON_URL || process.env.SDC_URL;
|
envProfile.url = process.env.SC_URL;
|
||||||
envProfile.keyId = process.env.TRITON_KEY_ID || process.env.SDC_KEY_ID;
|
envProfile.keyId = process.env.SC_KEY_ID;
|
||||||
|
|
||||||
|
if (process.env.SC_TLS_INSECURE) {
|
||||||
|
envProfile.insecure = common.boolFromString(
|
||||||
|
process.env.SC_TLS_INSECURE, undefined, 'SC_TLS_INSECURE');
|
||||||
|
} else if (process.env.SC_TESTING) {
|
||||||
|
// For compatibility with the legacy behavior of the smartdc
|
||||||
|
// tools, *any* set value but the empty string is considered true.
|
||||||
|
envProfile.insecure = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var attr in profileOverrides) {
|
||||||
|
envProfile[attr] = profileOverrides[attr];
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If none of the above envvars are defined, then there is no env profile.
|
* If missing any of the required vars, then there is no env profile.
|
||||||
*/
|
*/
|
||||||
if (!envProfile.account && !envProfile.user && !envProfile.url &&
|
if (!envProfile.account || !envProfile.url || !envProfile.keyId) {
|
||||||
!envProfile.keyId)
|
|
||||||
{
|
|
||||||
return null;
|
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');
|
validateProfile(envProfile, 'environment variables');
|
||||||
|
|
||||||
return envProfile;
|
return envProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _profileFromPath(profilePath, name) {
|
function _profileFromPath(profilePath, name, profileOverrides) {
|
||||||
if (! fs.existsSync(profilePath)) {
|
if (! fs.existsSync(profilePath)) {
|
||||||
throw new errors.ConfigError('no such profile: ' + name);
|
throw new errors.ConfigError('no such profile: ' + name);
|
||||||
}
|
}
|
||||||
@ -305,6 +327,9 @@ function _profileFromPath(profilePath, name) {
|
|||||||
}
|
}
|
||||||
profile.name = name;
|
profile.name = name;
|
||||||
|
|
||||||
|
for (var attr in profileOverrides) {
|
||||||
|
profile[attr] = profileOverrides[attr];
|
||||||
|
}
|
||||||
validateProfile(profile, profilePath);
|
validateProfile(profile, profilePath);
|
||||||
|
|
||||||
return profile;
|
return profile;
|
||||||
@ -314,12 +339,13 @@ function _profileFromPath(profilePath, name) {
|
|||||||
function loadProfile(opts) {
|
function loadProfile(opts) {
|
||||||
assert.string(opts.name, 'opts.name');
|
assert.string(opts.name, 'opts.name');
|
||||||
assert.optionalString(opts.configDir, 'opts.configDir');
|
assert.optionalString(opts.configDir, 'opts.configDir');
|
||||||
|
assert.optionalObject(opts.profileOverrides, 'opts.profileOverrides');
|
||||||
|
|
||||||
if (opts.name === 'env') {
|
if (opts.name === 'env') {
|
||||||
var envProfile = _loadEnvProfile();
|
var envProfile = _loadEnvProfile(opts.profileOverrides);
|
||||||
if (!envProfile) {
|
if (!envProfile) {
|
||||||
throw new errors.ConfigError('could not load "env" profile '
|
throw new errors.ConfigError('could not load "env" profile '
|
||||||
+ '(missing TRITON_*, or SDC_*, environment variables)');
|
+ '(missing SC_* environment variables)');
|
||||||
}
|
}
|
||||||
return envProfile;
|
return envProfile;
|
||||||
} else if (!opts.configDir) {
|
} else if (!opts.configDir) {
|
||||||
@ -329,17 +355,18 @@ function loadProfile(opts) {
|
|||||||
var profilePath = path.resolve(
|
var profilePath = path.resolve(
|
||||||
common.tildeSync(opts.configDir), 'profiles.d',
|
common.tildeSync(opts.configDir), 'profiles.d',
|
||||||
opts.name + '.json');
|
opts.name + '.json');
|
||||||
return _profileFromPath(profilePath, opts.name);
|
return _profileFromPath(profilePath, opts.name, opts.profileOverrides);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadAllProfiles(opts) {
|
function loadAllProfiles(opts) {
|
||||||
assert.string(opts.configDir, 'opts.configDir');
|
assert.string(opts.configDir, 'opts.configDir');
|
||||||
assert.object(opts.log, 'opts.log');
|
assert.object(opts.log, 'opts.log');
|
||||||
|
assert.optionalObject(opts.profileOverrides, 'opts.profileOverrides');
|
||||||
|
|
||||||
var profiles = [];
|
var profiles = [];
|
||||||
|
|
||||||
var envProfile = _loadEnvProfile();
|
var envProfile = _loadEnvProfile(opts.profileOverrides);
|
||||||
if (envProfile) {
|
if (envProfile) {
|
||||||
profiles.push(envProfile);
|
profiles.push(envProfile);
|
||||||
}
|
}
|
||||||
@ -406,7 +433,7 @@ function saveProfileSync(opts) {
|
|||||||
mkdirp.sync(path.dirname(profilePath));
|
mkdirp.sync(path.dirname(profilePath));
|
||||||
}
|
}
|
||||||
fs.writeFileSync(profilePath, JSON.stringify(toSave, null, 4), 'utf8');
|
fs.writeFileSync(profilePath, JSON.stringify(toSave, null, 4), 'utf8');
|
||||||
console.log('Saved profile "%s"', name);
|
console.log('Saved profile "%s".', name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -414,7 +441,7 @@ function saveProfileSync(opts) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
loadConfig: loadConfig,
|
loadConfig: loadConfig,
|
||||||
setConfigVar: setConfigVar,
|
setConfigVars: setConfigVars,
|
||||||
|
|
||||||
validateProfile: validateProfile,
|
validateProfile: validateProfile,
|
||||||
loadProfile: loadProfile,
|
loadProfile: loadProfile,
|
||||||
|
121
lib/constants.js
Normal file
121
lib/constants.js
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
@ -21,28 +21,35 @@ function do_get(subcmd, opts, args, callback) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.top.tritonapi.cloudapi.getAccount(function (err, account) {
|
var tritonapi = this.top.tritonapi;
|
||||||
if (err) {
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
callback(err);
|
if (setupErr) {
|
||||||
|
callback(setupErr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
tritonapi.cloudapi.getAccount(function (err, account) {
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
console.log(JSON.stringify(account));
|
console.log(JSON.stringify(account));
|
||||||
} else {
|
} else {
|
||||||
// pretty print
|
// pretty print
|
||||||
var dates = ['updated', 'created'];
|
var dates = ['updated', 'created'];
|
||||||
Object.keys(account).forEach(function (key) {
|
Object.keys(account).forEach(function (key) {
|
||||||
var val = account[key];
|
var val = account[key];
|
||||||
if (dates.indexOf(key) >= 0) {
|
if (dates.indexOf(key) >= 0) {
|
||||||
console.log('%s: %s (%s)', key, val,
|
console.log('%s: %s (%s)', key, val,
|
||||||
common.longAgo(new Date(val)));
|
common.longAgo(new Date(val)));
|
||||||
} else {
|
} else {
|
||||||
console.log('%s: %s', key, val);
|
console.log('%s: %s', key, val);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
callback();
|
callback();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,13 +65,15 @@ do_get.options = [
|
|||||||
help: 'JSON output.'
|
help: 'JSON output.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
do_get.help = (
|
|
||||||
'Show account information\n'
|
do_get.synopses = ['{{name}} {{cmd}}'];
|
||||||
+ '\n'
|
|
||||||
+ 'Usage:\n'
|
do_get.help = [
|
||||||
+ ' {{name}} get\n'
|
'Show account information',
|
||||||
+ '\n'
|
'',
|
||||||
+ '{{options}}'
|
'{{usage}}',
|
||||||
);
|
'',
|
||||||
|
'{{options}}'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
module.exports = do_get;
|
module.exports = do_get;
|
||||||
|
@ -30,7 +30,9 @@ function do_update(subcmd, opts, args, callback) {
|
|||||||
var log = this.log;
|
var log = this.log;
|
||||||
var tritonapi = this.top.tritonapi;
|
var tritonapi = this.top.tritonapi;
|
||||||
|
|
||||||
vasync.pipeline({arg: {}, funcs: [
|
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||||
|
common.cliSetupTritonApi,
|
||||||
|
|
||||||
function gatherDataArgs(ctx, next) {
|
function gatherDataArgs(ctx, next) {
|
||||||
if (opts.file) {
|
if (opts.file) {
|
||||||
next();
|
next();
|
||||||
@ -70,12 +72,8 @@ function do_update(subcmd, opts, args, callback) {
|
|||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var stdin = '';
|
|
||||||
process.stdin.resume();
|
common.readStdin(function gotStdin(stdin) {
|
||||||
process.stdin.on('data', function (chunk) {
|
|
||||||
stdin += chunk;
|
|
||||||
});
|
|
||||||
process.stdin.on('end', function () {
|
|
||||||
try {
|
try {
|
||||||
ctx.data = JSON.parse(stdin);
|
ctx.data = JSON.parse(stdin);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -90,36 +88,18 @@ function do_update(subcmd, opts, args, callback) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
function validateIt(ctx, next) {
|
function validateIt(ctx, next) {
|
||||||
var keys = Object.keys(ctx.data);
|
try {
|
||||||
for (var i = 0; i < keys.length; i++) {
|
common.validateObject(ctx.data, UPDATE_ACCOUNT_FIELDS);
|
||||||
var key = keys[i];
|
} catch (e) {
|
||||||
var value = ctx.data[key];
|
next(e);
|
||||||
var type = UPDATE_ACCOUNT_FIELDS[key];
|
return;
|
||||||
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();
|
next();
|
||||||
},
|
},
|
||||||
|
|
||||||
function updateAway(ctx, next) {
|
function updateAway(ctx, next) {
|
||||||
var keys = Object.keys(ctx.data);
|
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) {
|
tritonapi.cloudapi.updateAccount(ctx.data, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -148,13 +128,17 @@ do_update.options = [
|
|||||||
'JSON from stdin.'
|
'JSON from stdin.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_update.synopses = [
|
||||||
|
'{{name}} {{cmd}} [FIELD=VALUE ...]',
|
||||||
|
'{{name}} {{cmd}} -f JSON-FILE'
|
||||||
|
];
|
||||||
|
|
||||||
do_update.help = [
|
do_update.help = [
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
'Update account information',
|
'Update account information',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} update [FIELD=VALUE ...]',
|
|
||||||
' {{name}} update -f JSON-FILE',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}',
|
'{{options}}',
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ function AccountCLI(top) {
|
|||||||
name: top.name + ' account',
|
name: top.name + ' account',
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
desc: [
|
desc: [
|
||||||
'Get and update your Triton account.'
|
'Get and update your Spearhead account.'
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
/* END JSSTYLED */
|
/* END JSSTYLED */
|
||||||
helpOpts: {
|
helpOpts: {
|
||||||
|
@ -11,7 +11,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
var http = require('http');
|
var http = require('http');
|
||||||
|
var vasync = require('vasync');
|
||||||
|
|
||||||
|
var common = require('./common');
|
||||||
var errors = require('./errors');
|
var errors = require('./errors');
|
||||||
|
|
||||||
|
|
||||||
@ -57,32 +59,39 @@ function do_cloudapi(subcmd, opts, args, callback) {
|
|||||||
reqOpts.data = JSON.parse(opts.data);
|
reqOpts.data = JSON.parse(opts.data);
|
||||||
} catch (parseErr) {
|
} catch (parseErr) {
|
||||||
callback(new errors.TritonError(parseErr,
|
callback(new errors.TritonError(parseErr,
|
||||||
'given <data> is not valid JSON: ' + parseErr.message));
|
'given DATA is not valid JSON: ' + parseErr.message));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tritonapi.cloudapi._request(reqOpts, function (err, req, res, body) {
|
vasync.pipeline({arg: {cli: this}, funcs: [
|
||||||
if (err) {
|
common.cliSetupTritonApi,
|
||||||
callback(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')
|
function callCloudapi(ctx, next) {
|
||||||
console.log(JSON.stringify(body, null, 4));
|
var cloudapi = ctx.cli.tritonapi.cloudapi;
|
||||||
callback();
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
]}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
do_cloudapi.options = [
|
do_cloudapi.options = [
|
||||||
@ -94,13 +103,13 @@ do_cloudapi.options = [
|
|||||||
{
|
{
|
||||||
names: ['method', 'X'],
|
names: ['method', 'X'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
helpArg: '<method>',
|
helpArg: 'METHOD',
|
||||||
help: 'Request method to use. Default is "GET".'
|
help: 'Request method to use. Default is "GET".'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
names: ['header', 'H'],
|
names: ['header', 'H'],
|
||||||
type: 'arrayOfString',
|
type: 'arrayOfString',
|
||||||
helpArg: '<header>',
|
helpArg: 'HEADER',
|
||||||
help: 'Headers to send with request.'
|
help: 'Headers to send with request.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -111,19 +120,25 @@ do_cloudapi.options = [
|
|||||||
{
|
{
|
||||||
names: ['data', 'd'],
|
names: ['data', 'd'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
helpArg: '<data>',
|
helpArg: 'DATA',
|
||||||
help: 'Add POST data. This must be valid JSON.'
|
help: 'Add POST data. This must be valid JSON.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
do_cloudapi.help = (
|
|
||||||
'Raw cloudapi request.\n'
|
do_cloudapi.synopses = [
|
||||||
+ '\n'
|
'{{name}} {{cmd}} [-X METHOD] [-H HEADER=VAL] [-d DATA] ENDPOINT'
|
||||||
+ 'Usage:\n'
|
];
|
||||||
+ ' {{name}} cloudapi [-X <method>] [-H <header=value>] \\\n'
|
|
||||||
+ ' [-d <data>] <endpoint>\n'
|
do_cloudapi.help = [
|
||||||
+ '\n'
|
'Raw cloudapi request.',
|
||||||
+ '{{options}}'
|
'',
|
||||||
);
|
'{{usage}}',
|
||||||
|
'',
|
||||||
|
'{{options}}',
|
||||||
|
'Examples:',
|
||||||
|
' {{name}} {{cmd}} /--ping',
|
||||||
|
' {{name}} {{cmd}} /my/machines'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
do_cloudapi.hidden = true;
|
do_cloudapi.hidden = true;
|
||||||
|
|
||||||
|
@ -66,12 +66,16 @@ do_completion.options = [
|
|||||||
do_completion.help = [
|
do_completion.help = [
|
||||||
'Emit bash completion. See help for installation.',
|
'Emit bash completion. See help for installation.',
|
||||||
'',
|
'',
|
||||||
'Installation:',
|
'Installation (Mac):',
|
||||||
' {{name}} completion > /usr/local/etc/bash_completion.d/{{name}} # Mac',
|
' {{name}} completion > /usr/local/etc/bash_completion.d/{{name}} \\',
|
||||||
' sudo {{name}} completion > /etc/bash_completion.d/{{name}} # Linux',
|
' && source /usr/local/etc/bash_completion.d/{{name}}',
|
||||||
|
'',
|
||||||
|
'Installation (Linux):',
|
||||||
|
' sudo {{name}} completion > /etc/bash_completion.d/{{name}} \\',
|
||||||
|
' && source /etc/bash_completion.d/{{name}}',
|
||||||
'',
|
'',
|
||||||
'Alternative installation:',
|
'Alternative installation:',
|
||||||
' {{name}} completion > ~/.{{name}}.completion',
|
' {{name}} completion > ~/.{{name}}.completion # or to whatever path',
|
||||||
' echo "source ~/.{{name}}.completion" >> ~/.bashrc',
|
' echo "source ~/.{{name}}.completion" >> ~/.bashrc',
|
||||||
'',
|
'',
|
||||||
'{{options}}'
|
'{{options}}'
|
||||||
|
@ -5,11 +5,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* Copyright 2017 Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton create ...` bwcompat shortcut for `triton instance create ...`.
|
* `triton create ...` bwcompat shortcut for `triton instance create ...`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var targ = require('./do_instance/do_create');
|
||||||
|
|
||||||
function do_create(subcmd, opts, args, callback) {
|
function do_create(subcmd, opts, args, callback) {
|
||||||
this.handlerFromSubcmd('instance').dispatch({
|
this.handlerFromSubcmd('instance').dispatch({
|
||||||
subcmd: 'create',
|
subcmd: 'create',
|
||||||
@ -18,7 +20,10 @@ function do_create(subcmd, opts, args, callback) {
|
|||||||
}, callback);
|
}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
do_create.help = 'A shortcut for "triton instance create".';
|
do_create.help = 'A shortcut for "spearhead instance create".\n' + targ.help;
|
||||||
do_create.options = require('./do_instance/do_create').options;
|
do_create.helpOpts = targ.helpOpts;
|
||||||
|
do_create.synopses = targ.synopses;
|
||||||
|
do_create.options = targ.options;
|
||||||
|
do_create.completionArgtypes = targ.completionArgtypes;
|
||||||
|
|
||||||
module.exports = do_create;
|
module.exports = do_create;
|
||||||
|
@ -31,36 +31,43 @@ function do_datacenters(subcmd, opts, args, callback) {
|
|||||||
|
|
||||||
var columns = opts.o.split(',');
|
var columns = opts.o.split(',');
|
||||||
var sort = opts.s.split(',');
|
var sort = opts.s.split(',');
|
||||||
|
var tritonapi = this.tritonapi;
|
||||||
|
|
||||||
this.tritonapi.cloudapi.listDatacenters(function (err, datacenters) {
|
common.cliSetupTritonApi({cli: this}, function onSetup(setupErr) {
|
||||||
if (err) {
|
if (setupErr) {
|
||||||
callback(err);
|
callback(setupErr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
tritonapi.cloudapi.listDatacenters(function (err, datacenters) {
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
console.log(JSON.stringify(datacenters));
|
console.log(JSON.stringify(datacenters));
|
||||||
} else {
|
} else {
|
||||||
/*
|
/*
|
||||||
* datacenters are returned in the form of:
|
* datacenters are returned in the form of:
|
||||||
* {name: 'url', name2: 'url2', ...}
|
* {name: 'url', name2: 'url2', ...}
|
||||||
* we "normalize" them for use by tabula by making them an array
|
* we "normalize" them for use by tabula by making them an array
|
||||||
*/
|
*/
|
||||||
var dcs = [];
|
var dcs = [];
|
||||||
Object.keys(datacenters).forEach(function (key) {
|
Object.keys(datacenters).forEach(function (key) {
|
||||||
dcs.push({
|
dcs.push({
|
||||||
name: key,
|
name: key,
|
||||||
url: datacenters[key]
|
url: datacenters[key]
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
tabula(dcs, {
|
||||||
tabula(dcs, {
|
skipHeader: opts.H,
|
||||||
skipHeader: opts.H,
|
columns: columns,
|
||||||
columns: columns,
|
sort: sort,
|
||||||
sort: sort,
|
dottedLookup: true
|
||||||
dottedLookup: true
|
});
|
||||||
});
|
}
|
||||||
}
|
callback();
|
||||||
callback();
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,15 +82,16 @@ do_datacenters.options = [
|
|||||||
sortDefault: sortDefault
|
sortDefault: sortDefault
|
||||||
}));
|
}));
|
||||||
|
|
||||||
do_datacenters.help = (
|
do_datacenters.synopses = ['{{name}} {{cmd}}'];
|
||||||
'Show datacenters in this cloud.\n'
|
|
||||||
+ 'A "cloud" is a set of related datacenters that share account\n'
|
do_datacenters.help = [
|
||||||
+ 'information.\n'
|
'Show datacenters in this cloud.',
|
||||||
+ '\n'
|
'A "cloud" is a set of related datacenters that share account',
|
||||||
+ 'Usage:\n'
|
'information.',
|
||||||
+ ' {{name}} datacenters\n'
|
'',
|
||||||
+ '\n'
|
'{{usage}}',
|
||||||
+ '{{options}}'
|
'',
|
||||||
);
|
'{{options}}'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
module.exports = do_datacenters;
|
module.exports = do_datacenters;
|
||||||
|
@ -5,11 +5,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* Copyright 2017 Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton delete ...` bwcompat shortcut for `triton instance delete ...`.
|
* `triton delete ...` bwcompat shortcut for `triton instance delete ...`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var targ = require('./do_instance/do_delete');
|
||||||
|
|
||||||
function do_delete(subcmd, opts, args, callback) {
|
function do_delete(subcmd, opts, args, callback) {
|
||||||
this.handlerFromSubcmd('instance').dispatch({
|
this.handlerFromSubcmd('instance').dispatch({
|
||||||
subcmd: 'delete',
|
subcmd: 'delete',
|
||||||
@ -18,8 +20,11 @@ function do_delete(subcmd, opts, args, callback) {
|
|||||||
}, callback);
|
}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
do_delete.help = 'A shortcut for "triton instance delete".';
|
do_delete.help = 'A shortcut for "spearhead instance delete".\n' + targ.help;
|
||||||
|
do_delete.synopses = targ.synopses;
|
||||||
|
do_delete.options = targ.options;
|
||||||
|
do_delete.completionArgtypes = targ.completionArgtypes;
|
||||||
|
|
||||||
do_delete.aliases = ['rm'];
|
do_delete.aliases = ['rm'];
|
||||||
do_delete.options = require('./do_instance/do_delete').options;
|
|
||||||
|
|
||||||
module.exports = do_delete;
|
module.exports = do_delete;
|
||||||
|
180
lib/do_env.js
180
lib/do_env.js
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2015 Joyent Inc.
|
* Copyright 2016 Joyent Inc.
|
||||||
*
|
*
|
||||||
* `triton env ...`
|
* `triton env ...`
|
||||||
*/
|
*/
|
||||||
@ -7,6 +7,7 @@
|
|||||||
var assert = require('assert-plus');
|
var assert = require('assert-plus');
|
||||||
var format = require('util').format;
|
var format = require('util').format;
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
var strsplit = require('strsplit');
|
var strsplit = require('strsplit');
|
||||||
var sshpk = require('sshpk');
|
var sshpk = require('sshpk');
|
||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
@ -18,6 +19,7 @@ var mod_config = require('./config');
|
|||||||
|
|
||||||
|
|
||||||
function do_env(subcmd, opts, args, cb) {
|
function do_env(subcmd, opts, args, cb) {
|
||||||
|
var self = this;
|
||||||
if (opts.help) {
|
if (opts.help) {
|
||||||
this.do_help('help', {}, [subcmd], cb);
|
this.do_help('help', {}, [subcmd], cb);
|
||||||
return;
|
return;
|
||||||
@ -27,16 +29,30 @@ function do_env(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var profileName = args[0] || this.tritonapi.profile.name;
|
var profileName = args[0] || this.tritonapi.profile.name;
|
||||||
var allClientTypes = ['smartdc', 'triton'];
|
var allClientTypes = ['triton', 'docker', 'smartdc'];
|
||||||
var clientTypes = [];
|
var clientTypes = [];
|
||||||
if (opts.smartdc) {
|
var explicit;
|
||||||
clientTypes.push('smartdc');
|
var shortOpts = '';
|
||||||
}
|
|
||||||
if (opts.triton) {
|
if (opts.triton) {
|
||||||
|
shortOpts += 't';
|
||||||
clientTypes.push('triton');
|
clientTypes.push('triton');
|
||||||
}
|
}
|
||||||
|
if (opts.docker) {
|
||||||
|
shortOpts += 'd';
|
||||||
|
clientTypes.push('docker');
|
||||||
|
}
|
||||||
|
if (opts.smartdc) {
|
||||||
|
shortOpts += 's';
|
||||||
|
clientTypes.push('smartdc');
|
||||||
|
}
|
||||||
|
if (opts.unset) {
|
||||||
|
shortOpts += 'u';
|
||||||
|
}
|
||||||
if (clientTypes.length === 0) {
|
if (clientTypes.length === 0) {
|
||||||
|
explicit = false;
|
||||||
clientTypes = allClientTypes;
|
clientTypes = allClientTypes;
|
||||||
|
} else {
|
||||||
|
explicit = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -52,27 +68,89 @@ function do_env(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var p = console.log;
|
var p = console.log;
|
||||||
var shortOpts = '';
|
|
||||||
clientTypes.forEach(function (clientType) {
|
clientTypes.forEach(function (clientType) {
|
||||||
switch (clientType) {
|
switch (clientType) {
|
||||||
case 'triton':
|
case 'triton':
|
||||||
shortOpts += 't';
|
p('# triton');
|
||||||
p('export TRITON_PROFILE="%s"', profile.name);
|
if (opts.unset) {
|
||||||
|
[
|
||||||
|
'SC_PROFILE',
|
||||||
|
'SC_URL',
|
||||||
|
'SC_ACCOUNT',
|
||||||
|
'SC_USER',
|
||||||
|
'SC_KEY_ID',
|
||||||
|
'SC_TLS_INSECURE'
|
||||||
|
].forEach(function (key) {
|
||||||
|
p('unset %s', key);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
p('export SC_PROFILE="%s"', profile.name);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'docker':
|
||||||
|
p('# docker');
|
||||||
|
var setupJson = path.resolve(self.configDir, 'docker',
|
||||||
|
common.profileSlug(profile), 'setup.json');
|
||||||
|
if (fs.existsSync(setupJson)) {
|
||||||
|
var setup;
|
||||||
|
try {
|
||||||
|
setup = JSON.parse(fs.readFileSync(setupJson));
|
||||||
|
} catch (err) {
|
||||||
|
cb(new errors.ConfigError(err, format(
|
||||||
|
'error determining Docker environment from "%s": %s',
|
||||||
|
setupJson, err)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.keys(setup.env).forEach(function (key) {
|
||||||
|
var val = setup.env[key];
|
||||||
|
if (opts.unset || val === null) {
|
||||||
|
p('unset %s', key);
|
||||||
|
} else {
|
||||||
|
p('export %s=%s', key, val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (opts.unset) {
|
||||||
|
[
|
||||||
|
'DOCKER_HOST',
|
||||||
|
'DOCKER_CERT_PATH',
|
||||||
|
'DOCKER_TLS_VERIFY',
|
||||||
|
'COMPOSE_HTTP_TIMEOUT'
|
||||||
|
].forEach(function (key) {
|
||||||
|
p('unset %s', key);
|
||||||
|
});
|
||||||
|
} else if (explicit) {
|
||||||
|
cb(new errors.ConfigError(format('could not find Docker '
|
||||||
|
+ 'environment setup for profile "%s":\n Run `triton '
|
||||||
|
+ 'profile docker-setup %s` to setup.',
|
||||||
|
profile.name, profile.name)));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'smartdc':
|
case 'smartdc':
|
||||||
shortOpts += 's';
|
p('# smartdc');
|
||||||
p('export SDC_URL="%s"', profile.url);
|
if (opts.unset) {
|
||||||
p('export SDC_ACCOUNT="%s"', profile.account);
|
[
|
||||||
if (profile.user) {
|
'SDC_URL',
|
||||||
p('export SDC_USER="%s"', profile.user);
|
'SDC_ACCOUNT',
|
||||||
|
'SDC_USER',
|
||||||
|
'SDC_KEY_ID',
|
||||||
|
'SDC_TESTING'
|
||||||
|
].forEach(function (key) {
|
||||||
|
p('unset %s', key);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
p('unset SDC_USER');
|
p('export SDC_URL="%s"', profile.url);
|
||||||
}
|
p('export SDC_ACCOUNT="%s"', profile.account);
|
||||||
p('export SDC_KEY_ID="%s"', profile.keyId);
|
if (profile.user) {
|
||||||
if (profile.insecure) {
|
p('export SDC_USER="%s"', profile.user);
|
||||||
p('export SDC_TESTING="%s"', profile.insecure);
|
} else {
|
||||||
} else {
|
p('unset SDC_USER');
|
||||||
p('unset SDC_TESTING');
|
}
|
||||||
|
p('export SDC_KEY_ID="%s"', profile.keyId);
|
||||||
|
if (profile.insecure) {
|
||||||
|
p('export SDC_TESTING="%s"', profile.insecure);
|
||||||
|
} else {
|
||||||
|
p('unset SDC_TESTING');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -82,8 +160,11 @@ function do_env(subcmd, opts, args, cb) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
p('# Run this command to configure your shell:');
|
p('# Run this command to configure your shell:');
|
||||||
p('# eval "$(triton env%s %s)"',
|
p('# eval "$(spearhead env%s%s)"',
|
||||||
(shortOpts ? ' -'+shortOpts : ''), profile.name);
|
(shortOpts ? ' -'+shortOpts : ''),
|
||||||
|
(profile.name === this.tritonapi.profile.name
|
||||||
|
? '' : ' ' + profile.name));
|
||||||
|
cb();
|
||||||
}
|
}
|
||||||
|
|
||||||
do_env.options = [
|
do_env.options = [
|
||||||
@ -93,35 +174,64 @@ do_env.options = [
|
|||||||
help: 'Show this help.'
|
help: 'Show this help.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
names: ['smartdc', 's'],
|
group: ''
|
||||||
type: 'bool',
|
|
||||||
help: 'Emit environment for node-smartdc (i.e. the "SDC_*" variables).'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
names: ['triton', 't'],
|
names: ['triton', 't'],
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
help: 'Emit environment commands for node-triton itself (i.e. the ' +
|
help: 'Emit environment commands for node-triton itself (i.e. the ' +
|
||||||
'"TRITON_PROFILE" variable).'
|
'"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.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// TODO: support env for docker usage.
|
do_env.synopses = ['{{name}} {{cmd}} [PROFILE]'];
|
||||||
|
|
||||||
do_env.help = [
|
do_env.help = [
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
'Emit shell environment commands to setup clients for a particular CLI profile.',
|
'Emit shell commands to setup environment.',
|
||||||
'',
|
'',
|
||||||
'Supported "clients" here are: node-smartdc (i.e. the `sdc-*` tools),',
|
'Supported "clients" here are: node-smartdc (i.e. the `sdc-*` tools),',
|
||||||
'and node-triton itself. By default this emits the environment for all',
|
'node-triton and spearhead-node. By default this emits the environment ',
|
||||||
'supported tools. Use options to be specific.',
|
'for all supported tools. Use options to be specific.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} env [PROFILE]',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}'
|
'{{options}}',
|
||||||
|
'If no options are given, environment variables are emitted for all ',
|
||||||
|
'clients. If PROFILE is not given, the current profile is used.',
|
||||||
|
'',
|
||||||
|
'The following Bash function can be added to one\'s "~/.bashrc" to quickly',
|
||||||
|
'change between Spearhead profiles:',
|
||||||
|
' triton-select () { eval "$(triton env $1)"; }',
|
||||||
|
'for example:',
|
||||||
|
' $ triton-select west1',
|
||||||
|
' $ triton profile get | grep name',
|
||||||
|
' name: west1',
|
||||||
|
' $ triton-select east1',
|
||||||
|
' $ triton profile get | grep name',
|
||||||
|
' name: east1'
|
||||||
/* END JSSTYLED */
|
/* END JSSTYLED */
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
do_env.completionArgtypes = ['tritonprofile', 'none'];
|
||||||
do_env.hidden = true;
|
|
||||||
|
|
||||||
module.exports = do_env;
|
module.exports = do_env;
|
||||||
|
@ -28,7 +28,7 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
cb(new errors.UsageError('missing <fwrule> argument'));
|
cb(new errors.UsageError('missing FWRULE argument'));
|
||||||
return;
|
return;
|
||||||
} else if (args.length > 1) {
|
} else if (args.length > 1) {
|
||||||
cb(new errors.UsageError('incorrect number of arguments'));
|
cb(new errors.UsageError('incorrect number of arguments'));
|
||||||
@ -45,15 +45,22 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
createOpts.description = opts.description;
|
createOpts.description = opts.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.top.tritonapi.cloudapi.createFirewallRule(createOpts,
|
var tritonapi = this.top.tritonapi;
|
||||||
function (err, fwrule) {
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
if (err) {
|
if (setupErr) {
|
||||||
cb(err);
|
cb(setupErr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('Created firewall rule %s%s', fwrule.id,
|
tritonapi.cloudapi.createFirewallRule(
|
||||||
(!fwrule.enabled ? ' (disabled)' : ''));
|
createOpts, function (err, fwrule) {
|
||||||
cb();
|
if (err) {
|
||||||
|
cb(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Created firewall rule %s%s', fwrule.id,
|
||||||
|
(!fwrule.enabled ? ' (disabled)' : ''));
|
||||||
|
cb();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,23 +80,37 @@ do_create.options = [
|
|||||||
names: ['disabled', 'd'],
|
names: ['disabled', 'd'],
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
help: 'Disable the created firewall rule. By default a created '
|
help: 'Disable the created firewall rule. By default a created '
|
||||||
+ 'firewall rule is enabled. Use "triton fwrule enable" '
|
+ 'firewall rule is enabled. Use "spearhead fwrule enable" '
|
||||||
+ 'to enable it later.'
|
+ 'to enable it later.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
names: ['description', 'D'],
|
names: ['description', 'D'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
helpArg: '<desc>',
|
helpArg: 'DESC',
|
||||||
help: 'Description of the firewall rule.'
|
help: 'Description of the firewall rule.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_create.synopses = ['{{name}} {{cmd}} [OPTIONS] RULE-TEXT'];
|
||||||
|
|
||||||
do_create.help = [
|
do_create.help = [
|
||||||
|
/* BEGIN JSSTYLED */
|
||||||
'Create a firewall rule.',
|
'Create a firewall rule.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} create [<options>] <fwrule>',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}'
|
'{{options}}',
|
||||||
|
'Examples:',
|
||||||
|
' # Allow SSH access from any IP to all instances in a datacenter.',
|
||||||
|
' spearhead fwrule create -D "ssh" "FROM any TO all vms ALLOW tcp PORT 22"',
|
||||||
|
'',
|
||||||
|
' # Allow SSH access to a specific instance.',
|
||||||
|
' spearhead fwrule create \\',
|
||||||
|
' "FROM any TO vm ba2c95e9-1cdf-4295-8253-3fee371374d9 ALLOW tcp PORT 22"'
|
||||||
|
// TODO: link to
|
||||||
|
// https://github.com/joyent/sdc-fwrule/blob/master/docs/examples.md
|
||||||
|
// or docs.jo Cloud Firewall examples? What link? Ditto in parent.
|
||||||
|
/* END JSSTYLED */
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
do_create.helpOpts = {
|
do_create.helpOpts = {
|
||||||
|
@ -27,14 +27,15 @@ function do_delete(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.length < 1) {
|
if (args.length < 1) {
|
||||||
cb(new errors.UsageError('missing <fwrule-id> argument(s)'));
|
cb(new errors.UsageError('missing FWRULE argument(s)'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cli = this.top;
|
var tritonapi = this.top.tritonapi;
|
||||||
var ruleIds = args;
|
var ruleIds = args;
|
||||||
|
|
||||||
vasync.pipeline({funcs: [
|
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||||
|
common.cliSetupTritonApi,
|
||||||
function confirm(_, next) {
|
function confirm(_, next) {
|
||||||
if (opts.force) {
|
if (opts.force) {
|
||||||
return next();
|
return next();
|
||||||
@ -61,8 +62,8 @@ function do_delete(subcmd, opts, args, cb) {
|
|||||||
vasync.forEachParallel({
|
vasync.forEachParallel({
|
||||||
inputs: ruleIds,
|
inputs: ruleIds,
|
||||||
func: function deleteOne(id, nextId) {
|
func: function deleteOne(id, nextId) {
|
||||||
cli.tritonapi.deleteFirewallRule({
|
tritonapi.deleteFirewallRule({
|
||||||
id: id
|
id: id
|
||||||
}, function (err) {
|
}, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
nextId(err);
|
nextId(err);
|
||||||
@ -96,15 +97,20 @@ do_delete.options = [
|
|||||||
help: 'Skip confirmation of delete.'
|
help: 'Skip confirmation of delete.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_delete.synopses = ['{{name}} {{cmd}} FWRULE [FWRULE ...]'];
|
||||||
|
|
||||||
do_delete.help = [
|
do_delete.help = [
|
||||||
'Remove a firewall rule.',
|
'Remove a firewall rule.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} delete [<options>] <fwrule-id> [<fwrule-id>...]',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}'
|
'{{options}}',
|
||||||
|
'Where FWRULE is a firewall rule id (full UUID) or short id.'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
do_delete.aliases = ['rm'];
|
do_delete.aliases = ['rm'];
|
||||||
|
|
||||||
|
do_delete.completionArgtypes = ['tritonfwrule'];
|
||||||
|
|
||||||
module.exports = do_delete;
|
module.exports = do_delete;
|
||||||
|
@ -26,26 +26,31 @@ function do_disable(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
cb(new errors.UsageError('Missing <fwrule-id> argument(s)'));
|
cb(new errors.UsageError('missing FWRULE argument(s)'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cli = this.top;
|
var tritonapi = this.top.tritonapi;
|
||||||
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
vasync.forEachParallel({
|
if (setupErr) {
|
||||||
inputs: args,
|
cb(setupErr);
|
||||||
func: function disableOne(id, nextId) {
|
return;
|
||||||
cli.tritonapi.disableFirewallRule({ id: id }, function (err) {
|
|
||||||
if (err) {
|
|
||||||
nextId(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Disabled firewall rule %s', id);
|
|
||||||
nextId();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, cb);
|
vasync.forEachParallel({
|
||||||
|
inputs: args,
|
||||||
|
func: function disableOne(id, nextId) {
|
||||||
|
tritonapi.disableFirewallRule({ id: id }, function (err) {
|
||||||
|
if (err) {
|
||||||
|
nextId(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Disabled firewall rule %s', id);
|
||||||
|
nextId();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, cb);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -56,13 +61,18 @@ do_disable.options = [
|
|||||||
help: 'Show this help.'
|
help: 'Show this help.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_disable.synopses = ['{{name}} {{cmd}} FWRULE [FWRULE ...]'];
|
||||||
|
|
||||||
do_disable.help = [
|
do_disable.help = [
|
||||||
'Disable a specific firewall rule.',
|
'Disable a specific firewall rule.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} disable <fwrule-id> [<fwrule-id>...]',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}'
|
'{{options}}',
|
||||||
|
'Where FWRULE is a firewall rule id (full UUID) or short id.'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
do_disable.completionArgtypes = ['tritonfwrule'];
|
||||||
|
|
||||||
module.exports = do_disable;
|
module.exports = do_disable;
|
||||||
|
@ -26,26 +26,31 @@ function do_enable(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
cb(new errors.UsageError('Missing <fwrule-id> argument(s)'));
|
cb(new errors.UsageError('missing FWRULE argument(s)'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cli = this.top;
|
var tritonapi = this.top.tritonapi;
|
||||||
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
vasync.forEachParallel({
|
if (setupErr) {
|
||||||
inputs: args,
|
cb(setupErr);
|
||||||
func: function enableOne(id, nextId) {
|
return;
|
||||||
cli.tritonapi.enableFirewallRule({ id: id }, function (err) {
|
|
||||||
if (err) {
|
|
||||||
nextId(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Enabled firewall rule %s', id);
|
|
||||||
nextId();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, cb);
|
vasync.forEachParallel({
|
||||||
|
inputs: args,
|
||||||
|
func: function enableOne(id, nextId) {
|
||||||
|
tritonapi.enableFirewallRule({ id: id }, function (err) {
|
||||||
|
if (err) {
|
||||||
|
nextId(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Enabled firewall rule %s', id);
|
||||||
|
nextId();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, cb);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -56,13 +61,18 @@ do_enable.options = [
|
|||||||
help: 'Show this help.'
|
help: 'Show this help.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_enable.synopses = ['{{name}} {{cmd}} FWRULE [FWRULE ...]'];
|
||||||
|
|
||||||
do_enable.help = [
|
do_enable.help = [
|
||||||
'Enable a specific firewall rule.',
|
'Enable a specific firewall rule.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} enable <fwrule-id> [<fwrule-id>...]',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}'
|
'{{options}}',
|
||||||
|
'Where FWRULE is a firewall rule id (full UUID) or short id.'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
do_enable.completionArgtypes = ['tritonfwrule'];
|
||||||
|
|
||||||
module.exports = do_enable;
|
module.exports = do_enable;
|
||||||
|
@ -25,7 +25,7 @@ function do_get(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
cb(new errors.UsageError('missing <fwrule-id> argument'));
|
cb(new errors.UsageError('missing FWRULE argument'));
|
||||||
return;
|
return;
|
||||||
} else if (args.length > 1) {
|
} else if (args.length > 1) {
|
||||||
cb(new errors.UsageError('incorrect number of arguments'));
|
cb(new errors.UsageError('incorrect number of arguments'));
|
||||||
@ -33,21 +33,27 @@ function do_get(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var id = args[0];
|
var id = args[0];
|
||||||
var cli = this.top;
|
var tritonapi = this.top.tritonapi;
|
||||||
|
|
||||||
cli.tritonapi.getFirewallRule(id, function onRule(err, fwrule) {
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
if (err) {
|
if (setupErr) {
|
||||||
cb(err);
|
cb(setupErr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
tritonapi.getFirewallRule(id, function onRule(err, fwrule) {
|
||||||
|
if (err) {
|
||||||
|
cb(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
console.log(JSON.stringify(fwrule));
|
console.log(JSON.stringify(fwrule));
|
||||||
} else {
|
} else {
|
||||||
console.log(JSON.stringify(fwrule, null, 4));
|
console.log(JSON.stringify(fwrule, null, 4));
|
||||||
}
|
}
|
||||||
|
|
||||||
cb();
|
cb();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,13 +70,18 @@ do_get.options = [
|
|||||||
help: 'JSON stream output.'
|
help: 'JSON stream output.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_get.synopses = ['{{name}} {{cmd}} FWRULE'];
|
||||||
|
|
||||||
do_get.help = [
|
do_get.help = [
|
||||||
'Show a specific firewall rule.',
|
'Show a specific firewall rule.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} get <fwrule-id>',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}'
|
'{{options}}',
|
||||||
|
'Where FWRULE is a firewall rule id (full UUID) or short id.'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
do_get.completionArgtypes = ['tritonfwrule', 'none'];
|
||||||
|
|
||||||
module.exports = do_get;
|
module.exports = do_get;
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* Copyright 2018 Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton fwrule instances ...`
|
* `triton fwrule instances ...`
|
||||||
*/
|
*/
|
||||||
@ -30,7 +30,7 @@ function do_instances(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
cb(new errors.UsageError('missing <fwrule-id> argument'));
|
cb(new errors.UsageError('missing FWRULE argument'));
|
||||||
return;
|
return;
|
||||||
} else if (args.length > 1) {
|
} else if (args.length > 1) {
|
||||||
cb(new errors.UsageError('incorrect number of arguments'));
|
cb(new errors.UsageError('incorrect number of arguments'));
|
||||||
@ -54,73 +54,84 @@ function do_instances(subcmd, opts, args, cb) {
|
|||||||
|
|
||||||
var tritonapi = this.top.tritonapi;
|
var tritonapi = this.top.tritonapi;
|
||||||
|
|
||||||
vasync.parallel({funcs: [
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
function getTheImages(next) {
|
if (setupErr) {
|
||||||
tritonapi.listImages({useCache: true},
|
cb(setupErr);
|
||||||
function (err, _imgs) {
|
return;
|
||||||
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"
|
// map "uuid" => "image_name"
|
||||||
var imgmap = {};
|
var imgmap = {};
|
||||||
imgs.forEach(function (img) {
|
imgs.forEach(function (img) {
|
||||||
imgmap[img.id] = format('%s@%s', img.name, img.version);
|
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,22 +146,26 @@ do_instances.options = [
|
|||||||
sortDefault: SORT_DEFAULT
|
sortDefault: SORT_DEFAULT
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
do_instances.synopses = ['{{name}} {{cmd}} [OPTIONS] FWRULE'];
|
||||||
|
|
||||||
do_instances.help = [
|
do_instances.help = [
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
'List instances a firewall rule is applied to.',
|
'List instances to which a firewall rule applies',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} instances [<options>] <fwrule-id>',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}',
|
'{{options}}',
|
||||||
|
'Where FWRULE is a firewall rule id (full UUID) or short id.',
|
||||||
'',
|
'',
|
||||||
'Fields (most are self explanatory, "*" indicates a field added client-side',
|
'Fields (most are self explanatory, "*" indicates a field added client-side',
|
||||||
'for convenience):',
|
'for convenience):',
|
||||||
' shortid* A short ID prefix.',
|
' shortid* A short ID prefix.',
|
||||||
' flags* Single letter flags summarizing some fields:',
|
' flags* Single letter flags summarizing some fields:',
|
||||||
|
' "B" the brand is "bhyve"',
|
||||||
' "D" docker instance',
|
' "D" docker instance',
|
||||||
' "F" firewall is enabled',
|
' "F" firewall is enabled',
|
||||||
' "K" the brand is "kvm"',
|
' "K" the brand is "kvm"',
|
||||||
|
' "P" deletion protected',
|
||||||
' age* Approximate time since created, e.g. 1y, 2w.',
|
' age* Approximate time since created, e.g. 1y, 2w.',
|
||||||
' img* The image "name@version", if available, else its',
|
' img* The image "name@version", if available, else its',
|
||||||
' "shortid".'
|
' "shortid".'
|
||||||
@ -159,4 +174,6 @@ do_instances.help = [
|
|||||||
|
|
||||||
do_instances.aliases = ['insts'];
|
do_instances.aliases = ['insts'];
|
||||||
|
|
||||||
|
do_instances.completionArgtypes = ['tritonfwrule', 'none'];
|
||||||
|
|
||||||
module.exports = do_instances;
|
module.exports = do_instances;
|
||||||
|
@ -35,40 +35,46 @@ function do_list(subcmd, opts, args, cb) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cli = this.top;
|
var tritonapi = this.top.tritonapi;
|
||||||
cli.tritonapi.cloudapi.listFirewallRules({}, function onRules(err, rules) {
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
if (err) {
|
if (setupErr) {
|
||||||
cb(err);
|
cb(setupErr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
tritonapi.cloudapi.listFirewallRules({}, function onRules(err, rules) {
|
||||||
if (opts.json) {
|
if (err) {
|
||||||
common.jsonStream(rules);
|
cb(err);
|
||||||
} else {
|
return;
|
||||||
var columns = COLUMNS_DEFAULT;
|
|
||||||
|
|
||||||
if (opts.o) {
|
|
||||||
columns = opts.o;
|
|
||||||
} else if (opts.long) {
|
|
||||||
columns = COLUMNS_LONG;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
columns = columns.toLowerCase().split(',');
|
if (opts.json) {
|
||||||
var sort = opts.s.toLowerCase().split(',');
|
common.jsonStream(rules);
|
||||||
|
} else {
|
||||||
|
var columns = COLUMNS_DEFAULT;
|
||||||
|
|
||||||
if (columns.indexOf('shortid') !== -1) {
|
if (opts.o) {
|
||||||
rules.forEach(function (rule) {
|
columns = opts.o;
|
||||||
rule.shortid = common.uuidToShortId(rule.id);
|
} 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
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
tabula(rules, {
|
|
||||||
skipHeader: opts.H,
|
|
||||||
columns: columns,
|
|
||||||
sort: sort
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
cb();
|
cb();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,11 +90,12 @@ do_list.options = [
|
|||||||
sortDefault: SORT_DEFAULT
|
sortDefault: SORT_DEFAULT
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS]'];
|
||||||
|
|
||||||
do_list.help = [
|
do_list.help = [
|
||||||
'Show all firewall rules.',
|
'Show all firewall rules.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} list [<options>]',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}'
|
'{{options}}'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
@ -31,13 +31,15 @@ function do_update(subcmd, opts, args, cb) {
|
|||||||
var tritonapi = this.top.tritonapi;
|
var tritonapi = this.top.tritonapi;
|
||||||
|
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
cb(new errors.UsageError('missing <fwrule-id> argument'));
|
cb(new errors.UsageError('missing FWRULE argument'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var id = args.shift();
|
var id = args.shift();
|
||||||
|
|
||||||
vasync.pipeline({arg: {}, funcs: [
|
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||||
|
common.cliSetupTritonApi,
|
||||||
|
|
||||||
function gatherDataArgs(ctx, next) {
|
function gatherDataArgs(ctx, next) {
|
||||||
if (opts.file) {
|
if (opts.file) {
|
||||||
next();
|
next();
|
||||||
@ -82,14 +84,7 @@ function do_update(subcmd, opts, args, cb) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var stdin = '';
|
common.readStdin(function gotStdin(stdin) {
|
||||||
|
|
||||||
process.stdin.resume();
|
|
||||||
process.stdin.on('data', function (chunk) {
|
|
||||||
stdin += chunk;
|
|
||||||
});
|
|
||||||
|
|
||||||
process.stdin.on('end', function () {
|
|
||||||
try {
|
try {
|
||||||
ctx.data = JSON.parse(stdin);
|
ctx.data = JSON.parse(stdin);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -105,33 +100,13 @@ function do_update(subcmd, opts, args, cb) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
function validateIt(ctx, next) {
|
function validateIt(ctx, next) {
|
||||||
var keys = Object.keys(ctx.data);
|
try {
|
||||||
|
common.validateObject(ctx.data, UPDATE_FWRULE_FIELDS);
|
||||||
if (keys.length === 0) {
|
} catch (e) {
|
||||||
console.log('No fields given for firewall rule update');
|
next(e);
|
||||||
next();
|
|
||||||
return;
|
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();
|
next();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -164,17 +139,20 @@ do_update.options = [
|
|||||||
{
|
{
|
||||||
names: ['file', 'f'],
|
names: ['file', 'f'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
helpArg: '<json-file>',
|
helpArg: 'JSON-FILE',
|
||||||
help: 'A file holding a JSON file of updates, or "-" to read ' +
|
help: 'A file holding a JSON file of updates, or "-" to read ' +
|
||||||
'JSON from stdin.'
|
'JSON from stdin.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_update.synopses = [
|
||||||
|
'{{name}} {{cmd}} FWRULE [FIELD=VALUE ...]',
|
||||||
|
'{{name}} {{cmd}} -f JSON-FILE FWRULE'
|
||||||
|
];
|
||||||
do_update.help = [
|
do_update.help = [
|
||||||
'Update a firewall rule',
|
'Update a firewall rule',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} update <fwrule-id> [FIELD=VALUE ...]',
|
|
||||||
' {{name}} update -f <json-file> <fwrule-id>',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}',
|
'{{options}}',
|
||||||
|
|
||||||
@ -182,7 +160,8 @@ do_update.help = [
|
|||||||
' ' + Object.keys(UPDATE_FWRULE_FIELDS).sort().map(function (f) {
|
' ' + Object.keys(UPDATE_FWRULE_FIELDS).sort().map(function (f) {
|
||||||
return f + ' (' + UPDATE_FWRULE_FIELDS[f] + ')';
|
return f + ' (' + UPDATE_FWRULE_FIELDS[f] + ')';
|
||||||
}).join('\n '),
|
}).join('\n '),
|
||||||
''
|
'',
|
||||||
|
'Where FWRULE is a firewall rule id (full UUID) or short id.'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
do_update.completionArgtypes = ['tritonfwrule', 'tritonupdatefwrulefield'];
|
do_update.completionArgtypes = ['tritonfwrule', 'tritonupdatefwrulefield'];
|
||||||
|
@ -22,7 +22,7 @@ function FirewallRuleCLI(top) {
|
|||||||
|
|
||||||
Cmdln.call(this, {
|
Cmdln.call(this, {
|
||||||
name: top.name + ' fwrule',
|
name: top.name + ' fwrule',
|
||||||
desc: 'List and manage Triton firewall rules.',
|
desc: 'List and manage Spearhead firewall rules.',
|
||||||
helpSubcmds: [
|
helpSubcmds: [
|
||||||
'help',
|
'help',
|
||||||
'list',
|
'list',
|
||||||
|
30
lib/do_fwrules.js
Normal file
30
lib/do_fwrules.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
107
lib/do_image/do_clone.js
Normal file
107
lib/do_image/do_clone.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
119
lib/do_image/do_copy.js
Normal file
119
lib/do_image/do_copy.js
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
@ -26,7 +26,6 @@ var mat = require('../metadataandtags');
|
|||||||
// ---- the command
|
// ---- the command
|
||||||
|
|
||||||
function do_create(subcmd, opts, args, cb) {
|
function do_create(subcmd, opts, args, cb) {
|
||||||
var self = this;
|
|
||||||
if (opts.help) {
|
if (opts.help) {
|
||||||
this.do_help('help', {}, [subcmd], cb);
|
this.do_help('help', {}, [subcmd], cb);
|
||||||
return;
|
return;
|
||||||
@ -37,11 +36,12 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var log = this.top.log;
|
var log = this.top.log;
|
||||||
var cloudapi = this.top.tritonapi.cloudapi;
|
var tritonapi = this.top.tritonapi;
|
||||||
|
|
||||||
vasync.pipeline({arg: {}, funcs: [
|
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||||
|
common.cliSetupTritonApi,
|
||||||
function loadTags(ctx, next) {
|
function loadTags(ctx, next) {
|
||||||
mat.tagsFromOpts(opts, log, function (err, tags) {
|
mat.tagsFromCreateOpts(opts, log, function (err, tags) {
|
||||||
if (err) {
|
if (err) {
|
||||||
next(err);
|
next(err);
|
||||||
return;
|
return;
|
||||||
@ -76,7 +76,7 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.top.tritonapi.getInstance(id, function (err, inst) {
|
tritonapi.getInstance(id, function (err, inst) {
|
||||||
if (err) {
|
if (err) {
|
||||||
next(err);
|
next(err);
|
||||||
return;
|
return;
|
||||||
@ -113,20 +113,22 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cloudapi.createImageFromMachine(createOpts, function (err, img) {
|
tritonapi.cloudapi.createImageFromMachine(
|
||||||
if (err) {
|
createOpts, function (err, img) {
|
||||||
next(new errors.TritonError(err, 'error creating image'));
|
if (err) {
|
||||||
return;
|
next(new errors.TritonError(err,
|
||||||
}
|
'error creating image'));
|
||||||
ctx.img = img;
|
return;
|
||||||
if (opts.json) {
|
}
|
||||||
console.log(JSON.stringify(img));
|
ctx.img = img;
|
||||||
} else {
|
if (opts.json) {
|
||||||
console.log('Creating image %s@%s (%s)',
|
console.log(JSON.stringify(img));
|
||||||
img.name, img.version, img.id);
|
} else {
|
||||||
}
|
console.log('Creating image %s@%s (%s)',
|
||||||
next();
|
img.name, img.version, img.id);
|
||||||
});
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
function maybeWait(ctx, next) {
|
function maybeWait(ctx, next) {
|
||||||
if (!opts.wait) {
|
if (!opts.wait) {
|
||||||
@ -147,8 +149,8 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
ctx.img.state = 'running';
|
ctx.img.state = 'running';
|
||||||
waitCb(null, ctx.img);
|
waitCb(null, ctx.img);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
} : tritonapi.cloudapi.waitForImageStates.bind(
|
||||||
: cloudapi.waitForImageStates.bind(cloudapi));
|
tritonapi.cloudapi));
|
||||||
|
|
||||||
waiter({
|
waiter({
|
||||||
id: ctx.img.id,
|
id: ctx.img.id,
|
||||||
@ -250,20 +252,25 @@ do_create.options = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
do_create.help = (
|
do_create.synopses = [
|
||||||
|
'{{name}} {{cmd}} [OPTIONS] INST IMAGE-NAME IMAGE-VERSION'
|
||||||
|
];
|
||||||
|
|
||||||
|
do_create.help = [
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
'Create a new instance.\n' +
|
'Create a new instance.',
|
||||||
'\n' +
|
'',
|
||||||
'Usage:\n' +
|
'{{usage}}',
|
||||||
' {{name}} create [<options>] INSTANCE IMAGE-NAME IMAGE-VERSION\n' +
|
'',
|
||||||
'\n' +
|
'{{options}}',
|
||||||
'{{options}}'
|
'Where "INST" is an instance name, id, or short id.'
|
||||||
/* END JSSTYLED */
|
/* END JSSTYLED */
|
||||||
);
|
].join('\n');
|
||||||
|
|
||||||
do_create.helpOpts = {
|
do_create.helpOpts = {
|
||||||
maxHelpCol: 20
|
maxHelpCol: 20
|
||||||
};
|
};
|
||||||
|
|
||||||
|
do_create.completionArgtypes = ['tritoninstance', 'file'];
|
||||||
|
|
||||||
module.exports = do_create;
|
module.exports = do_create;
|
||||||
|
@ -26,7 +26,8 @@ function do_delete(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
var ids = args;
|
var ids = args;
|
||||||
|
|
||||||
vasync.pipeline({arg: {}, funcs: [
|
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||||
|
common.cliSetupTritonApi,
|
||||||
/*
|
/*
|
||||||
* Lookup images, if not given UUIDs: we'll need to do it anyway
|
* Lookup images, if not given UUIDs: we'll need to do it anyway
|
||||||
* for the DeleteImage call(s), and doing so explicitly here allows
|
* for the DeleteImage call(s), and doing so explicitly here allows
|
||||||
@ -125,19 +126,21 @@ function do_delete(subcmd, opts, args, cb) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
do_delete.synopses = ['{{name}} {{cmd}} [OPTIONS] IMAGE [IMAGE ...]'];
|
||||||
|
|
||||||
do_delete.help = [
|
do_delete.help = [
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
'Delete one or more images.',
|
'Delete one or more images.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} delete IMAGE [IMAGE...]',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}',
|
'{{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"',
|
'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).'
|
'(selects latest match by "published_at"), or an image short ID (ID prefix).'
|
||||||
/* END JSSTYLED */
|
/* END JSSTYLED */
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
do_delete.options = [
|
do_delete.options = [
|
||||||
{
|
{
|
||||||
names: ['help', 'h'],
|
names: ['help', 'h'],
|
||||||
@ -151,5 +154,7 @@ do_delete.options = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_delete.completionArgtypes = ['tritonimage'];
|
||||||
|
|
||||||
do_delete.aliases = ['rm'];
|
do_delete.aliases = ['rm'];
|
||||||
module.exports = do_delete;
|
module.exports = do_delete;
|
||||||
|
120
lib/do_image/do_export.js
Normal file
120
lib/do_image/do_export.js
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
var format = require('util').format;
|
var format = require('util').format;
|
||||||
|
|
||||||
|
var common = require('../common');
|
||||||
var errors = require('../errors');
|
var errors = require('../errors');
|
||||||
|
|
||||||
|
|
||||||
@ -24,17 +25,28 @@ function do_get(subcmd, opts, args, callback) {
|
|||||||
'incorrect number of args (%d)', args.length)));
|
'incorrect number of args (%d)', args.length)));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.top.tritonapi.getImage(args[0], function onRes(err, img) {
|
var tritonapi = this.top.tritonapi;
|
||||||
if (err) {
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
return callback(err);
|
if (setupErr) {
|
||||||
|
callback(setupErr);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
var getOpts = {
|
||||||
|
name: args[0],
|
||||||
|
excludeInactive: !opts.all
|
||||||
|
};
|
||||||
|
tritonapi.getImage(getOpts, function onRes(err, img) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
console.log(JSON.stringify(img));
|
console.log(JSON.stringify(img));
|
||||||
} else {
|
} else {
|
||||||
console.log(JSON.stringify(img, null, 4));
|
console.log(JSON.stringify(img, null, 4));
|
||||||
}
|
}
|
||||||
callback();
|
callback();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,23 +60,36 @@ do_get.options = [
|
|||||||
names: ['json', 'j'],
|
names: ['json', 'j'],
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
help: 'JSON stream output.'
|
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.help = (
|
|
||||||
|
do_get.synopses = ['{{name}} {{cmd}} [OPTIONS] IMAGE'];
|
||||||
|
|
||||||
|
do_get.help = [
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
'Get an image.\n' +
|
'Get an image.',
|
||||||
'\n' +
|
'',
|
||||||
'Usage:\n' +
|
'{{usage}}',
|
||||||
' {{name}} get [<options>] ID|NAME\n' +
|
'',
|
||||||
'\n' +
|
'{{options}}',
|
||||||
'{{options}}' +
|
'Where "IMAGE" is an image id (a full UUID), an image name (selects the',
|
||||||
'\n' +
|
'latest, by "published_at", image with that name), an image "name@version"',
|
||||||
'If there is more than one image with the given "NAME", the latest\n' +
|
'(selects latest match by "published_at"), or an image short ID (ID prefix).',
|
||||||
'image (by "published_at") is returned.\n' +
|
'',
|
||||||
'\n' +
|
'Note: Currently this dumps prettified JSON by default. That might change',
|
||||||
'Note: Currently this dumps prettified JSON by default. That might change\n' +
|
'in the future. Use "-j" to explicitly get JSON output.'
|
||||||
'in the future. Use "-j" to explicitly get JSON output.\n'
|
|
||||||
/* END JSSTYLED */
|
/* END JSSTYLED */
|
||||||
);
|
].join('\n');
|
||||||
|
|
||||||
|
do_get.completionArgtypes = ['tritonimage', 'none'];
|
||||||
|
|
||||||
module.exports = do_get;
|
module.exports = do_get;
|
||||||
|
@ -5,13 +5,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* Copyright 2018 Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton image list ...`
|
* `triton image list ...`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var assert = require('assert-plus');
|
||||||
var format = require('util').format;
|
var format = require('util').format;
|
||||||
var tabula = require('tabula');
|
var tabula = require('tabula');
|
||||||
|
var vasync = require('vasync');
|
||||||
|
|
||||||
var common = require('../common');
|
var common = require('../common');
|
||||||
var errors = require('../errors');
|
var errors = require('../errors');
|
||||||
@ -54,7 +56,11 @@ function do_list(subcmd, opts, args, callback) {
|
|||||||
|
|
||||||
var listOpts;
|
var listOpts;
|
||||||
try {
|
try {
|
||||||
listOpts = common.kvToObj(args, validFilters);
|
listOpts = common.objFromKeyValueArgs(args, {
|
||||||
|
disableDotted: true,
|
||||||
|
validKeys: validFilters,
|
||||||
|
disableTypeConversions: true
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback(e);
|
callback(e);
|
||||||
return;
|
return;
|
||||||
@ -63,43 +69,92 @@ function do_list(subcmd, opts, args, callback) {
|
|||||||
listOpts.state = 'all';
|
listOpts.state = 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.top.tritonapi.listImages(listOpts, function onRes(err, imgs, res) {
|
var self = this;
|
||||||
if (err) {
|
var tritonapi = this.top.tritonapi;
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.json) {
|
vasync.pipeline({ arg: {}, funcs: [
|
||||||
common.jsonStream(imgs);
|
function setupTritonApi(_, next) {
|
||||||
} else {
|
common.cliSetupTritonApi({cli: self.top}, next);
|
||||||
// Add some convenience fields
|
},
|
||||||
// Added fields taken from imgapi-cli.git.
|
function getImages(ctx, next) {
|
||||||
for (var i = 0; i < imgs.length; i++) {
|
tritonapi.listImages(listOpts, function onRes(err, imgs, res) {
|
||||||
var img = imgs[i];
|
if (err) {
|
||||||
img.shortid = img.id.split('-', 1)[0];
|
next(err);
|
||||||
if (img.published_at) {
|
return;
|
||||||
// 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]) {
|
ctx.imgs = imgs;
|
||||||
img.size = img.files[0].size;
|
next();
|
||||||
}
|
|
||||||
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
|
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
function getUserAccount(ctx, next) {
|
||||||
|
// If using json output, or when there are no images that use an ACL
|
||||||
|
// - we don't need to fetch the account, as the account is only used
|
||||||
|
// to check if the image is shared (i.e. the account is in the image
|
||||||
|
// ACL) so it can output image flags in non-json mode.
|
||||||
|
if (opts.json || ctx.imgs.every(function _checkAcl(img) {
|
||||||
|
return !Array.isArray(img.acl) || img.acl.length === 0;
|
||||||
|
})) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tritonapi.cloudapi.getAccount(function _accountCb(err, account) {
|
||||||
|
if (err) {
|
||||||
|
next(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.account = account;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function formatImages(ctx, next) {
|
||||||
|
var imgs = ctx.imgs;
|
||||||
|
if (opts.json) {
|
||||||
|
common.jsonStream(imgs);
|
||||||
|
} else {
|
||||||
|
// Add some convenience fields
|
||||||
|
// Added fields taken from imgapi-cli.git.
|
||||||
|
for (var i = 0; i < imgs.length; i++) {
|
||||||
|
var img = imgs[i];
|
||||||
|
img.shortid = img.id.split('-', 1)[0];
|
||||||
|
if (img.published_at) {
|
||||||
|
// Just the date.
|
||||||
|
img.pubdate = img.published_at.slice(0, 10);
|
||||||
|
// Normalize on no milliseconds.
|
||||||
|
img.pub = img.published_at.replace(/\.\d+Z$/, 'Z');
|
||||||
|
}
|
||||||
|
if (img.files && img.files[0]) {
|
||||||
|
img.size = img.files[0].size;
|
||||||
|
}
|
||||||
|
var flags = [];
|
||||||
|
if (img.origin) flags.push('I');
|
||||||
|
if (img['public']) flags.push('P');
|
||||||
|
if (img.state !== 'active') flags.push('X');
|
||||||
|
|
||||||
|
// Add image sharing flags.
|
||||||
|
if (Array.isArray(img.acl) && img.acl.length > 0) {
|
||||||
|
assert.string(ctx.account.id, 'ctx.account.id');
|
||||||
|
if (img.owner === ctx.account.id) {
|
||||||
|
// This image has been shared with other accounts.
|
||||||
|
flags.push('+');
|
||||||
|
}
|
||||||
|
if (img.acl.indexOf(ctx.account.id) !== -1) {
|
||||||
|
// This image has been shared with this account.
|
||||||
|
flags.push('S');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img.flags = flags.length ? flags.join('') : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
tabula(imgs, {
|
||||||
|
skipHeader: opts.H,
|
||||||
|
columns: columns,
|
||||||
|
sort: sort
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next();
|
||||||
}
|
}
|
||||||
callback();
|
]}, callback);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
do_list.options = [
|
do_list.options = [
|
||||||
@ -122,16 +177,16 @@ do_list.options = [
|
|||||||
sortDefault: sortDefault
|
sortDefault: sortDefault
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] [FILTERS]'];
|
||||||
|
|
||||||
do_list.help = [
|
do_list.help = [
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
'List images.',
|
'List images.',
|
||||||
'',
|
'',
|
||||||
'Note: Currently, *docker* images are not included in this endpoint\'s responses.',
|
'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.',
|
'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}}',
|
'{{options}}',
|
||||||
'Filters:',
|
'Filters:',
|
||||||
@ -145,6 +200,8 @@ do_list.help = [
|
|||||||
' shortid* A short ID prefix.',
|
' shortid* A short ID prefix.',
|
||||||
' flags* Single letter flags summarizing some fields:',
|
' flags* Single letter flags summarizing some fields:',
|
||||||
' "P" image is public',
|
' "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)',
|
' "I" an incremental image (i.e. has an origin)',
|
||||||
' "X" has a state *other* than "active"',
|
' "X" has a state *other* than "active"',
|
||||||
' pubdate* Short form of "published_at" with just the date',
|
' pubdate* Short form of "published_at" with just the date',
|
||||||
|
115
lib/do_image/do_share.js
Normal file
115
lib/do_image/do_share.js
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
115
lib/do_image/do_unshare.js
Normal file
115
lib/do_image/do_unshare.js
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
|
|
||||||
|
var common = require('../common');
|
||||||
var distractions = require('../distractions');
|
var distractions = require('../distractions');
|
||||||
var errors = require('../errors');
|
var errors = require('../errors');
|
||||||
|
|
||||||
@ -34,7 +35,8 @@ function do_wait(subcmd, opts, args, cb) {
|
|||||||
var done = 0;
|
var done = 0;
|
||||||
var imgFromId = {};
|
var imgFromId = {};
|
||||||
|
|
||||||
vasync.pipeline({funcs: [
|
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||||
|
common.cliSetupTritonApi,
|
||||||
function getImgs(_, next) {
|
function getImgs(_, next) {
|
||||||
vasync.forEachParallel({
|
vasync.forEachParallel({
|
||||||
inputs: ids,
|
inputs: ids,
|
||||||
@ -112,17 +114,19 @@ function do_wait(subcmd, opts, args, cb) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
do_wait.synopses = ['{{name}} {{cmd}} [-s STATES] IMAGE [IMAGE ...]'];
|
||||||
|
|
||||||
do_wait.help = [
|
do_wait.help = [
|
||||||
'Wait for images to change to a particular state.',
|
'Wait for images to change to a particular state.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} wait [-s STATES] IMAGE [IMAGE ...]',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}',
|
'{{options}}',
|
||||||
'Where "states" is a comma-separated list of target instance states,',
|
'Where "states" is a comma-separated list of target instance states,',
|
||||||
'by default "active,failed". In other words, "triton img wait foo0" will',
|
'by default "active,failed". In other words, "spearhead img wait foo0" will',
|
||||||
'wait for image "foo0" to complete creation.'
|
'wait for image "foo0" to complete creation.'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
do_wait.options = [
|
do_wait.options = [
|
||||||
{
|
{
|
||||||
names: ['help', 'h'],
|
names: ['help', 'h'],
|
||||||
@ -139,4 +143,6 @@ do_wait.options = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_wait.completionArgtypes = ['tritonimage'];
|
||||||
|
|
||||||
module.exports = do_wait;
|
module.exports = do_wait;
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2015 Joyent, Inc.
|
* Copyright (c) 2018, Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton image ...`
|
* `triton image ...`
|
||||||
*/
|
*/
|
||||||
@ -23,7 +23,7 @@ function ImageCLI(top) {
|
|||||||
name: top.name + ' image',
|
name: top.name + ' image',
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
desc: [
|
desc: [
|
||||||
'List and manage Triton images.'
|
'List and manage Spearhead images.'
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
/* END JSSTYLED */
|
/* END JSSTYLED */
|
||||||
helpOpts: {
|
helpOpts: {
|
||||||
@ -33,8 +33,13 @@ function ImageCLI(top) {
|
|||||||
'help',
|
'help',
|
||||||
'list',
|
'list',
|
||||||
'get',
|
'get',
|
||||||
|
'clone',
|
||||||
|
'copy',
|
||||||
'create',
|
'create',
|
||||||
'delete',
|
'delete',
|
||||||
|
'export',
|
||||||
|
'share',
|
||||||
|
'unshare',
|
||||||
'wait'
|
'wait'
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@ -48,8 +53,13 @@ ImageCLI.prototype.init = function init(opts, args, cb) {
|
|||||||
|
|
||||||
ImageCLI.prototype.do_list = require('./do_list');
|
ImageCLI.prototype.do_list = require('./do_list');
|
||||||
ImageCLI.prototype.do_get = require('./do_get');
|
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_create = require('./do_create');
|
||||||
ImageCLI.prototype.do_delete = require('./do_delete');
|
ImageCLI.prototype.do_delete = require('./do_delete');
|
||||||
|
ImageCLI.prototype.do_export = require('./do_export');
|
||||||
|
ImageCLI.prototype.do_share = require('./do_share');
|
||||||
|
ImageCLI.prototype.do_unshare = require('./do_unshare');
|
||||||
ImageCLI.prototype.do_wait = require('./do_wait');
|
ImageCLI.prototype.do_wait = require('./do_wait');
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,11 +5,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* Copyright 2017 Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton images ...` bwcompat shortcut for `triton image list ...`.
|
* `triton images ...` bwcompat shortcut for `triton image list ...`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var targ = require('./do_image/do_list');
|
||||||
|
|
||||||
function do_images(subcmd, opts, args, callback) {
|
function do_images(subcmd, opts, args, callback) {
|
||||||
this.handlerFromSubcmd('image').dispatch({
|
this.handlerFromSubcmd('image').dispatch({
|
||||||
subcmd: 'list',
|
subcmd: 'list',
|
||||||
@ -18,9 +20,12 @@ function do_images(subcmd, opts, args, callback) {
|
|||||||
}, callback);
|
}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
do_images.help = 'A shortcut for "triton image list".';
|
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.aliases = ['imgs'];
|
do_images.aliases = ['imgs'];
|
||||||
do_images.hidden = true;
|
do_images.hidden = true;
|
||||||
do_images.options = require('./do_image/do_list').options;
|
|
||||||
|
|
||||||
module.exports = do_images;
|
module.exports = do_images;
|
||||||
|
141
lib/do_info.js
141
lib/do_info.js
@ -28,69 +28,76 @@ function do_info(subcmd, opts, args, callback) {
|
|||||||
|
|
||||||
var out = {};
|
var out = {};
|
||||||
var i = 0;
|
var i = 0;
|
||||||
|
var tritonapi = this.tritonapi;
|
||||||
|
|
||||||
this.tritonapi.cloudapi.getAccount(cb.bind('account')); i++;
|
common.cliSetupTritonApi({cli: this}, function onSetup(setupErr) {
|
||||||
this.tritonapi.cloudapi.listMachines(cb.bind('machines')); i++;
|
if (setupErr) {
|
||||||
|
callback(setupErr);
|
||||||
function cb(err, data) {
|
|
||||||
if (err) {
|
|
||||||
callback(err);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
out[this.toString()] = data;
|
tritonapi.cloudapi.getAccount(cb.bind('account')); i++;
|
||||||
if (--i === 0)
|
tritonapi.cloudapi.listMachines(cb.bind('machines')); i++;
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
function done() {
|
function cb(err, data) {
|
||||||
// parse name
|
if (err) {
|
||||||
var name;
|
callback(err);
|
||||||
if (out.account.firstName && out.account.lastName)
|
return;
|
||||||
name = format('%s %s', out.account.firstName,
|
}
|
||||||
out.account.lastName);
|
out[this.toString()] = data;
|
||||||
else if (out.account.firstName)
|
if (--i === 0)
|
||||||
name = out.account.firstName;
|
done();
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
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;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
do_info.options = [
|
do_info.options = [
|
||||||
@ -105,13 +112,15 @@ do_info.options = [
|
|||||||
help: 'JSON output.'
|
help: 'JSON output.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
do_info.help = (
|
|
||||||
'Print an account summary.\n'
|
do_info.synopses = ['{{name}} {{cmd}}'];
|
||||||
+ '\n'
|
|
||||||
+ 'Usage:\n'
|
do_info.help = [
|
||||||
+ ' {{name}} info\n'
|
'Print an account summary.',
|
||||||
+ '\n'
|
'',
|
||||||
+ '{{options}}'
|
'{{usage}}',
|
||||||
);
|
'',
|
||||||
|
'{{options}}'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
module.exports = do_info;
|
module.exports = do_info;
|
||||||
|
@ -27,14 +27,15 @@ var sortDefault = 'id,time';
|
|||||||
|
|
||||||
|
|
||||||
function do_audit(subcmd, opts, args, cb) {
|
function do_audit(subcmd, opts, args, cb) {
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if (opts.help) {
|
if (opts.help) {
|
||||||
this.do_help('help', {}, [subcmd], cb);
|
this.do_help('help', {}, [subcmd], cb);
|
||||||
return;
|
return;
|
||||||
} else if (args.length !== 1) {
|
} else if (args.length === 0) {
|
||||||
//XXX Support multiple machines.
|
cb(new errors.UsageError('missing INST argument'));
|
||||||
return cb(new Error('incorrect args: ' + args));
|
return;
|
||||||
|
} else if (args.length > 1) {
|
||||||
|
cb(new errors.UsageError('too many arguments: ' + args));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var columns = columnsDefault;
|
var columns = columnsDefault;
|
||||||
@ -49,23 +50,29 @@ function do_audit(subcmd, opts, args, cb) {
|
|||||||
|
|
||||||
var arg = args[0];
|
var arg = args[0];
|
||||||
var uuid;
|
var uuid;
|
||||||
|
var tritonapi = this.top.tritonapi;
|
||||||
|
|
||||||
if (common.isUUID(arg)) {
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
uuid = arg;
|
if (setupErr) {
|
||||||
go1();
|
cb(setupErr);
|
||||||
} else {
|
return;
|
||||||
self.top.tritonapi.getInstance(arg, function (err, inst) {
|
}
|
||||||
if (err) {
|
if (common.isUUID(arg)) {
|
||||||
cb(err);
|
uuid = arg;
|
||||||
return;
|
|
||||||
}
|
|
||||||
uuid = inst.id;
|
|
||||||
go1();
|
go1();
|
||||||
});
|
} else {
|
||||||
}
|
tritonapi.getInstance(arg, function (err, inst) {
|
||||||
|
if (err) {
|
||||||
|
cb(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uuid = inst.id;
|
||||||
|
go1();
|
||||||
|
});
|
||||||
|
}});
|
||||||
|
|
||||||
function go1() {
|
function go1() {
|
||||||
self.top.tritonapi.cloudapi.machineAudit(uuid, function (err, audit) {
|
tritonapi.cloudapi.machineAudit(uuid, function (err, audit) {
|
||||||
if (err) {
|
if (err) {
|
||||||
cb(err);
|
cb(err);
|
||||||
return;
|
return;
|
||||||
@ -102,13 +109,16 @@ do_audit.options = [
|
|||||||
sortDefault: sortDefault
|
sortDefault: sortDefault
|
||||||
}));
|
}));
|
||||||
|
|
||||||
do_audit.help = (
|
do_audit.synopses = ['{{name}} {{cmd}} [OPTIONS] INST'];
|
||||||
'List instance actions.\n'
|
do_audit.help = [
|
||||||
+ '\n'
|
'List instance actions.',
|
||||||
+ 'Usage:\n'
|
'',
|
||||||
+ ' {{name}} audit <alias|id>\n'
|
'{{usage}}',
|
||||||
+ '\n'
|
'',
|
||||||
+ '{{options}}'
|
'{{options}}',
|
||||||
);
|
'Where "INST" is an instance name, id, or short id.'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
do_audit.completionArgtypes = ['tritoninstance', 'none'];
|
||||||
|
|
||||||
module.exports = do_audit;
|
module.exports = do_audit;
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2015 Joyent, Inc.
|
* Copyright 2019 Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton instance create ...`
|
* `triton instance create ...`
|
||||||
*/
|
*/
|
||||||
@ -19,21 +19,160 @@ var common = require('../common');
|
|||||||
var distractions = require('../distractions');
|
var distractions = require('../distractions');
|
||||||
var errors = require('../errors');
|
var errors = require('../errors');
|
||||||
var mat = require('../metadataandtags');
|
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) {
|
function do_create(subcmd, opts, args, cb) {
|
||||||
var self = this;
|
|
||||||
if (opts.help) {
|
if (opts.help) {
|
||||||
this.do_help('help', {}, [subcmd], cb);
|
this.do_help('help', {}, [subcmd], cb);
|
||||||
return;
|
return;
|
||||||
} else if (args.length !== 2) {
|
} else if (args.length !== 2) {
|
||||||
return cb(new errors.UsageError('incorrect number of args'));
|
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 log = this.top.log;
|
||||||
var cloudapi = this.top.tritonapi.cloudapi;
|
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();
|
||||||
|
},
|
||||||
|
|
||||||
vasync.pipeline({arg: {}, funcs: [
|
|
||||||
function loadMetadata(ctx, next) {
|
function loadMetadata(ctx, next) {
|
||||||
mat.metadataFromOpts(opts, log, function (err, metadata) {
|
mat.metadataFromOpts(opts, log, function (err, metadata) {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -64,9 +203,10 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
function getImg(ctx, next) {
|
function getImg(ctx, next) {
|
||||||
var _opts = {
|
var _opts = {
|
||||||
name: args[0],
|
name: args[0],
|
||||||
|
excludeInactive: true,
|
||||||
useCache: true
|
useCache: true
|
||||||
};
|
};
|
||||||
self.top.tritonapi.getImage(_opts, function (err, img) {
|
tritonapi.getImage(_opts, function (err, img) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
@ -87,7 +227,7 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.top.tritonapi.getPackage(id, function (err, pkg) {
|
tritonapi.getPackage(id, function (err, pkg) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
@ -105,7 +245,7 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
vasync.forEachPipeline({
|
vasync.forEachPipeline({
|
||||||
inputs: opts.network,
|
inputs: opts.network,
|
||||||
func: function getOneNetwork(name, nextNet) {
|
func: function getOneNetwork(name, nextNet) {
|
||||||
self.top.tritonapi.getNetwork(name, function (err, net) {
|
tritonapi.getNetwork(name, function (err, net) {
|
||||||
if (err) {
|
if (err) {
|
||||||
nextNet(err);
|
nextNet(err);
|
||||||
} else {
|
} else {
|
||||||
@ -116,14 +256,30 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
}, next);
|
}, next);
|
||||||
},
|
},
|
||||||
|
|
||||||
function createInst(ctx, next) {
|
function createInst(ctx, next) {
|
||||||
|
assert.optionalArrayOfObject(ctx.volMounts, 'ctx.volMounts');
|
||||||
|
|
||||||
var createOpts = {
|
var createOpts = {
|
||||||
name: opts.name,
|
name: opts.name,
|
||||||
image: ctx.img.id,
|
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) {
|
if (ctx.metadata) {
|
||||||
Object.keys(ctx.metadata).forEach(function (key) {
|
Object.keys(ctx.metadata).forEach(function (key) {
|
||||||
createOpts['metadata.'+key] = ctx.metadata[key];
|
createOpts['metadata.'+key] = ctx.metadata[key];
|
||||||
@ -134,11 +290,16 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
createOpts['tag.'+key] = ctx.tags[key];
|
createOpts['tag.'+key] = ctx.tags[key];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (opts.allow_shared_images) {
|
||||||
|
createOpts.allow_shared_images = true;
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = 0; i < opts._order.length; i++) {
|
for (var i = 0; i < opts._order.length; i++) {
|
||||||
var opt = opts._order[i];
|
var opt = opts._order[i];
|
||||||
if (opt.key === 'firewall') {
|
if (opt.key === 'firewall') {
|
||||||
createOpts.firewall_enabled = opt.value;
|
createOpts.firewall_enabled = opt.value;
|
||||||
|
} else if (opt.key === 'deletion_protection') {
|
||||||
|
createOpts.deletion_protection = opt.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +317,7 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
cloudapi.createMachine(createOpts, function (err, inst) {
|
tritonapi.cloudapi.createMachine(createOpts, function (err, inst) {
|
||||||
if (err) {
|
if (err) {
|
||||||
next(new errors.TritonError(err,
|
next(new errors.TritonError(err,
|
||||||
'error creating instance'));
|
'error creating instance'));
|
||||||
@ -192,8 +353,8 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
ctx.inst.state = 'running';
|
ctx.inst.state = 'running';
|
||||||
waitCb(null, ctx.inst);
|
waitCb(null, ctx.inst);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
} : tritonapi.cloudapi.waitForMachineStates.bind(
|
||||||
: cloudapi.waitForMachineStates.bind(cloudapi));
|
tritonapi.cloudapi));
|
||||||
|
|
||||||
waiter({
|
waiter({
|
||||||
id: ctx.inst.id,
|
id: ctx.inst.id,
|
||||||
@ -240,12 +401,80 @@ do_create.options = [
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
help: 'Instance name. If not given, one will be generated server-side.'
|
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
|
// TODO: add boolNegationPrefix:'no-' when that cmdln pull is in
|
||||||
names: ['firewall'],
|
names: ['firewall'],
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
help: 'Enable Cloud Firewall on this instance. See ' +
|
help: 'Enable Cloud Firewall on this instance. See ' +
|
||||||
'<https://docs.joyent.com/public-cloud/network/firewall>'
|
'<https://docs.spearhead.cloud/network/firewall>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['deletion-protection'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Enable Deletion Protection on this instance. Such an instance ' +
|
||||||
|
'cannot be deleted until the protection is disabled. See ' +
|
||||||
|
'<https://apidocs.joyent.com/cloudapi/#deletion-protection>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['volume', 'v'],
|
||||||
|
type: 'arrayOfString',
|
||||||
|
help: 'Mount a volume into the instance (non-KVM only). VOLMOUNT is ' +
|
||||||
|
'"<volume-name:/mount/point>[:access-mode]" where access mode is ' +
|
||||||
|
'one of "ro" for read-only or "rw" for read-write (default). For ' +
|
||||||
|
'example: "-v myvolume:/mnt:ro" to mount "myvolume" read-only on ' +
|
||||||
|
'/mnt in this instance.',
|
||||||
|
helpArg: 'VOLMOUNT',
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
group: ''
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
names: ['metadata', 'm'],
|
names: ['metadata', 'm'],
|
||||||
@ -274,25 +503,10 @@ do_create.options = [
|
|||||||
'of the instance. This is a shortcut for `-M user-script=FILE`.'
|
'of the instance. This is a shortcut for `-M user-script=FILE`.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
names: ['tag', 't'],
|
names: ['allow-shared-images'],
|
||||||
type: 'arrayOfString',
|
type: 'bool',
|
||||||
helpArg: 'TAG',
|
help: 'Allow instance creation to use a shared image.'
|
||||||
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'
|
group: 'Other options'
|
||||||
@ -315,20 +529,25 @@ do_create.options = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
do_create.help = (
|
do_create.synopses = ['{{name}} {{cmd}} [OPTIONS] IMAGE PACKAGE'];
|
||||||
|
|
||||||
|
do_create.help = [
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
'Create a new instance.\n' +
|
'Create a new instance.',
|
||||||
'\n' +
|
'',
|
||||||
'Usage:\n' +
|
'{{usage}}',
|
||||||
' {{name}} create [<options>] IMAGE PACKAGE\n' +
|
'',
|
||||||
'\n' +
|
'{{options}}',
|
||||||
'{{options}}'
|
'Where IMAGE is an image name, name@version, id, or short id (from ',
|
||||||
|
'`spearhead image list`) and PACKAGE is a package name, id, or short id',
|
||||||
|
'(from `spearhead package list`).'
|
||||||
/* END JSSTYLED */
|
/* END JSSTYLED */
|
||||||
);
|
].join('\n');
|
||||||
|
|
||||||
do_create.helpOpts = {
|
do_create.helpOpts = {
|
||||||
maxHelpCol: 18
|
maxHelpCol: 16
|
||||||
};
|
};
|
||||||
|
|
||||||
|
do_create.completionArgtypes = ['tritonimage', 'tritonpackage', 'none'];
|
||||||
|
|
||||||
module.exports = do_create;
|
module.exports = do_create;
|
||||||
|
125
lib/do_instance/do_disable_deletion_protection.js
Normal file
125
lib/do_instance/do_disable_deletion_protection.js
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
112
lib/do_instance/do_disable_firewall.js
Normal file
112
lib/do_instance/do_disable_firewall.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
125
lib/do_instance/do_enable_deletion_protection.js
Normal file
125
lib/do_instance/do_enable_deletion_protection.js
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
112
lib/do_instance/do_enable_firewall.js
Normal file
112
lib/do_instance/do_enable_firewall.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
113
lib/do_instance/do_fwrule/do_list.js
Normal file
113
lib/do_instance/do_fwrule/do_list.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
45
lib/do_instance/do_fwrule/index.js
Normal file
45
lib/do_instance/do_fwrule/index.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* `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;
|
@ -5,99 +5,22 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* `triton instance fwrules ...` shortcut for
|
||||||
*
|
* `triton instance fwrule list ...`.
|
||||||
* `triton instance fwrules ...`
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var assert = require('assert-plus');
|
function do_fwrules(subcmd, opts, args, callback) {
|
||||||
var tabula = require('tabula');
|
this.handlerFromSubcmd('fwrule').dispatch({
|
||||||
|
subcmd: 'list',
|
||||||
var common = require('../common');
|
opts: opts,
|
||||||
var errors = require('../errors');
|
args: args
|
||||||
|
}, callback);
|
||||||
|
|
||||||
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.options = [
|
do_fwrules.help = do_fwrule_list.help;
|
||||||
{
|
do_fwrules.options = do_fwrule_list.options;
|
||||||
names: ['help', 'h'],
|
do_fwrules.synopses = do_fwrule_list.synopses;
|
||||||
type: 'bool',
|
do_fwrules.completionArgtypes = do_fwrule_list.completionArgtypes;
|
||||||
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;
|
module.exports = do_fwrules;
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2015 Joyent, Inc.
|
* Copyright 2016 Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton instance get ...`
|
* `triton instance get ...`
|
||||||
*/
|
*/
|
||||||
@ -19,17 +19,25 @@ function do_get(subcmd, opts, args, cb) {
|
|||||||
return cb(new Error('invalid args: ' + args));
|
return cb(new Error('invalid args: ' + args));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.top.tritonapi.getInstance(args[0], function (err, inst) {
|
var tritonapi = this.top.tritonapi;
|
||||||
if (err) {
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
return cb(err);
|
if (setupErr) {
|
||||||
|
cb(setupErr);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
tritonapi.getInstance({
|
||||||
if (opts.json) {
|
id: args[0],
|
||||||
console.log(JSON.stringify(inst));
|
credentials: opts.credentials
|
||||||
} else {
|
}, function onInst(err, inst) {
|
||||||
console.log(JSON.stringify(inst, null, 4));
|
if (inst) {
|
||||||
}
|
if (opts.json) {
|
||||||
cb();
|
console.log(JSON.stringify(inst));
|
||||||
|
} else {
|
||||||
|
console.log(JSON.stringify(inst, null, 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,24 +47,37 @@ do_get.options = [
|
|||||||
type: 'bool',
|
type: 'bool',
|
||||||
help: 'Show this help.'
|
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'],
|
names: ['json', 'j'],
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
help: 'JSON output.'
|
help: 'JSON output.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
do_get.help = (
|
do_get.synopses = ['{{name}} {{cmd}} [OPTIONS] INST'];
|
||||||
|
do_get.help = [
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
'Get an instance.\n'
|
'Get an instance.',
|
||||||
+ '\n'
|
'',
|
||||||
+ 'Usage:\n'
|
'{{usage}}',
|
||||||
+ ' {{name}} get <alias|id>\n'
|
'',
|
||||||
+ '\n'
|
'{{options}}',
|
||||||
+ '{{options}}'
|
'Where "INST" is an instance name, id, or short id.',
|
||||||
+ '\n'
|
'',
|
||||||
+ 'Note: Currently this dumps prettified JSON by default. That might change\n'
|
'A *deleted* instance may still respond with the instance object. In that',
|
||||||
+ 'in the future. Use "-j" to explicitly get JSON output.\n'
|
'case a the instance will be print *and* an error will be raised.',
|
||||||
|
'',
|
||||||
|
'Currently this dumps prettified JSON by default. That might change',
|
||||||
|
'in the future. Use "-j" to explicitly get JSON output.'
|
||||||
/* END JSSTYLED */
|
/* END JSSTYLED */
|
||||||
);
|
].join('\n');
|
||||||
|
|
||||||
|
do_get.completionArgtypes = ['tritoninstance', 'none'];
|
||||||
|
|
||||||
module.exports = do_get;
|
module.exports = do_get;
|
||||||
|
80
lib/do_instance/do_ip.js
Normal file
80
lib/do_instance/do_ip.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* Copyright (c) 2018, Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton instance list ...`
|
* `triton instance list ...`
|
||||||
*/
|
*/
|
||||||
@ -22,13 +22,16 @@ var common = require('../common');
|
|||||||
* See <https://apidocs.joyent.com/cloudapi/#ListMachines>.
|
* See <https://apidocs.joyent.com/cloudapi/#ListMachines>.
|
||||||
*/
|
*/
|
||||||
var validFilters = [
|
var validFilters = [
|
||||||
'type',
|
'brand', // Added in CloudAPI 8.0.0
|
||||||
'brand', // Added in CloudAPI 8.0.0
|
'docker', // Added in CloudAPI 8.0.0
|
||||||
'name',
|
|
||||||
'image',
|
'image',
|
||||||
'state',
|
|
||||||
'memory',
|
'memory',
|
||||||
'docker' // Added in CloudAPI 8.0.0
|
'name',
|
||||||
|
'state',
|
||||||
|
// jsl:ignore
|
||||||
|
/^tag\./,
|
||||||
|
// jsl:end
|
||||||
|
'type'
|
||||||
];
|
];
|
||||||
|
|
||||||
// columns default without -o
|
// columns default without -o
|
||||||
@ -47,6 +50,7 @@ function do_list(subcmd, opts, args, callback) {
|
|||||||
this.do_help('help', {}, [subcmd], callback);
|
this.do_help('help', {}, [subcmd], callback);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var log = self.top.log;
|
||||||
|
|
||||||
var columns = columnsDefault;
|
var columns = columnsDefault;
|
||||||
if (opts.o) {
|
if (opts.o) {
|
||||||
@ -60,7 +64,11 @@ function do_list(subcmd, opts, args, callback) {
|
|||||||
|
|
||||||
var listOpts;
|
var listOpts;
|
||||||
try {
|
try {
|
||||||
listOpts = common.kvToObj(args, validFilters);
|
listOpts = common.objFromKeyValueArgs(args, {
|
||||||
|
disableDotted: true,
|
||||||
|
validKeys: validFilters,
|
||||||
|
disableTypeConversions: true
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback(e);
|
callback(e);
|
||||||
return;
|
return;
|
||||||
@ -69,75 +77,99 @@ function do_list(subcmd, opts, args, callback) {
|
|||||||
listOpts.credentials = true;
|
listOpts.credentials = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var imgs = [];
|
||||||
var imgs;
|
|
||||||
var insts;
|
var insts;
|
||||||
|
|
||||||
vasync.parallel({funcs: [
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
function getTheImages(next) {
|
if (setupErr) {
|
||||||
self.top.tritonapi.listImages({useCache: true},
|
callback(setupErr);
|
||||||
function (err, _imgs) {
|
return;
|
||||||
if (err) {
|
}
|
||||||
next(err);
|
vasync.parallel({funcs: [
|
||||||
} else {
|
function getTheImages(next) {
|
||||||
imgs = _imgs;
|
self.top.tritonapi.listImages({
|
||||||
next();
|
state: 'all',
|
||||||
}
|
useCache: true
|
||||||
});
|
}, function (err, _imgs) {
|
||||||
},
|
if (err) {
|
||||||
function getTheMachines(next) {
|
if (err.statusCode === 403) {
|
||||||
self.top.tritonapi.cloudapi.listMachines(listOpts,
|
/*
|
||||||
|
* This could be a authorization error due
|
||||||
|
* to RBAC on a subuser. We don't want to
|
||||||
|
* fail `triton inst ls` if the subuser
|
||||||
|
* can ListMachines, but not ListImages.
|
||||||
|
*/
|
||||||
|
log.debug(
|
||||||
|
err,
|
||||||
|
'authz error listing images for insts info');
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
imgs = _imgs;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function getTheMachines(next) {
|
||||||
|
self.top.tritonapi.cloudapi.listMachines(
|
||||||
|
listOpts,
|
||||||
function (err, _insts) {
|
function (err, _insts) {
|
||||||
if (err) {
|
if (err) {
|
||||||
next(err);
|
next(err);
|
||||||
} else {
|
} else {
|
||||||
insts = _insts;
|
insts = _insts;
|
||||||
next();
|
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);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
]}, 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"
|
// Add extra fields for nice output.
|
||||||
var imgmap = {};
|
var now = new Date();
|
||||||
imgs.forEach(function (img) {
|
insts.forEach(function (inst) {
|
||||||
imgmap[img.id] = format('%s@%s', img.name, img.version);
|
var created = new Date(inst.created);
|
||||||
});
|
inst.age = common.longAgo(created, now);
|
||||||
|
inst.img = imgmap[inst.image] ||
|
||||||
// Add extra fields for nice output.
|
common.uuidToShortId(inst.image);
|
||||||
var now = new Date();
|
inst.shortid = inst.id.split('-', 1)[0];
|
||||||
insts.forEach(function (inst) {
|
var flags = [];
|
||||||
var created = new Date(inst.created);
|
if (inst.brand === 'bhyve') flags.push('B');
|
||||||
inst.age = common.longAgo(created, now);
|
if (inst.docker) flags.push('D');
|
||||||
inst.img = imgmap[inst.image] || common.uuidToShortId(inst.image);
|
if (inst.firewall_enabled) flags.push('F');
|
||||||
inst.shortid = inst.id.split('-', 1)[0];
|
if (inst.brand === 'kvm') flags.push('K');
|
||||||
var flags = [];
|
if (inst.deletion_protection) flags.push('P');
|
||||||
if (inst.docker) flags.push('D');
|
inst.flags = flags.length ? flags.join('') : undefined;
|
||||||
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();
|
if (opts.json) {
|
||||||
|
common.jsonStream(insts);
|
||||||
|
} else {
|
||||||
|
tabula(insts, {
|
||||||
|
skipHeader: opts.H,
|
||||||
|
columns: columns,
|
||||||
|
sort: sort,
|
||||||
|
dottedLookup: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,30 +191,37 @@ do_list.options = [
|
|||||||
sortDefault: sortDefault
|
sortDefault: sortDefault
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] [FILTERS...]'];
|
||||||
|
|
||||||
do_list.help = [
|
do_list.help = [
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
'List instances.',
|
'List instances.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} list [<filters>...]',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}',
|
'{{options}}',
|
||||||
'Filters:',
|
'Filters:',
|
||||||
' FIELD=VALUE Equality filter. Supported fields: type, brand, name,',
|
' FIELD=VALUE Equality filter. Supported fields: brand, image,',
|
||||||
' image, state, and memory',
|
' memory, name, state, tag.TAGNAME, and type.',
|
||||||
' FIELD=true|false Boolean filter. Supported fields: docker (added in',
|
' 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',
|
'Fields (most are self explanatory, "*" indicates a field added client-side',
|
||||||
'for convenience):',
|
'for convenience):',
|
||||||
' shortid* A short ID prefix.',
|
' shortid* A short ID prefix.',
|
||||||
' flags* Single letter flags summarizing some fields:',
|
' flags* Single letter flags summarizing some fields:',
|
||||||
|
' "B" the brand is "bhyve"',
|
||||||
' "D" docker instance',
|
' "D" docker instance',
|
||||||
' "F" firewall is enabled',
|
' "F" firewall is enabled',
|
||||||
' "K" the brand is "kvm"',
|
' "K" the brand is "kvm"',
|
||||||
|
' "P" deletion protected',
|
||||||
' age* Approximate time since created, e.g. 1y, 2w.',
|
' age* Approximate time since created, e.g. 1y, 2w.',
|
||||||
' img* The image "name@version", if available, else its',
|
' img* The image "name@version", if available, else its',
|
||||||
' "shortid".'
|
' "shortid".',
|
||||||
|
'',
|
||||||
|
'Examples:',
|
||||||
|
' {{name}} ls -Ho id state=running # IDs of running insts',
|
||||||
|
' {{name}} ls docker=true tag.foo=bar # Docker insts w/ "foo=bar" tag'
|
||||||
/* END JSSTYLED */
|
/* END JSSTYLED */
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
211
lib/do_instance/do_nic/do_create.js
Normal file
211
lib/do_instance/do_nic/do_create.js
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
126
lib/do_instance/do_nic/do_delete.js
Normal file
126
lib/do_instance/do_nic/do_delete.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
89
lib/do_instance/do_nic/do_get.js
Normal file
89
lib/do_instance/do_nic/do_get.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
154
lib/do_instance/do_nic/do_list.js
Normal file
154
lib/do_instance/do_nic/do_list.js
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
50
lib/do_instance/do_nic/index.js
Normal file
50
lib/do_instance/do_nic/index.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
@ -5,13 +5,101 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* Copyright 2017 Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton instance reboot ...`
|
* `triton instance reboot ...`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var gen_do_ACTION = require('./gen_do_ACTION');
|
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 do_reboot = gen_do_ACTION({action: 'reboot'});
|
|
||||||
module.exports = do_reboot;
|
module.exports = do_reboot;
|
||||||
|
88
lib/do_instance/do_rename.js
Normal file
88
lib/do_instance/do_rename.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
88
lib/do_instance/do_resize.js
Normal file
88
lib/do_instance/do_resize.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* Copyright 2018 Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton snapshot create ...`
|
* `triton snapshot create ...`
|
||||||
*/
|
*/
|
||||||
@ -15,7 +15,6 @@ var format = require('util').format;
|
|||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
|
|
||||||
var common = require('../../common');
|
var common = require('../../common');
|
||||||
var distractions = require('../../distractions');
|
|
||||||
var errors = require('../../errors');
|
var errors = require('../../errors');
|
||||||
|
|
||||||
|
|
||||||
@ -48,7 +47,8 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
createOpts.name = opts.name;
|
createOpts.name = opts.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
vasync.pipeline({arg: {}, funcs: [
|
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||||
|
common.cliSetupTritonApi,
|
||||||
function createSnapshot(ctx, next) {
|
function createSnapshot(ctx, next) {
|
||||||
ctx.start = Date.now();
|
ctx.start = Date.now();
|
||||||
|
|
||||||
@ -72,13 +72,6 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
return next();
|
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 cloudapi = cli.tritonapi.cloudapi;
|
||||||
var waiter = cloudapi.waitForSnapshotStates.bind(cloudapi);
|
var waiter = cloudapi.waitForSnapshotStates.bind(cloudapi);
|
||||||
|
|
||||||
@ -87,10 +80,6 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
name: ctx.name,
|
name: ctx.name,
|
||||||
states: ['created', 'failed']
|
states: ['created', 'failed']
|
||||||
}, function (err, snap) {
|
}, function (err, snap) {
|
||||||
if (distraction) {
|
|
||||||
distraction.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
@ -126,24 +115,27 @@ do_create.options = [
|
|||||||
{
|
{
|
||||||
names: ['name', 'n'],
|
names: ['name', 'n'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
helpArg: '<snapname>',
|
helpArg: 'SNAPNAME',
|
||||||
help: 'An optional name for a snapshot.'
|
help: 'An optional name for a snapshot.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
names: ['wait', 'w'],
|
names: ['wait', 'w'],
|
||||||
type: 'arrayOfBool',
|
type: 'bool',
|
||||||
help: 'Wait for the creation to complete. Use multiple times for a ' +
|
help: 'Wait for the creation to complete.'
|
||||||
'spinner.'
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_create.synopses = ['{{name}} {{cmd}} [OPTIONS] INST'];
|
||||||
|
|
||||||
do_create.help = [
|
do_create.help = [
|
||||||
'Create a snapshot of an instance.',
|
'Create a snapshot of an instance.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} create [<options>] <inst>',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}',
|
'{{options}}',
|
||||||
'Snapshot do not work for instances of type "kvm".'
|
'Snapshots do not work for instances of type "bhyve" or "kvm".'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
do_create.completionArgtypes = ['tritoninstance', 'none'];
|
||||||
|
|
||||||
module.exports = do_create;
|
module.exports = do_create;
|
||||||
|
@ -15,7 +15,6 @@ var format = require('util').format;
|
|||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
|
|
||||||
var common = require('../../common');
|
var common = require('../../common');
|
||||||
var distractions = require('../../distractions');
|
|
||||||
var errors = require('../../errors');
|
var errors = require('../../errors');
|
||||||
|
|
||||||
|
|
||||||
@ -28,7 +27,7 @@ function do_delete(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.length < 2) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,13 +36,6 @@ function do_delete(subcmd, opts, args, cb) {
|
|||||||
var names = args.slice(1, args.length);
|
var names = args.slice(1, args.length);
|
||||||
|
|
||||||
function wait(instId, name, startTime, next) {
|
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 cloudapi = cli.tritonapi.cloudapi;
|
||||||
var waiter = cloudapi.waitForSnapshotStates.bind(cloudapi);
|
var waiter = cloudapi.waitForSnapshotStates.bind(cloudapi);
|
||||||
|
|
||||||
@ -52,12 +44,10 @@ function do_delete(subcmd, opts, args, cb) {
|
|||||||
name: name,
|
name: name,
|
||||||
states: ['deleted']
|
states: ['deleted']
|
||||||
}, function (err, snap) {
|
}, function (err, snap) {
|
||||||
if (distraction) {
|
|
||||||
distraction.destroy();
|
|
||||||
}
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snap.state === 'deleted') {
|
if (snap.state === 'deleted') {
|
||||||
var duration = Date.now() - startTime;
|
var duration = Date.now() - startTime;
|
||||||
var durStr = common.humanDurationFromMs(duration);
|
var durStr = common.humanDurationFromMs(duration);
|
||||||
@ -71,7 +61,8 @@ function do_delete(subcmd, opts, args, cb) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
vasync.pipeline({funcs: [
|
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||||
|
common.cliSetupTritonApi,
|
||||||
function confirm(_, next) {
|
function confirm(_, next) {
|
||||||
if (opts.force) {
|
if (opts.force) {
|
||||||
return next();
|
return next();
|
||||||
@ -145,20 +136,25 @@ do_delete.options = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
names: ['wait', 'w'],
|
names: ['wait', 'w'],
|
||||||
type: 'arrayOfBool',
|
type: 'bool',
|
||||||
help: 'Wait for the deletion to complete. Use multiple times for a ' +
|
help: 'Wait for the deletion to complete.'
|
||||||
'spinner.'
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_delete.synopses = ['{{name}} {{cmd}} [OPTIONS] INST SNAPNAME [SNAPNAME...]'];
|
||||||
|
|
||||||
do_delete.help = [
|
do_delete.help = [
|
||||||
'Remove a snapshot from an instance.',
|
'Remove a snapshot from an instance.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} delete [<options>] <inst> <snapname> [<snapname>...]',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}'
|
'{{options}}'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
do_delete.aliases = ['rm'];
|
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;
|
module.exports = do_delete;
|
||||||
|
@ -25,7 +25,7 @@ function do_get(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.length < 2) {
|
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;
|
return;
|
||||||
} else if (args.length > 2) {
|
} else if (args.length > 2) {
|
||||||
cb(new errors.UsageError('incorrect number of arguments'));
|
cb(new errors.UsageError('incorrect number of arguments'));
|
||||||
@ -36,26 +36,31 @@ function do_get(subcmd, opts, args, cb) {
|
|||||||
var name = args[1];
|
var name = args[1];
|
||||||
var cli = this.top;
|
var cli = this.top;
|
||||||
|
|
||||||
cli.tritonapi.getInstanceSnapshot({
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
id: id,
|
if (setupErr) {
|
||||||
name: name
|
cb(setupErr);
|
||||||
}, function onSnapshot(err, snapshot) {
|
|
||||||
if (err) {
|
|
||||||
cb(err);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
cli.tritonapi.getInstanceSnapshot({
|
||||||
|
id: id,
|
||||||
|
name: name
|
||||||
|
}, function onSnapshot(err, snapshot) {
|
||||||
|
if (err) {
|
||||||
|
cb(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
console.log(JSON.stringify(snapshot));
|
console.log(JSON.stringify(snapshot));
|
||||||
} else {
|
} else {
|
||||||
console.log(JSON.stringify(snapshot, null, 4));
|
console.log(JSON.stringify(snapshot, null, 4));
|
||||||
}
|
}
|
||||||
|
|
||||||
cb();
|
cb();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
do_get.options = [
|
do_get.options = [
|
||||||
{
|
{
|
||||||
names: ['help', 'h'],
|
names: ['help', 'h'],
|
||||||
@ -68,13 +73,19 @@ do_get.options = [
|
|||||||
help: 'JSON stream output.'
|
help: 'JSON stream output.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_get.synopses = ['{{name}} {{cmd}} [OPTIONS] INST SNAPNAME'];
|
||||||
|
|
||||||
do_get.help = [
|
do_get.help = [
|
||||||
'Show a specific snapshot of an instance.',
|
'Show a specific snapshot of an instance.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} get <inst> <snapname>',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}'
|
'{{options}}'
|
||||||
].join('\n');
|
].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;
|
module.exports = do_get;
|
||||||
|
@ -30,7 +30,7 @@ function do_list(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
cb(new errors.UsageError('missing <inst> argument'));
|
cb(new errors.UsageError('missing INST argument'));
|
||||||
return;
|
return;
|
||||||
} else if (args.length > 1) {
|
} else if (args.length > 1) {
|
||||||
cb(new errors.UsageError('incorrect number of arguments'));
|
cb(new errors.UsageError('incorrect number of arguments'));
|
||||||
@ -40,35 +40,41 @@ function do_list(subcmd, opts, args, cb) {
|
|||||||
var cli = this.top;
|
var cli = this.top;
|
||||||
var machineId = args[0];
|
var machineId = args[0];
|
||||||
|
|
||||||
cli.tritonapi.listInstanceSnapshots({
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
id: machineId
|
if (setupErr) {
|
||||||
}, function onSnapshots(err, snapshots) {
|
cb(setupErr);
|
||||||
if (err) {
|
|
||||||
cb(err);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
cli.tritonapi.listInstanceSnapshots({
|
||||||
if (opts.json) {
|
id: machineId
|
||||||
common.jsonStream(snapshots);
|
}, function onSnapshots(err, snapshots) {
|
||||||
} else {
|
if (err) {
|
||||||
var columns = COLUMNS_DEFAULT;
|
cb(err);
|
||||||
|
return;
|
||||||
if (opts.o) {
|
|
||||||
columns = opts.o;
|
|
||||||
} else if (opts.long) {
|
|
||||||
columns = COLUMNS_DEFAULT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
columns = columns.split(',');
|
if (opts.json) {
|
||||||
var sort = opts.s.split(',');
|
common.jsonStream(snapshots);
|
||||||
|
} else {
|
||||||
|
var columns = COLUMNS_DEFAULT;
|
||||||
|
|
||||||
tabula(snapshots, {
|
if (opts.o) {
|
||||||
skipHeader: opts.H,
|
columns = opts.o;
|
||||||
columns: columns,
|
} else if (opts.long) {
|
||||||
sort: sort
|
columns = COLUMNS_DEFAULT;
|
||||||
});
|
}
|
||||||
}
|
|
||||||
cb();
|
columns = columns.split(',');
|
||||||
|
var sort = opts.s.split(',');
|
||||||
|
|
||||||
|
tabula(snapshots, {
|
||||||
|
skipHeader: opts.H,
|
||||||
|
columns: columns,
|
||||||
|
sort: sort
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cb();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,15 +90,18 @@ do_list.options = [
|
|||||||
sortDefault: SORT_DEFAULT
|
sortDefault: SORT_DEFAULT
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] INST'];
|
||||||
|
|
||||||
do_list.help = [
|
do_list.help = [
|
||||||
'Show all of an instance\'s snapshots.',
|
'Show all of an instance\'s snapshots.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} list [<options>] <inst>',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}'
|
'{{options}}'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
do_list.completionArgtypes = ['tritoninstance', 'none'];
|
||||||
|
|
||||||
do_list.aliases = ['ls'];
|
do_list.aliases = ['ls'];
|
||||||
|
|
||||||
module.exports = do_list;
|
module.exports = do_list;
|
||||||
|
@ -22,7 +22,7 @@ function SnapshotCLI(top) {
|
|||||||
|
|
||||||
Cmdln.call(this, {
|
Cmdln.call(this, {
|
||||||
name: top.name + ' snapshot',
|
name: top.name + ' snapshot',
|
||||||
desc: 'List, get, create and delete Triton instance snapshots.',
|
desc: 'List, get, create and delete Spearhead instance snapshots.',
|
||||||
helpSubcmds: [
|
helpSubcmds: [
|
||||||
'help',
|
'help',
|
||||||
'create',
|
'create',
|
||||||
@ -31,7 +31,7 @@ function SnapshotCLI(top) {
|
|||||||
'delete'
|
'delete'
|
||||||
],
|
],
|
||||||
helpBody: 'Instances can be rolled back to a snapshot using\n' +
|
helpBody: 'Instances can be rolled back to a snapshot using\n' +
|
||||||
'`triton instance start --snapshot=<snapname>`.'
|
'`spearhead instance start --snapshot=SNAPNAME`.'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
util.inherits(SnapshotCLI, Cmdln);
|
util.inherits(SnapshotCLI, Cmdln);
|
||||||
|
@ -5,12 +5,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* Copyright 2017 Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton instance snapshots ...` shortcut for
|
* `triton instance snapshots ...` shortcut for
|
||||||
* `triton instance snapshot list ...`.
|
* `triton instance snapshot list ...`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var targ = require('./do_snapshot/do_list');
|
||||||
|
|
||||||
function do_snapshots(subcmd, opts, args, callback) {
|
function do_snapshots(subcmd, opts, args, callback) {
|
||||||
this.handlerFromSubcmd('snapshot').dispatch({
|
this.handlerFromSubcmd('snapshot').dispatch({
|
||||||
subcmd: 'list',
|
subcmd: 'list',
|
||||||
@ -19,8 +21,12 @@ function do_snapshots(subcmd, opts, args, callback) {
|
|||||||
}, callback);
|
}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
do_snapshots.help = 'A shortcut for "triton instance snapshot list".';
|
do_snapshots.help = 'A shortcut for "spearhead instance snapshot list".\n' +
|
||||||
do_snapshots.options = require('./do_snapshot/do_list').options;
|
targ.help;
|
||||||
|
do_snapshots.synopses = targ.synopses;
|
||||||
|
do_snapshots.options = targ.options;
|
||||||
|
do_snapshots.completionArgtypes = targ.completionArgtypes;
|
||||||
|
|
||||||
do_snapshots.hidden = true;
|
do_snapshots.hidden = true;
|
||||||
|
|
||||||
module.exports = do_snapshots;
|
module.exports = do_snapshots;
|
||||||
|
@ -5,56 +5,304 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* Copyright (c) 2018, Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton instance ssh ...`
|
* `triton instance ssh ...`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var assert = require('assert-plus');
|
||||||
|
var path = require('path');
|
||||||
var spawn = require('child_process').spawn;
|
var spawn = require('child_process').spawn;
|
||||||
|
var vasync = require('vasync');
|
||||||
|
|
||||||
var common = require('../common');
|
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) {
|
function do_ssh(subcmd, opts, args, callback) {
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if (opts.help) {
|
if (opts.help) {
|
||||||
this.do_help('help', {}, [subcmd], callback);
|
this.do_help('help', {}, [subcmd], callback);
|
||||||
return;
|
return;
|
||||||
} else if (args.length === 0) {
|
} else if (args.length === 0) {
|
||||||
callback(new Error('invalid args: ' + args));
|
callback(new errors.UsageError('missing INST arg'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var id = args.shift();
|
var id = args.shift();
|
||||||
|
|
||||||
var user = 'root';
|
var user;
|
||||||
|
var overrideUser = false;
|
||||||
var i = id.indexOf('@');
|
var i = id.indexOf('@');
|
||||||
if (i >= 0) {
|
if (i >= 0) {
|
||||||
user = id.substr(0, i);
|
user = id.substr(0, i);
|
||||||
id = id.substr(i + 1);
|
id = id.substr(i + 1);
|
||||||
|
overrideUser = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.top.tritonapi.getInstance(id, function (err, inst) {
|
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||||
if (err) {
|
common.cliSetupTritonApi,
|
||||||
callback(err);
|
|
||||||
return;
|
function getInstanceIp(ctx, next) {
|
||||||
|
ctx.cli.tritonapi.getInstance(id, function (err, inst) {
|
||||||
|
if (err) {
|
||||||
|
next(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.inst = inst;
|
||||||
|
|
||||||
|
if (inst.tags && inst.tags[TAG_SSH_IP]) {
|
||||||
|
ctx.ip = inst.tags[TAG_SSH_IP];
|
||||||
|
if (!inst.ips || inst.ips.indexOf(ctx.ip) === -1) {
|
||||||
|
next(new Error('IP address ' + ctx.ip + ' not ' +
|
||||||
|
'attached to the instance'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.ip = inst.primaryIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.ip) {
|
||||||
|
next(new Error('IP address not found for instance'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function getInstanceBastionIp(ctx, next) {
|
||||||
|
if (opts.no_proxy) {
|
||||||
|
setImmediate(next);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.inst.tags || !ctx.inst.tags[TAG_SSH_PROXY]) {
|
||||||
|
setImmediate(next);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.cli.tritonapi.getInstance(ctx.inst.tags[TAG_SSH_PROXY],
|
||||||
|
function (err, proxy) {
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
next(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxy.tags && proxy.tags[TAG_SSH_IP]) {
|
||||||
|
ctx.proxyIp = proxy.tags[TAG_SSH_IP];
|
||||||
|
if (!proxy.ips || proxy.ips.indexOf(ctx.proxyIp) === -1) {
|
||||||
|
next(new Error('IP address ' + ctx.proxyIp + ' not ' +
|
||||||
|
'attached to the instance'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.proxyIp = proxy.primaryIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.proxyImage = proxy.image;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Selecting the right user to use for the proxy connection is
|
||||||
|
* somewhat nuanced, in order to allow for various useful
|
||||||
|
* configurations. We wish to enable the following cases:
|
||||||
|
*
|
||||||
|
* 1. The least sophisticated configuration; i.e., using two
|
||||||
|
* instances (the target instance and the proxy instnace)
|
||||||
|
* with the default "root" (or, e.g., "ubuntu") account
|
||||||
|
* and smartlogin or authorized_keys metadata for SSH key
|
||||||
|
* management.
|
||||||
|
*
|
||||||
|
* 2. The user has set up their own accounts (e.g., "roberta")
|
||||||
|
* in all of their instances and does their own SSH key
|
||||||
|
* management. They connect with:
|
||||||
|
*
|
||||||
|
* triton inst ssh roberta@instance
|
||||||
|
*
|
||||||
|
* In this case we will use "roberta" for both the proxy
|
||||||
|
* and the target instance. This means a user provided on
|
||||||
|
* the command line will override the per-image default
|
||||||
|
* user (e.g., "root" or "ubuntu") -- if the user wants to
|
||||||
|
* retain the default account for the proxy, they should
|
||||||
|
* use case 3 below.
|
||||||
|
*
|
||||||
|
* 3. The user has set up their own accounts in the target
|
||||||
|
* instance (e.g., "felicity"), but the proxy instance is
|
||||||
|
* using a single specific account that should be used by
|
||||||
|
* all users in the organisation (e.g., "partyline"). In
|
||||||
|
* this case, we want the user to be able to specify the
|
||||||
|
* global proxy account setting as a tag on the proxy
|
||||||
|
* instance, so that for:
|
||||||
|
*
|
||||||
|
* triton inst ssh felicity@instance
|
||||||
|
*
|
||||||
|
* ... we will use "-o ProxyJump partyline@proxy" but
|
||||||
|
* still use "felicity" for the target connection. This
|
||||||
|
* last case requires the proxy user tag (if set) to
|
||||||
|
* override a user provided on the command line.
|
||||||
|
*/
|
||||||
|
if (proxy.tags && proxy.tags[TAG_SSH_PROXY_USER]) {
|
||||||
|
ctx.proxyUser = proxy.tags[TAG_SSH_PROXY_USER];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.proxyIp) {
|
||||||
|
next(new Error('IP address not found for proxy instance'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function getUser(ctx, next) {
|
||||||
|
if (overrideUser) {
|
||||||
|
assert.string(user, 'user');
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.cli.tritonapi.getImage({
|
||||||
|
name: ctx.inst.image,
|
||||||
|
useCache: true
|
||||||
|
}, function (getImageErr, image) {
|
||||||
|
if (getImageErr) {
|
||||||
|
next(getImageErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is a convention as seen on Joyent's "ubuntu-certified"
|
||||||
|
* KVM images.
|
||||||
|
*/
|
||||||
|
if (image.tags && image.tags.default_user) {
|
||||||
|
user = image.tags.default_user;
|
||||||
|
} else {
|
||||||
|
user = 'root';
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function getBastionUser(ctx, next) {
|
||||||
|
if (!ctx.proxyImage || ctx.proxyUser) {
|
||||||
|
/*
|
||||||
|
* If there is no image for the proxy host, or an override user
|
||||||
|
* was already provided in the tags of the proxy instance
|
||||||
|
* itself, we don't need to look up the default user.
|
||||||
|
*/
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrideUser) {
|
||||||
|
/*
|
||||||
|
* A user was provided on the command line, but no user
|
||||||
|
* override tag was present on the proxy instance. To enable
|
||||||
|
* use case 2 (see comments above) we'll prefer this user over
|
||||||
|
* the image default.
|
||||||
|
*/
|
||||||
|
assert.string(user, 'user');
|
||||||
|
ctx.proxyUser = user;
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.cli.tritonapi.getImage({
|
||||||
|
name: ctx.proxyImage,
|
||||||
|
useCache: true
|
||||||
|
}, function (getImageErr, image) {
|
||||||
|
if (getImageErr) {
|
||||||
|
next(getImageErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is a convention as seen on Joyent's "ubuntu-certified"
|
||||||
|
* KVM images.
|
||||||
|
*/
|
||||||
|
assert.ok(!ctx.proxyUser, 'proxy user set twice');
|
||||||
|
if (image.tags && image.tags.default_user) {
|
||||||
|
ctx.proxyUser = image.tags.default_user;
|
||||||
|
} else {
|
||||||
|
ctx.proxyUser = 'root';
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function doSsh(ctx, next) {
|
||||||
|
args = ['-l', user, ctx.ip].concat(args);
|
||||||
|
|
||||||
|
if (ctx.proxyIp) {
|
||||||
|
assert.string(ctx.proxyUser, 'ctx.proxyUser');
|
||||||
|
args = [
|
||||||
|
'-o', 'ProxyJump=' + ctx.proxyUser + '@' + ctx.proxyIp
|
||||||
|
].concat(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default we disable ControlMaster (aka mux, aka SSH
|
||||||
|
* connection multiplexing) because of
|
||||||
|
* https://github.com/joyent/node-triton/issues/52
|
||||||
|
*/
|
||||||
|
if (!opts.no_disable_mux) {
|
||||||
|
/*
|
||||||
|
* A simple `-o ControlMaster=no` doesn't work. With
|
||||||
|
* just that option, a `ControlPath` option (from
|
||||||
|
* ~/.ssh/config) will still be used if it exists. Our
|
||||||
|
* hack is to set a ControlPath we know should not
|
||||||
|
* exist. Using '/dev/null' wasn't a good alternative
|
||||||
|
* because `ssh` tries "$ControlPath.$somerandomnum"
|
||||||
|
* and also because Windows.
|
||||||
|
*/
|
||||||
|
var nullSshControlPath = path.resolve(
|
||||||
|
ctx.cli.tritonapi.config._configDir, 'tmp',
|
||||||
|
'nullSshControlPath');
|
||||||
|
args = [
|
||||||
|
'-o', 'ControlMaster=no',
|
||||||
|
'-o', 'ControlPath='+nullSshControlPath
|
||||||
|
].concat(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.cli.log.info({args: args}, 'forking ssh');
|
||||||
|
var child = spawn('ssh', args, {stdio: 'inherit'});
|
||||||
|
child.on('close', function (code) {
|
||||||
|
/*
|
||||||
|
* Once node 0.10 support is dropped we could instead:
|
||||||
|
* process.exitCode = code;
|
||||||
|
* callback();
|
||||||
|
*/
|
||||||
|
process.exit(code);
|
||||||
|
/* process.exit does not return so no need to call next(). */
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
]}, callback);
|
||||||
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 = [
|
do_ssh.options = [
|
||||||
@ -62,17 +310,61 @@ do_ssh.options = [
|
|||||||
names: ['help', 'h'],
|
names: ['help', 'h'],
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
help: 'Show this help.'
|
help: 'Show this help.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['no-proxy'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Disable SSH proxy support (ignore "tritoncli.ssh.proxy" tag)'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
do_ssh.help = (
|
do_ssh.synopses = ['{{name}} ssh [-h] [USER@]INST [SSH-ARGUMENTS]'];
|
||||||
'SSH to the primary IP of an instance\n'
|
do_ssh.help = [
|
||||||
+ '\n'
|
/* BEGIN JSSTYLED */
|
||||||
+ 'Usage:\n'
|
'SSH to the primary IP of an instance',
|
||||||
+ ' {{name}} ssh <alias|id> [arguments]\n'
|
'',
|
||||||
+ '\n'
|
'{{usage}}',
|
||||||
+ '{{options}}'
|
'',
|
||||||
);
|
'{{options}}',
|
||||||
|
'Where INST is the name, id, or short id of an instance. Note that',
|
||||||
|
'the INST argument must come before any `ssh` options or arguments.',
|
||||||
|
'Where USER is the username to use on the instance. If not specified,',
|
||||||
|
'the instance\'s image is inspected for the default_user tag.',
|
||||||
|
'If USER is not specified and the default_user tag is not set, the user',
|
||||||
|
'is assumed to be \"root\".',
|
||||||
|
'',
|
||||||
|
'The "tritoncli.ssh.proxy" tag on the target instance may be set to',
|
||||||
|
'the name or the UUID of another instance through which to proxy this',
|
||||||
|
'SSH connection. If set, the primary IP of the proxy instance will be',
|
||||||
|
'loaded and passed to SSH via the ProxyJump option. The --no-proxy',
|
||||||
|
'flag can be used to ignore the tag and force a direct connection.',
|
||||||
|
'',
|
||||||
|
'For example, to proxy connections to zone "narnia" through "wardrobe":',
|
||||||
|
' triton instance tag set narnia tritoncli.ssh.proxy=wardrobe',
|
||||||
|
'',
|
||||||
|
'The "tritoncli.ssh.ip" tag on the target instance may be set to the',
|
||||||
|
'IP address to use for SSH connections. This may be useful if the',
|
||||||
|
'primary IP address is not available for SSH connections. This address',
|
||||||
|
'must be set to one of the IP addresses attached to the instance.',
|
||||||
|
'',
|
||||||
|
'The "tritoncli.ssh.proxyuser" tag on the proxy instance may be set to',
|
||||||
|
'the user account that should be used for the proxy connection (i.e., via',
|
||||||
|
'the SSH ProxyJump option). This is useful when all users of the proxy',
|
||||||
|
'instance should use a special common account, and will override the USER',
|
||||||
|
'value (if one is provided) for the SSH connection to the target instance.',
|
||||||
|
'',
|
||||||
|
'There is a known issue with SSH connection multiplexing (a.k.a. ',
|
||||||
|
'ControlMaster, mux) where stdout/stderr is lost. As a workaround, `ssh`',
|
||||||
|
'is spawned with options disabling ControlMaster. See ',
|
||||||
|
'<https://github.com/joyent/node-triton/issues/52> for details. If you ',
|
||||||
|
'want to use ControlMaster, an alternative is:',
|
||||||
|
' ssh root@$(spearhead ip INST)'
|
||||||
|
/* END JSSTYLED */
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
do_ssh.interspersedOptions = false;
|
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;
|
module.exports = do_ssh;
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
|
|
||||||
|
var common = require('../../common');
|
||||||
var errors = require('../../errors');
|
var errors = require('../../errors');
|
||||||
|
|
||||||
|
|
||||||
@ -29,41 +30,47 @@ function do_delete(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
var waitTimeoutMs = opts.wait_timeout * 1000; /* seconds to ms */
|
var waitTimeoutMs = opts.wait_timeout * 1000; /* seconds to ms */
|
||||||
|
|
||||||
if (opts.all) {
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
self.top.tritonapi.deleteAllInstanceTags({
|
if (setupErr) {
|
||||||
id: args[0],
|
cb(setupErr);
|
||||||
wait: opts.wait,
|
return;
|
||||||
waitTimeout: waitTimeoutMs
|
}
|
||||||
}, function (err) {
|
if (opts.all) {
|
||||||
console.log('Deleted all tags on instance %s', args[0]);
|
self.top.tritonapi.deleteAllInstanceTags({
|
||||||
cb(err);
|
id: args[0],
|
||||||
});
|
wait: opts.wait,
|
||||||
} else {
|
waitTimeout: waitTimeoutMs
|
||||||
// Uniq'ify the given names.
|
}, function (err) {
|
||||||
var names = {};
|
console.log('Deleted all tags on instance %s', args[0]);
|
||||||
args.slice(1).forEach(function (arg) { names[arg] = true; });
|
cb(err);
|
||||||
names = Object.keys(names);
|
});
|
||||||
|
} 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
|
// TODO: Instead of waiting for each delete, let's delete
|
||||||
// wait for the set.
|
// them all then wait for the set.
|
||||||
vasync.forEachPipeline({
|
vasync.forEachPipeline({
|
||||||
inputs: names,
|
inputs: names,
|
||||||
func: function deleteOne(name, next) {
|
func: function deleteOne(name, next) {
|
||||||
self.top.tritonapi.deleteInstanceTag({
|
self.top.tritonapi.deleteInstanceTag({
|
||||||
id: args[0],
|
id: args[0],
|
||||||
tag: name,
|
tag: name,
|
||||||
wait: opts.wait,
|
wait: opts.wait,
|
||||||
waitTimeout: waitTimeoutMs
|
waitTimeout: waitTimeoutMs
|
||||||
}, function (err) {
|
}, function (err) {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
console.log('Deleted tag %s on instance %s',
|
console.log('Deleted tag %s on instance %s',
|
||||||
name, args[0]);
|
name, args[0]);
|
||||||
}
|
}
|
||||||
next(err);
|
next(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, cb);
|
}, cb);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
do_delete.options = [
|
do_delete.options = [
|
||||||
@ -91,22 +98,25 @@ do_delete.options = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_delete.synopses = [
|
||||||
|
'{{name}} {{cmd}} INST [NAME ...]',
|
||||||
|
'{{name}} {{cmd}} --all INST # delete all tags'
|
||||||
|
];
|
||||||
|
|
||||||
do_delete.help = [
|
do_delete.help = [
|
||||||
/* BEGIN JSSTYLED */
|
|
||||||
'Delete one or more instance tags.',
|
'Delete one or more instance tags.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} delete <inst> [<name> ...]',
|
|
||||||
' {{name}} delete --all <inst> # delete all tags',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}',
|
'{{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',
|
'Changing instance tags is asynchronous. Use "--wait" to not return until',
|
||||||
'the changes are completed.'
|
'the changes are completed.'
|
||||||
/* END JSSTYLED */
|
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
do_delete.aliases = ['rm'];
|
do_delete.aliases = ['rm'];
|
||||||
|
|
||||||
|
do_delete.completionArgtypes = ['tritoninstance', 'none'];
|
||||||
|
|
||||||
module.exports = do_delete;
|
module.exports = do_delete;
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
* `triton instance tag get ...`
|
* `triton instance tag get ...`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var common = require('../../common');
|
||||||
var errors = require('../../errors');
|
var errors = require('../../errors');
|
||||||
|
|
||||||
|
|
||||||
@ -23,20 +24,26 @@ function do_get(subcmd, opts, args, cb) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.top.tritonapi.getInstanceTag({
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
id: args[0],
|
if (setupErr) {
|
||||||
tag: args[1]
|
cb(setupErr);
|
||||||
}, function (err, value) {
|
|
||||||
if (err) {
|
|
||||||
cb(err);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (opts.json) {
|
self.top.tritonapi.getInstanceTag({
|
||||||
console.log(JSON.stringify(value));
|
id: args[0],
|
||||||
} else {
|
tag: args[1]
|
||||||
console.log(value);
|
}, function (err, value) {
|
||||||
}
|
if (err) {
|
||||||
cb();
|
cb(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(value));
|
||||||
|
} else {
|
||||||
|
console.log(value);
|
||||||
|
}
|
||||||
|
cb();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,16 +60,18 @@ do_get.options = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_get.synopses = ['{{name}} {{cmd}} INST NAME'];
|
||||||
|
|
||||||
do_get.help = [
|
do_get.help = [
|
||||||
/* BEGIN JSSTYLED */
|
|
||||||
'Get an instance tag.',
|
'Get an instance tag.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} get <inst> <name>',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}',
|
'{{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');
|
].join('\n');
|
||||||
|
|
||||||
|
// TODO: When have 'tritoninstancetag' completion, add that in.
|
||||||
|
do_get.completionArgtypes = ['tritoninstance', 'none'];
|
||||||
|
|
||||||
module.exports = do_get;
|
module.exports = do_get;
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
* `triton instance tag list ...`
|
* `triton instance tag list ...`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var common = require('../../common');
|
||||||
var errors = require('../../errors');
|
var errors = require('../../errors');
|
||||||
|
|
||||||
function do_list(subcmd, opts, args, cb) {
|
function do_list(subcmd, opts, args, cb) {
|
||||||
@ -22,17 +23,24 @@ function do_list(subcmd, opts, args, cb) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.top.tritonapi.listInstanceTags({id: args[0]}, function (err, tags) {
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
if (err) {
|
if (setupErr) {
|
||||||
cb(err);
|
cb(setupErr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (opts.json) {
|
self.top.tritonapi.listInstanceTags(
|
||||||
console.log(JSON.stringify(tags));
|
{id: args[0]}, function (err, tags) {
|
||||||
} else {
|
if (err) {
|
||||||
console.log(JSON.stringify(tags, null, 4));
|
cb(err);
|
||||||
}
|
return;
|
||||||
cb();
|
}
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(tags));
|
||||||
|
} else {
|
||||||
|
console.log(JSON.stringify(tags, null, 4));
|
||||||
|
}
|
||||||
|
cb();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,21 +57,22 @@ do_list.options = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_list.synopses = ['{{name}} {{cmd}} INST'];
|
||||||
|
|
||||||
do_list.help = [
|
do_list.help = [
|
||||||
/* BEGIN JSSTYLED */
|
|
||||||
'List instance tags.',
|
'List instance tags.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} list <inst>',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}',
|
'{{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',
|
'Note: Currently this dumps prettified JSON by default. That might change',
|
||||||
'in the future. Use "-j" to explicitly get JSON output.'
|
'in the future. Use "-j" to explicitly get JSON output.'
|
||||||
/* END JSSTYLED */
|
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
do_list.aliases = ['ls'];
|
do_list.aliases = ['ls'];
|
||||||
|
|
||||||
|
do_list.completionArgtypes = ['tritoninstance', 'none'];
|
||||||
|
|
||||||
module.exports = do_list;
|
module.exports = do_list;
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
|
|
||||||
|
var common = require('../../common');
|
||||||
var errors = require('../../errors');
|
var errors = require('../../errors');
|
||||||
var mat = require('../../metadataandtags');
|
var mat = require('../../metadataandtags');
|
||||||
|
|
||||||
@ -27,7 +28,8 @@ function do_replace_all(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
var log = self.log;
|
var log = self.log;
|
||||||
|
|
||||||
vasync.pipeline({arg: {}, funcs: [
|
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||||
|
common.cliSetupTritonApi,
|
||||||
function gatherTags(ctx, next) {
|
function gatherTags(ctx, next) {
|
||||||
mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) {
|
mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -108,17 +110,19 @@ 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 = [
|
do_replace_all.help = [
|
||||||
/* BEGIN JSSTYLED */
|
|
||||||
'Replace all tags on the given instance.',
|
'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}}',
|
'{{options}}',
|
||||||
'Where <inst> is an instance id, name, or shortid; <name> is a tag name;',
|
'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 ',
|
'and VALUE is a tag value (bool and numeric "value" are converted to ',
|
||||||
'that type).',
|
'that type).',
|
||||||
'',
|
'',
|
||||||
'Currently this dumps prettified JSON by default. That might change in the',
|
'Currently this dumps prettified JSON by default. That might change in the',
|
||||||
@ -126,7 +130,8 @@ do_replace_all.help = [
|
|||||||
'',
|
'',
|
||||||
'Changing instance tags is asynchronous. Use "--wait" to not return until',
|
'Changing instance tags is asynchronous. Use "--wait" to not return until',
|
||||||
'the changes are completed.'
|
'the changes are completed.'
|
||||||
/* END JSSTYLED */
|
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
do_replace_all.completionArgtypes = ['tritoninstance', 'file'];
|
||||||
|
|
||||||
module.exports = do_replace_all;
|
module.exports = do_replace_all;
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
|
|
||||||
|
var common = require('../../common');
|
||||||
var errors = require('../../errors');
|
var errors = require('../../errors');
|
||||||
var mat = require('../../metadataandtags');
|
var mat = require('../../metadataandtags');
|
||||||
|
|
||||||
@ -27,7 +28,8 @@ function do_set(subcmd, opts, args, cb) {
|
|||||||
}
|
}
|
||||||
var log = self.log;
|
var log = self.log;
|
||||||
|
|
||||||
vasync.pipeline({arg: {}, funcs: [
|
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||||
|
common.cliSetupTritonApi,
|
||||||
function gatherTags(ctx, next) {
|
function gatherTags(ctx, next) {
|
||||||
mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) {
|
mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -109,17 +111,20 @@ do_set.options = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_set.synopses = [
|
||||||
|
'{{name}} set INST [NAME=VALUE ...]',
|
||||||
|
'{{name}} set INST -f FILE # tags from file'
|
||||||
|
];
|
||||||
|
|
||||||
do_set.help = [
|
do_set.help = [
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
'Set one or more instance tags.',
|
'Set one or more instance tags.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} set <inst> [<name>=<value> ...]',
|
|
||||||
' {{name}} set <inst> -f <file> # tags from file',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}',
|
'{{options}}',
|
||||||
'Where <inst> is an instance id, name, or shortid; <name> is a tag name;',
|
'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 ',
|
'and VALUE is a tag value (bool and numeric "value" are converted to ',
|
||||||
'that type).',
|
'that type).',
|
||||||
'',
|
'',
|
||||||
'Currently this dumps prettified JSON by default. That might change in the',
|
'Currently this dumps prettified JSON by default. That might change in the',
|
||||||
@ -130,4 +135,6 @@ do_set.help = [
|
|||||||
/* END JSSTYLED */
|
/* END JSSTYLED */
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
do_set.completionArgtypes = ['tritoninstance', 'file'];
|
||||||
|
|
||||||
module.exports = do_set;
|
module.exports = do_set;
|
||||||
|
@ -22,7 +22,7 @@ function InstanceTagCLI(parent) {
|
|||||||
name: parent.name + ' tag',
|
name: parent.name + ' tag',
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
desc: [
|
desc: [
|
||||||
'List, get, set and delete tags on Triton instances.'
|
'List, get, set and delete tags on Spearhead instances.'
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
/* END JSSTYLED */
|
/* END JSSTYLED */
|
||||||
helpOpts: {
|
helpOpts: {
|
||||||
|
@ -5,11 +5,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* Copyright 2017 Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton instance tags ...` shortcut for `triton instance tag list ...`.
|
* `triton instance tags ...` shortcut for `triton instance tag list ...`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var targ = require('./do_tag/do_list');
|
||||||
|
|
||||||
function do_tags(subcmd, opts, args, callback) {
|
function do_tags(subcmd, opts, args, callback) {
|
||||||
this.handlerFromSubcmd('tag').dispatch({
|
this.handlerFromSubcmd('tag').dispatch({
|
||||||
subcmd: 'list',
|
subcmd: 'list',
|
||||||
@ -18,8 +20,11 @@ function do_tags(subcmd, opts, args, callback) {
|
|||||||
}, callback);
|
}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
do_tags.help = 'A shortcut for "triton instance tag list".';
|
do_tags.help = 'A shortcut for "spearhead instance tag list".\n' + targ.help;
|
||||||
do_tags.options = require('./do_tag/do_list').options;
|
do_tags.synopses = targ.synopses;
|
||||||
|
do_tags.options = targ.options;
|
||||||
|
do_tags.completionArgtypes = targ.completionArgtypes;
|
||||||
|
|
||||||
do_tags.hidden = true;
|
do_tags.hidden = true;
|
||||||
|
|
||||||
module.exports = do_tags;
|
module.exports = do_tags;
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
|
|
||||||
|
var common = require('../common');
|
||||||
var distractions = require('../distractions');
|
var distractions = require('../distractions');
|
||||||
var errors = require('../errors');
|
var errors = require('../errors');
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ function do_wait(subcmd, opts, args, cb) {
|
|||||||
if (opts.help) {
|
if (opts.help) {
|
||||||
return this.do_help('help', {}, [subcmd], cb);
|
return this.do_help('help', {}, [subcmd], cb);
|
||||||
} else if (args.length < 1) {
|
} else if (args.length < 1) {
|
||||||
return cb(new errors.UsageError('missing INSTANCE arg(s)'));
|
return cb(new errors.UsageError('missing INST arg(s)'));
|
||||||
}
|
}
|
||||||
var ids = args;
|
var ids = args;
|
||||||
var states = [];
|
var states = [];
|
||||||
@ -34,7 +35,8 @@ function do_wait(subcmd, opts, args, cb) {
|
|||||||
var done = 0;
|
var done = 0;
|
||||||
var instFromId = {};
|
var instFromId = {};
|
||||||
|
|
||||||
vasync.pipeline({funcs: [
|
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||||
|
common.cliSetupTritonApi,
|
||||||
function getInsts(_, next) {
|
function getInsts(_, next) {
|
||||||
vasync.forEachParallel({
|
vasync.forEachParallel({
|
||||||
inputs: ids,
|
inputs: ids,
|
||||||
@ -110,16 +112,19 @@ function do_wait(subcmd, opts, args, cb) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
do_wait.synopses = ['{{name}} {{cmd}} [-s STATES] INST [INST ...]'];
|
||||||
do_wait.help = [
|
do_wait.help = [
|
||||||
|
/* BEGIN JSSTYLED */
|
||||||
'Wait on instances changing state.',
|
'Wait on instances changing state.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} wait [-s STATES] INSTANCE [INSTANCE ...]',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}',
|
'{{options}}',
|
||||||
'Where "states" is a comma-separated list of target instance states,',
|
'Where "INST" is an instance name, id, or short id; and "STATES" is a',
|
||||||
'by default "running,failed". In other words, "triton inst wait foo0" will',
|
'comma-separated list of target instance states, by default "running,failed".',
|
||||||
'wait for instance "foo0" to complete provisioning.'
|
'In other words, "spearhead inst wait foo0" will wait for instance "foo0" to',
|
||||||
|
'complete provisioning.'
|
||||||
|
/* END JSSTYLED */
|
||||||
].join('\n');
|
].join('\n');
|
||||||
do_wait.options = [
|
do_wait.options = [
|
||||||
{
|
{
|
||||||
@ -137,4 +142,6 @@ do_wait.options = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_wait.completionArgtypes = ['tritoninstance'];
|
||||||
|
|
||||||
module.exports = do_wait;
|
module.exports = do_wait;
|
||||||
|
@ -5,12 +5,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2015 Joyent, Inc.
|
* Copyright 2016 Joyent, Inc.
|
||||||
*
|
*
|
||||||
* Shared support for:
|
* Shared support for:
|
||||||
* `triton instance start ...`
|
* `triton instance start ...`
|
||||||
* `triton instance stop ...`
|
* `triton instance stop ...`
|
||||||
* `triton instance reboot ...`
|
|
||||||
* `triton instance delete ...`
|
* `triton instance delete ...`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -18,9 +17,7 @@ var assert = require('assert-plus');
|
|||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
|
|
||||||
var common = require('../common');
|
var common = require('../common');
|
||||||
|
var errors = require('../errors');
|
||||||
|
|
||||||
var f = require('util').format;
|
|
||||||
|
|
||||||
|
|
||||||
function perror(err) {
|
function perror(err) {
|
||||||
@ -34,24 +31,26 @@ function gen_do_ACTION(opts) {
|
|||||||
assert.optionalArrayOfString(opts.aliases, 'opts.aliases');
|
assert.optionalArrayOfString(opts.aliases, 'opts.aliases');
|
||||||
var action = opts.action;
|
var action = opts.action;
|
||||||
|
|
||||||
assert.ok(['start', 'stop', 'reboot', 'delete'].indexOf(action) >= 0,
|
assert.ok(['start', 'stop', 'delete'].indexOf(action) >= 0,
|
||||||
'invalid action');
|
'invalid action');
|
||||||
|
|
||||||
function do_ACTION(subcmd, _opts, args, callback) {
|
function do_ACTION(subcmd, _opts, args, callback) {
|
||||||
return _doTheAction.call(this, action, subcmd, _opts, args, callback);
|
return _doTheAction.call(this, action, subcmd, _opts, args, callback);
|
||||||
}
|
}
|
||||||
|
do_ACTION.name = 'do_' + action;
|
||||||
|
|
||||||
if (opts.aliases) {
|
if (opts.aliases) {
|
||||||
do_ACTION.aliases = opts.aliases;
|
do_ACTION.aliases = opts.aliases;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
do_ACTION.synopses = ['{{name}} ' + action + ' [OPTIONS] INST [INST ...]'];
|
||||||
do_ACTION.help = [
|
do_ACTION.help = [
|
||||||
f('%s one or more instances.', common.capitalize(action)),
|
common.capitalize(action) + ' one or more instances.',
|
||||||
f(''),
|
'',
|
||||||
f('Usage:'),
|
'{{usage}}',
|
||||||
f(' {{name}} %s <alias|id> ...', action),
|
'',
|
||||||
f(''),
|
'{{options}}',
|
||||||
f('{{options}}')
|
'Where "INST" is an instance name, id, or short id.'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
do_ACTION.options = [
|
do_ACTION.options = [
|
||||||
{
|
{
|
||||||
@ -66,11 +65,14 @@ function gen_do_ACTION(opts) {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_ACTION.completionArgtypes = ['tritoninstance'];
|
||||||
|
|
||||||
if (action === 'start') {
|
if (action === 'start') {
|
||||||
do_ACTION.options.push({
|
do_ACTION.options.push({
|
||||||
names: ['snapshot'],
|
names: ['snapshot'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
help: 'Name of snapshot to start machine with.'
|
help: 'Name of snapshot with which to start the instance.',
|
||||||
|
helpArg: 'SNAPNAME'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,8 +82,6 @@ function gen_do_ACTION(opts) {
|
|||||||
function _doTheAction(action, subcmd, opts, args, callback) {
|
function _doTheAction(action, subcmd, opts, args, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var now = Date.now();
|
|
||||||
|
|
||||||
var command, state;
|
var command, state;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'start':
|
case 'start':
|
||||||
@ -93,10 +93,6 @@ function _doTheAction(action, subcmd, opts, args, callback) {
|
|||||||
command = 'stopMachine';
|
command = 'stopMachine';
|
||||||
state = 'stopped';
|
state = 'stopped';
|
||||||
break;
|
break;
|
||||||
case 'reboot':
|
|
||||||
command = 'rebootMachine';
|
|
||||||
state = 'running';
|
|
||||||
break;
|
|
||||||
case 'delete':
|
case 'delete':
|
||||||
command = 'deleteMachine';
|
command = 'deleteMachine';
|
||||||
state = 'deleted';
|
state = 'deleted';
|
||||||
@ -110,10 +106,21 @@ function _doTheAction(action, subcmd, opts, args, callback) {
|
|||||||
this.do_help('help', {}, [subcmd], callback);
|
this.do_help('help', {}, [subcmd], callback);
|
||||||
return;
|
return;
|
||||||
} else if (args.length < 1) {
|
} else if (args.length < 1) {
|
||||||
callback(new Error('invalid args: ' + args));
|
callback(new errors.UsageError('missing INST arg(s)'));
|
||||||
return;
|
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({
|
vasync.forEachParallel({
|
||||||
func: function (arg, cb) {
|
func: function (arg, cb) {
|
||||||
var alias, uuid;
|
var alias, uuid;
|
||||||
@ -187,7 +194,7 @@ function _doTheAction(action, subcmd, opts, args, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
inputs: args
|
inputs: instances
|
||||||
}, function (err, results) {
|
}, function (err, results) {
|
||||||
var e = err ? (new Error('command failure')) : null;
|
var e = err ? (new Error('command failure')) : null;
|
||||||
callback(e);
|
callback(e);
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2015 Joyent, Inc.
|
* Copyright 2018 Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton instance ...`
|
* `triton instance ...`
|
||||||
*/
|
*/
|
||||||
@ -22,7 +22,7 @@ function InstanceCLI(top) {
|
|||||||
name: top.name + ' instance',
|
name: top.name + ' instance',
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
desc: [
|
desc: [
|
||||||
'List and manage Triton instances.'
|
'List and manage Spearhead instances.'
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
/* END JSSTYLED */
|
/* END JSSTYLED */
|
||||||
helpOpts: {
|
helpOpts: {
|
||||||
@ -34,15 +34,25 @@ function InstanceCLI(top) {
|
|||||||
'get',
|
'get',
|
||||||
'create',
|
'create',
|
||||||
'delete',
|
'delete',
|
||||||
|
'resize',
|
||||||
|
'rename',
|
||||||
{ group: '' },
|
{ group: '' },
|
||||||
'start',
|
'start',
|
||||||
'stop',
|
'stop',
|
||||||
'reboot',
|
'reboot',
|
||||||
{ group: '' },
|
{ group: '' },
|
||||||
|
'fwrules',
|
||||||
|
'enable-firewall',
|
||||||
|
'disable-firewall',
|
||||||
|
{ group: '' },
|
||||||
|
'enable-deletion-protection',
|
||||||
|
'disable-deletion-protection',
|
||||||
|
{ group: '' },
|
||||||
'ssh',
|
'ssh',
|
||||||
|
'ip',
|
||||||
'wait',
|
'wait',
|
||||||
'audit',
|
'audit',
|
||||||
'fwrules',
|
'nic',
|
||||||
'snapshot',
|
'snapshot',
|
||||||
'tag'
|
'tag'
|
||||||
]
|
]
|
||||||
@ -59,15 +69,28 @@ InstanceCLI.prototype.do_list = require('./do_list');
|
|||||||
InstanceCLI.prototype.do_get = require('./do_get');
|
InstanceCLI.prototype.do_get = require('./do_get');
|
||||||
InstanceCLI.prototype.do_create = require('./do_create');
|
InstanceCLI.prototype.do_create = require('./do_create');
|
||||||
InstanceCLI.prototype.do_delete = require('./do_delete');
|
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_start = require('./do_start');
|
||||||
InstanceCLI.prototype.do_stop = require('./do_stop');
|
InstanceCLI.prototype.do_stop = require('./do_stop');
|
||||||
InstanceCLI.prototype.do_reboot = require('./do_reboot');
|
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_ssh = require('./do_ssh');
|
||||||
|
InstanceCLI.prototype.do_ip = require('./do_ip');
|
||||||
InstanceCLI.prototype.do_wait = require('./do_wait');
|
InstanceCLI.prototype.do_wait = require('./do_wait');
|
||||||
InstanceCLI.prototype.do_audit = require('./do_audit');
|
InstanceCLI.prototype.do_audit = require('./do_audit');
|
||||||
InstanceCLI.prototype.do_fwrules = require('./do_fwrules');
|
InstanceCLI.prototype.do_nic = require('./do_nic');
|
||||||
InstanceCLI.prototype.do_snapshot = require('./do_snapshot');
|
InstanceCLI.prototype.do_snapshot = require('./do_snapshot');
|
||||||
InstanceCLI.prototype.do_snapshots = require('./do_snapshots');
|
InstanceCLI.prototype.do_snapshots = require('./do_snapshots');
|
||||||
InstanceCLI.prototype.do_tag = require('./do_tag');
|
InstanceCLI.prototype.do_tag = require('./do_tag');
|
||||||
|
@ -5,11 +5,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* Copyright 2017 Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton instances ...` bwcompat shortcut for `triton instance list ...`.
|
* `triton instances ...` bwcompat shortcut for `triton instance list ...`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var targ = require('./do_instance/do_list');
|
||||||
|
|
||||||
function do_instances(subcmd, opts, args, callback) {
|
function do_instances(subcmd, opts, args, callback) {
|
||||||
this.handlerFromSubcmd('instance').dispatch({
|
this.handlerFromSubcmd('instance').dispatch({
|
||||||
subcmd: 'list',
|
subcmd: 'list',
|
||||||
@ -18,8 +20,11 @@ function do_instances(subcmd, opts, args, callback) {
|
|||||||
}, callback);
|
}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
do_instances.help = 'A shortcut for "triton instance list".';
|
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.aliases = ['insts', 'ls'];
|
do_instances.aliases = ['insts', 'ls'];
|
||||||
do_instances.options = require('./do_instance/do_list').options;
|
|
||||||
|
|
||||||
module.exports = do_instances;
|
module.exports = do_instances;
|
||||||
|
28
lib/do_ip.js
Normal file
28
lib/do_ip.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
@ -40,19 +40,14 @@ function do_add(subcmd, opts, args, cb) {
|
|||||||
var filePath = args[0];
|
var filePath = args[0];
|
||||||
var cli = this.top;
|
var cli = this.top;
|
||||||
|
|
||||||
vasync.pipeline({arg: {}, funcs: [
|
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||||
|
common.cliSetupTritonApi,
|
||||||
function gatherDataStdin(ctx, next) {
|
function gatherDataStdin(ctx, next) {
|
||||||
if (filePath !== '-') {
|
if (filePath !== '-') {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
var stdin = '';
|
common.readStdin(function gotStdin(stdin) {
|
||||||
process.stdin.resume();
|
|
||||||
process.stdin.on('data', function (chunk) {
|
|
||||||
stdin += chunk;
|
|
||||||
});
|
|
||||||
|
|
||||||
process.stdin.on('end', function () {
|
|
||||||
ctx.data = stdin;
|
ctx.data = stdin;
|
||||||
ctx.from = '<stdin>';
|
ctx.from = '<stdin>';
|
||||||
next();
|
next();
|
||||||
@ -125,11 +120,13 @@ do_add.options = [
|
|||||||
help: 'An optional name for an added key.'
|
help: 'An optional name for an added key.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_add.synopses = ['{{name}} {{cmd}} [OPTIONS] FILE'];
|
||||||
|
|
||||||
do_add.help = [
|
do_add.help = [
|
||||||
'Add an SSH key to an account.',
|
'Add an SSH key to an account.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} add [<options>] FILE',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}',
|
'{{options}}',
|
||||||
'Where "FILE" must be a file path to an SSH public key, ',
|
'Where "FILE" must be a file path to an SSH public key, ',
|
||||||
|
@ -35,7 +35,8 @@ function do_delete(subcmd, opts, args, cb) {
|
|||||||
|
|
||||||
var cli = this.top;
|
var cli = this.top;
|
||||||
|
|
||||||
vasync.pipeline({funcs: [
|
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||||
|
common.cliSetupTritonApi,
|
||||||
function confirm(_, next) {
|
function confirm(_, next) {
|
||||||
if (opts.yes) {
|
if (opts.yes) {
|
||||||
return next();
|
return next();
|
||||||
@ -104,11 +105,13 @@ do_delete.options = [
|
|||||||
help: 'Answer yes to confirmation to delete.'
|
help: 'Answer yes to confirmation to delete.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_delete.synopses = ['{{name}} {{cmd}} [OPTIONS] KEY [KEY ...]'];
|
||||||
|
|
||||||
do_delete.help = [
|
do_delete.help = [
|
||||||
'Remove an SSH key from an account.',
|
'Remove an SSH key from an account.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} delete [<options>] KEY [KEY...]',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}',
|
'{{options}}',
|
||||||
'Where "KEY" is an SSH key "name" or "fingerprint".'
|
'Where "KEY" is an SSH key "name" or "fingerprint".'
|
||||||
@ -116,4 +119,6 @@ do_delete.help = [
|
|||||||
|
|
||||||
do_delete.aliases = ['rm'];
|
do_delete.aliases = ['rm'];
|
||||||
|
|
||||||
|
do_delete.completionArgtypes = ['tritonkey'];
|
||||||
|
|
||||||
module.exports = do_delete;
|
module.exports = do_delete;
|
||||||
|
@ -35,22 +35,28 @@ function do_get(subcmd, opts, args, cb) {
|
|||||||
var id = args[0];
|
var id = args[0];
|
||||||
var cli = this.top;
|
var cli = this.top;
|
||||||
|
|
||||||
cli.tritonapi.cloudapi.getKey({
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
// Currently `cloudapi.getUserKey` isn't picky about the `name` being
|
if (setupErr) {
|
||||||
// passed in as the `opts.fingerprint` arg.
|
cb(setupErr);
|
||||||
fingerprint: id
|
|
||||||
}, function onKey(err, key) {
|
|
||||||
if (err) {
|
|
||||||
cb(err);
|
|
||||||
return;
|
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) {
|
if (opts.json) {
|
||||||
console.log(JSON.stringify(key));
|
console.log(JSON.stringify(key));
|
||||||
} else {
|
} else {
|
||||||
console.log(common.chomp(key.key));
|
console.log(common.chomp(key.key));
|
||||||
}
|
}
|
||||||
cb();
|
cb();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,14 +73,18 @@ do_get.options = [
|
|||||||
help: 'JSON stream output.'
|
help: 'JSON stream output.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
do_get.synopses = ['{{name}} {{cmd}} KEY'];
|
||||||
|
|
||||||
do_get.help = [
|
do_get.help = [
|
||||||
'Show a specific SSH key in an account.',
|
'Show a specific SSH key in an account.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} get KEY',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}',
|
'{{options}}',
|
||||||
'Where "KEY" is an SSH key "name" or "fingerprint".'
|
'Where "KEY" is an SSH key "name" or "fingerprint".'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
do_get.completionArgtypes = ['tritonkey', 'none'];
|
||||||
|
|
||||||
module.exports = do_get;
|
module.exports = do_get;
|
||||||
|
@ -37,37 +37,43 @@ function do_list(subcmd, opts, args, cb) {
|
|||||||
|
|
||||||
var cli = this.top;
|
var cli = this.top;
|
||||||
|
|
||||||
cli.tritonapi.cloudapi.listKeys({}, function onKeys(err, keys) {
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
if (err) {
|
if (setupErr) {
|
||||||
cb(err);
|
cb(setupErr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
cli.tritonapi.cloudapi.listKeys({}, function onKeys(err, keys) {
|
||||||
if (opts.json) {
|
if (err) {
|
||||||
common.jsonStream(keys);
|
cb(err);
|
||||||
} else if (opts.authorized_keys) {
|
return;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
columns = columns.split(',');
|
if (opts.json) {
|
||||||
var sort = opts.s.split(',');
|
common.jsonStream(keys);
|
||||||
|
} else if (opts.authorized_keys) {
|
||||||
|
keys.forEach(function (key) {
|
||||||
|
console.log(common.chomp(key.key));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var columns = COLUMNS_DEFAULT;
|
||||||
|
|
||||||
tabula(keys, {
|
if (opts.o) {
|
||||||
skipHeader: false,
|
columns = opts.o;
|
||||||
columns: columns,
|
} else if (opts.long) {
|
||||||
sort: sort
|
columns = COLUMNS_LONG;
|
||||||
});
|
}
|
||||||
}
|
|
||||||
cb();
|
columns = columns.split(',');
|
||||||
|
var sort = opts.s.split(',');
|
||||||
|
|
||||||
|
tabula(keys, {
|
||||||
|
skipHeader: false,
|
||||||
|
columns: columns,
|
||||||
|
sort: sort
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cb();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,11 +96,11 @@ do_list.options = [
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS]'];
|
||||||
do_list.help = [
|
do_list.help = [
|
||||||
'Show all of an account\'s SSH keys.',
|
'Show all of an account\'s SSH keys.',
|
||||||
'',
|
'',
|
||||||
'Usage:',
|
'{{usage}}',
|
||||||
' {{name}} list [<options>]',
|
|
||||||
'',
|
'',
|
||||||
'{{options}}'
|
'{{options}}'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
@ -5,11 +5,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* Copyright 2017 Joyent, Inc.
|
||||||
*
|
*
|
||||||
* `triton keys ...` bwcompat shortcut for `triton key list ...`.
|
* `triton keys ...` bwcompat shortcut for `triton key list ...`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var targ = require('./do_key/do_list');
|
||||||
|
|
||||||
function do_keys(subcmd, opts, args, callback) {
|
function do_keys(subcmd, opts, args, callback) {
|
||||||
this.handlerFromSubcmd('key').dispatch({
|
this.handlerFromSubcmd('key').dispatch({
|
||||||
subcmd: 'list',
|
subcmd: 'list',
|
||||||
@ -18,8 +20,11 @@ function do_keys(subcmd, opts, args, callback) {
|
|||||||
}, callback);
|
}, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
do_keys.help = 'A shortcut for "triton key list".';
|
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.hidden = true;
|
do_keys.hidden = true;
|
||||||
do_keys.options = require('./do_key/do_list').options;
|
|
||||||
|
|
||||||
module.exports = do_keys;
|
module.exports = do_keys;
|
||||||
|
273
lib/do_network/do_create.js
Normal file
273
lib/do_network/do_create.js
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
85
lib/do_network/do_delete.js
Normal file
85
lib/do_network/do_delete.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
@ -25,17 +25,25 @@ function do_get(subcmd, opts, args, cb) {
|
|||||||
'incorrect number of args (%d)', args.length)));
|
'incorrect number of args (%d)', args.length)));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.top.tritonapi.getNetwork(args[0], function (err, net) {
|
var tritonapi = this.top.tritonapi;
|
||||||
if (err) {
|
|
||||||
return cb(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.json) {
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
console.log(JSON.stringify(net));
|
if (setupErr) {
|
||||||
} else {
|
cb(setupErr);
|
||||||
console.log(JSON.stringify(net, null, 4));
|
return;
|
||||||
}
|
}
|
||||||
cb();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,13 +59,18 @@ do_get.options = [
|
|||||||
help: 'JSON output.'
|
help: 'JSON output.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
do_get.help = (
|
|
||||||
'Show a network.\n'
|
do_get.synopses = ['{{name}} {{cmd}} NETWORK'];
|
||||||
+ '\n'
|
|
||||||
+ 'Usage:\n'
|
do_get.help = [
|
||||||
+ ' {{name}} get <id|name>\n'
|
'Show a network.',
|
||||||
+ '\n'
|
'',
|
||||||
+ '{{options}}'
|
'{{usage}}',
|
||||||
);
|
'',
|
||||||
|
'{{options}}',
|
||||||
|
'Where NETWORK is a network id (full UUID), name, or short id.'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
do_get.completionArgtypes = ['tritonnetwork', 'none'];
|
||||||
|
|
||||||
module.exports = do_get;
|
module.exports = do_get;
|
||||||
|
78
lib/do_network/do_get_default.js
Normal file
78
lib/do_network/do_get_default.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
81
lib/do_network/do_ip/do_get.js
Normal file
81
lib/do_network/do_ip/do_get.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
129
lib/do_network/do_ip/do_list.js
Normal file
129
lib/do_network/do_ip/do_list.js
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
193
lib/do_network/do_ip/do_update.js
Normal file
193
lib/do_network/do_ip/do_update.js
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
/*
|
||||||
|
* 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
Reference in New Issue
Block a user