Compare commits

..

1 Commits

Author SHA1 Message Date
Josh Wilsdon
de9505d569 initial work on node-triton for PUBAPI-1420 2017-08-29 20:48:52 -07:00
126 changed files with 1256 additions and 7988 deletions

3
.gitignore vendored
View File

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

View File

@ -4,170 +4,10 @@ Known issues:
- `triton ssh ...` disables ssh ControlMaster to avoid issue #52. - `triton ssh ...` disables ssh ControlMaster to avoid issue #52.
## not yet released ## not yet released
(nothing) (nothing yet)
## 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 ## 5.3.1

427
README.md
View File

@ -1,38 +1,427 @@
![logo](https://code.spearhead.cloud/Spearhead/node-spearhead/raw/branch/master/tools/sphsp.png) ![logo](./tools/triton-text.png)
# node-spearhead # node-triton
This repository holds the node-spearhead CLI tool to work with the Spearhead This repository is part of the Joyent Triton project. See the [contribution
Cloud. It is a fork of [node-triton](https://github.com/joyent/node-triton). guidelines](https://github.com/joyent/triton/blob/master/CONTRIBUTING.md) --
*Triton does not use GitHub PRs* -- and general documentation at the main
[Triton project](https://github.com/joyent/triton) page.
## Installation and configuration `triton` is a CLI tool for working with the CloudAPI for Joyent's Triton [Public Cloud]
(https://docs.joyent.com/public-cloud) and [Private Cloud] (https://docs.joyent.com/private-cloud).
CloudAPI is a RESTful API for end users of the cloud to manage their accounts, instances,
networks, images, and to inquire other relevant details. CloudAPI provides a single view of
docker containers, infrastructure containers and hardware virtual machines available in the
Triton solution.
### Get a Spearhead Cloud account There is currently another CLI tool known as [node-smartdc](https://github.com/joyent/node-smartdc)
for CloudAPI. `node-smartdc` CLI works off the 32-character object UUID to uniquely
identify object instances in API requests, and returns response payload in JSON format.
The CLI covers both basic and advanced usage of [CloudAPI](https://apidocs.joyent.com/cloudapi/).
Create an account on the Spearhead Cloud and upload your SSH key. You can create an account **The `triton` CLI is currently in beta (effectively because it does not yet
[here](https://spearhead.cloud/). have *complete* coverage of all commands from node-smartdc) and will be
expanded over time to support all CloudAPI commands, eventually replacing
`node-smartdc` as both the API client library for Triton cloud and the command
line tool.**
## Setup
### User accounts, authentication, and security
Before you can use the CLI you'll need an account on the cloud to which you are connecting and
an SSH key uploaded. The SSH key is used to identify and secure SSH access to containers and
other resources in Triton.
If you do not already have an account on Joyent Public Cloud, sign up [here](https://www.joyent.com/public-cloud).
### Data-centers ### API endpoint
The list of available Spearhead Cloud data-centers is available Each data center has a single CloudAPI endpoint. For Joyent Public Cloud, you can find the
[here](https://spearhead.cloud/datacenters). list of data centers [here](https://docs.joyent.com/public-cloud/data-centers).
For private cloud implementations, please consult the private cloud operator for the correct URL.
Have the URL handy as you'll need it in the next step.
### Installation ### Installation
Install [node.js](http://nodejs.org/), then: Install [node.js](http://nodejs.org/), then:
npm install -g spearhead npm install -g triton
Verify that it is installed and on your PATH: Verify that it is installed and on your PATH:
$ spearhead --version
Spearhead CLI 6.1.4 $ triton --version
https://code.spearhead.cloud/Spearhead/node-spearhead Triton CLI 4.15.0
https://github.com/joyent/node-triton
Now you ca use `spearhead` to interact with our Public Cloud. More details
about installation and configuration are available To use `triton`, you'll need to configure it to talk to a Triton DataCenter
[here](https://docs.spearhead.cloud). API endpoint (called CloudAPI). Commonly that is done using a Triton profile:
$ triton profile create
A profile name. A short string to identify a CloudAPI endpoint to the
`triton` CLI.
name: sw1
The CloudAPI endpoint URL.
url: https://us-sw-1.api.joyent.com
Your account login name.
account: bob
Available SSH keys:
1. 2048-bit RSA key with fingerprint 4e:e7:56:9a:b0:91:31:3e:23:8d:f8:62:12:58:a2:ec
* [in homedir] bob-20160704 id_rsa
The fingerprint of the SSH key you want to use, or its index in the list
above. If the key you want to use is not listed, make sure it is either saved
in your SSH keys directory or loaded into the SSH agent.
keyId: 1
Saved profile "sw1".
WARNING: Docker uses TLS-based authentication with a different security model
from SSH keys. As a result, the Docker client cannot currently support
encrypted (password protected) keys or SSH agents. If you continue, the
Triton CLI will attempt to format a copy of your SSH *private* key as an
unencrypted TLS cert and place the copy in ~/.triton/docker for use by the
Docker client.
Continue? [y/n] y
Setting up profile "sw1" to use Docker.
Setup profile "sw1" to use Docker (v1.12.3). Try this:
eval "$(triton env --docker sw1)"
docker info
Set "sw1" as current profile (because it is your only profile).
Or instead of using profiles, you can set the required environment variables
(`triton` defaults to an "env" profile that uses these environment variables if
no profile is set). For example:
TRITON_URL=https://us-sw-1.api.joyent.com
TRITON_ACCOUNT=bob
TRITON_KEY_ID=SHA256:j2WoSeOWhFy69BQ0uCR3FAySp9qCZTSCEyT2vRKcL+s
For compatibility with the older [sdc-* tools from
node-smartdc](https://github.com/joyent/node-smartdc), `triton` also supports
`SDC_URL`, `SDC_ACCOUNT`, etc. environment variables.
### 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 appropriate for a command-line tool is:
```javascript
var mod_bunyan = require('bunyan');
var mod_triton = require('triton');
var log = mod_bunyan.createLogger({name: 'my-tool'});
// See the `createClient` block comment for full usage details:
// https://github.com/joyent/node-triton/blob/master/lib/index.js
mod_triton.createClient({
log: log,
// Use 'env' to pick up 'TRITON_/SDC_' env vars. Or manually specify a
// `profile` object.
profileName: 'env',
unlockKeyFn: mod_triton.promptPassphraseUnlockKey
}, function (err, client) {
if (err) {
// handle err
}
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));
}
});
});
```
See the following for more details:
- The block-comment for `createClient` in [lib/index.js](lib/index.js).
- Some module-usage examples in [examples/](examples/).
- The lower-level details in the top-comment in
[lib/tritonapi.js](lib/tritonapi.js).
## 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.
## 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": "g4-highcpu-128M",
"resizePackage": "g4-highcpu-256M"
}
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.
## Release process
Here is how to cut a release:
1. Make a commit to set the intended version in "package.json#version" and changing `## not yet released` at the top of "CHANGES.md" to:
```
## not yet released
## $version
```
2. Get that commit approved and merged via <https://cr.joyent.us>, as with all
commits to this repo. See the discussion of contribution at the top of this
readme.
3. Once that is merged and you've updated your local copy, run:
```
make cutarelease
```
This will run a couple checks (clean working copy, versions in package.json
and CHANGES.md match), then will git tag and npm publish.
## License ## License
MPL 2.0 MPL 2.0

45
examples/example-get-account.js Executable file
View File

@ -0,0 +1,45 @@
#!/usr/bin/env node
/**
* Example creating a Triton API client and using it to get account info.
*
* Usage:
* ./example-get-account.js
*
* # With trace-level logging
* LOG_LEVEL=trace ./example-get-account.js 2>&1 | bunyan
*/
var bunyan = require('bunyan');
var path = require('path');
var triton = require('../'); // typically `require('triton');`
var log = bunyan.createLogger({
name: path.basename(__filename),
level: process.env.LOG_LEVEL || 'info',
stream: process.stderr
});
triton.createClient({
log: log,
// Use 'env' to pick up 'TRITON_/SDC_' env vars. Or manually specify a
// `profile` object.
profileName: 'env',
unlockKeyFn: triton.promptPassphraseUnlockKey
}, function createdClient(err, client) {
if (err) {
console.error('error creating Triton client: %s\n%s', err, err.stack);
process.exitStatus = 1;
return;
}
// TODO: Eventually the top-level TritonApi will have `.getAccount()`.
client.cloudapi.getAccount(function (err, account) {
client.close(); // Remember to close the client to close TCP conn.
if (err) {
console.error('getAccount error: %s\n%s', err, err.stack);
process.exitStatus = 1;
} else {
console.log(JSON.stringify(account, null, 4));
}
});
});

View File

@ -0,0 +1,46 @@
#!/usr/bin/env node
/**
* Example creating a Triton API client and using it to list instances.
*
* Usage:
* ./example-list-instances.js
*
* # With trace-level logging
* LOG_LEVEL=trace ./example-list-instances.js 2>&1 | bunyan
*/
var bunyan = require('bunyan');
var path = require('path');
var triton = require('../'); // typically `require('triton');`
var log = bunyan.createLogger({
name: path.basename(__filename),
level: process.env.LOG_LEVEL || 'info',
stream: process.stderr
});
triton.createClient({
log: log,
// Use 'env' to pick up 'TRITON_/SDC_' env vars. Or manually specify a
// `profile` object.
profileName: 'env',
unlockKeyFn: triton.promptPassphraseUnlockKey
}, function createdClient(err, client) {
if (err) {
console.error('error creating Triton client: %s\n%s', err, err.stack);
process.exitStatus = 1;
return;
}
// TODO: Eventually the top-level TritonApi will have `.listInstances()`.
client.cloudapi.listMachines(function (err, insts) {
client.close(); // Remember to close the client to close TCP conn.
if (err) {
console.error('listInstances error: %s\n%s', err, err.stack);
process.exitStatus = 1;
} else {
console.log(JSON.stringify(insts, null, 4));
}
});
});

View File

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

View File

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

View File

@ -58,9 +58,9 @@ var OPTIONS = [
names: ['profile', 'p'], names: ['profile', 'p'],
type: 'string', type: 'string',
completionType: 'tritonprofile', completionType: 'tritonprofile',
env: 'SC_PROFILE', env: 'TRITON_PROFILE',
helpArg: 'NAME', helpArg: 'NAME',
help: 'Spearhead Cloud client profile to use.' help: 'Triton client profile to use.'
}, },
{ {
@ -81,7 +81,8 @@ var OPTIONS = [
{ {
names: ['account', 'a'], names: ['account', 'a'],
type: 'string', type: 'string',
help: 'Account (login name). Environment: SC_ACCOUNT=ACCOUNT ', help: 'Account (login name). Environment: TRITON_ACCOUNT=ACCOUNT ' +
'or SDC_ACCOUNT=ACCOUNT.',
helpArg: 'ACCOUNT' helpArg: 'ACCOUNT'
}, },
{ {
@ -89,48 +90,51 @@ 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 ' +
'are audited on the CloudAPI server side.', '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: SC_USER=USER', help: 'RBAC user (login name). Environment: TRITON_USER=USER ' +
'or SDC_USER=USER.',
helpArg: 'USER' helpArg: 'USER'
}, },
{ {
names: ['role', 'r'], names: ['role', 'r'],
type: 'arrayOfCommaSepString', type: 'arrayOfCommaSepString',
env: 'SC_ROLE', env: 'TRITON_ROLE',
help: 'Assume an RBAC role. Use multiple times or once with a list', help: 'Assume an RBAC role. Use multiple times or once with a list',
helpArg: 'ROLE,...' helpArg: 'ROLE,...'
}, },
{ {
names: ['keyId', 'k'], names: ['keyId', 'k'],
type: 'string', type: 'string',
help: 'SSH key fingerprint. Environment: SC_KEY_ID=FINGERPRINT.', help: 'SSH key fingerprint. Environment: TRITON_KEY_ID=FINGERPRINT ' +
'or SDC_KEY_ID=FINGERPRINT.',
helpArg: 'FP' helpArg: 'FP'
}, },
{ {
names: ['url', 'U'], names: ['url', 'U'],
type: 'string', type: 'string',
help: 'Spearhead Cloud Datacenter URL. Environment: SC_URL=URL.', help: 'CloudAPI URL. Environment: TRITON_URL=URL or SDC_URL=URL.',
helpArg: 'URL' helpArg: 'URL'
}, },
{ {
names: ['J'], names: ['J'],
type: 'string', type: 'string',
hidden: true, hidden: true,
help: 'Spearhead Cloud (SC) datacenter name. This is ' + help: 'Joyent Public Cloud (JPC) datacenter name. This is ' +
'a shortcut to the "https://$dc.api.spearhead.cloud" ' + 'a shortcut to the "https://$dc.api.joyent.com" ' +
'cloudapi URL.' 'cloudapi URL.'
}, },
{ {
names: ['insecure', 'i'], names: ['insecure', 'i'],
type: 'bool', type: 'bool',
help: 'Do not validate the SSL certificate. Environment: ' + help: 'Do not validate the CloudAPI SSL certificate. Environment: ' +
'SC_TLS_INSECURE=1 (or the deprecated SC_TESTING=1).', 'TRITON_TLS_INSECURE=1, SDC_TLS_INSECURE=1 (or the deprecated ' +
'SDC_TESTING=1).',
'default': false 'default': false
}, },
{ {
@ -139,10 +143,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 `spearhead cloudapi /--ping` to list supported versions. ' + 'See `triton cloudapi /--ping` to list supported versions. ' +
'The default is "' + lib_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 ' +
'`spearhead` processing of responses to break.*', '`triton` processing of responses to break.*',
hidden: true hidden: true
} }
]; ];
@ -179,7 +183,7 @@ cmdln.dashdash.addOptionType({
function CLI() { function CLI() {
Cmdln.call(this, { Cmdln.call(this, {
name: 'spearhead', name: 'triton',
desc: packageJson.description, desc: packageJson.description,
options: OPTIONS, options: OPTIONS,
helpOpts: { helpOpts: {
@ -206,7 +210,6 @@ function CLI() {
'package', 'package',
'network', 'network',
'fwrule', 'fwrule',
'vlan',
{ group: 'Other Commands' }, { group: 'Other Commands' },
'info', 'info',
'account', 'account',
@ -245,7 +248,7 @@ CLI.prototype.init = function (opts, args, callback) {
} }
if (opts.version) { if (opts.version) {
console.log('Spearhead CLI', packageJson.version); console.log('Triton CLI', packageJson.version);
console.log(packageJson.homepage); console.log(packageJson.homepage);
callback(false); callback(false);
return; return;
@ -255,7 +258,7 @@ 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.spearhead.cloud', opts.J); opts.url = format('https://%s.api.joyent.com', opts.J);
} }
this.configDir = constants.CLI_CONFIG_DIR; this.configDir = constants.CLI_CONFIG_DIR;
@ -295,8 +298,9 @@ CLI.prototype.init = function (opts, args, callback) {
/* BEGIN JSSTYLED */ /* BEGIN JSSTYLED */
pErr.message += '\n' pErr.message += '\n'
+ ' No profile information could be loaded.\n' + ' No profile information could be loaded.\n'
+ ' Use "spearhead profile create" to create a profile or provide\n' + ' Use "triton profile create" to create a profile or provide\n'
+ ' the required "CloudAPI options" described in "spearhead --help".'; + ' the required "CloudAPI options" described in "triton --help".\n'
+ ' See https://github.com/joyent/node-triton#setup for more help.';
/* END JSSTYLED */ /* END JSSTYLED */
} }
throw pErr; throw pErr;
@ -318,9 +322,9 @@ CLI.prototype.init = function (opts, args, callback) {
return self._tritonapi; return self._tritonapi;
}); });
if (process.env.SC_COMPLETE) { if (process.env.TRITON_COMPLETE) {
/* /*
* If `SC_COMPLETE=<type>` is set (typically only in the * If `TRITON_COMPLETE=<type>` is set (typically only in the
* Triton CLI bash completion driver, see * Triton CLI bash completion driver, see
* "etc/triton-bash-completion-types.sh"), then Bash completions are * "etc/triton-bash-completion-types.sh"), then Bash completions are
* fetched and printed, instead of the usual subcommand handling. * fetched and printed, instead of the usual subcommand handling.
@ -329,9 +333,9 @@ CLI.prototype.init = function (opts, args, callback) {
* to avoid hitting the server for data everytime. * to avoid hitting the server for data everytime.
* *
* Example usage: * Example usage:
* SC_COMPLETE=images triton -p my-profile create * TRITON_COMPLETE=images triton -p my-profile create
*/ */
self._emitCompletions(process.env.SC_COMPLETE, function (err) { self._emitCompletions(process.env.TRITON_COMPLETE, function (err) {
callback(err || false); callback(err || false);
}); });
} else { } else {
@ -555,7 +559,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
}); });
break; break;
default: default:
process.stderr.write('warning: unknown spearhead completion type: ' process.stderr.write('warning: unknown triton completion type: '
+ type + '\n'); + type + '\n');
next(); next();
break; break;
@ -701,9 +705,6 @@ 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');

View File

@ -5,7 +5,7 @@
*/ */
/* /*
* Copyright (c) 2018, Joyent, Inc. * Copyright 2017 Joyent, Inc.
* *
* Client library for the SmartDataCenter Cloud API (cloudapi). * Client library for the SmartDataCenter Cloud API (cloudapi).
* http://apidocs.joyent.com/cloudapi/ * http://apidocs.joyent.com/cloudapi/
@ -154,6 +154,7 @@ function CloudApi(options) {
this.client = new SaferJsonClient(options); this.client = new SaferJsonClient(options);
} }
CloudApi.prototype.close = function close(callback) { CloudApi.prototype.close = function close(callback) {
this.log.trace({host: this.client.url && this.client.url.host}, this.log.trace({host: this.client.url && this.client.url.host},
'close cloudapi http client'); 'close cloudapi http client');
@ -357,48 +358,6 @@ CloudApi.prototype.ping = function ping(opts, cb) {
}; };
// ---- config
/**
* Get config object for the current user.
*
* @param {Object} opts
* @param {Function} cb of the form `function (err, config, res)`
*/
CloudApi.prototype.getConfig = function getConfig(opts, cb) {
assert.object(opts, 'opts');
assert.func(cb, 'cb');
var endpoint = this._path(format('/%s/config', this.account));
this._request(endpoint, function (err, req, res, body) {
cb(err, body, res);
});
};
/**
* Set config object for the current user.
*
* @param {Object} opts
* - {String} default_network: network fabric docker containers are
* provisioned on. Optional.
* @param {Function} cb of the form `function (err, config, res)`
*/
CloudApi.prototype.updateConfig = function updateConfig(opts, cb) {
assert.object(opts, 'opts');
assert.optionalUuid(opts.default_network, 'opts.default_network');
assert.func(cb, 'cb');
this._request({
method: 'PUT',
path: format('/%s/config', this.account),
data: opts
}, function (err, req, res, body) {
cb(err, body, res);
});
};
// ---- networks // ---- networks
/** /**
@ -427,300 +386,6 @@ CloudApi.prototype.getNetwork = function getNetwork(id, cb) {
}); });
}; };
/**
* <http://apidocs.joyent.com/cloudapi/#ListNetworkIPs>
*
* @param {String} id - The network UUID. Required.
* @param {Function} callback of the form `function (err, ips, res)`
*/
CloudApi.prototype.listNetworkIps = function listNetworkIps(id, cb) {
assert.uuid(id, 'id');
assert.func(cb, 'cb');
var endpoint = this._path(format('/%s/networks/%s/ips', this.account, id));
this._request(endpoint, function (err, req, res, body) {
cb(err, body, res);
});
};
/**
* <http://apidocs.joyent.com/cloudapi/#GetNetworkIP>
*
* @param {Object} opts
* - {String} id - The network UUID. Required.
* - {String} ip - The IP. Required.
* @param {Function} callback of the form `function (err, ip, res)`
*/
CloudApi.prototype.getNetworkIp = function getNetworkIp(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.string(opts.ip, 'opts.ip');
assert.func(cb, 'cb');
var endpoint = this._path(format('/%s/networks/%s/ips/%s',
this.account, opts.id, opts.ip));
this._request(endpoint, function (err, req, res, body) {
cb(err, body, res);
});
};
/**
* <http://apidocs.joyent.com/cloudapi/#UpdateNetworkIP>
*
* @param {Object} opts
* - {String} id - The network UUID. Required.
* - {String} ip - The IP. Required.
* - {Boolean} reserved - Reserve the IP. Required.
* @param {Function} callback of the form `function (err, body, res)`
*/
CloudApi.prototype.updateNetworkIp = function updateNetworkIp(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.string(opts.ip, 'opts.ip');
assert.bool(opts.reserved, 'opts.reserved');
assert.func(cb, 'cb');
var endpoint = this._path(format('/%s/networks/%s/ips/%s',
this.account, opts.id, opts.ip));
var data = {
reserved: opts.reserved
};
this._request({
method: 'PUT',
path: endpoint,
data: data
}, function (err, req, res, body) {
cb(err, body, res);
});
};
// <updatable network ip field> -> <expected typeof>
CloudApi.prototype.UPDATE_NETWORK_IP_FIELDS = {
reserved: 'boolean'
};
// --- Fabric VLANs
/**
* Creates a network on a fabric VLAN.
*
* @param {Object} options object containing:
* - {Integer} vlan_id (required) VLAN's id, between 0-4095.
* - {String} name (required) A name to identify the network.
* - {String} subnet (required) CIDR description of the network.
* - {String} provision_start_ip (required) First assignable IP addr.
* - {String} provision_end_ip (required) Last assignable IP addr.
* - {String} gateway (optional) Gateway IP address.
* - {Array} resolvers (optional) DNS resolvers for hosts on network.
* - {Object} routes (optional) Static routes for hosts on network.
* - {String} description (optional)
* - {Boolean} internet_nat (optional) Whether to provision an Internet
* NAT on the gateway address (default: true).
* @param {Function} callback of the form f(err, vlan, res).
*/
CloudApi.prototype.createFabricNetwork =
function createFabricNetwork(opts, cb) {
assert.object(opts, 'opts');
assert.number(opts.vlan_id, 'opts.vlan_id');
assert.string(opts.name, 'opts.name');
assert.string(opts.subnet, 'opts.subnet');
assert.string(opts.provision_start_ip, 'opts.provision_start_ip');
assert.string(opts.provision_end_ip, 'opts.provision_end_ip');
assert.optionalString(opts.gateway, 'opts.gateway');
assert.optionalArrayOfString(opts.resolvers, 'opts.resolvers');
assert.optionalObject(opts.routes, 'opts.routes');
assert.optionalBool(opts.internet_nat, 'opts.internet_nat');
var data = common.objCopy(opts);
var vlanId = data.vlan_id;
delete data.vlan_id;
this._request({
method: 'POST',
path: format('/%s/fabrics/default/vlans/%d/networks', this.account,
vlanId),
data: data
}, function reqCb(err, req, res, body) {
cb(err, body, res);
});
};
/**
* Lists all networks on a VLAN.
*
* Returns an array of objects.
*
* @param {Object} options object containing:
* - {Integer} vlan_id (required) VLAN's id, between 0-4095.
* @param {Function} callback of the form f(err, networks, res).
*/
CloudApi.prototype.listFabricNetworks =
function listFabricNetworks(opts, cb) {
assert.object(opts, 'opts');
assert.number(opts.vlan_id, 'opts.vlan_id');
assert.func(cb, 'cb');
var endpoint = format('/%s/fabrics/default/vlans/%d/networks',
this.account, opts.vlan_id);
this._passThrough(endpoint, opts, cb);
};
/**
* Remove a fabric network
*
* @param {Object} opts (object)
* - {String} id: The network id. Required.
* - {Integer} vlan_id: The VLAN id. Required.
* @param {Function} cb of the form `function (err, res)`
*/
CloudApi.prototype.deleteFabricNetwork =
function deleteFabricNetwork(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.number(opts.vlan_id, 'opts.vlan_id');
assert.func(cb, 'cb');
this._request({
method: 'DELETE',
path: format('/%s/fabrics/default/vlans/%d/networks/%s', this.account,
opts.vlan_id, opts.id)
}, function (err, req, res) {
cb(err, res);
});
};
/**
* Creates a VLAN on a fabric.
*
* @param {Object} options object containing:
* - {Integer} vlan_id (required) VLAN's id, between 0-4095.
* - {String} name (required) A name to identify the VLAN.
* - {String} description (optional)
* @param {Function} callback of the form f(err, vlan, res).
*/
CloudApi.prototype.createFabricVlan =
function createFabricVlan(opts, cb) {
assert.object(opts, 'opts');
assert.number(opts.vlan_id, 'opts.vlan_id');
assert.string(opts.name, 'opts.name');
assert.optionalString(opts.description, 'opts.description');
var data = {
vlan_id: opts.vlan_id
};
Object.keys(this.UPDATE_VLAN_FIELDS).forEach(function (attr) {
if (opts[attr] !== undefined)
data[attr] = opts[attr];
});
this._request({
method: 'POST',
path: format('/%s/fabrics/default/vlans', this.account),
data: data
}, function reqCb(err, req, res, body) {
cb(err, body, res);
});
};
/**
* Lists all the VLANs.
*
* Returns an array of objects.
*
* @param opts {Object} Options
* @param {Function} callback of the form f(err, vlans, res).
*/
CloudApi.prototype.listFabricVlans =
function listFabricVlans(opts, cb) {
assert.object(opts, 'opts');
assert.func(cb, 'cb');
var endpoint = format('/%s/fabrics/default/vlans', this.account);
this._passThrough(endpoint, opts, cb);
};
/**
* Retrieves a VLAN.
*
* @param {Integer} id: The VLAN id.
* @param {Function} callback of the form `function (err, vlan, res)`
*/
CloudApi.prototype.getFabricVlan =
function getFabricVlan(opts, cb) {
assert.object(opts, 'opts');
assert.number(opts.vlan_id, 'opts.vlan_id');
assert.func(cb, 'cb');
var endpoint = format('/%s/fabrics/default/vlans/%d', this.account,
opts.vlan_id);
this._request(endpoint, function (err, req, res, body) {
cb(err, body, res);
});
};
// <updatable account field> -> <expected typeof>
CloudApi.prototype.UPDATE_VLAN_FIELDS = {
name: 'string',
description: 'string'
};
/**
* Updates a VLAN.
*
* @param {Object} opts object containing:
* - {Integer} id: The VLAN id. Required.
* - {String} name: The VLAN name. Optional.
* - {String} description: Description of the VLAN. Optional.
* @param {Function} callback of the form `function (err, vlan, res)`
*/
CloudApi.prototype.updateFabricVlan =
function updateFabricVlan(opts, cb) {
assert.object(opts, 'opts');
assert.number(opts.vlan_id, 'opts.vlan_id');
assert.optionalString(opts.rule, 'opts.name');
assert.optionalString(opts.description, 'opts.description');
assert.func(cb, 'cb');
var data = {};
Object.keys(this.UPDATE_VLAN_FIELDS).forEach(function (attr) {
if (opts[attr] !== undefined)
data[attr] = opts[attr];
});
var vlanId = opts.vlan_id;
this._request({
method: 'POST',
path: format('/%s/fabrics/default/vlans/%d', this.account, vlanId),
data: data
}, function onReq(err, req, res, body) {
cb(err, body, res);
});
};
/**
* Remove a VLAN.
*
* @param {Object} opts (object)
* - {Integer} vlan_id: The vlan id. Required.
* @param {Function} cb of the form `function (err, res)`
*/
CloudApi.prototype.deleteFabricVlan =
function deleteFabricVlan(opts, cb) {
assert.object(opts, 'opts');
assert.number(opts.vlan_id, 'opts.vlan_id');
assert.func(cb, 'cb');
this._request({
method: 'DELETE',
path: format('/%s/fabrics/default/vlans/%d', this.account, opts.vlan_id)
}, function onReq(err, req, res) {
cb(err, res);
});
};
// ---- datacenters // ---- datacenters
@ -1031,92 +696,6 @@ CloudApi.prototype.exportImage = function exportImage(opts, cb) {
}); });
}; };
/**
* Update an image.
* <http://apidocs.joyent.com/cloudapi/#UpdateImage>
*
* @param {Object} opts
* - {UUID} id Required. The id of the image to update.
* - {Object} fields Required. The fields to update in the image.
* @param {Function} cb of the form `function (err, body, res)`
*/
CloudApi.prototype.updateImage = function updateImage(opts, cb) {
assert.uuid(opts.id, 'id');
assert.object(opts.fields, 'fields');
assert.func(cb, 'cb');
this._request({
method: 'POST',
path: format('/%s/images/%s?action=update', this.account, opts.id),
data: opts.fields
}, function (err, req, res, body) {
if (err) {
cb(err, null, res);
return;
}
cb(null, body, res);
});
};
/**
* Clone an image.
* <http://apidocs.joyent.com/cloudapi/#CloneImage>
*
* @param {Object} opts
* - {UUID} id Required. The id of the image to update.
* @param {Function} cb of the form `function (err, body, res)`
*/
CloudApi.prototype.cloneImage = function cloneImage(opts, cb) {
assert.uuid(opts.id, 'id');
assert.func(cb, 'cb');
this._request({
method: 'POST',
path: format('/%s/images/%s?action=clone', this.account, opts.id),
data: {}
}, function (err, req, res, body) {
if (err) {
cb(err, null, res);
return;
}
cb(null, body, res);
});
};
/**
* Import image from another datacenter in the same cloud.
* <http://apidocs.joyent.com/cloudapi/#ImportImageFromDatacenter>
*
* @param {Object} opts
* - {String} datacenter Required. The datacenter to import from.
* - {UUID} id Required. The id of the image to update.
* @param {Function} cb of the form `function (err, body, res)`
*/
CloudApi.prototype.importImageFromDatacenter =
function importImageFromDatacenter(opts, cb) {
assert.string(opts.datacenter, 'datacenter');
assert.uuid(opts.id, 'id');
assert.func(cb, 'cb');
var p = this._path(format('/%s/images', this.account), {
action: 'import-from-datacenter',
datacenter: opts.datacenter,
id: opts.id
});
this._request({
method: 'POST',
path: p,
data: {}
}, function (err, req, res, body) {
if (err) {
cb(err, null, res);
return;
}
cb(null, body, res);
});
};
/** /**
* Wait for an image to go one of a set of specfic states. * Wait for an image to go one of a set of specfic states.
* *
@ -1196,12 +775,13 @@ CloudApi.prototype.getPackage = function getPackage(opts, cb) {
/** /**
* Get a machine by id. * Get a machine by id.
* *
* XXX add getCredentials equivalent
* XXX cloudapi docs don't doc the credentials=true option
*
* For backwards compat, calling with `getMachine(id, cb)` is allowed. * For backwards compat, calling with `getMachine(id, cb)` is allowed.
* *
* @param {Object} opts * @param {Object} opts
* - {UUID} id - Required. The machine id. * - id {UUID} Required. The machine id.
* - {Boolean} credentials - Optional. Set to true to include generated
* credentials for this machine in `machine.metadata.credentials`.
* @param {Function} cb of the form `function (err, machine, res)` * @param {Function} cb of the form `function (err, machine, res)`
*/ */
CloudApi.prototype.getMachine = function getMachine(opts, cb) { CloudApi.prototype.getMachine = function getMachine(opts, cb) {
@ -1210,14 +790,9 @@ CloudApi.prototype.getMachine = function getMachine(opts, cb) {
} }
assert.object(opts, 'opts'); assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id'); assert.uuid(opts.id, 'opts.id');
assert.optionalBool(opts.credentials, 'opts.credentials');
var query = {}; var endpoint = format('/%s/machines/%s', this.account, opts.id);
if (opts.credentials) { this._request(endpoint, function (err, req, res, body) {
query.credentials = 'true';
}
var p = this._path(format('/%s/machines/%s', this.account, opts.id), query);
this._request(p, function (err, req, res, body) {
cb(err, body, res); cb(err, body, res);
}); });
}; };
@ -1333,6 +908,7 @@ function enableMachineFirewall(uuid, callback) {
return this._doMachine('enable_firewall', uuid, callback); return this._doMachine('enable_firewall', uuid, callback);
}; };
/** /**
* Disables machine firewall. * Disables machine firewall.
* *
@ -1344,28 +920,6 @@ function disableMachineFirewall(uuid, callback) {
return this._doMachine('disable_firewall', uuid, callback); return this._doMachine('disable_firewall', uuid, callback);
}; };
/**
* Enables machine deletion protection.
*
* @param {String} id (required) The machine id.
* @param {Function} callback of the form `function (err, null, res)`
*/
CloudApi.prototype.enableMachineDeletionProtection =
function enableMachineDeletionProtection(uuid, callback) {
return this._doMachine('enable_deletion_protection', uuid, callback);
};
/**
* Disables machine deletion protection.
*
* @param {String} id (required) The machine id.
* @param {Function} callback of the form `function (err, null, res)`
*/
CloudApi.prototype.disableMachineDeletionProtection =
function disableMachineDeletionProtection(uuid, callback) {
return this._doMachine('disable_deletion_protection', uuid, callback);
};
/** /**
* internal function for start/stop/reboot/enable_firewall/disable_firewall * internal function for start/stop/reboot/enable_firewall/disable_firewall
*/ */
@ -1498,7 +1052,7 @@ CloudApi.prototype.createMachine = function createMachine(options, callback) {
assert.optionalString(options.name, 'options.name'); assert.optionalString(options.name, 'options.name');
assert.uuid(options.image, 'options.image'); assert.uuid(options.image, 'options.image');
assert.uuid(options.package, 'options.package'); assert.uuid(options.package, 'options.package');
assert.optionalArray(options.networks, 'options.networks'); assert.optionalArrayOfUuid(options.networks, 'options.networks');
// TODO: assert the other fields // TODO: assert the other fields
assert.func(callback, 'callback'); assert.func(callback, 'callback');
@ -1576,53 +1130,6 @@ function waitForMachineFirewallEnabled(opts, cb) {
}; };
/**
* Wait for a machine's `deletion_protection` field to go true or
* false/undefined.
*
* @param {Object} options
* - {String} id: Required. The machine UUID.
* - {Boolean} state: Required. The desired `deletion_protection` state.
* - {Number} interval: Optional. Time (in ms) to poll.
* @param {Function} callback of the form f(err, machine, res).
*/
CloudApi.prototype.waitForDeletionProtectionEnabled =
function waitForDeletionProtectionEnabled(opts, cb) {
var self = this;
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.bool(opts.state, 'opts.state');
assert.optionalNumber(opts.interval, 'opts.interval');
assert.func(cb, 'cb');
var interval = opts.interval || 1000;
assert.ok(interval > 0, 'interval must be a positive number');
poll();
function poll() {
self.getMachine({
id: opts.id
}, function getMachineCb(err, machine, res) {
if (err) {
cb(err, null, res);
return;
}
// !! converts an undefined to a false
if (opts.state === !!machine.deletion_protection) {
cb(null, machine, res);
return;
}
setTimeout(poll, interval);
});
}
};
// --- machine tags // --- machine tags
/** /**
@ -1931,150 +1438,6 @@ function deleteMachineSnapshot(opts, cb) {
}; };
// --- NICs
/**
* Adds a NIC on a network to an instance.
*
* @param {Object} options object containing:
* - {String} id (required) the instance id.
* - {String|Object} (required) network uuid or network object.
* @param {Function} callback of the form f(err, nic, res).
*/
CloudApi.prototype.addNic =
function addNic(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.ok(opts.network, 'opts.network');
var data = {
network: opts.network
};
this._request({
method: 'POST',
path: format('/%s/machines/%s/nics', this.account, opts.id),
data: data
}, function (err, req, res, body) {
cb(err, body, res);
});
};
/**
* Lists all NICs on an instance.
*
* Returns an array of objects.
*
* @param opts {Object} Options
* - {String} id (required) the instance id.
* @param {Function} callback of the form f(err, nics, res).
*/
CloudApi.prototype.listNics =
function listNics(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.func(cb, 'cb');
var endpoint = format('/%s/machines/%s/nics', this.account, opts.id);
this._passThrough(endpoint, opts, cb);
};
/**
* Retrieves a NIC on an instance.
*
* @param {Object} options object containing:
* - {UUID} id: The instance id. Required.
* - {String} mac: The NIC's MAC. Required.
* @param {Function} callback of the form `function (err, nic, res)`
*/
CloudApi.prototype.getNic =
function getNic(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.string(opts.mac, 'opts.mac');
assert.func(cb, 'cb');
var mac = opts.mac.replace(/:/g, '');
var endpoint = format('/%s/machines/%s/nics/%s', this.account, opts.id,
mac);
this._request(endpoint, function (err, req, res, body) {
cb(err, body, res);
});
};
/**
* Remove a NIC off an instance.
*
* @param {Object} opts (object)
* - {UUID} id: The instance id. Required.
* - {String} mac: The NIC's MAC. Required.
* @param {Function} cb of the form `function (err, res)`
*/
CloudApi.prototype.removeNic =
function removeNic(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.string(opts.mac, 'opts.mac');
assert.func(cb, 'cb');
var mac = opts.mac.replace(/:/g, '');
this._request({
method: 'DELETE',
path: format('/%s/machines/%s/nics/%s', this.account, opts.id, mac)
}, function (err, req, res) {
cb(err, res);
});
};
/**
* Wait for a machine's nic to go one of a set of specfic states.
*
* @param {Object} options
* - {String} id {required} machine id
* - {String} mac {required} mac for new nic
* - {Array of String} states - desired state
* - {Number} interval (optional) - time in ms to poll
* @param {Function} callback of the form f(err, nic, res).
*/
CloudApi.prototype.waitForNicStates =
function waitForNicStates(opts, cb) {
var self = this;
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.string(opts.mac, 'opts.mac');
assert.arrayOfString(opts.states, 'opts.states');
assert.optionalNumber(opts.interval, 'opts.interval');
assert.func(cb, 'cb');
var interval = opts.interval || 1000;
assert.ok(interval > 0, 'interval must be a positive number');
poll();
function poll() {
self.getNic({
id: opts.id,
mac: opts.mac
}, function onPoll(err, nic, res) {
if (err) {
cb(err, null, res);
return;
}
if (opts.states.indexOf(nic.state) !== -1) {
cb(null, nic, res);
return;
}
setTimeout(poll, interval);
});
}
};
// --- firewall rules // --- firewall rules
/** /**
@ -2963,17 +2326,6 @@ CloudApi.prototype.listVolumes = function listVolumes(options, cb) {
this._passThrough(endpoint, options, cb); this._passThrough(endpoint, options, cb);
}; };
/**
* List the account's volume sizes.
*
* @param {Object} options
* @param {Function} callback - called like `function (err, volumeSizes)`
*/
CloudApi.prototype.listVolumeSizes = function listVolumeSizes(options, cb) {
var endpoint = format('/%s/volumesizes', this.account);
this._passThrough(endpoint, options, cb);
};
/** /**
* Create a volume for the account. * Create a volume for the account.
* *

View File

@ -5,7 +5,7 @@
*/ */
/* /*
* Copyright (c) 2018, Joyent, Inc. * Copyright 2017 Joyent, Inc.
*/ */
var assert = require('assert-plus'); var assert = require('assert-plus');
@ -24,8 +24,7 @@ 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
@ -136,8 +135,7 @@ function jsonStream(arr, stream) {
* types, e.g the string 'false' is converted to the boolean primitive "false". * types, e.g the string 'false' is converted to the boolean primitive "false".
* *
* @param {String} kv * @param {String} kv
* @param {Array} validKeys: Optional array of strings or regexes matching * @param {String[]} validKeys: Optional
* valid keys.
* @param {Object} options: Optional * @param {Object} options: Optional
* - @param disableTypeConversions {Boolean} Optional. If true, then no * - @param disableTypeConversions {Boolean} Optional. If true, then no
* type conversion of values is performed, and all values are returned as * type conversion of values is performed, and all values are returned as
@ -150,7 +148,7 @@ function jsonStream(arr, stream) {
*/ */
function _parseKeyValue(kv, validKeys, options) { function _parseKeyValue(kv, validKeys, options) {
assert.string(kv, 'kv'); assert.string(kv, 'kv');
assert.optionalArray(validKeys, 'validKeys'); assert.optionalArrayOfString(validKeys, 'validKeys');
assert.optionalObject(options, 'options'); assert.optionalObject(options, 'options');
options = options || {}; options = options || {};
assert.optionalBool(options.disableTypeConversions, assert.optionalBool(options.disableTypeConversions,
@ -158,7 +156,6 @@ function _parseKeyValue(kv, validKeys, options) {
assert.optionalObject(options.typeHintFromKey, 'options.typeHintFromKey'); assert.optionalObject(options.typeHintFromKey, 'options.typeHintFromKey');
assert.optionalBool(options.failOnEmptyValue, 'options.failOnEmptyValue'); assert.optionalBool(options.failOnEmptyValue, 'options.failOnEmptyValue');
var i;
var idx = kv.indexOf('='); var idx = kv.indexOf('=');
if (idx === -1) { if (idx === -1) {
throw new errors.UsageError(format('invalid key=value: "%s"', kv)); throw new errors.UsageError(format('invalid key=value: "%s"', kv));
@ -166,23 +163,11 @@ function _parseKeyValue(kv, validKeys, options) {
var k = kv.slice(0, idx); var k = kv.slice(0, idx);
var typeHint; var typeHint;
var v = kv.slice(idx + 1); var v = kv.slice(idx + 1);
var validKey;
if (validKeys) { if (validKeys && validKeys.indexOf(k) === -1) {
var foundMatch = false; throw new errors.UsageError(format(
for (i = 0; i < validKeys.length; i++) { 'invalid key: "%s" (must be one of "%s")',
validKey = validKeys[i]; k, validKeys.join('", "')));
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) { if (v === '' && options.failOnEmptyValue) {
@ -219,13 +204,14 @@ function _parseKeyValue(kv, validKeys, options) {
* given an array of key=value pairs, break them into a JSON predicate * 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} validKeys: Optional array of strings or regexes matching * @param {Object[]} validKeys - an array of objects representing valid keys
* valid keys. * that can be used in the first argument "kvs".
* @param {String} compositionType - the way each key/value pair will be * @param {String} compositionType - the way each key/value pair will be
* combined to form a JSON predicate. Valid values are 'or' and 'and'. * combined to form a JSON predicate. Valid values are 'or' and 'and'.
*/ */
function jsonPredFromKv(kvs, validKeys, compositionType) { function jsonPredFromKv(kvs, validKeys, compositionType) {
assert.arrayOfString(kvs, 'kvs'); assert.arrayOfString(kvs, 'kvs');
assert.arrayOfString(validKeys, 'validKeys');
assert.string(compositionType, 'string'); assert.string(compositionType, 'string');
assert.ok(compositionType === 'or' || compositionType === 'and', assert.ok(compositionType === 'or' || compositionType === 'and',
'compositionType'); 'compositionType');
@ -618,9 +604,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);
} }
@ -1139,8 +1125,8 @@ 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 * - @param validKeys {String[]} Optional. List of valid keys. By default
* matching valid keys. By default all keys are valid. * all keys are valid.
* - @param failOnEmptyValue {Boolean} Optional. If true, then a key with a * - @param failOnEmptyValue {Boolean} Optional. If true, then a key with a
* value that is the empty string throws an error. Default is false. * value that is the empty string throws an error. Default is false.
*/ */
@ -1153,6 +1139,7 @@ function objFromKeyValueArgs(args, opts)
assert.optionalBool(opts.disableTypeConversions, assert.optionalBool(opts.disableTypeConversions,
'opts.disableTypeConversions'); 'opts.disableTypeConversions');
assert.optionalObject(opts.typeHintFromKey, opts.typeHintFromKey); assert.optionalObject(opts.typeHintFromKey, opts.typeHintFromKey);
assert.optionalArrayOfString(opts.validKeys, 'opts.validKeys');
assert.optionalBool(opts.failOnEmptyValue, 'opts.failOnEmptyValue'); assert.optionalBool(opts.failOnEmptyValue, 'opts.failOnEmptyValue');
var obj = {}; var obj = {};
@ -1302,177 +1289,6 @@ function argvFromLine(line) {
return argv; 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
@ -1511,11 +1327,6 @@ module.exports = {
objFromKeyValueArgs: objFromKeyValueArgs, objFromKeyValueArgs: objFromKeyValueArgs,
argvFromLine: argvFromLine, argvFromLine: argvFromLine,
jsonPredFromKv: jsonPredFromKv, jsonPredFromKv: jsonPredFromKv,
monotonicTimeDiffMs: monotonicTimeDiffMs, monotonicTimeDiffMs: monotonicTimeDiffMs
readStdin: readStdin,
validateObject: validateObject,
ipv4ToLong: ipv4ToLong,
parseNicStr: parseNicStr,
imageRepr: imageRepr
}; };
// vim: set softtabstop=4 shiftwidth=4: // vim: set softtabstop=4 shiftwidth=4:

View File

@ -221,18 +221,18 @@ 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' ? 'SC_URL' : 'profile.url'); profile.name === 'env' ? 'TRITON_URL or SDC_URL' : 'profile.url');
assert.string(profile.account, assert.string(profile.account,
profile.name === 'env' ? 'SC_ACCOUNT' profile.name === 'env' ? 'TRITON_ACCOUNT or SDC_ACCOUNT'
: 'profile.account'); : 'profile.account');
assert.string(profile.keyId, assert.string(profile.keyId,
profile.name === 'env' ? 'SC_KEY_ID' profile.name === 'env' ? 'TRITON_KEY_ID or SDC_KEY_ID'
: 'profile.keyId'); : 'profile.keyId');
assert.optionalBool(profile.insecure, assert.optionalBool(profile.insecure,
profile.name === 'env' ? 'SC_INSECURE' profile.name === 'env' ? 'TRITON_INSECURE or SDC_INSECURE'
: 'profile.insecure'); : 'profile.insecure');
assert.optionalString(profile.user, assert.optionalString(profile.user,
profile.name === 'env' ? 'SC_USER' profile.name === 'env' ? 'TRITON_USER or SDC_USER'
: 'profile.user'); : 'profile.user');
assert.optionalString(profile.actAsAccount, 'profile.actAsAccount'); assert.optionalString(profile.actAsAccount, 'profile.actAsAccount');
assert.optionalArrayOfString(profile.roles, 'profile.roles'); assert.optionalArrayOfString(profile.roles, 'profile.roles');
@ -276,18 +276,21 @@ function _loadEnvProfile(profileOverrides) {
name: 'env' name: 'env'
}; };
envProfile.account = process.env.SC_ACCOUNT; envProfile.account = process.env.TRITON_ACCOUNT || process.env.SDC_ACCOUNT;
var user = process.env.SC_USER; var user = process.env.TRITON_USER || process.env.SDC_USER;
if (user) { if (user) {
envProfile.user = user; envProfile.user = user;
} }
envProfile.url = process.env.SC_URL; envProfile.url = process.env.TRITON_URL || process.env.SDC_URL;
envProfile.keyId = process.env.SC_KEY_ID; envProfile.keyId = process.env.TRITON_KEY_ID || process.env.SDC_KEY_ID;
if (process.env.SC_TLS_INSECURE) { if (process.env.TRITON_TLS_INSECURE) {
envProfile.insecure = common.boolFromString( envProfile.insecure = common.boolFromString(
process.env.SC_TLS_INSECURE, undefined, 'SC_TLS_INSECURE'); process.env.TRITON_TLS_INSECURE, undefined, 'TRITON_TLS_INSECURE');
} else if (process.env.SC_TESTING) { } else if (process.env.SDC_TLS_INSECURE) {
envProfile.insecure = common.boolFromString(
process.env.SDC_TLS_INSECURE, undefined, 'SDC_TLS_INSECURE');
} else if (process.env.SDC_TESTING) {
// For compatibility with the legacy behavior of the smartdc // For compatibility with the legacy behavior of the smartdc
// tools, *any* set value but the empty string is considered true. // tools, *any* set value but the empty string is considered true.
envProfile.insecure = true; envProfile.insecure = true;
@ -296,11 +299,12 @@ function _loadEnvProfile(profileOverrides) {
for (var attr in profileOverrides) { for (var attr in profileOverrides) {
envProfile[attr] = profileOverrides[attr]; envProfile[attr] = profileOverrides[attr];
} }
/* /*
* If missing any of the required vars, then there is no env profile. * If none of the above envvars are defined, then there is no env profile.
*/ */
if (!envProfile.account || !envProfile.url || !envProfile.keyId) { if (!envProfile.account && !envProfile.user && !envProfile.url &&
!envProfile.keyId)
{
return null; return null;
} }
validateProfile(envProfile, 'environment variables'); validateProfile(envProfile, 'environment variables');
@ -345,7 +349,7 @@ function loadProfile(opts) {
var envProfile = _loadEnvProfile(opts.profileOverrides); 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 SC_* environment variables)'); + '(missing TRITON_*, or SDC_*, environment variables)');
} }
return envProfile; return envProfile;
} else if (!opts.configDir) { } else if (!opts.configDir) {
@ -362,11 +366,10 @@ function loadProfile(opts) {
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(opts.profileOverrides); var envProfile = _loadEnvProfile();
if (envProfile) { if (envProfile) {
profiles.push(envProfile); profiles.push(envProfile);
} }

View File

@ -30,8 +30,8 @@ var mod_path = require('path');
* For *testing* only, we allow override of this dir. * For *testing* only, we allow override of this dir.
*/ */
var CLI_CONFIG_DIR; var CLI_CONFIG_DIR;
if (process.env.SCTEST_CLI_CONFIG_DIR) { if (process.env.TRITONTEST_CLI_CONFIG_DIR) {
CLI_CONFIG_DIR = process.env.SCTEST_CLI_CONFIG_DIR; CLI_CONFIG_DIR = process.env.TRITONTEST_CLI_CONFIG_DIR;
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
/* /*
* For better or worse we are using APPDATA (i.e. the *Roaming* AppData * For better or worse we are using APPDATA (i.e. the *Roaming* AppData
@ -41,23 +41,16 @@ if (process.env.SCTEST_CLI_CONFIG_DIR) {
* TODO: We should likely separate out the *cache* subdir to * TODO: We should likely separate out the *cache* subdir to
* machine-specific data dir. * machine-specific data dir.
*/ */
CLI_CONFIG_DIR = mod_path.resolve(process.env.APPDATA, 'Spearhead', 'sc'); CLI_CONFIG_DIR = mod_path.resolve(process.env.APPDATA, 'Joyent', 'Triton');
} else { } else {
CLI_CONFIG_DIR = mod_path.resolve(process.env.HOME, '.spearhead'); CLI_CONFIG_DIR = mod_path.resolve(process.env.HOME, '.triton');
} }
// <Network Object Key> -> <expected typeof>
var NETWORK_OBJECT_FIELDS = {
ipv4_uuid: 'string',
ipv4_ips: 'string'
};
// ---- exports // ---- exports
module.exports = { module.exports = {
CLI_CONFIG_DIR: CLI_CONFIG_DIR, CLI_CONFIG_DIR: CLI_CONFIG_DIR
NETWORK_OBJECT_FIELDS: NETWORK_OBJECT_FIELDS
}; };

View File

@ -72,8 +72,12 @@ function do_update(subcmd, opts, args, callback) {
next(); next();
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) {
@ -88,18 +92,36 @@ function do_update(subcmd, opts, args, callback) {
}, },
function validateIt(ctx, next) { function validateIt(ctx, next) {
try { var keys = Object.keys(ctx.data);
common.validateObject(ctx.data, UPDATE_ACCOUNT_FIELDS); for (var i = 0; i < keys.length; i++) {
} catch (e) { var key = keys[i];
next(e); var value = ctx.data[key];
return; var type = UPDATE_ACCOUNT_FIELDS[key];
} if (!type) {
next(new errors.UsageError(format('unknown or ' +
'unupdateable field: %s (updateable fields are: %s)',
key,
Object.keys(UPDATE_ACCOUNT_FIELDS).sort().join(', '))));
return;
}
if (typeof (value) !== type) {
next(new errors.UsageError(format('field "%s" must be ' +
'of type "%s", but got a value of type "%s"', key,
type, typeof (value))));
return;
}
}
next(); 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) {

View File

@ -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 Spearhead account.' 'Get and update your Triton account.'
].join('\n'), ].join('\n'),
/* END JSSTYLED */ /* END JSSTYLED */
helpOpts: { helpOpts: {

View File

@ -20,7 +20,7 @@ function do_create(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_create.help = 'A shortcut for "spearhead instance create".\n' + targ.help; do_create.help = 'A shortcut for "triton instance create".\n' + targ.help;
do_create.helpOpts = targ.helpOpts; do_create.helpOpts = targ.helpOpts;
do_create.synopses = targ.synopses; do_create.synopses = targ.synopses;
do_create.options = targ.options; do_create.options = targ.options;

View File

@ -20,7 +20,7 @@ function do_delete(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_delete.help = 'A shortcut for "spearhead instance delete".\n' + targ.help; do_delete.help = 'A shortcut for "triton instance delete".\n' + targ.help;
do_delete.synopses = targ.synopses; do_delete.synopses = targ.synopses;
do_delete.options = targ.options; do_delete.options = targ.options;
do_delete.completionArgtypes = targ.completionArgtypes; do_delete.completionArgtypes = targ.completionArgtypes;

View File

@ -74,17 +74,17 @@ function do_env(subcmd, opts, args, cb) {
p('# triton'); p('# triton');
if (opts.unset) { if (opts.unset) {
[ [
'SC_PROFILE', 'TRITON_PROFILE',
'SC_URL', 'TRITON_URL',
'SC_ACCOUNT', 'TRITON_ACCOUNT',
'SC_USER', 'TRITON_USER',
'SC_KEY_ID', 'TRITON_KEY_ID',
'SC_TLS_INSECURE' 'TRITON_TLS_INSECURE'
].forEach(function (key) { ].forEach(function (key) {
p('unset %s', key); p('unset %s', key);
}); });
} else { } else {
p('export SC_PROFILE="%s"', profile.name); p('export TRITON_PROFILE="%s"', profile.name);
} }
break; break;
case 'docker': case 'docker':
@ -160,11 +160,10 @@ 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 "$(spearhead env%s%s)"', p('# eval "$(triton env%s%s)"',
(shortOpts ? ' -'+shortOpts : ''), (shortOpts ? ' -'+shortOpts : ''),
(profile.name === this.tritonapi.profile.name (profile.name === this.tritonapi.profile.name
? '' : ' ' + profile.name)); ? '' : ' ' + profile.name));
cb();
} }
do_env.options = [ do_env.options = [
@ -180,7 +179,7 @@ do_env.options = [
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 ' +
'"SC_PROFILE" variable).' '"TRITON_PROFILE" variable).'
}, },
{ {
names: ['docker', 'd'], names: ['docker', 'd'],
@ -210,8 +209,8 @@ do_env.help = [
'Emit shell commands to setup environment.', '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),',
'node-triton and spearhead-node. By default this emits the environment ', 'and node-triton itself. By default this emits the environment for all',
'for all supported tools. Use options to be specific.', 'supported tools. Use options to be specific.',
'', '',
'{{usage}}', '{{usage}}',
'', '',
@ -220,7 +219,7 @@ do_env.help = [
'clients. If PROFILE is not given, the current profile is used.', 'clients. If PROFILE is not given, the current profile is used.',
'', '',
'The following Bash function can be added to one\'s "~/.bashrc" to quickly', 'The following Bash function can be added to one\'s "~/.bashrc" to quickly',
'change between Spearhead profiles:', 'change between Triton profiles:',
' triton-select () { eval "$(triton env $1)"; }', ' triton-select () { eval "$(triton env $1)"; }',
'for example:', 'for example:',
' $ triton-select west1', ' $ triton-select west1',

View File

@ -80,7 +80,7 @@ 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 "spearhead fwrule enable" ' + 'firewall rule is enabled. Use "triton fwrule enable" '
+ 'to enable it later.' + 'to enable it later.'
}, },
{ {
@ -102,10 +102,10 @@ do_create.help = [
'{{options}}', '{{options}}',
'Examples:', 'Examples:',
' # Allow SSH access from any IP to all instances in a datacenter.', ' # 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"', ' triton fwrule create -D "ssh" "FROM any TO all vms ALLOW tcp PORT 22"',
'', '',
' # Allow SSH access to a specific instance.', ' # Allow SSH access to a specific instance.',
' spearhead fwrule create \\', ' triton fwrule create \\',
' "FROM any TO vm ba2c95e9-1cdf-4295-8253-3fee371374d9 ALLOW tcp PORT 22"' ' "FROM any TO vm ba2c95e9-1cdf-4295-8253-3fee371374d9 ALLOW tcp PORT 22"'
// TODO: link to // TODO: link to
// https://github.com/joyent/sdc-fwrule/blob/master/docs/examples.md // https://github.com/joyent/sdc-fwrule/blob/master/docs/examples.md

View File

@ -5,7 +5,7 @@
*/ */
/* /*
* Copyright 2018 Joyent, Inc. * Copyright 2016 Joyent, Inc.
* *
* `triton fwrule instances ...` * `triton fwrule instances ...`
*/ */
@ -111,11 +111,9 @@ function do_instances(subcmd, opts, args, cb) {
common.uuidToShortId(inst.image); common.uuidToShortId(inst.image);
inst.shortid = inst.id.split('-', 1)[0]; inst.shortid = inst.id.split('-', 1)[0];
var flags = []; var flags = [];
if (inst.brand === 'bhyve') flags.push('B');
if (inst.docker) flags.push('D'); if (inst.docker) flags.push('D');
if (inst.firewall_enabled) flags.push('F'); if (inst.firewall_enabled) flags.push('F');
if (inst.brand === 'kvm') flags.push('K'); if (inst.brand === 'kvm') flags.push('K');
if (inst.deletion_protection) flags.push('P');
inst.flags = flags.length ? flags.join('') : undefined; inst.flags = flags.length ? flags.join('') : undefined;
}); });
@ -161,11 +159,9 @@ do_instances.help = [
'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".'

View File

@ -84,7 +84,14 @@ function do_update(subcmd, opts, args, cb) {
return; return;
} }
common.readStdin(function gotStdin(stdin) { var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
try { try {
ctx.data = JSON.parse(stdin); ctx.data = JSON.parse(stdin);
} catch (err) { } catch (err) {
@ -100,13 +107,33 @@ function do_update(subcmd, opts, args, cb) {
}, },
function validateIt(ctx, next) { function validateIt(ctx, next) {
try { var keys = Object.keys(ctx.data);
common.validateObject(ctx.data, UPDATE_FWRULE_FIELDS);
} catch (e) { if (keys.length === 0) {
next(e); console.log('No fields given for firewall rule update');
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();
}, },

View File

@ -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 Spearhead firewall rules.', desc: 'List and manage Triton firewall rules.',
helpSubcmds: [ helpSubcmds: [
'help', 'help',
'list', 'list',

View File

@ -20,7 +20,7 @@ function do_fwrules(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_fwrules.help = 'A shortcut for "spearhead fwrule list".\n' + targ.help; do_fwrules.help = 'A shortcut for "triton fwrule list".\n' + targ.help;
do_fwrules.synopses = targ.synopses; do_fwrules.synopses = targ.synopses;
do_fwrules.options = targ.options; do_fwrules.options = targ.options;
do_fwrules.completionArgtypes = targ.completionArgtypes; do_fwrules.completionArgtypes = targ.completionArgtypes;

View File

@ -1,107 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright (c) 2018, Joyent, Inc.
*
* `triton image clone ...`
*/
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
// ---- the command
function do_clone(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length !== 1) {
cb(new errors.UsageError(
'incorrect number of args: expected 1, got ' + args.length));
return;
}
var log = this.top.log;
var tritonapi = this.top.tritonapi;
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function cloneImage(ctx, next) {
log.trace({dryRun: opts.dry_run, account: ctx.account},
'image clone account');
if (opts.dry_run) {
next();
return;
}
tritonapi.cloneImage({image: args[0]}, function _cloneCb(err, img) {
if (err) {
next(new errors.TritonError(err, 'error cloning image'));
return;
}
log.trace({img: img}, 'image clone result');
if (opts.json) {
console.log(JSON.stringify(img));
} else {
console.log('Cloned image %s to %s',
args[0], common.imageRepr(img));
}
next();
});
}
]}, cb);
}
do_clone.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
group: 'Other options'
},
{
names: ['dry-run'],
type: 'bool',
help: 'Go through the motions without actually cloning.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
do_clone.synopses = [
'{{name}} {{cmd}} [OPTIONS] IMAGE'
];
do_clone.help = [
/* BEGIN JSSTYLED */
'Clone a shared image.',
'',
'{{usage}}',
'',
'{{options}}',
'Where "IMAGE" is an image id (a full UUID), an image name (selects the',
'latest, by "published_at", image with that name), an image "name@version"',
'(selects latest match by "published_at"), or an image short ID (ID prefix).',
'',
'Note: Only shared images can be cloned.'
/* END JSSTYLED */
].join('\n');
do_clone.completionArgtypes = ['tritonimage', 'none'];
module.exports = do_clone;

View File

@ -1,119 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright (c) 2018, Joyent, Inc.
*
* `triton image copy ...`
*/
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
// ---- the command
function do_copy(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length !== 2) {
cb(new errors.UsageError(
'incorrect number of args: expected 2, got ' + args.length));
return;
}
var log = this.top.log;
var tritonapi = this.top.tritonapi;
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function copyImage(ctx, next) {
log.trace({dryRun: opts.dry_run, account: ctx.account, args: args},
'image copy');
if (opts.dry_run) {
next();
return;
}
tritonapi.copyImageToDatacenter(
{image: args[0], datacenter: args[1]},
function (err, img) {
if (err) {
next(new errors.TritonError(err, 'error copying image'));
return;
}
log.trace({img: img}, 'image copy result');
if (opts.json) {
console.log(JSON.stringify(img));
} else {
console.log('Copied image %s to datacenter %s',
common.imageRepr(img), args[1]);
}
next();
});
}
]}, function (err) {
cb(err);
});
}
do_copy.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
group: 'Other options'
},
{
names: ['dry-run'],
type: 'bool',
help: 'Go through the motions without actually copying.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
do_copy.synopses = [
'{{name}} {{cmd}} [OPTIONS] IMAGE DATACENTER'
];
do_copy.help = [
/* BEGIN JSSTYLED */
'Copy image to another datacenter.',
'',
'{{usage}}',
'',
'{{options}}',
'Where "IMAGE" is an image id (a full UUID), an image name (selects the',
'latest, by "published_at", image with that name), an image "name@version"',
'(selects latest match by "published_at"), or an image short ID (ID prefix).',
'You must be the owner of the image to copy it. (You can use `triton image',
'clone` to get your own image clone of an image shared to you.)',
'',
'"DATACENTER" is the name of the datacenter to which to copy your image.',
'Use `triton datacenters` to show the available datacenter names.'
/* END JSSTYLED */
].join('\n');
do_copy.aliases = ['cp'];
// TODO: tritonimage should really be 'tritonownedimage' or something to
// limit to images owned by this account
// TODO: tritondatacenter bash completion
do_copy.completionArgtypes = ['tritonimage', 'tritondatacenter', 'none'];
module.exports = do_copy;

View File

@ -31,11 +31,7 @@ function do_get(subcmd, opts, args, callback) {
callback(setupErr); callback(setupErr);
return; return;
} }
var getOpts = { tritonapi.getImage(args[0], function onRes(err, img) {
name: args[0],
excludeInactive: !opts.all
};
tritonapi.getImage(getOpts, function onRes(err, img) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
@ -60,15 +56,6 @@ 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.'
} }
]; ];

View File

@ -5,15 +5,13 @@
*/ */
/* /*
* Copyright 2018 Joyent, Inc. * Copyright 2016 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');
@ -69,45 +67,17 @@ function do_list(subcmd, opts, args, callback) {
listOpts.state = 'all'; listOpts.state = 'all';
} }
var self = this;
var tritonapi = this.top.tritonapi; var tritonapi = this.top.tritonapi;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
vasync.pipeline({ arg: {}, funcs: [ if (setupErr) {
function setupTritonApi(_, next) { callback(setupErr);
common.cliSetupTritonApi({cli: self.top}, next); return;
}, }
function getImages(ctx, next) { tritonapi.listImages(listOpts, function onRes(err, imgs, res) {
tritonapi.listImages(listOpts, function onRes(err, imgs, res) { if (err) {
if (err) { return callback(err);
next(err);
return;
}
ctx.imgs = imgs;
next();
});
},
function getUserAccount(ctx, next) {
// If using json output, or when there are no images that use an ACL
// - we don't need to fetch the account, as the account is only used
// to check if the image is shared (i.e. the account is in the image
// ACL) so it can output image flags in non-json mode.
if (opts.json || ctx.imgs.every(function _checkAcl(img) {
return !Array.isArray(img.acl) || img.acl.length === 0;
})) {
next();
return;
} }
tritonapi.cloudapi.getAccount(function _accountCb(err, account) {
if (err) {
next(err);
return;
}
ctx.account = account;
next();
});
},
function formatImages(ctx, next) {
var imgs = ctx.imgs;
if (opts.json) { if (opts.json) {
common.jsonStream(imgs); common.jsonStream(imgs);
} else { } else {
@ -129,20 +99,6 @@ function do_list(subcmd, opts, args, callback) {
if (img.origin) flags.push('I'); if (img.origin) flags.push('I');
if (img['public']) flags.push('P'); if (img['public']) flags.push('P');
if (img.state !== 'active') flags.push('X'); 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; img.flags = flags.length ? flags.join('') : undefined;
} }
@ -152,9 +108,9 @@ function do_list(subcmd, opts, args, callback) {
sort: sort sort: sort
}); });
} }
next(); callback();
} });
]}, callback); });
} }
do_list.options = [ do_list.options = [
@ -185,6 +141,7 @@ do_list.help = [
'', '',
'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}}',
'', '',
@ -200,8 +157,6 @@ 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',

View File

@ -1,115 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright (c) 2018, Joyent, Inc.
*
* `triton image share ...`
*/
var format = require('util').format;
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
// ---- the command
function do_share(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length !== 2) {
cb(new errors.UsageError(
'incorrect number of args: expect 2, got ' + args.length));
return;
}
var log = this.top.log;
var tritonapi = this.top.tritonapi;
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function shareImage(ctx, next) {
log.trace({dryRun: opts.dry_run, account: ctx.account},
'image share account');
if (opts.dry_run) {
next();
return;
}
tritonapi.shareImage({
image: args[0],
account: args[1]
}, function (err, img) {
if (err) {
next(new errors.TritonError(err, 'error sharing image'));
return;
}
log.trace({img: img}, 'image share result');
if (opts.json) {
console.log(JSON.stringify(img));
} else {
console.log('Shared image %s with account %s',
args[0], args[1]);
}
next();
});
}
]}, function (err) {
cb(err);
});
}
do_share.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
group: 'Other options'
},
{
names: ['dry-run'],
type: 'bool',
help: 'Go through the motions without actually sharing.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
do_share.synopses = [
'{{name}} {{cmd}} [OPTIONS] IMAGE ACCOUNT'
];
do_share.help = [
/* BEGIN JSSTYLED */
'Share an image with another account.',
'',
'{{usage}}',
'',
'{{options}}',
'Where "IMAGE" is an image id (a full UUID), an image name (selects the',
'latest, by "published_at", image with that name), an image "name@version"',
'(selects latest match by "published_at"), or an image short ID (ID prefix).',
'',
'Where "ACCOUNT" is the full account UUID.',
'',
'Note: Only images that are owned by the account can be shared.'
/* END JSSTYLED */
].join('\n');
do_share.completionArgtypes = ['tritonimage', 'none'];
module.exports = do_share;

View File

@ -1,115 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright (c) 2018, Joyent, Inc.
*
* `triton image unshare ...`
*/
var format = require('util').format;
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
// ---- the command
function do_unshare(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length !== 2) {
cb(new errors.UsageError(
'incorrect number of args: expect 2, got ' + args.length));
return;
}
var log = this.top.log;
var tritonapi = this.top.tritonapi;
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function unshareImage(ctx, next) {
log.trace({dryRun: opts.dry_run, account: ctx.account},
'image unshare account');
if (opts.dry_run) {
next();
return;
}
tritonapi.unshareImage({
image: args[0],
account: args[1]
}, function (err, img) {
if (err) {
next(new errors.TritonError(err, 'error unsharing image'));
return;
}
log.trace({img: img}, 'image unshare result');
if (opts.json) {
console.log(JSON.stringify(img));
} else {
console.log('Unshared image %s with account %s',
args[0], args[1]);
}
next();
});
}
]}, function (err) {
cb(err);
});
}
do_unshare.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
group: 'Other options'
},
{
names: ['dry-run'],
type: 'bool',
help: 'Go through the motions without actually sharing.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
do_unshare.synopses = [
'{{name}} {{cmd}} [OPTIONS] IMAGE ACCOUNT'
];
do_unshare.help = [
/* BEGIN JSSTYLED */
'Unshare an image with another account.',
'',
'{{usage}}',
'',
'{{options}}',
'Where "IMAGE" is an image id (a full UUID), an image name (selects the',
'latest, by "published_at", image with that name), an image "name@version"',
'(selects latest match by "published_at"), or an image short ID (ID prefix).',
'',
'Where "ACCOUNT" is the full account UUID.',
'',
'Note: Only images that are owned by the account can be unshared.'
/* END JSSTYLED */
].join('\n');
do_unshare.completionArgtypes = ['tritonimage', 'none'];
module.exports = do_unshare;

View File

@ -123,7 +123,7 @@ do_wait.help = [
'', '',
'{{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, "spearhead img wait foo0" will', 'by default "active,failed". In other words, "triton img wait foo0" will',
'wait for image "foo0" to complete creation.' 'wait for image "foo0" to complete creation.'
].join('\n'); ].join('\n');

View File

@ -5,7 +5,7 @@
*/ */
/* /*
* Copyright (c) 2018, Joyent, Inc. * Copyright 2017 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 Spearhead images.' 'List and manage Triton images.'
].join('\n'), ].join('\n'),
/* END JSSTYLED */ /* END JSSTYLED */
helpOpts: { helpOpts: {
@ -33,13 +33,9 @@ function ImageCLI(top) {
'help', 'help',
'list', 'list',
'get', 'get',
'clone',
'copy',
'create', 'create',
'delete', 'delete',
'export', 'export',
'share',
'unshare',
'wait' 'wait'
] ]
}); });
@ -53,13 +49,9 @@ 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_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');

View File

@ -20,7 +20,7 @@ function do_images(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_images.help = 'A shortcut for "spearhead image list".\n' + targ.help; do_images.help = 'A shortcut for "triton image list".\n' + targ.help;
do_images.synopses = targ.synopses; do_images.synopses = targ.synopses;
do_images.options = targ.options; do_images.options = targ.options;
do_images.completionArgtypes = targ.completionArgtypes; do_images.completionArgtypes = targ.completionArgtypes;

View File

@ -5,7 +5,7 @@
*/ */
/* /*
* Copyright 2019 Joyent, Inc. * Copyright 2017 Joyent, Inc.
* *
* `triton instance create ...` * `triton instance create ...`
*/ */
@ -19,18 +19,15 @@ 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) { function parseVolume(volume) {
var components; var components;
var volMode;
var volMountpoint;
var volName;
var VALID_MODES = ['ro', 'rw']; var VALID_MODES = ['ro', 'rw'];
var VALID_VOLUME_NAME_REGEXP = /^[a-zA-Z0-9][a-zA-Z0-9_\.\-]+$/; var VALID_VOLUME_NAME_REGEXP = /^[a-zA-Z0-9][a-zA-Z0-9_\.\-]+$/;
assert.string(volume, 'volume'); if (typeof(volume) !== 'string') {
return new errors.UsageError('unparseable volume specified');
}
components = volume.split(':'); components = volume.split(':');
if (components.length !== 2 && components.length !== 3) { if (components.length !== 2 && components.length !== 3) {
@ -39,44 +36,42 @@ function parseVolMount(volume) {
'"'); '"');
} }
volName = components[0];
volMountpoint = components[1];
volMode = components[2];
// first component should be a volume name. We only check here that it // 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 // syntactically looks like a volume name, we'll leave the upstream to
// determine if it's not actually a volume. // determine if it's not actually a volume.
if (!VALID_VOLUME_NAME_REGEXP.test(volName)) { if (!VALID_VOLUME_NAME_REGEXP.test(components[0])) {
return new errors.UsageError('invalid volume name, got: "' + volume + return new errors.UsageError('invalid volume name, got: "' + volume +
'"'); '"');
} }
// second component should be an absolute path // second component should be an absolute path
// NOTE: if we ever move past node 0.10, we could use path.isAbsolute(path) // NOTE: if we ever move past node 0.10, we could use path.isAbsolute(path)
if (volMountpoint.length === 0 || volMountpoint[0] !== '/') { if (components[1].length === 0 || (components[1])[0] !== '/') {
return new errors.UsageError('invalid volume mountpoint, must be ' + return new errors.UsageError('invalid volume mountpoint, must be ' +
'absolute path, got: "' + volume + '"'); 'absolute path, got: "' + volume + '"');
} }
if (volMountpoint.indexOf('\0') !== -1) { if (components[1].indexOf('\0') !== -1) {
return new errors.UsageError('invalid volume mountpoint, contains ' + return new errors.UsageError('invalid volume mountpoint, contains ' +
'invalid characters, got: "' + volume + '"'); 'invalid characters, got: "' + volume + '"');
} }
if (volMountpoint.search(/[^\/]/) === -1) { if (components[1].search(/[^\/]/) === -1) {
return new errors.UsageError('invalid volume mountpoint, must contain' + return new errors.UsageError('invalid volume mountpoint, must contain ' +
' at least one non-/ character, got: "' + volume + '"'); 'at least one non-/ character, got: "' + volume + '"');
} }
// third component is optional mode: 'ro' or 'rw' // third component is optional mode: 'ro' or 'rw'
if (components.length === 3 && VALID_MODES.indexOf(volMode) === -1) { if (components.length === 3 && VALID_MODES.indexOf(components[2]) === -1) {
return new errors.UsageError('invalid volume mode, got: "' + volume + return new errors.UsageError('invalid volume mode, got: "' + volume +
'"'); '"');
} }
return { return {
mode: volMode || 'rw', mode: components[2] || 'rw',
mountpoint: volMountpoint, mountpoint: components[1],
name: volName name: components[0]
}; };
return undefined;
} }
function do_create(subcmd, opts, args, cb) { function do_create(subcmd, opts, args, cb) {
@ -85,9 +80,6 @@ function do_create(subcmd, opts, args, 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;
@ -95,15 +87,112 @@ function do_create(subcmd, opts, args, cb) {
vasync.pipeline({arg: {cli: this.top}, funcs: [ vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi, common.cliSetupTritonApi,
/* BEGIN JSSTYLED */
/*
* Parse --affinity options for validity to `ctx.affinities`.
* Later (in `resolveLocality`) we'll translate this to locality hints
* that CloudAPI speaks.
*
* Some examples. Inspired by
* <https://docs.docker.com/swarm/scheduler/filter/#how-to-write-filter-expressions>
*
* instance==vm1
* container==vm1 # alternative to 'instance'
* inst==vm1 # alternative to 'instance'
* inst=vm1 # '=' is shortcut for '=='
* inst!=vm1 # '!='
* inst==~vm1 # '~' for soft/non-strict
* inst!=~vm1
*
* inst==vm* # globbing (not yet supported)
* inst!=/vm\d/ # regex (not yet supported)
*
* some-tag!=db # tags (not yet supported)
*
* Limitations:
* - no support for tags yet
* - no globbing or regex yet
* - we resolve name -> instance id *client-side* for now (until
* CloudAPI supports that)
* - Triton doesn't support mixed strict and non-strict, so we error
* out on that. We *could* just drop the non-strict, but that is
* slightly different.
*/
/* END JSSTYLED */
function parseAffinity(ctx, next) {
if (!opts.affinity) {
next();
return;
}
var affinities = [];
// TODO: stricter rules on the value part
// JSSTYLED
var affinityRe = /((instance|inst|container)(==~|!=~|==|!=|=~|=))?(.*?)$/;
for (var i = 0; i < opts.affinity.length; i++) {
var raw = opts.affinity[i];
var match = affinityRe.exec(raw);
if (!match) {
next(new errors.UsageError(format('invalid affinity: "%s"',
raw)));
return;
}
var key = match[2];
if ([undefined, 'inst', 'container'].indexOf(key) !== -1) {
key = 'instance';
}
assert.equal(key, 'instance');
var op = match[3];
if ([undefined, '='].indexOf(op) !== -1) {
op = '==';
}
var strict = true;
if (op[op.length - 1] === '~') {
strict = false;
op = op.slice(0, op.length - 1);
}
var val = match[4];
// Guard against mixed strictness (Triton can't handle those).
if (affinities.length > 0) {
var lastAff = affinities[affinities.length - 1];
if (strict !== lastAff.strict) {
next(new errors.TritonError(format('mixed strict and '
+ 'non-strict affinities are not supported: '
+ '%j (%s) and %j (%s)',
lastAff.raw,
(lastAff.strict ? 'strict' : 'non-strict'),
raw, (strict ? 'strict' : 'non-strict'))));
return;
}
}
affinities.push({
raw: raw,
key: key,
op: op,
strict: strict,
val: val
});
}
if (affinities.length) {
log.trace({affinities: affinities}, 'affinities');
ctx.affinities = affinities;
}
next();
},
/* /*
* Make sure if volumes were passed, they're in the correct form. * Make sure if volumes were passed, they're in the correct form.
*/ */
function parseVolMounts(ctx, next) { function parseVolumes(ctx, next) {
var idx; var idx;
var validationErrs = []; var validationErr;
var parsedObj; var volume;
var volMounts = []; var volumes = [];
if (!opts.volume) { if (!opts.volume) {
next(); next();
@ -111,66 +200,78 @@ function do_create(subcmd, opts, args, cb) {
} }
for (idx = 0; idx < opts.volume.length; idx++) { for (idx = 0; idx < opts.volume.length; idx++) {
parsedObj = parseVolMount(opts.volume[idx]); volume = parseVolume(opts.volume[idx]);
if (parsedObj instanceof Error) { if (volume instanceof Error) {
validationErrs.push(parsedObj); validationErr = volume;
} else { next(validationErr);
// if it's not an error, it's a volume return;
volMounts.push(parsedObj);
} }
volumes.push(volume);
} }
if (validationErrs.length > 0) { if (volumes.length > 0) {
next(new errors.MultiError(validationErrs)); ctx.volumes = volumes;
return;
}
if (volMounts.length > 0) {
ctx.volMounts = volMounts;
} }
next(); next();
}, },
/* /*
* Parse any nics given via `--nic` * Determine `ctx.locality` according to what CloudAPI supports
* based on `ctx.affinities` parsed earlier.
*/ */
function parseNics(ctx, next) { function resolveLocality(ctx, next) {
if (!opts.nic) { if (!ctx.affinities) {
next(); next();
return; return;
} }
ctx.nics = []; var strict;
var i; var near = [];
var networksSeen = {}; var far = [];
var nic;
var nics = opts.nic;
log.trace({nics: nics}, 'parsing nics'); vasync.forEachPipeline({
inputs: ctx.affinities,
func: function resolveAffinity(aff, nextAff) {
assert.ok(['==', '!='].indexOf(aff.op) !== -1,
'unexpected op: ' + aff.op);
var nearFar = (aff.op == '==' ? near : far);
for (i = 0; i < nics.length; i++) { strict = aff.strict;
nic = nics[i].split(','); if (common.isUUID(aff.val)) {
nearFar.push(aff.val);
try { nextAff();
nic = common.parseNicStr(nic); } else {
if (networksSeen[nic.ipv4_uuid]) { tritonapi.getInstance({
throw new errors.UsageError(format( id: aff.val,
'only 1 ip on a network allowed ' fields: ['id']
+ '(network %s specified multiple times)', }, function (err, inst) {
nic.ipv4_uuid)); if (err) {
nextAff(err);
} else {
log.trace({val: aff.val, inst: inst.id},
'resolveAffinity');
nearFar.push(inst.id);
nextAff();
}
});
} }
networksSeen[nic.ipv4_uuid] = true; }
ctx.nics.push(nic); }, function (err) {
} catch (err) { if (err) {
next(err); next(err);
return; return;
} }
}
log.trace({nics: ctx.nics}, 'parsed nics'); ctx.locality = {
strict: strict
};
if (near.length > 0) ctx.locality.near = near;
if (far.length > 0) ctx.locality.far = far;
log.trace({locality: ctx.locality}, 'resolveLocality');
next(); next();
});
}, },
function loadMetadata(ctx, next) { function loadMetadata(ctx, next) {
@ -203,7 +304,6 @@ 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
}; };
tritonapi.getImage(_opts, function (err, img) { tritonapi.getImage(_opts, function (err, img) {
@ -258,27 +358,16 @@ function do_create(subcmd, opts, args, cb) {
}, },
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; }),
volumes: ctx.volumes && ctx.volumes
}; };
if (ctx.locality) {
if (ctx.nets) { createOpts.locality = ctx.locality;
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) {
@ -290,16 +379,11 @@ 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;
} }
} }
@ -423,7 +507,9 @@ do_create.options = [
'INST), `instance==~INST` (*attempt* to place on the same server ' + 'INST), `instance==~INST` (*attempt* to place on the same server ' +
'as INST), or `instance!=~INST` (*attempt* to place on a server ' + 'as INST), or `instance!=~INST` (*attempt* to place on a server ' +
'other than INST\'s). `INST` is an existing instance name or ' + 'other than INST\'s). `INST` is an existing instance name or ' +
'id. Use this option more than once for multiple rules.', 'id. There are two shortcuts: `inst` may be used instead of ' +
'`instance` and `instance==INST` can be shortened to just ' +
'`INST`. Use this option more than once for multiple rules.',
completionType: 'tritonaffinityrule' completionType: 'tritonaffinityrule'
}, },
@ -438,39 +524,12 @@ do_create.options = [
'This option can be used multiple times.', 'This option can be used multiple times.',
completionType: 'tritonnetwork' 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.spearhead.cloud/network/firewall>' '<https://docs.joyent.com/public-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
}, },
{ {
@ -502,11 +561,6 @@ do_create.options = [
'Joyent-provided images, the user-script is run at every boot ' + 'Joyent-provided images, the user-script is run at every boot ' +
'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: ['allow-shared-images'],
type: 'bool',
help: 'Allow instance creation to use a shared image.'
},
{ {
group: 'Other options' group: 'Other options'
@ -526,6 +580,12 @@ do_create.options = [
names: ['json', 'j'], names: ['json', 'j'],
type: 'bool', type: 'bool',
help: 'JSON stream output.' help: 'JSON stream output.'
},
{
names: ['volume', 'v'],
type: 'arrayOfString',
help: 'Mount a volume into the container (non-KVM only) ' +
'<volume-name:/mount/point>[:access-mode]'
} }
]; ];
@ -539,8 +599,8 @@ do_create.help = [
'', '',
'{{options}}', '{{options}}',
'Where IMAGE is an image name, name@version, id, or short id (from ', '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', '`triton image list`) and PACKAGE is a package name, id, or short id',
'(from `spearhead package list`).' '(from `triton package list`).'
/* END JSSTYLED */ /* END JSSTYLED */
].join('\n'); ].join('\n');

View File

@ -1,125 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2018 Joyent, Inc.
*
* `triton instance disable-deletion-protection ...`
*/
var assert = require('assert-plus');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
function do_disable_deletion_protection(subcmd, opts, args, cb) {
assert.object(opts, 'opts');
assert.arrayOfString(args, 'args');
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length === 0) {
cb(new errors.UsageError('missing INST argument(s)'));
return;
}
var cli = this.top;
function wait(name, id, next) {
assert.string(name, 'name');
assert.uuid(id, 'id');
assert.func(next, 'next');
cli.tritonapi.cloudapi.waitForDeletionProtectionEnabled({
id: id,
state: false
}, function (err, inst) {
if (err) {
next(err);
return;
}
assert.ok(!inst.deletion_protection, 'inst ' + id
+ ' deletion_protection not in expected state after '
+ 'waitForDeletionProtectionEnabled');
console.log('Disabled deletion protection for instance "%s"', name);
next();
});
}
function disableOne(name, next) {
assert.string(name, 'name');
assert.func(next, 'next');
cli.tritonapi.disableInstanceDeletionProtection({
id: name
}, function disableProtectionCb(err, fauxInst) {
if (err) {
next(err);
return;
}
console.log('Disabling deletion protection for instance "%s"',
name);
if (opts.wait) {
wait(name, fauxInst.id, next);
} else {
next();
}
});
}
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
vasync.forEachParallel({
inputs: args,
func: disableOne
}, function vasyncCb(err) {
cb(err);
});
});
}
do_disable_deletion_protection.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['wait', 'w'],
type: 'bool',
help: 'Wait for deletion protection to be removed.'
}
];
do_disable_deletion_protection.synopses = [
'{{name}} disable-deletion-protection [OPTIONS] INST [INST ...]'
];
do_disable_deletion_protection.help = [
'Disable deletion protection on one or more instances.',
'',
'{{usage}}',
'',
'{{options}}',
'Where "INST" is an instance name, id, or short id.'
].join('\n');
do_disable_deletion_protection.completionArgtypes = ['tritoninstance'];
module.exports = do_disable_deletion_protection;

View File

@ -1,125 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2018 Joyent, Inc.
*
* `triton instance enable-deletion-protection ...`
*/
var assert = require('assert-plus');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
function do_enable_deletion_protection(subcmd, opts, args, cb) {
assert.object(opts, 'opts');
assert.arrayOfString(args, 'args');
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length === 0) {
cb(new errors.UsageError('missing INST argument(s)'));
return;
}
var cli = this.top;
function wait(name, id, next) {
assert.string(name, 'name');
assert.uuid(id, 'id');
assert.func(next, 'next');
cli.tritonapi.cloudapi.waitForDeletionProtectionEnabled({
id: id,
state: true
}, function (err, inst) {
if (err) {
next(err);
return;
}
assert.ok(inst.deletion_protection, 'inst ' + id
+ ' deletion_protection not in expected state after '
+ 'waitForDeletionProtectionEnabled');
console.log('Enabled deletion protection for instance "%s"', name);
next();
});
}
function enableOne(name, next) {
assert.string(name, 'name');
assert.func(next, 'next');
cli.tritonapi.enableInstanceDeletionProtection({
id: name
}, function enableProtectionCb(err, fauxInst) {
if (err) {
next(err);
return;
}
console.log('Enabling deletion protection for instance "%s"',
name);
if (opts.wait) {
wait(name, fauxInst.id, next);
} else {
next();
}
});
}
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
vasync.forEachParallel({
inputs: args,
func: enableOne
}, function vasyncCb(err) {
cb(err);
});
});
}
do_enable_deletion_protection.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['wait', 'w'],
type: 'bool',
help: 'Wait for deletion protection to be enabled.'
}
];
do_enable_deletion_protection.synopses = [
'{{name}} enable-deletion-protection [OPTIONS] INST [INST ...]'
];
do_enable_deletion_protection.help = [
'Enable deletion protection for one or more instances.',
'',
'{{usage}}',
'',
'{{options}}',
'Where "INST" is an instance name, id, or short id.'
].join('\n');
do_enable_deletion_protection.completionArgtypes = ['tritoninstance'];
module.exports = do_enable_deletion_protection;

View File

@ -19,7 +19,7 @@ function InstanceFwruleCLI(parent) {
Cmdln.call(this, { Cmdln.call(this, {
name: parent.name + ' fwrule', name: parent.name + ' fwrule',
desc: [ desc: [
'List fwrules on Spearhead instances.' 'List fwrules on Triton instances.'
].join('\n'), ].join('\n'),
helpOpts: { helpOpts: {
minHelpCol: 24 /* line up with option help */ minHelpCol: 24 /* line up with option help */

View File

@ -25,10 +25,7 @@ function do_get(subcmd, opts, args, cb) {
cb(setupErr); cb(setupErr);
return; return;
} }
tritonapi.getInstance({ tritonapi.getInstance(args[0], function (err, inst) {
id: args[0],
credentials: opts.credentials
}, function onInst(err, inst) {
if (inst) { if (inst) {
if (opts.json) { if (opts.json) {
console.log(JSON.stringify(inst)); console.log(JSON.stringify(inst));
@ -47,13 +44,6 @@ 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',
@ -68,6 +58,7 @@ do_get.help = [
'{{usage}}', '{{usage}}',
'', '',
'{{options}}', '{{options}}',
'',
'Where "INST" is an instance name, id, or short id.', 'Where "INST" is an instance name, id, or short id.',
'', '',
'A *deleted* instance may still respond with the instance object. In that', 'A *deleted* instance may still respond with the instance object. In that',

View File

@ -71,7 +71,7 @@ do_ip.help = [
'', '',
'{{options}}', '{{options}}',
'Where "INST" is an instance name, id, or short id.', 'Where "INST" is an instance name, id, or short id.',
'For example: ssh root@$(spearhead ip my-instance)' 'For example: ssh root@$(triton ip my-instance)'
].join('\n'); ].join('\n');

View File

@ -5,7 +5,7 @@
*/ */
/* /*
* Copyright (c) 2018, Joyent, Inc. * Copyright 2016 Joyent, Inc.
* *
* `triton instance list ...` * `triton instance list ...`
*/ */
@ -22,16 +22,13 @@ var common = require('../common');
* See <https://apidocs.joyent.com/cloudapi/#ListMachines>. * See <https://apidocs.joyent.com/cloudapi/#ListMachines>.
*/ */
var validFilters = [ var validFilters = [
'brand', // Added in CloudAPI 8.0.0 'type',
'docker', // Added in CloudAPI 8.0.0 'brand', // Added in CloudAPI 8.0.0
'image',
'memory',
'name', 'name',
'image',
'state', 'state',
// jsl:ignore 'memory',
/^tag\./, 'docker' // Added in CloudAPI 8.0.0
// jsl:end
'type'
]; ];
// columns default without -o // columns default without -o
@ -77,6 +74,7 @@ function do_list(subcmd, opts, args, callback) {
listOpts.credentials = true; listOpts.credentials = true;
} }
var imgs = []; var imgs = [];
var insts; var insts;
@ -150,11 +148,9 @@ function do_list(subcmd, opts, args, callback) {
common.uuidToShortId(inst.image); common.uuidToShortId(inst.image);
inst.shortid = inst.id.split('-', 1)[0]; inst.shortid = inst.id.split('-', 1)[0];
var flags = []; var flags = [];
if (inst.brand === 'bhyve') flags.push('B');
if (inst.docker) flags.push('D'); if (inst.docker) flags.push('D');
if (inst.firewall_enabled) flags.push('F'); if (inst.firewall_enabled) flags.push('F');
if (inst.brand === 'kvm') flags.push('K'); if (inst.brand === 'kvm') flags.push('K');
if (inst.deletion_protection) flags.push('P');
inst.flags = flags.length ? flags.join('') : undefined; inst.flags = flags.length ? flags.join('') : undefined;
}); });
@ -201,27 +197,21 @@ do_list.help = [
'', '',
'{{options}}', '{{options}}',
'Filters:', 'Filters:',
' FIELD=VALUE Equality filter. Supported fields: brand, image,', ' FIELD=VALUE Equality filter. Supported fields: type, brand, name,',
' memory, name, state, tag.TAGNAME, and type.', ' image, state, and memory',
' 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');

View File

@ -1,211 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2018 Joyent, Inc.
*
* `triton instance nic create ...`
*/
var assert = require('assert-plus');
var common = require('../../common');
var errors = require('../../errors');
function do_create(subcmd, opts, args, cb) {
assert.optionalBool(opts.wait, 'opts.wait');
assert.optionalBool(opts.json, 'opts.json');
assert.optionalBool(opts.help, 'opts.help');
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length < 2) {
cb(new errors.UsageError('missing INST and NETWORK or INST and' +
' NICOPT=VALUE arguments'));
return;
}
var cli = this.top;
var netObj;
var netObjArgs = [];
var regularArgs = [];
var createOpts = {};
args.forEach(function forEachArg(arg) {
if (arg.indexOf('=') !== -1) {
netObjArgs.push(arg);
return;
}
regularArgs.push(arg);
});
if (netObjArgs.length > 0) {
if (regularArgs.length > 1) {
cb(new errors.UsageError('cannot specify INST and NETWORK when'
+ ' passing in ipv4 arguments'));
return;
}
if (regularArgs.length !== 1) {
cb(new errors.UsageError('missing INST argument'));
return;
}
try {
netObj = common.parseNicStr(netObjArgs);
} catch (err) {
cb(err);
return;
}
}
if (netObj) {
assert.array(regularArgs, 'regularArgs');
assert.equal(regularArgs.length, 1, 'instance uuid');
createOpts.id = regularArgs[0];
createOpts.network = netObj;
} else {
assert.array(args, 'args');
assert.equal(args.length, 2, 'INST and NETWORK');
createOpts.id = args[0];
createOpts.network = args[1];
}
function wait(instId, mac, next) {
assert.string(instId, 'instId');
assert.string(mac, 'mac');
assert.func(next, 'next');
var waiter = cli.tritonapi.waitForNicStates.bind(cli.tritonapi);
/*
* We request state running|stopped because net-agent is doing work to
* keep a NICs state in sync with the VMs state. If a user adds a NIC
* to a stopped instance the final state of the NIC should also be
* stopped.
*/
waiter({
id: instId,
mac: mac,
states: ['running', 'stopped']
}, next);
}
// same signature as wait(), but is a nop
function waitNop(instId, mac, next) {
assert.string(instId, 'instId');
assert.string(mac, 'mac');
assert.func(next, 'next');
next();
}
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
cli.tritonapi.addNic(createOpts, function onAddNic(err, nic) {
if (err) {
cb(err);
return;
}
// If a NIC exists on the network already we will receive a 302
if (!nic) {
var errMsg = 'Instance already has a NIC on that network';
cb(new errors.TritonError(errMsg));
return;
}
// either wait or invoke a nop stub
var func = opts.wait ? wait : waitNop;
if (opts.wait && !opts.json) {
console.log('Creating NIC %s', nic.mac);
}
func(createOpts.id, nic.mac, function onWait(err2, createdNic) {
if (err2) {
cb(err2);
return;
}
var nicInfo = createdNic || nic;
if (opts.json) {
console.log(JSON.stringify(nicInfo));
} else {
console.log('Created NIC %s', nic.mac);
}
cb();
});
});
});
}
do_create.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
},
{
names: ['wait', 'w'],
type: 'bool',
help: 'Wait for the creation to complete.'
}
];
do_create.synopses = [
'{{name}} {{cmd}} [OPTIONS] INST NETWORK',
'{{name}} {{cmd}} [OPTIONS] INST NICOPT=VALUE [NICOPT=VALUE ...]'
];
do_create.help = [
'Create a NIC.',
'',
'{{usage}}',
'',
'{{options}}',
'INST is an instance id (full UUID), name, or short id,',
'and NETWORK is a network id (full UUID), name, or short id.',
'',
'NICOPTs are NIC options. The following NIC options are supported:',
'ipv4_uuid=<full network uuid> (required),' +
' and ipv4_ips=<a single IP string>.',
'',
'Be aware that adding NICs to an instance will cause that instance to',
'reboot.',
'',
'Example:',
' triton instance nic create --wait 22b75576 ca8aefb9',
' triton instance nic create 22b75576' +
' ipv4_uuid=651446a8-dab0-439e-a2c4-2c841ab07c51' +
' ipv4_ips=192.168.128.13'
].join('\n');
do_create.helpOpts = {
helpCol: 25
};
do_create.completionArgtypes = ['tritoninstance', 'tritonnic', 'none'];
module.exports = do_create;

View File

@ -1,126 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2018 Joyent, Inc.
*
* `triton instance nic delete ...`
*/
var assert = require('assert-plus');
var vasync = require('vasync');
var common = require('../../common');
var errors = require('../../errors');
function do_delete(subcmd, opts, args, cb) {
assert.object(opts, 'opts');
assert.optionalBool(opts.force, 'opts.force');
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length < 2) {
cb(new errors.UsageError('missing INST and MAC argument(s)'));
return;
} else if (args.length > 2) {
cb(new errors.UsageError('incorrect number of arguments'));
return;
}
var inst = args[0];
var mac = args[1];
var cli = this.top;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
confirm({mac: mac, force: opts.force}, function onConfirm(confirmErr) {
if (confirmErr) {
console.error('Aborting');
cb();
return;
}
cli.tritonapi.removeNic({
id: inst,
mac: mac
}, function onRemove(err) {
if (err) {
cb(err);
return;
}
console.log('Deleted NIC %s', mac);
cb();
});
});
});
}
// Request confirmation before deleting, unless --force flag given.
// If user declines, terminate early.
function confirm(opts, cb) {
assert.object(opts, 'opts');
assert.func(cb, 'cb');
if (opts.force) {
cb();
return;
}
common.promptYesNo({
msg: 'Delete NIC "' + opts.mac + '"? [y/n] '
}, function (answer) {
if (answer !== 'y') {
cb(new Error('Aborted NIC deletion'));
} else {
cb();
}
});
}
do_delete.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['force', 'f'],
type: 'bool',
help: 'Force removal.'
}
];
do_delete.synopses = ['{{name}} {{cmd}} INST MAC'];
do_delete.help = [
'Remove a NIC from an instance.',
'',
'{{usage}}',
'',
'{{options}}',
'Where INST is an instance id (full UUID), name, or short id.',
'',
'Be aware that removing NICs from an instance will cause that instance to',
'reboot.'
].join('\n');
do_delete.aliases = ['rm'];
do_delete.completionArgtypes = ['tritoninstance', 'none'];
module.exports = do_delete;

View File

@ -1,89 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2018 Joyent, Inc.
*
* `triton instance nic get ...`
*/
var assert = require('assert-plus');
var common = require('../../common');
var errors = require('../../errors');
function do_get(subcmd, opts, args, cb) {
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length < 2) {
cb(new errors.UsageError('missing INST and MAC arguments'));
return;
} else if (args.length > 2) {
cb(new errors.UsageError('incorrect number of arguments'));
return;
}
var inst = args[0];
var mac = args[1];
var cli = this.top;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
cli.tritonapi.getNic({id: inst, mac: mac}, function onNic(err, nic) {
if (err) {
cb(err);
return;
}
if (opts.json) {
console.log(JSON.stringify(nic));
} else {
console.log(JSON.stringify(nic, null, 4));
}
cb();
});
});
}
do_get.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
do_get.synopses = ['{{name}} {{cmd}} INST MAC'];
do_get.help = [
'Show a specific NIC.',
'',
'{{usage}}',
'',
'{{options}}',
'Where INST is an instance id (full UUID), name, or short id.'
].join('\n');
do_get.completionArgtypes = ['tritoninstance', 'none'];
module.exports = do_get;

View File

@ -1,154 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2018 Joyent, Inc.
*
* `triton instance nic list ...`
*/
var assert = require('assert-plus');
var tabula = require('tabula');
var common = require('../../common');
var errors = require('../../errors');
var VALID_FILTERS = ['ip', 'mac', 'state', 'network', 'primary', 'gateway'];
var COLUMNS_DEFAULT = 'ip,mac,state,network';
var COLUMNS_DEFAULT_LONG = 'ip,mac,state,network,primary,gateway';
var SORT_DEFAULT = 'ip';
function do_list(subcmd, opts, args, cb) {
assert.array(args, 'args');
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length < 1) {
cb(new errors.UsageError('missing INST argument'));
return;
}
var inst = args.shift();
try {
var filters = common.objFromKeyValueArgs(args, {
validKeys: VALID_FILTERS,
disableDotted: true
});
} catch (e) {
cb(e);
return;
}
var cli = this.top;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
cli.tritonapi.listNics({id: inst}, function onNics(err, nics) {
if (err) {
cb(err);
return;
}
// do filtering
Object.keys(filters).forEach(function filterByKey(key) {
var val = filters[key];
nics = nics.filter(function filterByNic(nic) {
return nic[key] === val;
});
});
if (opts.json) {
common.jsonStream(nics);
} else {
nics.forEach(function onNic(nic) {
nic.network = nic.network.split('-')[0];
nic.ip = nic.ip + '/' + convertCidrSuffix(nic.netmask);
});
var columns = COLUMNS_DEFAULT;
if (opts.o) {
columns = opts.o;
} else if (opts.long) {
columns = COLUMNS_DEFAULT_LONG;
}
columns = columns.split(',');
var sort = opts.s.split(',');
tabula(nics, {
skipHeader: opts.H,
columns: columns,
sort: sort
});
}
cb();
});
});
}
function convertCidrSuffix(netmask) {
var bitmask = netmask.split('.').map(function (octet) {
return (+octet).toString(2);
}).join('');
var i = 0;
for (i = 0; i < bitmask.length; i++) {
if (bitmask[i] === '0')
break;
}
return i;
}
do_list.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
}
].concat(common.getCliTableOptions({
includeLong: true,
sortDefault: SORT_DEFAULT
}));
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] [FILTERS]'];
do_list.help = [
'Show all NICs on an instance.',
'',
'{{usage}}',
'',
'{{options}}',
'',
'Where INST is an instance id (full UUID), name, or short id.',
'',
'Filters:',
' FIELD=<string> String filter. Supported fields: ip, mac, state,',
' network, netmask',
'',
'Filters are applied client-side (i.e. done by the triton command itself).'
].join('\n');
do_list.completionArgtypes = ['tritoninstance', 'none'];
do_list.aliases = ['ls'];
module.exports = do_list;

View File

@ -1,50 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2018 Joyent, Inc.
*
* `triton inst nic ...`
*/
var Cmdln = require('cmdln').Cmdln;
var util = require('util');
// ---- CLI class
function NicCLI(top) {
this.top = top.top;
Cmdln.call(this, {
name: top.name + ' nic',
desc: 'List and manage instance NICs.',
helpSubcmds: [
'help',
'list',
'get',
'create',
'delete'
],
helpOpts: {
minHelpCol: 23
}
});
}
util.inherits(NicCLI, Cmdln);
NicCLI.prototype.init = function init(opts, args, cb) {
this.log = this.top.log;
Cmdln.prototype.init.apply(this, arguments);
};
NicCLI.prototype.do_list = require('./do_list');
NicCLI.prototype.do_create = require('./do_create');
NicCLI.prototype.do_get = require('./do_get');
NicCLI.prototype.do_delete = require('./do_delete');
module.exports = NicCLI;

View File

@ -5,7 +5,7 @@
*/ */
/* /*
* Copyright 2018 Joyent, Inc. * Copyright 2016 Joyent, Inc.
* *
* `triton snapshot create ...` * `triton snapshot create ...`
*/ */
@ -133,7 +133,7 @@ do_create.help = [
'{{usage}}', '{{usage}}',
'', '',
'{{options}}', '{{options}}',
'Snapshots do not work for instances of type "bhyve" or "kvm".' 'Snapshot do not work for instances of type "kvm".'
].join('\n'); ].join('\n');
do_create.completionArgtypes = ['tritoninstance', 'none']; do_create.completionArgtypes = ['tritoninstance', 'none'];

View File

@ -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 Spearhead instance snapshots.', desc: 'List, get, create and delete Triton 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' +
'`spearhead instance start --snapshot=SNAPNAME`.' '`triton instance start --snapshot=SNAPNAME`.'
}); });
} }
util.inherits(SnapshotCLI, Cmdln); util.inherits(SnapshotCLI, Cmdln);

View File

@ -21,7 +21,7 @@ function do_snapshots(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_snapshots.help = 'A shortcut for "spearhead instance snapshot list".\n' + do_snapshots.help = 'A shortcut for "triton instance snapshot list".\n' +
targ.help; targ.help;
do_snapshots.synopses = targ.synopses; do_snapshots.synopses = targ.synopses;
do_snapshots.options = targ.options; do_snapshots.options = targ.options;

View File

@ -5,12 +5,11 @@
*/ */
/* /*
* Copyright (c) 2018, Joyent, Inc. * Copyright 2017 Joyent, Inc.
* *
* `triton instance ssh ...` * `triton instance ssh ...`
*/ */
var assert = require('assert-plus');
var path = require('path'); var path = require('path');
var spawn = require('child_process').spawn; var spawn = require('child_process').spawn;
var vasync = require('vasync'); var vasync = require('vasync');
@ -18,30 +17,6 @@ var vasync = require('vasync');
var common = require('../common'); var common = require('../common');
var errors = require('../errors'); 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) {
if (opts.help) { if (opts.help) {
@ -55,12 +30,10 @@ function do_ssh(subcmd, opts, args, callback) {
var id = args.shift(); var id = args.shift();
var user; 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;
} }
vasync.pipeline({arg: {cli: this.top}, funcs: [ vasync.pipeline({arg: {cli: this.top}, funcs: [
@ -75,112 +48,17 @@ function do_ssh(subcmd, opts, args, callback) {
ctx.inst = inst; ctx.inst = inst;
if (inst.tags && inst.tags[TAG_SSH_IP]) { ctx.ip = inst.primaryIp;
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) { if (!ctx.ip) {
next(new Error('IP address not found for instance')); next(new Error('primaryIp not found for instance'));
return; return;
} }
next(); 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) { function getUser(ctx, next) {
if (overrideUser) { if (user) {
assert.string(user, 'user');
next(); next();
return; return;
} }
@ -195,8 +73,8 @@ function do_ssh(subcmd, opts, args, callback) {
} }
/* /*
* This is a convention as seen on Joyent's "ubuntu-certified" * This is a convention as seen on Joyent's
* KVM images. * "ubuntu-certified" KVM images.
*/ */
if (image.tags && image.tags.default_user) { if (image.tags && image.tags.default_user) {
user = image.tags.default_user; user = image.tags.default_user;
@ -208,64 +86,9 @@ function do_ssh(subcmd, opts, args, callback) {
}); });
}, },
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) { function doSsh(ctx, next) {
args = ['-l', user, ctx.ip].concat(args); 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 * By default we disable ControlMaster (aka mux, aka SSH
* connection multiplexing) because of * connection multiplexing) because of
@ -310,11 +133,6 @@ 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.synopses = ['{{name}} ssh [-h] [USER@]INST [SSH-ARGUMENTS]']; do_ssh.synopses = ['{{name}} ssh [-h] [USER@]INST [SSH-ARGUMENTS]'];
@ -332,32 +150,12 @@ do_ssh.help = [
'If USER is not specified and the default_user tag is not set, the user', 'If USER is not specified and the default_user tag is not set, the user',
'is assumed to be \"root\".', '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. ', 'There is a known issue with SSH connection multiplexing (a.k.a. ',
'ControlMaster, mux) where stdout/stderr is lost. As a workaround, `ssh`', 'ControlMaster, mux) where stdout/stderr is lost. As a workaround, `ssh`',
'is spawned with options disabling ControlMaster. See ', 'is spawned with options disabling ControlMaster. See ',
'<https://github.com/joyent/node-triton/issues/52> for details. If you ', '<https://github.com/joyent/node-triton/issues/52> for details. If you ',
'want to use ControlMaster, an alternative is:', 'want to use ControlMaster, an alternative is:',
' ssh root@$(spearhead ip INST)' ' ssh root@$(triton ip INST)'
/* END JSSTYLED */ /* END JSSTYLED */
].join('\n'); ].join('\n');

View File

@ -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 Spearhead instances.' 'List, get, set and delete tags on Triton instances.'
].join('\n'), ].join('\n'),
/* END JSSTYLED */ /* END JSSTYLED */
helpOpts: { helpOpts: {

View File

@ -20,7 +20,7 @@ function do_tags(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_tags.help = 'A shortcut for "spearhead instance tag list".\n' + targ.help; do_tags.help = 'A shortcut for "triton instance tag list".\n' + targ.help;
do_tags.synopses = targ.synopses; do_tags.synopses = targ.synopses;
do_tags.options = targ.options; do_tags.options = targ.options;
do_tags.completionArgtypes = targ.completionArgtypes; do_tags.completionArgtypes = targ.completionArgtypes;

View File

@ -122,7 +122,7 @@ do_wait.help = [
'{{options}}', '{{options}}',
'Where "INST" is an instance name, id, or short id; and "STATES" is a', 'Where "INST" is an instance name, id, or short id; and "STATES" is a',
'comma-separated list of target instance states, by default "running,failed".', 'comma-separated list of target instance states, by default "running,failed".',
'In other words, "spearhead inst wait foo0" will wait for instance "foo0" to', 'In other words, "triton inst wait foo0" will wait for instance "foo0" to',
'complete provisioning.' 'complete provisioning.'
/* END JSSTYLED */ /* END JSSTYLED */
].join('\n'); ].join('\n');

View File

@ -5,7 +5,7 @@
*/ */
/* /*
* Copyright 2018 Joyent, Inc. * Copyright 2015 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 Spearhead instances.' 'List and manage Triton instances.'
].join('\n'), ].join('\n'),
/* END JSSTYLED */ /* END JSSTYLED */
helpOpts: { helpOpts: {
@ -45,14 +45,10 @@ function InstanceCLI(top) {
'enable-firewall', 'enable-firewall',
'disable-firewall', 'disable-firewall',
{ group: '' }, { group: '' },
'enable-deletion-protection',
'disable-deletion-protection',
{ group: '' },
'ssh', 'ssh',
'ip', 'ip',
'wait', 'wait',
'audit', 'audit',
'nic',
'snapshot', 'snapshot',
'tag' 'tag'
] ]
@ -81,16 +77,10 @@ InstanceCLI.prototype.do_fwrules = require('./do_fwrules');
InstanceCLI.prototype.do_enable_firewall = require('./do_enable_firewall'); InstanceCLI.prototype.do_enable_firewall = require('./do_enable_firewall');
InstanceCLI.prototype.do_disable_firewall = require('./do_disable_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_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_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');

View File

@ -20,7 +20,7 @@ function do_instances(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_instances.help = 'A shortcut for "spearhead instance list".\n' + targ.help; do_instances.help = 'A shortcut for "triton instance list".\n' + targ.help;
do_instances.synopses = targ.synopses; do_instances.synopses = targ.synopses;
do_instances.options = targ.options; do_instances.options = targ.options;
do_instances.completionArgtypes = targ.completionArgtypes; do_instances.completionArgtypes = targ.completionArgtypes;

View File

@ -20,7 +20,7 @@ function do_ip(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_ip.help = 'A shortcut for "spearhead instance ip".\n' + targ.help; do_ip.help = 'A shortcut for "triton instance ip".\n' + targ.help;
do_ip.synopses = targ.synopses; do_ip.synopses = targ.synopses;
do_ip.options = targ.options; do_ip.options = targ.options;
do_ip.completionArgtypes = targ.completionArgtypes; do_ip.completionArgtypes = targ.completionArgtypes;

View File

@ -47,7 +47,13 @@ function do_add(subcmd, opts, args, cb) {
return next(); return next();
} }
common.readStdin(function gotStdin(stdin) { var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('end', function () {
ctx.data = stdin; ctx.data = stdin;
ctx.from = '<stdin>'; ctx.from = '<stdin>';
next(); next();

View File

@ -20,7 +20,7 @@ function do_keys(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_keys.help = 'A shortcut for "spearhead key list".\n' + targ.help; do_keys.help = 'A shortcut for "triton key list".\n' + targ.help;
do_keys.synopses = targ.synopses; do_keys.synopses = targ.synopses;
do_keys.options = targ.options; do_keys.options = targ.options;
do_keys.completionArgtypes = targ.completionArgtypes; do_keys.completionArgtypes = targ.completionArgtypes;

View File

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

View File

@ -1,85 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2017 Joyent, Inc.
*
* `triton network delete ...`
*/
var assert = require('assert-plus');
var format = require('util').format;
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
function do_delete(subcmd, opts, args, cb) {
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length < 1) {
cb(new errors.UsageError('missing NETWORK argument(s)'));
return;
}
var cli = this.top;
var networks = args;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
vasync.forEachParallel({
inputs: networks,
func: function deleteOne(id, next) {
cli.tritonapi.deleteFabricNetwork({ id: id },
function onDelete(err) {
if (err) {
next(err);
return;
}
console.log('Deleted network %s', id);
next();
});
}
}, cb);
});
}
do_delete.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
}
];
do_delete.synopses = ['{{name}} {{cmd}} NETWORK [NETWORK ...]'];
do_delete.help = [
'Remove a fabric network.',
'',
'{{usage}}',
'',
'{{options}}',
'Where NETWORK is a network id (full UUID), name, or short id.'
].join('\n');
do_delete.aliases = ['rm'];
do_delete.completionArgtypes = ['tritonnetwork'];
module.exports = do_delete;

View File

@ -1,78 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright (c) 2018, Joyent, Inc.
*
* `triton network get-default ...`
*/
var assert = require('assert-plus');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
function do_get_default(subcmd, opts, args, cb) {
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length > 0) {
cb(new errors.UsageError('incorrect number of arguments'));
return;
}
var cli = this.top;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
cli.tritonapi.cloudapi.getConfig({}, function getConf(err, conf) {
if (err) {
cb(err);
return;
}
var defaultNetwork = conf.default_network;
if (!defaultNetwork) {
cb(new Error('account has no default network configured'));
return;
}
cli.handlerFromSubcmd('network').dispatch({
subcmd: 'get',
opts: opts,
args: [defaultNetwork]
}, cb);
});
});
}
do_get_default.options = require('./do_get').options;
do_get_default.synopses = ['{{name}} {{cmd}}'];
do_get_default.help = [
'Get default network.',
'',
'{{usage}}',
'',
'{{options}}'
].join('\n');
do_get_default.completionArgtypes = ['tritonnetwork'];
module.exports = do_get_default;

View File

@ -1,81 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2017 Joyent, Inc.
*
* `triton network ip get ...`
*/
var format = require('util').format;
var common = require('../../common');
var errors = require('../../errors');
function do_get(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length !== 2) {
return cb(new errors.UsageError(format(
'incorrect number of args (%d)', args.length)));
}
var tritonapi = this.top.tritonapi;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
var getIpOpts = {
id: args[0],
ip: args[1]
};
tritonapi.getNetworkIp(getIpOpts, function (err, ip, res) {
if (err) {
return cb(err);
}
if (opts.json) {
console.log(JSON.stringify(ip));
} else {
console.log(JSON.stringify(ip, null, 4));
}
cb();
});
});
}
do_get.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON output.'
}
];
do_get.synopses = ['{{name}} {{cmd}} NETWORK IP'];
do_get.help = [
'Show a network ip.',
'',
'{{usage}}',
'',
'{{options}}',
'Where NETWORK is a network id, and IP is the ip address you want to get.'
].join('\n');
do_get.completionArgtypes = ['tritonnetwork', 'tritonnetworkip', 'none'];
module.exports = do_get;

View File

@ -1,129 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2017 Joyent, Inc.
*
* `triton network ip list ...`
*/
var format = require('util').format;
var assert = require('assert-plus');
var tabula = require('tabula');
var vasync = require('vasync');
var common = require('../../common');
var errors = require('../../errors');
// columns default without -o
var columnsDefault = 'ip,managed,reserved,owner_uuid,belongs_to_uuid';
// sort default with -s
var sortDefault = 'ip';
function do_list(subcmd, opts, args, callback) {
var self = this;
if (opts.help) {
this.do_help('help', {}, [subcmd], callback);
return;
} else if (args.length !== 1) {
return callback(new errors.UsageError(format(
'incorrect number of args (%d)', args.length)));
}
var columns = columnsDefault;
if (opts.o) {
columns = opts.o;
}
columns = columns.split(',');
var sort = opts.s.split(',').map(function mapSort(field) {
var so = {};
field = field.trim();
assert.ok(field, 'non-empty field');
if (field[0] === '-') {
so.field = field.slice(1);
so.reverse = true;
} else {
so.field = field;
}
switch (so.field) {
case 'ip':
so.keyFunc = common.ipv4ToLong;
break;
default:
break;
}
return so;
});
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function listIps(arg, next) {
self.top.tritonapi.listNetworkIps(args[0],
function (err, ips, res) {
if (err) {
next(err);
return;
}
arg.ips = ips;
next();
});
},
function doneIps(arg, next) {
var ips = arg.ips;
if (opts.json) {
common.jsonStream(ips);
} else {
tabula(ips, {
skipHeader: opts.H,
columns: columns,
sort: sort
});
}
next();
}
]}, callback);
}
do_list.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
}
].concat(common.getCliTableOptions({
includeLong: true,
sortDefault: sortDefault
}));
do_list.synopses = ['{{name}} {{cmd}} NETWORK'];
do_list.help = [
'List network IPs.',
'',
'{{usage}}',
'',
'{{options}}',
'Fields (most are self explanatory, the significant ones are as follows):',
' managed IP is manged by Spearhead and cannot be modified directly.',
'',
'See https://apidocs.joyent.com/cloudapi/#ListNetworkIPs for a full' +
' listing.'
].join('\n');
do_list.aliases = ['ls'];
do_list.completionArgtypes = ['tritonnetwork', 'none'];
module.exports = do_list;

View File

@ -1,193 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2017 Joyent, Inc.
*
* `triton network ip update ...`
*/
var format = require('util').format;
var fs = require('fs');
var vasync = require('vasync');
var common = require('../../common');
var errors = require('../../errors');
var UPDATE_NETWORK_IP_FIELDS
= require('../../cloudapi2').CloudApi.prototype.UPDATE_NETWORK_IP_FIELDS;
function do_update(subcmd, opts, args, callback) {
if (opts.help) {
this.do_help('help', {}, [subcmd], callback);
return;
} else if (args.length < 2) {
callback(new errors.UsageError(format(
'incorrect number of args (%d)', args.length)));
return;
}
var log = this.log;
var tritonapi = this.top.tritonapi;
var updateIpOpts = {
id: args.shift(),
ip: args.shift()
};
if (args.length === 0 && !opts.file) {
callback(new errors.UsageError(
'FIELD=VALUE arguments or "-f FILE" must be specified'));
return;
}
vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function gatherDataArgs(ctx, next) {
if (opts.file) {
next();
return;
}
try {
ctx.data = common.objFromKeyValueArgs(args, {
disableDotted: true,
typeHintFromKey: UPDATE_NETWORK_IP_FIELDS
});
} catch (err) {
next(err);
return;
}
next();
},
function gatherDataFile(ctx, next) {
if (!opts.file || opts.file === '-') {
next();
return;
}
var input = fs.readFileSync(opts.file, 'utf8');
try {
ctx.data = JSON.parse(input);
} catch (err) {
next(new errors.TritonError(format(
'invalid JSON for network IP update in "%s": %s',
opts.file, err)));
return;
}
next();
},
function gatherDataStdin(ctx, next) {
if (opts.file !== '-') {
next();
return;
}
common.readStdin(function gotStdin(stdin) {
try {
ctx.data = JSON.parse(stdin);
} catch (err) {
log.trace({stdin: stdin},
'invalid network IP update JSON on stdin');
next(new errors.TritonError(format(
'invalid JSON for network IP update on stdin: %s',
err)));
return;
}
next();
});
},
function validateIt(ctx, next) {
try {
common.validateObject(ctx.data, UPDATE_NETWORK_IP_FIELDS);
} catch (e) {
next(e);
return;
}
next();
},
function updateNetworkIP(ctx, next) {
Object.keys(ctx.data).forEach(function (key) {
updateIpOpts[key] = ctx.data[key];
});
tritonapi.updateNetworkIp(updateIpOpts, function (err, body, res) {
if (err) {
next(err);
return;
}
if (opts.json) {
console.log(JSON.stringify(body));
next();
return;
}
console.log('Updated network %s IP %s (fields: %s)',
updateIpOpts.id, updateIpOpts.ip,
Object.keys(ctx.data).join(', '));
next();
});
}
]}, callback);
}
do_update.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['file', 'f'],
type: 'string',
helpArg: 'FILE',
help: 'A file holding a JSON file of updates, or "-" to read ' +
'JSON from stdin.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON output.'
}
];
do_update.synopses = [
'{{name}} {{cmd}} NETWORK IP [FIELD=VALUE ...]',
'{{name}} {{cmd}} NETWORK IP -f JSON-FILE'
];
do_update.help = [
/* BEGIN JSSTYLED */
'Update a network ip.',
'',
'{{usage}}',
'',
'{{options}}',
'Where NETWORK is a network id, and IP is the ip address you want to update.',
'',
'Updateable fields:',
' ' + Object.keys(UPDATE_NETWORK_IP_FIELDS).sort().map(function (field) {
return field + ' (' + UPDATE_NETWORK_IP_FIELDS[field] + ')';
}).join('\n '),
''
/* END JSSTYLED */
].join('\n');
do_update.completionArgtypes = [
'tritonnetwork',
'tritonnetworkip',
'tritonupdatenetworkipfield'
];
module.exports = do_update;

View File

@ -1,51 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2017 Joyent, Inc.
*
* `triton network ip...`
*/
var Cmdln = require('cmdln').Cmdln;
var util = require('util');
// ---- CLI class
function IpCLI(top) {
this.top = top.top;
Cmdln.call(this, {
name: top.name + ' ip',
/* BEGIN JSSTYLED */
desc: [
'List and manage Spearhead network IPs.'
].join('\n'),
/* END JSSTYLED */
helpOpts: {
minHelpCol: 24 /* line up with option help */
},
helpSubcmds: [
'help',
'list',
'get',
'update'
]
});
}
util.inherits(IpCLI, Cmdln);
IpCLI.prototype.init = function init(opts, args, cb) {
this.log = this.top.log;
Cmdln.prototype.init.apply(this, arguments);
};
IpCLI.prototype.do_list = require('./do_list');
IpCLI.prototype.do_get = require('./do_get');
IpCLI.prototype.do_update = require('./do_update');
module.exports = IpCLI;

View File

@ -66,23 +66,14 @@ function do_list(subcmd, opts, args, callback) {
common.cliSetupTritonApi, common.cliSetupTritonApi,
function searchNetworks(arg, next) { function searchNetworks(arg, next) {
// since this command is also used by do_vlan/do_networks.js self.top.tritonapi.cloudapi.listNetworks(function (err, networks) {
if (opts.vlan_id) {
self.top.tritonapi.listFabricNetworks({
vlan_id: opts.vlan_id
}, listedNetworks);
} else {
self.top.tritonapi.cloudapi.listNetworks({}, listedNetworks);
}
function listedNetworks(err, networks) {
if (err) { if (err) {
next(err); next(err);
return; return;
} }
arg.networks = networks; arg.networks = networks;
next(); next();
} });
}, },
function filterNetworks(arg, next) { function filterNetworks(arg, next) {

View File

@ -1,92 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2017 Joyent, Inc.
*
* `triton network set-default ...`
*/
var assert = require('assert-plus');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
function do_set_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('missing NETWORK argument'));
return;
} else if (args.length > 1) {
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.getNetwork(args[0], function onNetwork(err, net) {
if (err) {
cb(err);
return;
}
var params = {
default_network: net.id
};
var cloudapi = cli.tritonapi.cloudapi;
cloudapi.updateConfig(params, function onUpdate(err2) {
if (err2) {
cb(err2);
return;
}
console.log('Set network %s (%s) as default.', net.name,
net.id);
cb();
});
});
});
}
do_set_default.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
}
];
do_set_default.synopses = ['{{name}} {{cmd}} NETWORK'];
do_set_default.help = [
'Set default network.',
'',
'{{usage}}',
'',
'{{options}}',
'Where NETWORK is a network id (full UUID), name, or short id.'
].join('\n');
do_set_default.completionArgtypes = ['tritonnetwork'];
module.exports = do_set_default;

View File

@ -23,7 +23,7 @@ function NetworkCLI(top) {
name: top.name + ' network', name: top.name + ' network',
/* BEGIN JSSTYLED */ /* BEGIN JSSTYLED */
desc: [ desc: [
'List and manage Spearhead networks.' 'List and manage Triton networks.'
].join('\n'), ].join('\n'),
/* END JSSTYLED */ /* END JSSTYLED */
helpOpts: { helpOpts: {
@ -32,12 +32,7 @@ function NetworkCLI(top) {
helpSubcmds: [ helpSubcmds: [
'help', 'help',
'list', 'list',
'get', 'get'
'ip',
'create',
'delete',
'get-default',
'set-default'
] ]
}); });
} }
@ -50,11 +45,6 @@ NetworkCLI.prototype.init = function init(opts, args, cb) {
NetworkCLI.prototype.do_list = require('./do_list'); NetworkCLI.prototype.do_list = require('./do_list');
NetworkCLI.prototype.do_get = require('./do_get'); NetworkCLI.prototype.do_get = require('./do_get');
NetworkCLI.prototype.do_ip = require('./do_ip');
NetworkCLI.prototype.do_create = require('./do_create');
NetworkCLI.prototype.do_delete = require('./do_delete');
NetworkCLI.prototype.do_get_default = require('./do_get_default');
NetworkCLI.prototype.do_set_default = require('./do_set_default');
module.exports = NetworkCLI; module.exports = NetworkCLI;

View File

@ -20,7 +20,7 @@ function do_networks(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_networks.help = 'A shortcut for "spearhead network list".\n' + targ.help; do_networks.help = 'A shortcut for "triton network list".\n' + targ.help;
do_networks.synopses = targ.synopses; do_networks.synopses = targ.synopses;
do_networks.options = targ.options; do_networks.options = targ.options;
do_networks.completionArgtypes = targ.completionArgtypes; do_networks.completionArgtypes = targ.completionArgtypes;

View File

@ -23,7 +23,7 @@ function PackageCLI(top) {
name: top.name + ' package', name: top.name + ' package',
/* BEGIN JSSTYLED */ /* BEGIN JSSTYLED */
desc: [ desc: [
'List and get Spearhead packages.', 'List and get Triton packages.',
'', '',
'A package is a collection of attributes -- for example disk quota,', 'A package is a collection of attributes -- for example disk quota,',
'amount of RAM -- used when creating an instance. They have a name', 'amount of RAM -- used when creating an instance. They have a name',

View File

@ -20,7 +20,7 @@ function do_packages(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_packages.help = 'A shortcut for "spearhead package list".\n' + targ.help; do_packages.help = 'A shortcut for "triton package list".\n' + targ.help;
do_packages.synopses = targ.synopses; do_packages.synopses = targ.synopses;
do_packages.options = targ.options; do_packages.options = targ.options;
do_packages.completionArgtypes = targ.completionArgtypes; do_packages.completionArgtypes = targ.completionArgtypes;

View File

@ -93,8 +93,12 @@ function _createProfile(opts, cb) {
next(); next();
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 {
data = JSON.parse(stdin); data = JSON.parse(stdin);
} catch (err) { } catch (err) {
@ -167,7 +171,7 @@ function _createProfile(opts, cb) {
defaults = ctx.copy; defaults = ctx.copy;
delete defaults.name; // we don't copy a profile name delete defaults.name; // we don't copy a profile name
} else { } else {
defaults.url = 'https://eu-ro-1.api.spearhead.cloud'; defaults.url = 'https://us-sw-1.api.joyent.com';
} }
/* /*
@ -216,7 +220,7 @@ function _createProfile(opts, cb) {
var fields = [ { var fields = [ {
desc: 'A profile name. A short string to identify this ' + desc: 'A profile name. A short string to identify this ' +
'profile to the `spearhead` command.', 'profile to the `triton` command.',
key: 'name', key: 'name',
default: defaults.name, default: defaults.name,
validate: function validateName(value, valCb) { validate: function validateName(value, valCb) {
@ -355,7 +359,7 @@ function _createProfile(opts, cb) {
console.log(common.ansiStylizeTty('\n\n# Docker setup\n', 'bold')); console.log(common.ansiStylizeTty('\n\n# Docker setup\n', 'bold'));
console.log(wrap80('This section will setup authentication to ' + console.log(wrap80('This section will setup authentication to ' +
'Spearhead Datacenter\'s Docker endpoint using your account ' + 'Triton DataCenter\'s Docker endpoint using your account ' +
'and key information specified above. This is only required ' + 'and key information specified above. This is only required ' +
'if you intend to use `docker` with this profile.\n')); 'if you intend to use `docker` with this profile.\n'));
@ -424,7 +428,7 @@ do_create.options = [
names: ['file', 'f'], names: ['file', 'f'],
type: 'string', type: 'string',
helpArg: 'FILE', helpArg: 'FILE',
help: 'A JSON file (of the same form as "spearhead profile get -j") ' + help: 'A JSON file (of the same form as "triton profile get -j") ' +
'with the profile, or "-" to read JSON from stdin.' 'with the profile, or "-" to read JSON from stdin.'
}, },
{ {
@ -437,7 +441,7 @@ do_create.options = [
{ {
names: ['no-docker'], names: ['no-docker'],
type: 'bool', type: 'bool',
help: 'As of Spearhead CLI 4.9, creating a profile will attempt (on ' help: 'As of Triton CLI 4.9, creating a profile will attempt (on '
+ 'non-Windows) to also setup for running Docker. This is ' + 'non-Windows) to also setup for running Docker. This is '
+ 'experimental and might fail. Use this option to disable ' + 'experimental and might fail. Use this option to disable '
+ 'the attempt.' + 'the attempt.'
@ -452,19 +456,19 @@ do_create.options = [
do_create.synopses = ['{{name}} {{cmd}} [OPTIONS]']; do_create.synopses = ['{{name}} {{cmd}} [OPTIONS]'];
do_create.help = [ do_create.help = [
'Create a Spearhead CLI profile.', 'Create a Triton CLI profile.',
'', '',
'{{usage}}', '{{usage}}',
'', '',
'{{options}}', '{{options}}',
'', '',
'Examples:', 'Examples:',
' spearhead profile create # interactively create a profile', ' triton profile create # interactively create a profile',
' spearhead profile create --copy env # ... copying from "env" profile', ' triton profile create --copy env # ... copying from "env" profile',
'', '',
' # Or non-interactively create from stdin or a file:', ' # Or non-interactively create from stdin or a file:',
' cat a-profile.json | spearhead profile create -f -', ' cat a-profile.json | triton profile create -f -',
' spearhead profile create -f another-profile.json' ' triton profile create -f another-profile.json'
].join('\n'); ].join('\n');

View File

@ -122,7 +122,7 @@ do_delete.options = [
do_delete.synopses = ['{{name}} {{cmd}} PROFILE']; do_delete.synopses = ['{{name}} {{cmd}} PROFILE'];
do_delete.help = [ do_delete.help = [
'Delete a Spearhead CLI profile.', 'Delete a Triton CLI profile.',
'', '',
'{{usage}}', '{{usage}}',
'', '',

View File

@ -23,8 +23,7 @@ function do_docker_setup(subcmd, opts, args, cb) {
cli: this.top, cli: this.top,
name: profileName, name: profileName,
implicit: false, implicit: false,
yes: opts.yes, yes: opts.yes
lifetime: opts.lifetime
}, cb); }, cb);
} }
@ -34,11 +33,6 @@ do_docker_setup.options = [
type: 'bool', type: 'bool',
help: 'Show this help.' help: 'Show this help.'
}, },
{
names: ['lifetime', 't'],
type: 'number',
help: 'Lifetime of the generated docker certificate, in days'
},
{ {
names: ['yes', 'y'], names: ['yes', 'y'],
type: 'bool', type: 'bool',
@ -49,21 +43,21 @@ do_docker_setup.options = [
do_docker_setup.synopses = ['{{name}} {{cmd}} [PROFILE]']; do_docker_setup.synopses = ['{{name}} {{cmd}} [PROFILE]'];
do_docker_setup.help = [ do_docker_setup.help = [
/* BEGIN JSSTYLED */ /* BEGIN JSSTYLED */
'Setup for using Docker with the current Spearhead CLI profile.', 'Setup for using Docker with the current Triton CLI profile.',
'', '',
'{{usage}}', '{{usage}}',
'', '',
'{{options}}', '{{options}}',
'A Spearhead datacenter can act as a virtual Docker Engine, where the entire', 'A Triton datacenter can act as a virtual Docker Engine, where the entire',
'datacenter is available for running containers. The datacenter provides', 'datacenter is available on for running containers. The datacenter provides',
'an endpoint against which you can run the regular `docker` client. This', 'an endpoint against which you can run the regular `docker` client. This',
'requires a one time setup to (a) generate a client TLS certificate to enable', 'requires a one time setup to (a) generate a client TLS certificate to enable',
'secure authentication with the Spearhead Docker Engine, and (b) to determine', 'secure authentication with the Triton Docker Engine, and (b) to determine',
'the DOCKER_HOST and related environment variables.', 'the DOCKER_HOST and related environment variables.',
'', '',
'After running this, you can setup your shell environment for `docker` via:', 'After running this, you can setup your shell environment for `docker` via:',
' eval "$(spearhead env --docker)"', ' eval "$(triton env --docker)"',
'or the equivalent. See `spearhead env --help` for details.' 'or the equivalent. See `triton env --help` for details.'
/* END JSSTYLED */ /* END JSSTYLED */
].join('\n'); ].join('\n');

View File

@ -157,7 +157,7 @@ do_edit.options = [
do_edit.synopses = ['{{name}} {{cmd}} [PROFILE]']; do_edit.synopses = ['{{name}} {{cmd}} [PROFILE]'];
do_edit.help = [ do_edit.help = [
'Edit a Spearhead CLI profile in your $EDITOR.', 'Edit a Triton CLI profile in your $EDITOR.',
'', '',
'{{usage}}', '{{usage}}',
'', '',

View File

@ -76,7 +76,7 @@ do_get.options = [
do_get.synopses = ['{{name}} {{cmd}} [PROFILE]']; do_get.synopses = ['{{name}} {{cmd}} [PROFILE]'];
do_get.help = [ do_get.help = [
'Get a Spearhead CLI profile.', 'Get a Triton CLI profile.',
'', '',
'{{usage}}', '{{usage}}',
'', '',

View File

@ -31,8 +31,7 @@ function _listProfiles(cli, opts, args, cb) {
try { try {
profiles = mod_config.loadAllProfiles({ profiles = mod_config.loadAllProfiles({
configDir: cli.configDir, configDir: cli.configDir,
log: cli.log, log: cli.log
profileOverrides: cli._cliOptsAsProfile()
}); });
} catch (e) { } catch (e) {
return cb(e); return cb(e);
@ -82,12 +81,12 @@ function _listProfiles(cli, opts, args, cb) {
if (profiles.length === 0) { if (profiles.length === 0) {
process.stderr.write('\nWarning: There is no current profile. ' process.stderr.write('\nWarning: There is no current profile. '
+ 'Use "triton profile create" to create one,\n' + 'Use "triton profile create" to create one,\n'
+ 'or set the required "SC_*" environment ' + 'or set the required "SDC_*/TRITON_*" environment '
+ 'variables: see "spearhead --help".\n'); + 'variables: see "triton --help".\n');
} else { } else {
process.stderr.write('\nWarning: There is no current profile. ' process.stderr.write('\nWarning: There is no current profile. '
+ 'Use "spearhead profile set-current ..."\n' + 'Use "triton profile set-current ..."\n'
+ 'to set one or "spearhead profile create" to create one.\n'); + 'to set one or "triton profile create" to create one.\n');
} }
} }
} }
@ -118,15 +117,15 @@ do_list.options = [
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS]']; do_list.synopses = ['{{name}} {{cmd}} [OPTIONS]'];
do_list.help = [ do_list.help = [
/* BEGIN JSSTYLED */ /* BEGIN JSSTYLED */
'List Spearhead CLI profiles.', 'List Triton CLI profiles.',
'', '',
'{{usage}}', '{{usage}}',
'', '',
'{{options}}', '{{options}}',
'A profile is a configured Spearhead Datacenter endpoint and associated info.', 'A profile is a configured Triton CloudAPI endpoint and associated info.',
'I.e. the URL, account name, SSH key fingerprint, etc. information required', 'I.e. the URL, account name, SSH key fingerprint, etc. information required',
'to call a CloudAPI endpoint in a Spearhead datacenter. You can then switch', 'to call a CloudAPI endpoint in a Triton datacenter. You can then switch',
'between profiles with `spearhead -p PROFILE`, the SC_PROFILE environment', 'between profiles with `triton -p PROFILE`, the TRITON_PROFILE environment',
'variable, or by setting your current profile.', 'variable, or by setting your current profile.',
'', '',
'The "CURR" column indicates which profile is the current one.' 'The "CURR" column indicates which profile is the current one.'

View File

@ -34,7 +34,7 @@ do_set_current.options = [
do_set_current.synopses = ['{{name}} {{cmd}} PROFILE']; do_set_current.synopses = ['{{name}} {{cmd}} PROFILE'];
do_set_current.help = [ do_set_current.help = [
'Set the current Spearhead CLI profile.', 'Set the current Triton CLI profile.',
'', '',
'{{usage}}', '{{usage}}',
'', '',
@ -43,7 +43,7 @@ do_set_current.help = [
'previously set profile.', 'previously set profile.',
'', '',
'The "current" profile is the one used by default, unless overridden by', 'The "current" profile is the one used by default, unless overridden by',
'`spearhead -p PROFILE-NAME ...` or the SC_PROFILE environment variable.' '`triton -p PROFILE-NAME ...` or the TRITON_PROFILE environment variable.'
].join('\n'); ].join('\n');
do_set_current.aliases = ['set']; do_set_current.aliases = ['set'];

View File

@ -23,12 +23,12 @@ function ProfileCLI(top) {
name: top.name + ' profile', name: top.name + ' profile',
/* BEGIN JSSTYLED */ /* BEGIN JSSTYLED */
desc: [ desc: [
'List, get, create and update Spearhead CLI profiles.', 'List, get, create and update Triton CLI profiles.',
'', '',
'A profile is a configured Spearhead Datacenter endpoint. I.e. the', 'A profile is a configured Triton CloudAPI endpoint. I.e. the',
'url, account, key, etc. information required to call a CloudAPI.', 'url, account, key, etc. information required to call a CloudAPI.',
'You can then switch between profiles with `triton -p PROFILE`', 'You can then switch between profiles with `triton -p PROFILE`',
'or the SC_PROFILE environment variable.' 'or the TRITON_PROFILE environment variable.'
].join('\n'), ].join('\n'),
/* END JSSTYLED */ /* END JSSTYLED */
helpOpts: { helpOpts: {

View File

@ -24,7 +24,6 @@ var rimraf = require('rimraf');
var semver = require('semver'); var semver = require('semver');
var sshpk = require('sshpk'); var sshpk = require('sshpk');
var mod_url = require('url'); var mod_url = require('url');
var crypto = require('crypto');
var vasync = require('vasync'); var vasync = require('vasync');
var which = require('which'); var which = require('which');
var wordwrap = require('wordwrap')(78); var wordwrap = require('wordwrap')(78);
@ -129,6 +128,7 @@ function setCurrentProfile(opts, cb) {
}); });
} }
/** /**
* Setup the given profile for Docker usage. This means checking the cloudapi * Setup the given profile for Docker usage. This means checking the cloudapi
* has a Docker service (ListServices), finding the user's SSH *private* key, * has a Docker service (ListServices), finding the user's SSH *private* key,
@ -143,21 +143,14 @@ function setCurrentProfile(opts, cb) {
* implicit, we silently skip if ListServices shows no Docker service. * implicit, we silently skip if ListServices shows no Docker service.
* - {Boolean} yes: Optional. Boolean indicating if confirmation prompts * - {Boolean} yes: Optional. Boolean indicating if confirmation prompts
* should be skipped, assuming a "yes" answer. * should be skipped, assuming a "yes" answer.
* - {Number} lifetime: Optional. Number of days to make the Docker
* certificate valid for. Defaults to 3650 (10 years).
*/ */
function profileDockerSetup(opts, cb) { function profileDockerSetup(opts, cb) {
assert.object(opts.cli, 'opts.cli'); assert.object(opts.cli, 'opts.cli');
assert.string(opts.name, 'opts.name'); assert.string(opts.name, 'opts.name');
assert.optionalBool(opts.implicit, 'opts.implicit'); assert.optionalBool(opts.implicit, 'opts.implicit');
assert.optionalBool(opts.yes, 'opts.yes'); assert.optionalBool(opts.yes, 'opts.yes');
assert.optionalNumber(opts.lifetime, 'opts.lifetime');
assert.func(cb, 'cb'); assert.func(cb, 'cb');
/* Default to a 10 year certificate. */
if (!opts.lifetime)
opts.lifetime = 3650;
var cli = opts.cli; var cli = opts.cli;
var tritonapi = cli.tritonapiFromProfileName({profileName: opts.name}); var tritonapi = cli.tritonapiFromProfileName({profileName: opts.name});
@ -172,22 +165,18 @@ function profileDockerSetup(opts, cb) {
function dockerKeyWarning(arg, next) { function dockerKeyWarning(arg, next) {
console.log(wordwrap('WARNING: Docker uses authentication via ' + console.log(wordwrap('WARNING: Docker uses authentication via ' +
'client TLS certificates that do not support encrypted ' + 'client TLS certificates that do not support encrypted ' +
'(passphrase protected) keys or SSH agents.\n')); '(passphrase protected) keys or SSH agents. If you continue, ' +
console.log(wordwrap('If you continue, this profile setup will ' + 'this profile setup will attempt to write a copy of your ' +
'create a fresh private key to be written unencrypted to ' + 'SSH private key formatted as an unencrypted TLS certificate ' +
'disk in "~/.spearhead/docker" for use by the Docker client. ' + 'in "~/.triton/docker" for use by the Docker client.\n'));
'This key will be useable only for Docker.\n'));
if (yes) { if (yes) {
next(); next();
return; return;
} else {
console.log(wordwrap('If you do not specifically want to use ' +
'Docker, you can answer "no" here.\n'));
} }
common.promptYesNo({msg: 'Continue? [y/n] '}, function (answer) { common.promptYesNo({msg: 'Continue? [y/n] '}, function (answer) {
if (answer !== 'y') { if (answer !== 'y') {
console.error('Skipping Docker setup (you can run ' console.error('Skipping Docker setup (you can run '
+ '"spearhead profile docker-setup" later).'); + '"triton profile docker-setup" later).');
next(true); next(true);
} else { } else {
console.log(); console.log();
@ -278,7 +267,7 @@ function profileDockerSetup(opts, cb) {
if (err) { if (err) {
console.log(wordwrap('\nNote: No "docker" was found on ' console.log(wordwrap('\nNote: No "docker" was found on '
+ 'your PATH. It is not needed for this setup, but ' + 'your PATH. It is not needed for this setup, but '
+ 'will be to run docker commands against Spearhead. ' + 'will be to run docker commands against Triton. '
+ 'You can find out how to install it at ' + 'You can find out how to install it at '
+ '<https://docs.docker.com/engine/installation/>.')); + '<https://docs.docker.com/engine/installation/>.'));
} else { } else {
@ -322,143 +311,79 @@ function profileDockerSetup(opts, cb) {
}); });
}, },
function getSigningKey(arg, next) { /*
var kr = new auth.KeyRing(); * We need the private key to format as a client cert. If this profile's
var profileFp = sshpk.parseFingerprint(profile.keyId); * key was found in the SSH agent (and by default it prefers to take
kr.findSigningKeyPair(profileFp, * it from there), then we can't use `tritonapi.keyPair`, because
function unlockAndStash(findErr, keyPair) { * the SSH agent protocol will not allow us access to the private key
* data (by design).
*
* As a fallback we'll look (via KeyRing) for a local copy of the
* private key to use, and then unlock it if necessary.
*/
function getPrivKey(arg, next) {
// If the key pair already works, then use that...
try {
arg.privKey = tritonapi.keyPair.getPrivateKey();
next();
return;
} catch (_) {
// ... else fall through.
}
var kr = new auth.KeyRing();
var profileFp = sshpk.parseFingerprint(tritonapi.profile.keyId);
kr.find(profileFp, function (findErr, keyPairs) {
if (findErr) { if (findErr) {
next(findErr); next(findErr);
return; return;
} }
arg.signKeyPair = keyPair; /*
if (!keyPair.isLocked()) { * If our keyId was found, and with the 'homedir' plugin, then
next(); * we should have access to the private key (modulo unlocking).
return; */
var homedirKeyPair;
for (var i = 0; i < keyPairs.length; i++) {
if (keyPairs[i].plugin === 'homedir') {
homedirKeyPair = keyPairs[i];
break;
}
}
if (homedirKeyPair) {
common.promptPassphraseUnlockKey({
// Fake the `tritonapi` object, only `.keyPair` is used.
tritonapi: {keyPair: homedirKeyPair}
}, function (unlockErr) {
if (unlockErr) {
next(unlockErr);
return;
}
try {
arg.privKey = homedirKeyPair.getPrivateKey();
} catch (homedirErr) {
next(new errors.SetupError(homedirErr, format(
'could not obtain SSH private key for keyId ' +
'"%s" to create Docker certificate',
profile.keyId)));
return;
}
next();
});
} else {
next(new errors.SetupError(format('could not obtain SSH ' +
'private key for keyId "%s" to create Docker ' +
'certificate', profile.keyId)));
} }
common.promptPassphraseUnlockKey({
/* Fake the `tritonapi` object, only `.keyPair` is used. */
tritonapi: { keyPair: keyPair }
}, next);
}); });
}, },
function generateAndSignCert(arg, next) {
var key = arg.signKeyPair;
var pubKey = key.getPublicKey();
/* function genClientCert_dir(arg, next) {
* There isn't a particular reason this has to be ECDSA, but
* Docker supports it, and ECDSA keys are much easier to
* generate from inside node than RSA ones (since sshpk will
* do them for us instead of us shelling out and mucking with
* temporary files).
*/
arg.privKey = sshpk.generatePrivateKey('ecdsa');
var id = sshpk.identityFromDN('CN=' + profile.account);
var parentId = sshpk.identityFromDN('CN=' +
pubKey.fingerprint('md5').toString('base64'));
var serial = crypto.randomBytes(8);
/*
* Backdate the certificate by 5 minutes to account for clock
* sync -- we only allow 5 mins drift in cloudapi generally so
* using the same amount here seems fine.
*/
var validFrom = new Date();
validFrom.setTime(validFrom.getTime() - 300*1000);
var validUntil = new Date();
validUntil.setTime(validFrom.getTime() +
24*3600*1000*opts.lifetime);
/*
* Generate it self-signed for now -- we will clear this
* signature out and replace it with the real one below.
*/
var cert = sshpk.createCertificate(id, arg.privKey, parentId,
arg.privKey, { validFrom: validFrom, validUntil: validUntil,
purposes: ['clientAuth', 'joyentDocker'], serial: serial });
var algo = pubKey.type + '-' + pubKey.defaultHashAlgorithm();
/*
* This code is using private API in sshpk because there is
* no public API as of 1.14.x for async signing of certificates.
*
* If the sshpk version in package.json is updated (even a
* patch bump) this code could break! This will be fixed up
* eventually, but for now we just have to be careful.
*/
var x509 = require('sshpk/lib/formats/x509');
cert.signatures = {};
cert.signatures.x509 = {};
cert.signatures.x509.algo = algo;
var signer = key.createSign({
user: profile.account,
algorithm: algo
});
/*
* The smartdc-auth KeyPair signer produces an object with
* strings on it intended for http-signature instead of just a
* Signature instance (which is what the x509 format module
* expects). We wrap it up here to convert it.
*/
var signerConv = function (buf, ccb) {
signer(buf, function convertSignature(signErr, sigData) {
if (signErr) {
ccb(signErr);
return;
}
var algparts = sigData.algorithm.split('-');
var sig = sshpk.parseSignature(sigData.signature,
algparts[0], 'asn1');
sig.hashAlgorithm = algparts[1];
sig.curve = pubKey.curve;
ccb(null, sig);
});
};
/*
* Sign a "test" string first to double-check the hash algo
* it's going to use. The SSH agent may not support SHA256
* signatures, for example, and we will only find out by
* testing like this.
*/
signer('test', function afterTestSig(testErr, testSigData) {
if (testErr) {
next(new errors.SetupError(testErr, format(
'failed to sign Docker certificate using key ' +
'"%s"', profile.keyId)));
return;
}
cert.signatures.x509.algo = testSigData.algorithm;
x509.signAsync(cert, signerConv,
function afterCertSign(signErr) {
if (signErr) {
next(new errors.SetupError(signErr, format(
'failed to sign Docker certificate using key ' +
'"%s"', profile.keyId)));
return;
}
cert.issuerKey = undefined;
/* Double-check that it came out ok. */
assert.ok(cert.isSignedByKey(pubKey));
arg.cert = cert;
next();
});
});
},
function makeClientCertDir(arg, next) {
arg.dockerCertPath = path.resolve(cli.configDir, arg.dockerCertPath = path.resolve(cli.configDir,
'docker', common.profileSlug(profile)); 'docker', common.profileSlug(profile));
mkdirp(arg.dockerCertPath, next); mkdirp(arg.dockerCertPath, next);
}, },
function writeClientCertKey(arg, next) { function genClientCert_key(arg, next) {
arg.keyPath = path.resolve(arg.dockerCertPath, 'key.pem'); arg.keyPath = path.resolve(arg.dockerCertPath, 'key.pem');
var data = arg.privKey.toBuffer('pkcs1'); var data = arg.privKey.toBuffer('pkcs1');
fs.writeFile(arg.keyPath, data, function (err) { fs.writeFile(arg.keyPath, data, function (err) {
@ -470,9 +395,12 @@ function profileDockerSetup(opts, cb) {
} }
}); });
}, },
function writeClientCert(arg, next) { function genClientCert_cert(arg, next) {
arg.certPath = path.resolve(arg.dockerCertPath, 'cert.pem'); arg.certPath = path.resolve(arg.dockerCertPath, 'cert.pem');
var data = arg.cert.toBuffer('pem');
var id = sshpk.identityFromDN('CN=' + profile.account);
var cert = sshpk.createSelfSignedCertificate(id, arg.privKey);
var data = cert.toBuffer('pem');
fs.writeFile(arg.certPath, data, function (err) { fs.writeFile(arg.certPath, data, function (err) {
if (err) { if (err) {
@ -550,10 +478,10 @@ function profileDockerSetup(opts, cb) {
'Successfully setup profile "%s" to use Docker%s.', 'Successfully setup profile "%s" to use Docker%s.',
'', '',
'To setup environment variables to use the Docker client, run:', 'To setup environment variables to use the Docker client, run:',
' eval "$(spearhead env --docker %s)"', ' eval "$(triton env --docker %s)"',
' docker%s info', ' docker%s info',
'Or you can place the commands in your shell profile, e.g.:', 'Or you can place the commands in your shell profile, e.g.:',
' spearhead env --docker %s >> ~/.profile' ' triton env --docker %s >> ~/.profile'
].join('\n'), ].join('\n'),
profile.name, profile.name,
(arg.dockerVersion ? format(' (v%s)', arg.dockerVersion) : ''), (arg.dockerVersion ? format(' (v%s)', arg.dockerVersion) : ''),

View File

@ -20,7 +20,7 @@ function do_profiles(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_profiles.help = 'A shortcut for "spearhead profile list".\n' + targ.help; do_profiles.help = 'A shortcut for "triton profile list".\n' + targ.help;
do_profiles.synopses = targ.synopses; do_profiles.synopses = targ.synopses;
do_profiles.options = targ.options; do_profiles.options = targ.options;
do_profiles.completionArgtypes = targ.completionArgtypes; do_profiles.completionArgtypes = targ.completionArgtypes;

View File

@ -198,7 +198,7 @@ do_apply.options = [
{ {
names: ['dev-create-keys-and-profiles'], names: ['dev-create-keys-and-profiles'],
type: 'bool', type: 'bool',
help: 'Convenient option to generate keys and Spearhead CLI profiles ' + help: 'Convenient option to generate keys and Triton CLI profiles ' +
'for all users. For experimenting only. See section below.' 'for all users. For experimenting only. See section below.'
} }
]; ];
@ -214,7 +214,7 @@ do_apply.help = [
'{{options}}', '{{options}}',
'If "--file FILE" is not specified, this defaults to using "./rbac.json".', 'If "--file FILE" is not specified, this defaults to using "./rbac.json".',
'The RBAC configuration is loaded from FILE and compared to the live', 'The RBAC configuration is loaded from FILE and compared to the live',
'RBAC state (see `spearhead rbac info`). It then calculates necessary updates,', 'RBAC state (see `triton rbac info`). It then calculates necessary updates,',
'confirms, and applies them.', 'confirms, and applies them.',
'', '',
'Warning: Currently, RBAC state updates can take a few seconds to appear', 'Warning: Currently, RBAC state updates can take a few seconds to appear',
@ -223,10 +223,10 @@ do_apply.help = [
'', '',
'The "--dev-create-keys-and-profiles" option is provided for **experimenting', 'The "--dev-create-keys-and-profiles" option is provided for **experimenting',
'with, developing, or testing** Triton RBAC. It will create a key and setup a ', 'with, developing, or testing** Triton RBAC. It will create a key and setup a ',
'Spearhead CLI profile for each user (named "$currprofile-user-$login"). This ', 'Triton CLI profile for each user (named "$currprofile-user-$login"). This ',
'simplies using the CLI as that user:', 'simplies using the CLI as that user:',
' spearhead -p coal-user-bob create ...', ' triton -p coal-user-bob create ...',
' spearhead -p coal-user-sarah imgs', ' triton -p coal-user-sarah imgs',
'Note that proper production usage of RBAC should have the administrator', 'Note that proper production usage of RBAC should have the administrator',
'never seeing each user\'s private key.', 'never seeing each user\'s private key.',
'', '',

View File

@ -125,8 +125,12 @@ function _addUserKey(opts, cb) {
if (opts.file !== '-') { if (opts.file !== '-') {
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();

View File

@ -291,8 +291,12 @@ function _addPolicy(opts, cb) {
if (opts.file !== '-') { if (opts.file !== '-') {
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 () {
try { try {
data = JSON.parse(stdin); data = JSON.parse(stdin);
} catch (err) { } catch (err) {

View File

@ -287,8 +287,12 @@ function _addRole(opts, cb) {
if (opts.file !== '-') { if (opts.file !== '-') {
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 () {
try { try {
data = JSON.parse(stdin); data = JSON.parse(stdin);
} catch (err) { } catch (err) {

View File

@ -282,8 +282,12 @@ function _addUser(opts, cb) {
if (opts.file !== '-') { if (opts.file !== '-') {
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 () {
try { try {
data = JSON.parse(stdin); data = JSON.parse(stdin);
} catch (err) { } catch (err) {

View File

@ -25,7 +25,7 @@ function RbacCLI(top) {
desc: [ desc: [
'Role-based Access Control (RBAC) commands.', 'Role-based Access Control (RBAC) commands.',
'See <https://docs.joyent.com/public-cloud/rbac> for a general start.', 'See <https://docs.joyent.com/public-cloud/rbac> for a general start.',
'**Warning: `spearhead rbac ...` is experimental, not well tested and in flux.**' '**Warning: `triton rbac ...` is experimental, not well tested and in flux.**'
].join('\n'), ].join('\n'),
/* END JSSTYLED */ /* END JSSTYLED */
helpOpts: { helpOpts: {

View File

@ -20,7 +20,7 @@ function do_reboot(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_reboot.help = 'A shortcut for "spearhead instance reboot".\n' + targ.help; do_reboot.help = 'A shortcut for "triton instance reboot".\n' + targ.help;
do_reboot.synopses = targ.synopses; do_reboot.synopses = targ.synopses;
do_reboot.options = targ.options; do_reboot.options = targ.options;
do_reboot.completionArgtypes = targ.completionArgtypes; do_reboot.completionArgtypes = targ.completionArgtypes;

View File

@ -20,7 +20,7 @@ function do_ssh(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_ssh.help = 'A shortcut for "spearhead instance ssh".\n' + targ.help; do_ssh.help = 'A shortcut for "triton instance ssh".\n' + targ.help;
do_ssh.synopses = targ.synopses; do_ssh.synopses = targ.synopses;
do_ssh.interspersedOptions = targ.interspersedOptions; do_ssh.interspersedOptions = targ.interspersedOptions;
do_ssh.options = targ.options; do_ssh.options = targ.options;

View File

@ -20,7 +20,7 @@ function do_start(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_start.help = 'A shortcut for "spearhead instance start".\n' + targ.help; do_start.help = 'A shortcut for "triton instance start".\n' + targ.help;
do_start.synopses = targ.synopses; do_start.synopses = targ.synopses;
do_start.options = targ.options; do_start.options = targ.options;
do_start.completionArgtypes = targ.completionArgtypes; do_start.completionArgtypes = targ.completionArgtypes;

View File

@ -20,7 +20,7 @@ function do_stop(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_stop.help = 'A shortcut for "spearhead instance stop".\n' + targ.help; do_stop.help = 'A shortcut for "triton instance stop".\n' + targ.help;
do_stop.synopses = targ.synopses; do_stop.synopses = targ.synopses;
do_stop.options = targ.options; do_stop.options = targ.options;
do_stop.completionArgtypes = targ.completionArgtypes; do_stop.completionArgtypes = targ.completionArgtypes;

View File

@ -1,140 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright (c) 2018, Joyent, Inc.
*
* `triton vlan create ...`
*/
var assert = require('assert-plus');
var format = require('util').format;
var jsprim = require('jsprim');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
function do_create(subcmd, opts, args, cb) {
assert.optionalString(opts.name, 'opts.name');
assert.optionalString(opts.description, 'opts.description');
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 === 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.name) {
cb(new errors.UsageError('must provide a --name (-n)'));
return;
}
var createOpts = {
vlan_id: vlanId,
name: opts.name
};
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.createFabricVlan(createOpts, function onCreate(err, vlan) {
if (err) {
cb(err);
return;
}
if (opts.json) {
console.log(JSON.stringify(vlan));
} else {
if (vlan.name) {
console.log('Created vlan %s (%d)', vlan.name,
vlan.vlan_id);
} else {
console.log('Created vlan %d', vlan.vlan_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 VLAN.'
},
{
names: ['description', 'D'],
type: 'string',
helpArg: 'DESC',
help: 'Description of the VLAN.'
},
{
group: 'Other options'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
do_create.synopses = ['{{name}} {{cmd}} [OPTIONS] VLAN'];
do_create.help = [
'Create a VLAN.',
'',
'{{usage}}',
'',
'{{options}}',
'Example:',
' triton vlan create -n "dmz" -D "Demilitarized zone" 73'
].join('\n');
do_create.helpOpts = {
helpCol: 16
};
module.exports = do_create;

View File

@ -1,85 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2017 Joyent, Inc.
*
* `triton vlan 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 VLAN argument(s)'));
return;
}
var cli = this.top;
var vlanIds = args;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
vasync.forEachParallel({
inputs: vlanIds,
func: function deleteOne(id, next) {
cli.tritonapi.deleteFabricVlan({ vlan_id: id },
function onDelete(err) {
if (err) {
next(err);
return;
}
console.log('Deleted vlan %s', id);
next();
});
}
}, cb);
});
}
do_delete.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
}
];
do_delete.synopses = ['{{name}} {{cmd}} VLAN [VLAN ...]'];
do_delete.help = [
'Remove a VLAN.',
'',
'{{usage}}',
'',
'{{options}}',
'Where VLAN is a VLAN id or name.'
].join('\n');
do_delete.aliases = ['rm'];
do_delete.completionArgtypes = ['tritonvlan'];
module.exports = do_delete;

View File

@ -1,88 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2017 Joyent, Inc.
*
* `triton vlan get ...`
*/
var assert = require('assert-plus');
var common = require('../common');
var errors = require('../errors');
function do_get(subcmd, opts, args, cb) {
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length === 0) {
cb(new errors.UsageError('missing VLAN argument'));
return;
} else if (args.length > 1) {
cb(new errors.UsageError('incorrect number of arguments'));
return;
}
var id = args[0];
var cli = this.top;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
cli.tritonapi.getFabricVlan(id, function onGet(err, vlan) {
if (err) {
cb(err);
return;
}
if (opts.json) {
console.log(JSON.stringify(vlan));
} else {
console.log(JSON.stringify(vlan, 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}} VLAN'];
do_get.help = [
'Show a specific VLAN.',
'',
'{{usage}}',
'',
'{{options}}',
'Where VLAN is a VLAN id or name.'
].join('\n');
do_get.completionArgtypes = ['tritonvlan', 'none'];
module.exports = do_get;

View File

@ -1,123 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2018 Joyent, Inc.
*
* `triton vlan list ...`
*/
var assert = require('assert-plus');
var tabula = require('tabula');
var common = require('../common');
var errors = require('../errors');
var COLUMNS_DEFAULT = 'vlan_id,name,description';
var SORT_DEFAULT = 'vlan_id';
var VALID_FILTERS = ['vlan_id', 'name', 'description'];
function do_list(subcmd, opts, args, cb) {
assert.object(opts, 'opts');
assert.array(args, 'args');
assert.func(cb, 'cb');
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
try {
var filters = common.objFromKeyValueArgs(args, {
validKeys: VALID_FILTERS,
disableDotted: true
});
} catch (e) {
cb(e);
return;
}
if (filters.vlan_id !== undefined) {
filters.vlan_id = +filters.vlan_id;
}
var cli = this.top;
common.cliSetupTritonApi({cli: cli}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
return;
}
var cloudapi = cli.tritonapi.cloudapi;
cloudapi.listFabricVlans({}, function onList(err, vlans) {
if (err) {
cb(err);
return;
}
// do filtering
Object.keys(filters).forEach(function doFilter(key) {
var val = filters[key];
vlans = vlans.filter(function (vlan) {
return vlan[key] === val;
});
});
if (opts.json) {
common.jsonStream(vlans);
} else {
var columns = COLUMNS_DEFAULT;
if (opts.o) {
columns = opts.o;
}
columns = columns.split(',');
var sort = opts.s.split(',');
tabula(vlans, {
skipHeader: opts.H,
columns: columns,
sort: sort
});
}
cb();
});
});
}
do_list.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
}
].concat(common.getCliTableOptions({
sortDefault: SORT_DEFAULT
}));
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] [FILTERS]'];
do_list.help = [
'List VLANs.',
'',
'{{usage}}',
'',
'Filters:',
' FIELD=<integer> Number filter. Supported fields: vlan_id',
' FIELD=<string> String filter. Supported fields: name, description',
'',
'{{options}}',
'Filters are applied client-side (i.e. done by the triton command itself).'
].join('\n');
do_list.aliases = ['ls'];
module.exports = do_list;

View File

@ -1,52 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2017 Joyent, Inc.
*
* `triton vlan networks ...`
*/
var errors = require('../errors');
function do_networks(subcmd, opts, args, cb) {
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;
}
opts.vlan_id = args[0];
this.top.handlerFromSubcmd('network').dispatch({
subcmd: 'list',
opts: opts,
args: []
}, cb);
}
do_networks.synopses = ['{{name}} {{cmd}} [OPTIONS] VLAN'];
do_networks.help = [
'Show all networks on a VLAN.',
'',
'{{usage}}',
'',
'{{options}}',
'Where VLAN is a VLAN id or name.'
].join('\n');
do_networks.options = require('../do_network/do_list').options;
module.exports = do_networks;

View File

@ -1,201 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2018 Joyent, Inc.
*
* `triton vlan update ...`
*/
var assert = require('assert-plus');
var format = require('util').format;
var fs = require('fs');
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
var UPDATE_VLAN_FIELDS
= require('../cloudapi2').CloudApi.prototype.UPDATE_VLAN_FIELDS;
function do_update(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
var log = this.log;
var tritonapi = this.top.tritonapi;
if (args.length === 0) {
cb(new errors.UsageError('missing VLAN argument'));
return;
}
var id = args.shift();
vasync.pipeline({arg: {}, funcs: [
function gatherDataArgs(ctx, next) {
if (opts.file) {
next();
return;
}
try {
ctx.data = common.objFromKeyValueArgs(args, {
disableDotted: true,
typeHintFromKey: UPDATE_VLAN_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 vlan update in "%s": %s',
opts.file, err)));
return;
}
next();
},
function gatherDataStdin(ctx, next) {
if (opts.file !== '-') {
next();
return;
}
var stdin = '';
process.stdin.resume();
process.stdin.on('data', function (chunk) {
stdin += chunk;
});
process.stdin.on('error', console.error);
process.stdin.on('end', function () {
try {
ctx.data = JSON.parse(stdin);
} catch (err) {
log.trace({stdin: stdin},
'invalid VLAN update JSON on stdin');
next(new errors.TritonError(format(
'invalid JSON for VLAN update on stdin: %s',
err)));
return;
}
next();
});
},
function validateIt(ctx, next) {
assert.object(ctx.data, 'ctx.data');
var keys = Object.keys(ctx.data);
if (keys.length === 0) {
console.log('No fields given for VLAN update');
next();
return;
}
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = ctx.data[key];
var type = UPDATE_VLAN_FIELDS[key];
if (!type) {
next(new errors.UsageError(format('unknown or ' +
'unupdateable field: %s (updateable fields are: %s)',
key,
Object.keys(UPDATE_VLAN_FIELDS).sort().join(', '))));
return;
}
if (typeof (value) !== type) {
next(new errors.UsageError(format('field "%s" must be ' +
'of type "%s", but got a value of type "%s"', key,
type, typeof (value))));
return;
}
}
next();
},
function updateAway(ctx, next) {
var data = ctx.data;
data.vlan_id = id;
tritonapi.updateFabricVlan(data, function onUpdate(err) {
if (err) {
next(err);
return;
}
delete data.vlan_id;
console.log('Updated vlan %s (fields: %s)', id,
Object.keys(data).join(', '));
next();
});
}
]}, cb);
}
do_update.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['file', 'f'],
type: 'string',
helpArg: 'JSON-FILE',
help: 'A file holding a JSON file of updates, or "-" to read ' +
'JSON from stdin.'
}
];
do_update.synopses = [
'{{name}} {{cmd}} VLAN [FIELD=VALUE ...]',
'{{name}} {{cmd}} -f JSON-FILE VLAN'
];
do_update.help = [
'Update a VLAN.',
'',
'{{usage}}',
'',
'{{options}}',
'Updateable fields:',
' ' + Object.keys(UPDATE_VLAN_FIELDS).sort().map(function (f) {
return f + ' (' + UPDATE_VLAN_FIELDS[f] + ')';
}).join(', '),
'',
'Where VLAN is a VLAN id or name.'
].join('\n');
do_update.completionArgtypes = ['tritonvlan', 'tritonupdatevlanfield'];
module.exports = do_update;

View File

@ -1,55 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2017 Joyent, Inc.
*
* `triton vlan ...`
*/
var Cmdln = require('cmdln').Cmdln;
var util = require('util');
// ---- CLI class
function VlanCLI(top) {
this.top = top;
Cmdln.call(this, {
name: top.name + ' vlan',
desc: 'List and manage Triton fabric VLANs.',
helpSubcmds: [
'help',
'list',
'get',
'create',
'update',
'delete',
{ group: '' },
'networks'
],
helpOpts: {
minHelpCol: 23
}
});
}
util.inherits(VlanCLI, Cmdln);
VlanCLI.prototype.init = function init(opts, args, cb) {
this.log = this.top.log;
Cmdln.prototype.init.apply(this, arguments);
};
VlanCLI.prototype.do_list = require('./do_list');
VlanCLI.prototype.do_create = require('./do_create');
VlanCLI.prototype.do_get = require('./do_get');
VlanCLI.prototype.do_update = require('./do_update');
VlanCLI.prototype.do_delete = require('./do_delete');
VlanCLI.prototype.do_networks = require('./do_networks');
module.exports = VlanCLI;

View File

@ -63,25 +63,6 @@ function do_create(subcmd, opts, args, cb) {
self.top.tritonapi.createVolume(createVolumeParams, self.top.tritonapi.createVolume(createVolumeParams,
function onRes(volCreateErr, volume) { function onRes(volCreateErr, volume) {
/*
* VolumeSizeNotAvailable errors include additional
* information in their message
* about available volume sizes using units that are
* different than the units node-triton users have to use
* when specifying volume sizes on the command line
* (mebibytes vs gibibytes).
* As a result, we override this type of error to provide a
* simpler message that is less confusing, and users can use
* the "triton volume sizes" command to find out which
* sizes are available.
*/
if (volCreateErr &&
volCreateErr.name === 'VolumeSizeNotAvailableError') {
next(new Error('volume size not available, use ' +
'spearhead volume sizes command for available sizes'));
return;
}
if (!volCreateErr && !opts.json) { if (!volCreateErr && !opts.json) {
console.log('Creating volume %s (%s)', volume.name, console.log('Creating volume %s (%s)', volume.name,
volume.id); volume.id);
@ -163,11 +144,11 @@ do_create.options = [
names: ['size', 's'], names: ['size', 's'],
type: 'string', type: 'string',
helpArg: 'SIZE', helpArg: 'SIZE',
help: 'The size of the volume to create, in gibibytes, in the form ' + help: 'The size of the volume to create, in the form ' +
'`<integer>G`, e.g. `20G`. <integer> must be > 0. If a size is ' + '`<integer><unit>`, e.g. `20G`. <integer> must be > 0. Supported ' +
'not specified, the newly created volume will have a default ' + 'units are `G` or `g` for gibibytes and `M` or `m` for mebibytes.' +
'size corresponding to the smallest size available. Available ' + ' If a size is not specified, the newly created volume will have ' +
'volume sizes can be listed via the "volume sizes" sub-command.', 'a default size corresponding to the smallest size available.',
completionType: 'tritonvolumesize' completionType: 'tritonvolumesize'
}, },
{ {

View File

@ -96,13 +96,14 @@ function do_list(subcmd, opts, args, callback) {
var created; var created;
var volume = volumes[i]; var volume = volumes[i];
created = new Date(volume.created); created = new Date(volume.create_timestamp);
if (volume.filesystem_path !== undefined) { if (volume.filesystem_path !== undefined) {
volume.resource = volume.filesystem_path; volume.resource = volume.filesystem_path;
} }
volume.shortid = volume.id.split('-', 1)[0]; volume.shortid = volume.id.split('-', 1)[0];
volume.created = volume.create_timestamp;
volume.age = common.longAgo(created, now); volume.age = common.longAgo(created, now);
} }

View File

@ -1,90 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2017 Joyent, Inc.
*
* `triton volume sizes ...`
*/
var assert = require('assert-plus');
var format = require('util').format;
var jsprim = require('jsprim');
var tabula = require('tabula');
var VError = require('verror');
var common = require('../common');
var errors = require('../errors');
var COLUMNS = ['type', {name: 'SIZE', lookup: 'sizeHuman', align: 'right'}];
var MIBS_IN_GIB = 1024;
// sort default with -s
var sortDefault = 'size';
function do_sizes(subcmd, opts, args, callback) {
var self = this;
if (opts.help) {
this.do_help('help', {}, [subcmd], callback);
return;
}
var sort = opts.s.split(',');
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
callback(setupErr);
}
self.top.tritonapi.cloudapi.listVolumeSizes(
function onRes(listVolSizesErr, volumeSizes, res) {
if (listVolSizesErr) {
return callback(listVolSizesErr);
}
if (opts.json) {
common.jsonStream(volumeSizes);
} else {
volumeSizes =
volumeSizes.map(function renderVolSize(volumeSize) {
volumeSize.sizeHuman =
volumeSize.size / MIBS_IN_GIB + 'G';
return volumeSize;
});
tabula(volumeSizes, {
skipHeader: opts.H,
columns: COLUMNS,
sort: sort
});
}
callback();
});
});
}
do_sizes.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
}
].concat(common.getCliTableOptions({
sortDefault: sortDefault
}));
do_sizes.synopses = ['{{name}} {{cmd}} [OPTIONS]'];
do_sizes.help = [
'List volume sizes.',
'',
'{{usage}}',
'',
'{{options}}'
].join('\n');
module.exports = do_sizes;

View File

@ -19,7 +19,7 @@ function VolumeCLI(top) {
name: top.name + ' volume', name: top.name + ' volume',
/* BEGIN JSSTYLED */ /* BEGIN JSSTYLED */
desc: [ desc: [
'List and manage Spearhead volumes.' 'List and manage Triton volumes.'
].join('\n'), ].join('\n'),
/* END JSSTYLED */ /* END JSSTYLED */
helpOpts: { helpOpts: {
@ -30,8 +30,7 @@ function VolumeCLI(top) {
'list', 'list',
'get', 'get',
'create', 'create',
'delete', 'delete'
'sizes'
] ]
}); });
} }
@ -46,7 +45,6 @@ VolumeCLI.prototype.do_list = require('./do_list');
VolumeCLI.prototype.do_get = require('./do_get'); VolumeCLI.prototype.do_get = require('./do_get');
VolumeCLI.prototype.do_create = require('./do_create'); VolumeCLI.prototype.do_create = require('./do_create');
VolumeCLI.prototype.do_delete = require('./do_delete'); VolumeCLI.prototype.do_delete = require('./do_delete');
VolumeCLI.prototype.do_sizes = require('./do_sizes');
VolumeCLI.aliases = ['vol']; VolumeCLI.aliases = ['vol'];

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