├── .gitignore ├── .travis.yml ├── LICENSE.md ├── Makefile ├── README.md ├── bin └── scw ├── examples ├── create-image-from-s3.sh ├── create-image-livecd.sh ├── create-image-testing-server.sh ├── image-quick-checks.sh └── kernel-quick-checks.sh ├── lib ├── program.js └── utils.js ├── package.json └── test └── all.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | tmp/ 31 | 32 | # nar 33 | .nar/ 34 | *.nar 35 | 36 | bin/scw[.-]* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - "0.12" 7 | - "0.11" 8 | - "0.10" 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | =============== 3 | 4 | Copyright (c) **2014-2015 Scaleway ([@scaleway](https://twitter.com/scaleway))** 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: binaries 2 | 3 | 4 | binaries: node_modules 5 | # npm install -g enclose 6 | enclose -x -o ./bin/scw-$(shell uname -s)-$(shell uname -m) ./bin/scw 7 | 8 | 9 | node_modules: 10 | npm install 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-scaleway (known as Scaleway CLI) 2 | 3 | ## The official scaleway-cli was rewritten in Golang ([here](https://github.com/scaleway/scaleway-cli)), this project is now a node.js client to access the Scaleway API. 4 | 5 | [![Build Status (Travis)](https://img.shields.io/travis/moul/scaleway-cli-node.svg)](https://travis-ci.org/moul/scaleway-cli-node) 6 | [![Dependency Status](https://img.shields.io/david/moul/scaleway-cli-node.svg)](https://david-dm.org/moul/scaleway-cli-node) 7 | [![](https://img.shields.io/npm/dm/scaleway-cli.svg)](https://npmjs.org/package/scaleway-cli) 8 | [![](https://img.shields.io/npm/v/scaleway-cli.svg)](https://npmjs.org/package/scaleway-cli) 9 | [![](https://img.shields.io/npm/l/scaleway-cli.svg)](https://npmjs.org/package/scaleway-cli) 10 | 11 | Interact with Scaleway API from the command line. 12 | 13 | Uses [moul/node-scaleway](https://github.com/moul/node-scaleway) SDK. 14 | 15 | 16 | ## Usage 17 | 18 | Usage inspired by [Docker CLI](https://docs.docker.com/reference/commandline/cli/) 19 | 20 | ```console 21 | $ scw 22 | 23 | Usage: scw [options] [command] 24 | 25 | 26 | Commands: 27 | 28 | attach [options] attach (serial console) to a running server 29 | commit [options] [name] create a new snapshot from a server's volume 30 | create [options] create a new server but do not start it 31 | events get real time events from the API 32 | exec [options] [args...] run a command in a running server 33 | history [options] show the history of an image 34 | images [options] list images 35 | info display system-wide information 36 | inspect [options] return low-level information on a server or image 37 | kill kill a running server 38 | login [options] login to the API 39 | logout log out from the API 40 | ps [options] list servers 41 | restart restart a running server 42 | rm remove one or more servers 43 | rmi remove one or more images 44 | start [options] start a stopped server 45 | stop [options] stop a running server 46 | tag tag an image into a repository 47 | version show the version information 48 | wait block until a server stops 49 | 50 | Options: 51 | 52 | -h, --help output usage information 53 | -V, --version output the version number 54 | --api-endpoint set the API endpoint 55 | --dry-run do not execute actions 56 | -D, --debug enable debug mode 57 | ``` 58 | 59 | 60 | ## Examples 61 | 62 | Create a server with Ubuntu Trusty image and 3.2.34 bootscript 63 | 64 | ```console 65 | $ scw create trusty --bootscript=3.2.34 66 | df271f73-60ce-47fd-bd7b-37b5f698d8b2 67 | ``` 68 | 69 | 70 | Create a server with Fedora 21 image 71 | 72 | ```console 73 | $ scw create 1f164079 74 | 7313af22-62bf-4df1-9dc2-c4ffb4cb2d83 75 | ``` 76 | 77 | 78 | Create a server with an empty disc of 20G and rescue bootscript 79 | 80 | ```console 81 | $ scw create 20G --bootscript=rescue 82 | 5cf8058e-a0df-4fc3-a772-8d44e6daf582 83 | ``` 84 | 85 | 86 | Run a stopped server 87 | 88 | ```console 89 | $ scw start 7313af22 90 | 7313af22-62bf-4df1-9dc2-c4ffb4cb2d83 91 | ``` 92 | 93 | 94 | Run a stopped server and wait for SSH to be ready 95 | 96 | ```console 97 | $ scw start --wait myserver 98 | myserver 99 | $ scw exec myserver /bin/bash 100 | [root@noname ~]# 101 | ``` 102 | 103 | Run a stopped server and wait for SSH to be ready (inline version) 104 | 105 | ```console 106 | $ scw exec $(scw start --wait myserver) /bin/bash 107 | [root@noname ~]# 108 | ``` 109 | 110 | 111 | Create, start and ssh to a new server (inline version) 112 | 113 | ```console 114 | $ scw exec $(scw start --wait $(scw create ubuntu-trusty)) /bin/bash 115 | [root@noname ~]# 116 | ``` 117 | 118 | or 119 | 120 | ```console 121 | $ scw exec --wait $(scw start $(scw create ubuntu-trusty)) /bin/bash 122 | [root@noname ~]# 123 | ``` 124 | 125 | 126 | Wait for a server to be available, then execute a command 127 | 128 | ```console 129 | $ scw exec --wait myserver /bin/bash 130 | [root@noname ~]# 131 | ``` 132 | 133 | Run a command in background 134 | 135 | ```console 136 | $ scw exec alpine tmux new -d "sleep 10" 137 | ``` 138 | 139 | Run a stopped server and wait for SSH to be ready with: 140 | 141 | - a timeout of 120 seconds for kernel to start 142 | - a timeout of 60 seconds for SSH to be ready 143 | - a global timeout of 150 seconds 144 | 145 | ```console 146 | $ scw start --wait --boot-timeout=120 --ssh-timeout=60 --timeout=150 myserver 147 | global execution... failed: Operation timed out. 148 | ``` 149 | 150 | 151 | Wait for a server to be in 'stopped' state 152 | 153 | ```console 154 | $ scw wait 7313af22 155 | [...] some seconds later 156 | 0 157 | ``` 158 | 159 | 160 | Attach to server serial port 161 | 162 | ```console 163 | $ scw attach 7313af22 164 | [RET] 165 | Ubuntu Vivid Vervet (development branch) nfs-server ttyS0 166 | my-server login: 167 | ^C 168 | $ 169 | ``` 170 | 171 | 172 | Create a server with Fedora 21 image and start it 173 | 174 | ```console 175 | $ scw start `scw create 1f164079` 176 | 5cf8058e-a0df-4fc3-a772-8d44e6daf582 177 | ``` 178 | 179 | 180 | Execute a 'ls -la' on a server (via SSH) 181 | 182 | ```console 183 | $ scw exec myserver -- ls -la 184 | total 40 185 | drwx------. 4 root root 4096 Mar 26 05:56 . 186 | drwxr-xr-x. 18 root root 4096 Mar 26 05:56 .. 187 | -rw-r--r--. 1 root root 18 Jun 8 2014 .bash_logout 188 | -rw-r--r--. 1 root root 176 Jun 8 2014 .bash_profile 189 | -rw-r--r--. 1 root root 176 Jun 8 2014 .bashrc 190 | -rw-r--r--. 1 root root 100 Jun 8 2014 .cshrc 191 | drwxr-----. 3 root root 4096 Mar 16 06:31 .pki 192 | -rw-rw-r--. 1 root root 1240 Mar 12 08:16 .s3cfg.sample 193 | drwx------. 2 root root 4096 Mar 26 05:56 .ssh 194 | -rw-r--r--. 1 root root 129 Jun 8 2014 .tcshrc 195 | ``` 196 | 197 | 198 | Run a shell on a server (via SSH) 199 | 200 | ```console 201 | $ scw exec 5cf8058e /bin/bash 202 | [root@noname ~]# 203 | ``` 204 | 205 | 206 | List public images and my images 207 | 208 | ```console 209 | $ scw images 210 | REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE 211 | user/Alpine_Linux_3_1 latest 854eef72 10 days ago 50 GB 212 | Debian_Wheezy_7_8 latest cd66fa55 2 months ago 20 GB 213 | Ubuntu_Utopic_14_10 latest 1a702a4e 4 months ago 20 GB 214 | ... 215 | ``` 216 | 217 | 218 | List public images, my images and my snapshots 219 | 220 | ```console 221 | $ scw images -a 222 | REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE 223 | noname-snapshot 54df92d1 a minute ago 50 GB 224 | cool-snapshot 0dbbc64c 11 hours ago 20 GB 225 | user/Alpine_Linux_3_1 latest 854eef72 10 days ago 50 GB 226 | Debian_Wheezy_7_8 latest cd66fa55 2 months ago 20 GB 227 | Ubuntu_Utopic_14_10 latest 1a702a4e 4 months ago 20 GB 228 | ``` 229 | 230 | 231 | List running servers 232 | 233 | ```console 234 | $ scw ps 235 | SERVER ID IMAGE COMMAND CREATED STATUS PORTS NAME 236 | 7313af22 user/Alpine_Linux_3_1 13 minutes ago running noname 237 | 32070fa4 Ubuntu_Utopic_14_10 36 minutes ago running labs-8fe556 238 | ``` 239 | 240 | 241 | List all servers 242 | 243 | ```console 244 | $ scw ps -a 245 | SERVER ID IMAGE COMMAND CREATED STATUS PORTS NAME 246 | 7313af22 user/Alpine_Linux_3_1 13 minutes ago running noname 247 | 32070fa4 Ubuntu_Utopic_14_10 36 minutes ago running labs-8fe556 248 | 7fc76a15 Ubuntu_Utopic_14_10 11 hours ago stopped backup 249 | ``` 250 | 251 | 252 | Stop a running server 253 | 254 | ```console 255 | $ scw stop 5cf8058e 256 | 5cf8058e 257 | ``` 258 | 259 | 260 | Stop multiple running servers 261 | 262 | ```console 263 | $ scw stop myserver myotherserver 264 | 901d082d-9155-4046-a49d-94355344246b 265 | a0320ec6-141f-4e99-bf33-9e1a9de34171 266 | ``` 267 | 268 | 269 | Terminate a running server 270 | 271 | ```console 272 | $ scw stop -t myserver 273 | 901d082d-9155-4046-a49d-94355344246b 274 | ``` 275 | 276 | 277 | Stop all running servers matching 'mysql' 278 | 279 | ```console 280 | $ scw stop $(scw ps | grep mysql | awk '{print $1}') 281 | 901d082d-9155-4046-a49d-94355344246b 282 | a0320ec6-141f-4e99-bf33-9e1a9de34171 283 | 36756e6e-3146-4b89-8248-abb060fc5b61 284 | ``` 285 | 286 | 287 | Create a snapshot of the root volume of a server 288 | 289 | ```console 290 | $ scw commit 5cf8058e 291 | 54df92d1 292 | ``` 293 | 294 | 295 | Delete a stopped server 296 | 297 | ```console 298 | $ scw rm 5cf8 299 | 5cf8082d-9155-4046-a49d-94355344246b 300 | ``` 301 | 302 | 303 | Delete multiple stopped servers 304 | 305 | ```console 306 | $ scw rm myserver myotherserver 307 | 901d082d-9155-4046-a49d-94355344246b 308 | a0320ec6-141f-4e99-bf33-9e1a9de34171 309 | ``` 310 | 311 | 312 | Delete all stopped servers matching 'mysql' 313 | 314 | ```console 315 | $ scw rm $(scw ps -a | grep mysql | awk '{print $1}') 316 | 901d082d-9155-4046-a49d-94355344246b 317 | a0320ec6-141f-4e99-bf33-9e1a9de34171 318 | 36756e6e-3146-4b89-8248-abb060fc5b61 319 | ``` 320 | 321 | 322 | Create a snapshot of nbd1 323 | 324 | ```console 325 | $ scw commit 5cf8058e -v 1 326 | f1851f99 327 | ``` 328 | 329 | 330 | Create an image based on a snapshot 331 | 332 | ```console 333 | $ scw tag 87f4526b my_image 334 | 46689419 335 | ``` 336 | 337 | 338 | Delete an image 339 | 340 | ```console 341 | $ scw rmi 46689419 342 | ``` 343 | 344 | 345 | Send a 'halt' command via SSH 346 | 347 | ```console 348 | $ scw kill 5cf8058e 349 | 5cf8058e 350 | ``` 351 | 352 | 353 | Inspect a server 354 | 355 | ```console 356 | $ scw inspect 90074de6 357 | [ 358 | { 359 | "server": { 360 | "dynamic_ip_required": true, 361 | "name": "My server", 362 | "modification_date": "2015-03-26T09:01:07.691774+00:00", 363 | "tags": [ 364 | "web", 365 | "production" 366 | ], 367 | "state_detail": "booted", 368 | "public_ip": { 369 | "dynamic": true, 370 | "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", 371 | "address": "212.47.xxx.yyy" 372 | }, 373 | "state": "running", 374 | } 375 | ] 376 | ``` 377 | 378 | 379 | Show public ip address of a server 380 | 381 | ```console 382 | $ scw inspect 90074de6 -f '.server.public_ip.address' 383 | 212.47.xxx.yyy 384 | ``` 385 | 386 | 387 | ## Advanced commands 388 | 389 | We added some non-docker inspired commands (hidden in the usage) 390 | 391 | #### _patch 392 | 393 | Usage: 394 | 395 | ```console 396 | $ scw _patch item field1=value1 field2=value2 397 | ``` 398 | 399 | Example: 400 | 401 | ```console 402 | $ scw _patch myserver state_detail=booted 403 | - state_detail: booting kernel => booted 404 | myserver 405 | ``` 406 | 407 | 408 | ## Workflows 409 | 410 | 411 | For more examples, see [./examples/](https://github.com/moul/scaleway-cli-node/tree/master/examples) directory 412 | 413 | ```console 414 | # create a server with a nbd1 volume of 50G and rescue bootscript 415 | $ SERVER=$(scw create trusty --bootscript=rescue --volume=50000000000 --wait) 416 | # print the ip address of the server 417 | $ echo "Your server is ready and is available at: $(scw inspect ${SERVER} -f .server.public_ip.address)" 418 | ``` 419 | 420 | 421 | ## Debug 422 | 423 | `scaleway-cli` uses the [debug](https://www.npmjs.com/package/debug) package. 424 | 425 | To enable debug you can use the environment variable `DEBUG=` as : 426 | 427 | - `DEBUG='*' scw ...` to see debug for `scaleway-cli` and all dependencies 428 | - `DEBUG='scaleway-cli:*' scw ...` to see debug for `scaleway-cli` 429 | - `DEBUG='node-scaleway:*' scw ...` to see debug for `node-scaleway` 430 | 431 | ```console 432 | $ DEBUG='*' scw images 433 | node-scaleway:lib GET https://api.cloud.online.net/images? +0ms { method: 'GET', 434 | url: 'https://api.cloud.online.net/images?', 435 | headers: 436 | { Accept: 'application/json', 437 | 'User-Agent': 'node-scaleway', 438 | 'X-Auth-Token': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' }, 439 | resolveWithFullResponse: true, 440 | json: true } 441 | REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE 442 | Fedora_21_Twenty-one latest 1f164079 10 days ago 50 GB 443 | user/Archlinux_latest latest 1197ca91 10 days ago 50 GB 444 | ... 445 | scaleway-cli:utils saveEntities: removed 15 items +0ms 446 | scaleway-cli:utils saveEntities: inserted 15 items +4ms 447 | ``` 448 | 449 | 450 | ## Install 451 | 452 | 1. Install `Node.js` and `npm` (https://nodejs.org/download/) 453 | 2. Install `scaleway-cli`: `$ npm install -g scaleway-cli` 454 | 3. Setup token and organization: `$ scw login --token=XXXXX --organization=YYYYY` 455 | 4. Profit... `$ scw ps -a` 456 | 457 | 458 | ## License 459 | 460 | [MIT](https://github.com/moul/scaleway-cli-node/blob/master/LICENSE.md) 461 | -------------------------------------------------------------------------------- /bin/scw: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var program = require('..'); 4 | 5 | program.run(); 6 | -------------------------------------------------------------------------------- /examples/create-image-from-s3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | URL="${1}" 5 | 6 | if [ -z "${1}" ]; then 7 | echo "usage: $(basename ${0}) " 8 | echo "" 9 | echo "examples:" 10 | echo " - $(basename ${0}) http://test-images.fr-1.storage.online.net/scw-distrib-ubuntu-trusty.tar" 11 | echo " - VOLUME=20GB $(basename ${0}) http://test-images.fr-1.storage.online.net/scw-distrib-ubuntu-trusty.tar" 12 | exit 1 13 | fi 14 | 15 | # FIXME: add usage 16 | 17 | set -e 18 | 19 | NAME=$(basename "${URL}") 20 | NAME=${NAME%.*}-$(date +%Y-%m-%d_%H:%M) 21 | VOLUME_SIZE=${VOLUME_SIZE:-50GB} 22 | 23 | 24 | echo "[+] URL of the tarball: ${URL}" 25 | echo "[+] Target name: ${NAME}" 26 | 27 | 28 | echo "[+] Creating new server in rescue mode with a secondary volume..." 29 | SERVER=$(scw create --bootscript=rescue --volume="${VOLUME_SIZE}" --name="image-writer-${NAME}" 1GB) 30 | echo "[+] Server created: ${SERVER}" 31 | 32 | 33 | echo "[+] Booting..." 34 | scw start --wait --timeout=600 "${SERVER}" >/dev/null 35 | #IP=$(scw inspect -f .server.public_ip.address "${SERVER}") 36 | #echo "[+] SSH is ready (${IP})" 37 | echo "[+] Server is booted" 38 | scw exec "${SERVER}" 'uname -a' 39 | 40 | 41 | echo "[+] Formating and mounting /dev/nbd1..." 42 | scw exec "${SERVER}" 'service xnbd-common stop && service xnbd-common start && mkfs.ext4 /dev/nbd1 && mount /dev/nbd1 /mnt' 43 | echo "[+] /dev/nbd1 formatted in ext4 and mounted on /mnt" 44 | 45 | 46 | echo "[+] Download tarball from S3 and write it to /dev/nbd1" 47 | scw exec "${SERVER}" "wget -qO - ${URL} | tar -C /mnt/ -xf - && sync" 48 | echo "[+] Tarball extracted on /dev/nbd1" 49 | 50 | 51 | echo "[+] Stopping the server" 52 | scw stop "${SERVER}" 53 | scw wait "${SERVER}" 54 | echo "[+] Server stopped" 55 | 56 | 57 | echo "[+] Creating a snapshot of nbd1" 58 | SNAPSHOT=$(scw commit --volume=1 "${SERVER}" "${NAME}") 59 | echo "[+] Snapshot ${SNAPSHOT} created" 60 | 61 | 62 | echo "[+] Creating an image based of the snapshot" 63 | IMAGE=$(scw tag "${SNAPSHOT}" "${NAME}") 64 | echo "[+] Image created: ${IMAGE}" 65 | 66 | 67 | echo "[+] Deleting temporary server" 68 | scw rm "${SERVER}" 69 | echo "[+] Server deleted" 70 | -------------------------------------------------------------------------------- /examples/create-image-livecd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | URL="${1}" 5 | 6 | if [ -z "${1}" ]; then 7 | echo "usage: $(basename ${0}) " 8 | echo "" 9 | echo "examples:" 10 | echo " - $(basename ${0}) http://test-images.fr-1.storage.online.net/scw-distrib-ubuntu-trusty.tar" 11 | exit 1 12 | fi 13 | 14 | # FIXME: add usage 15 | 16 | NAME=$(basename "${URL}") 17 | NAME=${NAME%.*} 18 | 19 | echo "[+] URL of the tarball: ${URL}" >&2 20 | echo "[+] Target name: ${NAME}" >&2 21 | 22 | echo "[+] Creating new server in live mode..." >&2 23 | SERVER=$( 24 | scw create \ 25 | --bootscript=3.2.34 \ 26 | --name="[live] $NAME" \ 27 | --env="boot=live rescue_image=${URL}" \ 28 | 50GB 29 | ) 30 | echo "[+] Server created: ${SERVER}" >&2 31 | 32 | echo "[+] Booting..." >&2 33 | scw start "${SERVER}" >/dev/null 34 | echo "[+] Done" >&2 35 | 36 | echo "${SERVER}" 37 | -------------------------------------------------------------------------------- /examples/create-image-testing-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | URL="${1}" 5 | 6 | if [ -z "${1}" ]; then 7 | echo "usage: $(basename ${0}) " 8 | echo "" 9 | echo "examples:" 10 | echo " - $(basename ${0}) http://test-images.fr-1.storage.online.net/scw-distrib-ubuntu-trusty.tar" 11 | echo " - VOLUME=20GB $(basename ${0}) http://test-images.fr-1.storage.online.net/scw-distrib-ubuntu-trusty.tar" 12 | exit 1 13 | fi 14 | 15 | # FIXME: add usage 16 | 17 | NAME=$(basename "${URL}") 18 | NAME=${NAME%.*} 19 | 20 | 21 | echo "[+] URL of the tarball: ${URL}" 22 | echo "[+] Target name: ${NAME}" 23 | 24 | echo "[+] Creating new server in rescue mode..." 25 | SERVER=$( 26 | scw create \ 27 | --bootscript=rescue \ 28 | --name="[testing] $NAME" \ 29 | --env="boot=rescue rescue_image=${URL}" \ 30 | 1GB 31 | ) 32 | echo "[+] Server created: ${SERVER}" 33 | 34 | echo "[+] Booting..." 35 | scw start "${SERVER}" >/dev/null 36 | echo "[+] Done" 37 | -------------------------------------------------------------------------------- /examples/image-quick-checks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # I'm a script used to check the state of images. 4 | 5 | # parameters 6 | if [ $# -ne 1 ]; then 7 | echo "usage: $0 image-id" 8 | exit 1 9 | fi 10 | 11 | IMAGE_ID=$1 12 | NB_INSTANCES=16 13 | WORKDIR=$(mktemp -d 2>/dev/null || mktemp -d -t /tmp) 14 | INSTANCE_NAME='check-image' 15 | 16 | # destroy all existing servers matching name 17 | function cleanup { 18 | echo >&2 '[+] cleaning up existing servers...' 19 | for uuid in $(scw ps -a --no-trunc | tail -n +2 | awk '// { print $1, $NF; }' | grep "^.* ${INSTANCE_NAME}\-" | awk '// { print $1; }'); do 20 | scw stop -t $uuid 21 | done 22 | 23 | touch $WORKDIR/uuids.txt 24 | touch $WORKDIR/ips.txt 25 | } 26 | 27 | # create $NB_INSTANCES servers using the image 28 | function boot { 29 | echo >&2 "[+] creating $NB_INSTANCES servers..." 30 | for i in $(eval echo {1..$NB_INSTANCES}); do 31 | scw create --volume 1G --name "$INSTANCE_NAME-$i" $IMAGE_ID >> $WORKDIR/uuids.txt 32 | done 33 | cat $WORKDIR/uuids.txt 34 | 35 | echo >&2 "[+] booting $NB_INSTANCES servers..." 36 | for uuid in $(cat $WORKDIR/uuids.txt); do 37 | scw start -s --boot-timeout=120 --ssh-timeout=600 $uuid & 38 | done 39 | wait `jobs -p` 40 | 41 | echo >&2 "[+] fetching IPs..." 42 | for uuid in $(cat $WORKDIR/uuids.txt); do 43 | scw inspect $uuid | grep address | awk '// { print $2; }' | tr -d '"' | awk '// { print $1; }' >> $WORKDIR/ips.txt 44 | done 45 | } 46 | 47 | # run several tests and output a Markdown report 48 | function report { 49 | # status 50 | echo >&2 "[+] report status" 51 | echo "## Status of instances" 52 | echo "" 53 | NB_INSTANCES_OK=$(wc -l $WORKDIR/ips.txt | awk '// { print $1; }') 54 | echo "- $NB_INSTANCES_OK / $NB_INSTANCES have correctly booted" 55 | echo "" 56 | 57 | # fping 58 | echo >&2 "[+] report fping" 59 | echo "## fping" 60 | echo "" 61 | fping $(cat $WORKDIR/ips.txt) | sed 's/\(.*\)/ \1/' > $WORKDIR/fping 62 | NB_INSTANCES_OK=$(wc -l $WORKDIR/fping | awk '// { print $1; }') 63 | echo "- $NB_INSTANCES_OK / $NB_INSTANCES respond to ping" 64 | echo "" 65 | cat $WORKDIR/fping 66 | echo "" 67 | 68 | # reboot 69 | echo >&2 "[+] reboot" 70 | echo "## reboot" 71 | echo "" 72 | for uuid in $(cat $WORKDIR/uuids.txt); do 73 | scw exec --wait --timeout 60 $uuid '(which systemctl &>/dev/null && systemctl reboot) || reboot' 74 | done 75 | echo "" 76 | 77 | sleep 120 78 | 79 | # fping 80 | echo >&2 "[+] report fping 120 sec after reboot" 81 | echo "## fping after reboot" 82 | echo "" 83 | fping $(cat $WORKDIR/ips.txt) | sed 's/\(.*\)/ \1/' > $WORKDIR/fping 84 | NB_INSTANCES_OK=$(wc -l $WORKDIR/fping | awk '// { print $1; }') 85 | echo "- $NB_INSTANCES_OK / $NB_INSTANCES respond to ping" 86 | echo "" 87 | cat $WORKDIR/fping 88 | echo "" 89 | 90 | # uptime 91 | echo >&2 "[+] uptime" 92 | echo "## uptime" 93 | echo "" 94 | for uuid in $(cat $WORKDIR/uuids.txt); do 95 | scw exec --wait --timeout 600 $uuid 'uptime' 1>&2 96 | failed=$? 97 | if [ $failed -ne 0 ] 98 | then 99 | echo " - $uuid is DOWN" 100 | else 101 | echo " - $uuid is UP" 102 | fi 103 | done 104 | echo "" 105 | } 106 | 107 | function main { 108 | cleanup 109 | boot 110 | report 111 | } 112 | 113 | main 114 | -------------------------------------------------------------------------------- /examples/kernel-quick-checks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # I'm a script used to check the stability of new kernels. 4 | # 5 | # I spawn 16 servers, boot them, and perform several checks such as a 6 | # local iperf, some IOs (parallel download of 5GO on a NBD mounted 7 | # disk). 8 | # 9 | # I generate a Markdown report with results 10 | 11 | # parameters 12 | if [ $# -ne 1 ]; then 13 | echo "usage: $0 build-id" 14 | exit 1 15 | fi 16 | 17 | # globals 18 | BUILD_ID=$1 19 | WORKDIR=$(mktemp -d -t minibench) 20 | NB_INSTANCES=16 21 | INSTANCE_NAME='minibench-kernel' 22 | 23 | # we expect the following in the environment 24 | # 25 | # - JENKINS_MIRROR 26 | # - SANDBOX_DTB_UUID 27 | if [ -f env.bash ] 28 | then 29 | source env.bash 30 | fi 31 | 32 | # fetch kernel and publish to S3 33 | function prepare { 34 | echo >&2 '[+] preparing kernel...' 35 | local kernel=$(wget "${JENKINS_MIRROR}/" -O /dev/stdout 2>/dev/null | sed -e 's/.*/\1/' -e 'tx' -e 'd' -e ':x' | grep "^.*\-${BUILD_ID}$\|^.*\-${BUILD_ID}\-.*$") 36 | if [ -z "$kernel" ]; then 37 | echo "can't find $BUILD_ID on jenkins" 38 | exit 1 39 | fi 40 | 41 | wget "${JENKINS_MIRROR}/${kernel}/uImage" -O /tmp/uImage 2>/dev/null 42 | wget "${JENKINS_MIRROR}/${kernel}/dtbs/pimouss-computing.dtb" -O /tmp/dtb 2>/dev/null 43 | 44 | s3cmd put --acl-public /tmp/uImage s3://mxs/uImage-$kernel &>/dev/null 45 | s3cmd put --acl-public /tmp/dtb s3://mxs/dtb-$kernel &>/dev/null 46 | 47 | s3cmd put --acl-public /tmp/uImage s3://mxs/uImage-sandbox &>/dev/null 48 | s3cmd put --acl-public /tmp/dtb s3://mxs/dtb-sandbox &>/dev/null 49 | 50 | rm -f /tmp/uImage /tmp/dtb 51 | } 52 | 53 | # destroy all existing servers matching name 54 | function cleanup { 55 | echo >&2 '[+] cleaning up existing servers...' 56 | for uuid in $(scw ps -a --no-trunc | tail -n +2 | awk '// { print $1, $NF; }' | grep "^.* ${INSTANCE_NAME}\-" | awk '// { print $1; }'); do 57 | scw stop -t $uuid 58 | done 59 | 60 | touch $WORKDIR/uuids.txt 61 | touch $WORKDIR/ips.txt 62 | } 63 | 64 | # create $NB_INSTANCES servers with a bootscript pointing to the prepared kernel 65 | function boot { 66 | echo >&2 "[+] creating $NB_INSTANCES servers..." 67 | for i in $(eval echo {1..$NB_INSTANCES}); do 68 | scw create --bootscript $SANDBOX_DTB_UUID --volume 50G --name "$INSTANCE_NAME-$i" Ubuntu_Trusty_14_04_LTS >> $WORKDIR/uuids.txt 69 | done 70 | cat $WORKDIR/uuids.txt 71 | 72 | echo >&2 "[+] booting $NB_INSTANCES servers..." 73 | for uuid in $(cat $WORKDIR/uuids.txt); do 74 | scw start -s $uuid & 75 | sleep .5 76 | done 77 | wait `jobs -p` 78 | 79 | echo >&2 "[+] fetching IPs..." 80 | for uuid in $(cat $WORKDIR/uuids.txt); do 81 | scw inspect $uuid | grep address | awk '// { print $2; }' | tr -d '"' | awk '// { print $1; }' >> $WORKDIR/ips.txt 82 | done 83 | } 84 | 85 | # run several tests and output a Markdown report 86 | function report { 87 | # status 88 | echo >&2 "[+] report status" 89 | echo "## Status of instances" 90 | echo "" 91 | NB_INSTANCES_OK=$(wc -l $WORKDIR/ips.txt | awk '// { print $1; }') 92 | echo "- $NB_INSTANCES_OK / $NB_INSTANCES have correctly booted" 93 | echo "" 94 | 95 | # fping 96 | echo >&2 "[+] report fping" 97 | echo "## fping" 98 | echo "" 99 | fping $(cat $WORKDIR/ips.txt) | sed 's/\(.*\)/ \1/' 100 | echo "" 101 | 102 | # uname -a 103 | echo >&2 "[+] report uname" 104 | echo "## uname" 105 | echo "" 106 | for uuid in $(cat $WORKDIR/uuids.txt); do 107 | scw exec $uuid 'uname -a' | sed 's/\(.*\)/ \1/' 108 | done 109 | echo "" 110 | 111 | # iperf 112 | echo >&2 "[+] report iperf" 113 | echo "## iperf" 114 | echo "" 115 | for uuid in $(cat $WORKDIR/uuids.txt); do 116 | scw exec $uuid 'iperf -s & sleep 5 ; iperf -c localhost' | sed 's/\(.*\)/ \1/' 117 | done 118 | echo "" 119 | 120 | # quick stability check 121 | echo >&2 "[+] report stability 1st pass" 122 | echo "## stability" 123 | echo "" 124 | for uuid in $(cat $WORKDIR/uuids.txt); do 125 | scw exec $uuid 'find /usr -type f | xargs md5sum &> /tmp/a' 126 | scw exec $uuid 'find /usr -type f | xargs cat &> /tmp/megafile' 127 | scw exec $uuid 'for i in {1..5}; do wget --no-verbose --page-requisites http://ping.online.net/1000Mo.dat -O $i 2>/dev/null & done; wait $(jobs -p)' | sed 's/\(.*\)/ \1/' 128 | done 129 | 130 | echo >&2 "[+] report stability 2nd pass" 131 | for uuid in $(cat $WORKDIR/uuids.txt); do 132 | scw exec $uuid 'find /usr -type f | xargs md5sum &> /tmp/b' 133 | done 134 | 135 | echo >&2 "[+] report stability 3rd pass" 136 | for uuid in $(cat $WORKDIR/uuids.txt); do 137 | scw exec $uuid 'diff /tmp/a /tmp/b' | sed 's/\(.*\)/ \1/' 138 | done 139 | 140 | echo >&2 "[+] report stability fping" 141 | echo "" 142 | fping $(cat $WORKDIR/ips.txt) | sed 's/\(.*\)/ \1/' 143 | echo "" 144 | } 145 | 146 | function main { 147 | prepare 148 | cleanup 149 | boot 150 | report > report-${BUILD_ID}.md 151 | echo >&2 "[+] report is at report-${BUILD_ID}.md" 152 | cleanup 153 | } 154 | 155 | main 156 | rm -rf $WORKDIR 157 | -------------------------------------------------------------------------------- /lib/program.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'), 2 | _ = require('lodash'), 3 | async = require('async'), 4 | child_process = require('child_process'), 5 | debug = require('debug')('scaleway-cli:program'), 6 | filesize = require('filesize'), 7 | filesizeParser = require('filesize-parser'), 8 | fs = require('fs'), 9 | jsonPath = require('JSONPath'), 10 | moment = require('moment'), 11 | program = require('commander'), 12 | termJsCli = require('../node_modules/term.js-cli'), 13 | utils = require('./utils'); 14 | 15 | 16 | program 17 | .version(utils.getVersion('..')) 18 | .option('--api-endpoint ', 'set the API endpoint') 19 | .option('--dry-run', 'do not execute actions') 20 | .option('-D, --debug', 'enable debug mode'); 21 | 22 | 23 | program 24 | .command('attach ') 25 | .description('attach (serial console) to a running server') 26 | .option('-n, --no-newline', 'do not send a newline on connection') 27 | .option('-q, --quiet', 'do not print warning message') 28 | .action(function(server, options) { 29 | var client = utils.newApi(options); 30 | utils.searchEntity({input: server, _type: 'servers', client: client}, function(err, entity) { 31 | utils.assert(err); 32 | 33 | var ttyUrl = 'https://tty.cloud.online.net?server_id=' + entity._id 34 | + '&type=serial&auth_token=' + client.config.token; 35 | debug('tty url', ttyUrl); 36 | var serial = new termJsCli({ 37 | url: ttyUrl, 38 | sendNewLineOnConnect: options.newline 39 | }); 40 | serial.connect(function(err) { 41 | utils.assert(err); 42 | if (!options.quiet) { 43 | console.error("info: you are connected, type 'Ctrl+q' to quit. (hide this message with -q)"); 44 | } 45 | }); 46 | }); 47 | }); 48 | 49 | 50 | /* 51 | program 52 | .command('build ') 53 | .description('build an image from a file') 54 | .action(utils.notImplementedAction); 55 | */ 56 | 57 | 58 | program 59 | .command('commit [name]') 60 | .description("create a new snapshot from a server's volume") 61 | // .option('-a, --author ', 62 | // 'author (e.g., "Georges Abitbol ")') 63 | .option('--name ', 'assign a name to the snapshot', 'noname') 64 | // .option('-p, --pause', 'pause server during commit') 65 | .option('-v, --volume ', 'volume slot') 66 | .action(function(server, name, options) { 67 | var client = utils.newApi(options); 68 | var volumeIdx = options.volume || 0; 69 | 70 | utils.searchEntity({input: server, _type: 'servers', client: client}, function(err, entity) { 71 | utils.assert(err); 72 | 73 | client.get('/servers/' + entity._id) 74 | .then(function(res) { 75 | name = options.name || (res.body.server.name + '-snapshot'); 76 | client.post('/snapshots', { 77 | volume_id: res.body.server.volumes[volumeIdx.toString()].id, 78 | organization: res.body.server.organization, 79 | name: name 80 | }) 81 | .then(function(res) { 82 | utils.saveEntity(res.body.snapshot, 'snapshots'); 83 | console.log(res.body.snapshot.id); 84 | }) 85 | .catch(utils.panic); 86 | }) 87 | .catch(utils.panic); 88 | }); 89 | }); 90 | 91 | 92 | /* 93 | program 94 | .command('cp ') 95 | .description("copy files/folders from a server's filesystem to the host path") 96 | .action(utils.notImplementedAction); 97 | */ 98 | 99 | 100 | program 101 | .command('create ') 102 | .description('create a new server but do not start it') 103 | .option('--name ', 'assign a name to the server', 'noname') 104 | .option('--bootscript ', 'assign a bootscript') 105 | .option('-v, --volume ', 'attach additional volume', utils.collect, []) 106 | .option('-e, --env ', 107 | 'provide metadata tags passed to initrd (i.e., boot=rescue,INITRD_DEBUG=1)', 108 | utils.collect, []) 109 | .on('--help', function() { 110 | console.log(' Examples:'); 111 | console.log(); 112 | console.log(' $ scw create docker'); 113 | console.log(' $ scw create 10GB'); 114 | console.log(' $ scw create 50GB --bootscript=3.2.34 --env="boot=live" --env="rescue_image=http://test-images.fr-1.storage.online.net/ocs-distrib-ubuntu-trusty.tar"'); 115 | console.log(' $ SERVER=$(scw create 1GB --bootscript=rescue --volume="50GB"); scw inspect ${SERVER}'); 116 | console.log(); 117 | }) 118 | .action(function(image, options) { 119 | var client = utils.newApi(options); 120 | 121 | // Create volumes 122 | Q.all(_.map(options.volume, function(volume) { 123 | return client.post('/volumes', { 124 | organization: client.config.organization, 125 | size: parseInt(filesizeParser(volume, {base: 10})), 126 | name: volume, 127 | volume_type: 'l_ssd' 128 | }); 129 | })).then( 130 | function(results) { 131 | var volumes = _.pluck(_.pluck(results, 'body'), 'volume'); 132 | options.volumes = _.pluck(volumes, 'id'); 133 | _.forEach(volumes, function(volume) { 134 | utils.saveEntity(volume, 'volumes'); 135 | }); 136 | // Resolve bootscript 137 | utils.searchEntity({input: options.bootscript, _type: 'bootscripts', client: client}, function(err, bootscriptEntity) { 138 | options.bootscript = bootscriptEntity && bootscriptEntity._id; 139 | 140 | utils.getImageOrNewVolume(client, image, function(ret) { 141 | if (ret.volume) { 142 | options.root_volume = ret.volume; 143 | } else if (ret.image) { 144 | options.image = ret.image; 145 | } 146 | options.tags = options.env; 147 | utils.createServer(client, options); 148 | }); 149 | }); 150 | }); 151 | }); 152 | 153 | 154 | program 155 | .command('events') 156 | .description('get real time events from the API') 157 | // .option('-f, --filter ', 158 | // 'provide filter values. valid filters: (i.e., status=pending)', 159 | // utils.collect, []) 160 | // .option('--since ', 'show all events created since timestamp') 161 | // .option('--until ', 'stream events until this timestamp') 162 | .action(function(options) { 163 | var client = utils.newApi(options); 164 | client.get('/tasks') 165 | .then(function(res) { 166 | _.forEach(res.body.tasks, function(task) { 167 | console.log(task.started_at + ' ' + 168 | task.href_from + ': ' + 169 | task.description + ' ('+ 170 | task.status + ' ' + 171 | task.progress + ') ' + 172 | task.terminated_at); 173 | }); 174 | }) 175 | .catch(utils.panic); 176 | }); 177 | 178 | 179 | program 180 | .command('exec [args...]') 181 | .description('run a command in a running server') 182 | // .option('-d, --detach', 'detached mode: run command in the background') 183 | // .option('-i, --interactive', 'keep STDIN open even if not attached') 184 | // .option('-t, --tty', 'allocate a pseudo-TTY') 185 | .option('-k, --insecure', 'DEPRECATED') 186 | .option('-s, --secure', 'enable SSH strict host key checking') 187 | .option('-T, --timeout ', 'set all timeout values to secs') 188 | .option('--ssh-timeout ', 'set the ssh timeout to secs') 189 | .option('--boot-timeout ', 'set the boot timeout to secs') 190 | .option('-w, --wait', 'wait for server to be available') 191 | .on('--help', function() { 192 | console.log(" the '--secure' option can be enabled by setting 'scw_exec_secure=1' in environment"); 193 | console.log(); 194 | console.log(' Examples:'); 195 | console.log(); 196 | console.log(' $ scw exec myserver /bin/bash'); 197 | console.log(" $ scw exec --safe myserver 'tmux a'"); 198 | console.log(" $ export scw_exec_secure=1; scw exec myserver 'ls -la | grep .tar'"); 199 | console.log(" $ scw exec $(scw start --wait $(scw create docker)) /bin/bash"); 200 | console.log(' $ scw exec --timeout=30 myserver /usr/local/bin/long-command'); 201 | console.log(' $ scw exec --wait $(scw start $(scw create docker)) /bin/bash'); 202 | console.log(); 203 | }) 204 | .action(function(server, command, commandArgs, options) { 205 | if (options.insecure) { 206 | console.log("The 'exec --insecure' option is now the default behaviour"); 207 | console.log(); 208 | console.log("See https://github.com/moul/scaleway-cli/issues/5"); 209 | utils.panic("Exiting."); 210 | } 211 | 212 | var client = utils.newApi(options); 213 | 214 | var globalTimeout = utils.panicTimeout(options.timeout, 'global execution'), 215 | bootTimeout, sshTimeout; 216 | 217 | options.secure = options.secure || client.config.exec_secure; 218 | 219 | var execCallback = function(code) { 220 | clearTimeout(globalTimeout); 221 | process.exit(code); 222 | }; 223 | 224 | utils.searchEntity({input: server, _type: 'servers', client: client}, function(err, entity) { 225 | utils.assert(err); 226 | 227 | if (options.wait) { 228 | bootTimeout = utils.panicTimeout(options.bootTimeout, 'server state is ready'); 229 | utils.waitForServerState(client, entity._id, 'running', function(err, server) { 230 | clearTimeout(bootTimeout); 231 | 232 | sshTimeout = utils.panicTimeout(options.sshTimeout, 'ssh port is ready'); 233 | utils.waitPortOpen(server.public_ip.address, 22, function(err) { 234 | clearTimeout(sshTimeout); 235 | clearTimeout(globalTimeout); 236 | utils.assert(err); 237 | utils.serverExec(server.public_ip.address, command, commandArgs, options, execCallback); 238 | }); 239 | }); 240 | } else { 241 | client.get('/servers/' + entity._id) 242 | .then(function(res) { 243 | if (!res.body.server.public_ip) { 244 | utils.panic('Server ' + res.body.server.id + ' is not running'); 245 | } 246 | utils.serverExec(res.body.server.public_ip.address, command, commandArgs, options, execCallback); 247 | }).catch(utils.panic); 248 | } 249 | }); 250 | }); 251 | 252 | 253 | /* 254 | program 255 | .command('export ') 256 | .description('stream the contents of a server as a tar archive') 257 | .action(utils.notImplementedAction); 258 | */ 259 | 260 | 261 | program 262 | .command('history ') 263 | .description('show the history of an image') 264 | .option('--no-trunc', "don't truncate output") 265 | .option('-q, --quiet', 'only display numeric IDs') 266 | .action(function(image, options) { 267 | var client = utils.newApi(options); 268 | utils.searchEntity({input: image, _type: 'images', client: client}, function(err, entity) { 269 | utils.assert(err); 270 | client.get('/images/' + entity._id) 271 | .then(function(res) { 272 | if (options.quiet) { 273 | console.log(res.body.image.id); 274 | } else { 275 | var table = utils.newTable({ 276 | head: [ 277 | 'IMAGE', 'CREATED', 'CREATED BY', 'SIZE' 278 | ] 279 | }); 280 | 281 | var image = res.body.image; 282 | var row = [ 283 | image.id, 284 | moment(image.creation_date).fromNow(), 285 | image.root_volume.name, 286 | filesize(image.root_volume.size, {base: 10}) 287 | ]; 288 | if (options.trunc) { 289 | utils.truncateRow(row, [8, 25, 25, 25]); 290 | } 291 | table.push(row); 292 | console.log(table.toString()); 293 | } 294 | }) 295 | .catch(utils.panic); 296 | }); 297 | }); 298 | 299 | 300 | program 301 | .command('images') 302 | .description('list images') 303 | .option('-a, --all', 'show all images') 304 | // .option('-f, --filter ', 305 | // "provide filter values. (i.e., 'public=true')", utils.collect, []) 306 | .option('--no-trunc', "don't truncate output") 307 | .option('-q, --quiet', 'only display numeric IDs') 308 | .action(function(options) { 309 | var client = utils.newApi(options); 310 | var promises = []; 311 | 312 | var query = '/images?'; 313 | promises.push(client.get(query)); 314 | 315 | if (options.all) { 316 | promises.push(client.get('/snapshots')); 317 | promises.push(client.get('/bootscripts')); 318 | } 319 | 320 | Q.all(promises).then( 321 | function(results) { 322 | var entries = _.reduce( 323 | _.pluck(results, 'body'), 324 | function(entries, group) { 325 | return entries.concat.apply( 326 | entries, 327 | _.reduce( 328 | group, 329 | function(aggreg, n, key) { 330 | utils.saveEntities(n, key); 331 | return aggreg.concat.apply( 332 | aggreg, 333 | _.map(n, function(entry) { 334 | entry._type = key; 335 | return entry; 336 | }) 337 | ); 338 | }, []) 339 | ); 340 | }, []); 341 | 342 | if (options.quiet) { 343 | _.forEach( 344 | _.sortByOrder(entries, ['creation_date'], [false]), 345 | function(entry) { 346 | console.log(entry.id); 347 | }); 348 | } else { 349 | var table = utils.newTable({ 350 | head: [ 351 | 'REPOSITORY', 'TAG', 'IMAGE ID', 'CREATED', 'VIRTUAL SIZE' 352 | ] 353 | }); 354 | 355 | _.forEach(_.sortByOrder( 356 | entries, 357 | ['creation_date'], 358 | [false] 359 | ), function(entry) { 360 | var repository, tag, imageId, created, virtualSize; 361 | switch (entry._type) { 362 | case 'snapshots': 363 | var snapshot = entry; 364 | repository = utils.wordify(snapshot.name); 365 | tag = ''; 366 | imageId = snapshot.id; 367 | created = moment(snapshot.creation_date).fromNow(); 368 | virtualSize = filesize(snapshot.size, {base: 10}); 369 | break; 370 | case 'images': 371 | var image = entry; 372 | repository = utils.wordify(image.name); 373 | if (!image.public) { 374 | repository = 'user/' + utils.wordify(image.name); 375 | } 376 | tag = 'latest'; 377 | imageId = image.id; 378 | created = moment(image.creation_date).fromNow(); 379 | virtualSize = filesize(image.root_volume.size, {base: 10}); 380 | break; 381 | case 'bootscripts': 382 | var bootscript = entry; 383 | repository = utils.wordify(bootscript.title); 384 | tag = 'bootscript'; 385 | imageId = bootscript.id; 386 | created = 'n/a'; 387 | virtualSize = 'n/a'; 388 | break; 389 | } 390 | var row = [ 391 | repository, tag, imageId, created, virtualSize 392 | ]; 393 | if (options.trunc) { 394 | utils.truncateRow(row, [40, 25, 8, 25, 25]); 395 | } 396 | table.push(row); 397 | }); 398 | console.log(table.toString()); 399 | } 400 | 401 | }, utils.panic); 402 | }); 403 | 404 | 405 | /* 406 | program 407 | .command('import') 408 | .description('create a new filesystem image from the contents of a tarball') 409 | .action(utils.notImplementedAction); 410 | */ 411 | 412 | 413 | program 414 | .command('info') 415 | .description('display system-wide information') 416 | .action(function() { 417 | var rc = utils.rc(); 418 | console.log('Organization: ' + rc.organization); 419 | console.log('Token: ' + utils.anonymizeUUID(rc.token)); 420 | console.log('API Endpoint: ' + rc.api_endpoint); 421 | console.log('RC file: ' + rc.config); 422 | console.log('CLI path: ' + process.argv[1]); 423 | console.log('User: ' + process.env.USER); 424 | utils.db.count({}, function(err, count) { 425 | if (!err) { 426 | console.log('Cached entities: '+ count); 427 | } 428 | }); 429 | }); 430 | 431 | 432 | program 433 | .command('inspect ') 434 | .description('return low-level information on a server or image') 435 | .option('-f, --format ', 'format the output using the given template') 436 | .action(function(items, options) { 437 | var client = utils.newApi(options); 438 | var promises = []; 439 | 440 | var once = function(item, cb) { 441 | return [ 442 | client.get('/servers/' + item._id), 443 | client.get('/images/' + item._id), 444 | client.get('/volumes/' + item._id), 445 | client.get('/bootscripts/' + item._id), 446 | client.get('/snapshots/' + item._id) 447 | // client.get('/organizations/' + item._id), 448 | // client.get('/users/' + item._id), 449 | // client.get('/ips/' + item._id) 450 | ]; 451 | }; 452 | 453 | utils.searchEntities({inputs: items}, function(err, entities) { 454 | utils.assert(err); 455 | promises = promises.concat.apply(promises, entities.map(once)); 456 | 457 | Q.allSettled(promises).then( 458 | function(results) { 459 | var entries = _.compact(_.pluck(_.pluck(results, 'value'), 'body')); 460 | if (options.format) { 461 | _.map(entries, function(entry) { 462 | var parsed = jsonPath.eval(entry, '$' + options.format); 463 | if (typeof(parsed) === 'object' && parsed.length === 1) { 464 | console.log(parsed[0]); 465 | } else { 466 | console.log(parsed); 467 | } 468 | }); 469 | } else { 470 | console.log(JSON.stringify(entries, null, 2)); 471 | } 472 | }, utils.panic); 473 | }); 474 | }); 475 | 476 | 477 | program 478 | .command('kill ') 479 | .description('kill a running server') 480 | // .option('-s, --signal ', 'Signal to send to the server', 'KILL') 481 | .action(function(server, options) { 482 | var client = utils.newApi(options); 483 | 484 | utils.searchEntity({input: server, _type: 'servers', client: client}, function(err, entity) { 485 | utils.assert(err); 486 | client.get('/servers/' + entity._id) 487 | .then(function(res) { 488 | if (!res.body.server.public_ip) { 489 | utils.panic('Server ' + res.body.server.id + ' is not running'); 490 | } 491 | var ip = res.body.server.public_ip.address; 492 | 493 | utils.sshExec(ip, 'halt', {}, function(statusCode) { 494 | if (statusCode === 0) { 495 | console.log(server); 496 | } 497 | process.exit(statusCode); 498 | }); 499 | }) 500 | .catch(utils.panic); 501 | }); 502 | }); 503 | 504 | 505 | /* 506 | program 507 | .command('load') 508 | .description('load an image from a tar archive') 509 | .action(utils.notImplementedAction); 510 | */ 511 | 512 | 513 | program 514 | .command('login') 515 | .description('login to the API') 516 | .option('--organization ', 'set the organization') 517 | .option('--token ', 'token') 518 | .action(function(options) { 519 | var client = utils.newApi(options); 520 | var newConfig = _.cloneDeep(client.config); 521 | delete newConfig._; 522 | delete newConfig.configs; 523 | delete newConfig.config; 524 | var filepath = utils.defaultConfigPath(); 525 | fs.writeFile( 526 | filepath, 527 | JSON.stringify(newConfig, null, 2), 528 | function (err) { 529 | utils.assert(err); 530 | console.log('configuration written to ' + filepath); 531 | }); 532 | }); 533 | 534 | 535 | program 536 | .command('logout') 537 | .description('log out from the API') 538 | .action(function() { 539 | var filepath = utils.defaultConfigPath(); 540 | fs.unlink( 541 | filepath, 542 | function (err) { 543 | utils.panic(err); 544 | console.log('removed ' + filepath + ' configuration file'); 545 | }); 546 | }); 547 | 548 | 549 | /* 550 | program 551 | .command('logs ') 552 | .description('fetch the logs of a server') 553 | .action(utils.notImplementedAction); 554 | */ 555 | 556 | 557 | /* 558 | program 559 | .command('port') 560 | .description('list port security for the server') 561 | .action(utils.notImplementedAction); 562 | */ 563 | 564 | 565 | /* 566 | program 567 | .command('pause') 568 | .description('pause all processes within a server') 569 | .action(utils.notImplementedAction); 570 | */ 571 | 572 | 573 | program 574 | .command('ps') 575 | .description('list servers') 576 | .option('-a, --all', 577 | 'show all servers. only running servers are shown by default') 578 | // .option('--before ', 'show only server created before server, ' + 579 | // 'include non-running ones') 580 | // .option('-f, --filter ', 'provide filter values. valid filters: ' + 581 | // 'status=(starting|running|stopping|stopped)', utils.collect, []) 582 | .option('-l, --latest', 583 | 'show only the latest created server, include non-running ones') 584 | .option('-n ', 'show n last created servers, include non-running ones.', 585 | parseInt) 586 | .option('--no-trunc', "don't truncate output") 587 | .option('-q, --quiet', 'only display numeric IDs') 588 | // .option('-s, --size', 'display total file sizes') 589 | // .option('--since ', 590 | // 'show only servers created since server, include non-running ones') 591 | .action(function(options) { 592 | var client = utils.newApi(options); 593 | var query = '/servers?'; 594 | 595 | if (!options.all) { query += 'state=running&'; } 596 | if (options.latest) { query += 'limit=1&'; } 597 | if (options.n) { query += 'limit=' + options.n + '&'; } 598 | 599 | client.get(query) 600 | .then(function(res) { 601 | if (options.all) { 602 | utils.saveEntities(res.body.servers, 'servers'); 603 | } else { 604 | // FIXME: saveEntity 605 | } 606 | if (options.quiet) { 607 | _.forEach( 608 | _.sortByOrder(res.body.servers, ['creation_date'], [false]), 609 | function(server) { 610 | console.log(server.id); 611 | }); 612 | } else { 613 | var table = utils.newTable({ 614 | head: [ 615 | 'SERVER ID', 'IMAGE', 'COMMAND', 'CREATED', 'STATUS', 'PORTS', 616 | 'NAME' 617 | ] 618 | }); 619 | 620 | _.forEach(_.sortByOrder( 621 | res.body.servers, 622 | ['creation_date'], 623 | [false]), function(server) { 624 | var row = [ 625 | server.id, 626 | (server.image ? utils.wordify(server.image.root_volume.name) : ''), 627 | '', 628 | moment(server.creation_date).fromNow(), 629 | server.state, 630 | '', 631 | utils.wordify(server.name) 632 | ]; 633 | if (options.trunc) { 634 | utils.truncateRow(row, [8, 25, 25, 25, 25, 25, -1]); 635 | } 636 | table.push(row); 637 | }); 638 | console.log(table.toString()); 639 | } 640 | }) 641 | .catch(utils.panic); 642 | }); 643 | 644 | 645 | /* 646 | program 647 | .command('pull ') 648 | .description('pull an image or a repository') 649 | .action(utils.notImplementedAction); 650 | */ 651 | 652 | 653 | /* 654 | program 655 | .command('push ') 656 | .description('push an image or a repository') 657 | .action(utils.notImplementedAction); 658 | */ 659 | 660 | 661 | /* 662 | program 663 | .command('rename ') 664 | .description('rename an existing server') 665 | .action(utils.notImplementedAction); 666 | */ 667 | 668 | 669 | program 670 | .command('restart ') 671 | .description('restart a running server') 672 | // .option('-t, --time ', 'number of seconds to try to stop for ' + 673 | // 'before killing the server. once killed it will be restarted.') 674 | .action(function(servers, options) { 675 | var client = utils.newApi(options); 676 | 677 | utils.searchEntities({inputs: servers, filters: { _type: 'servers' }, any: true, client: client}, function(err, entities) { 678 | utils.assert(err); 679 | 680 | _.each(entities, function(server) { 681 | if (server.length === 0) { 682 | return null; 683 | } 684 | 685 | 686 | client.post('/servers/' + server._id + '/action', { 687 | action: 'reboot' 688 | }) 689 | .then(function() { 690 | console.log(server); 691 | }) 692 | .catch(function (err) { 693 | if (err.error.message !== 'server is being stopped or rebooted') { 694 | utils.panic(err); 695 | } 696 | }); 697 | return null; 698 | }); 699 | }); 700 | }); 701 | 702 | 703 | program 704 | .command('rm ') 705 | .description('remove one or more servers') 706 | .action(function(servers, options) { 707 | var client = utils.newApi(options); 708 | 709 | utils.searchEntities({inputs: servers, filters: { _type: 'servers' }, any: true, client: client}, function(err, entities) { 710 | utils.assert(err); 711 | 712 | _.each(entities, function(server) { 713 | if (server.length === 0) { 714 | return null; 715 | } 716 | 717 | return client.delete('/servers/' + server._id) 718 | .then(function(res) { 719 | if (res.statusCode !== 204) { 720 | error(res); 721 | } 722 | }) 723 | .catch(utils.error); 724 | }); 725 | }); 726 | }); 727 | 728 | 729 | program 730 | .command('rmi ') 731 | .description('remove one or more images') 732 | .action(function(images, options) { 733 | var client = utils.newApi(options); 734 | 735 | utils.searchEntities({inputs: images, filters: { _type: 'images' }, any: true, client: client}, function(err, entities) { 736 | utils.assert(err); 737 | 738 | _.each(entities, function(image) { 739 | if (image.length === 0) { 740 | return null; 741 | } 742 | 743 | return client.delete('/images/' + image._id) 744 | .then(function(res) { 745 | if (res.statusCode !== 204) { 746 | error(res); 747 | } 748 | }) 749 | .catch(utils.error); 750 | }); 751 | }); 752 | }); 753 | 754 | 755 | /* 756 | program 757 | .command('run ') 758 | .description('run a command in a new server') 759 | .action(utils.notImplementedAction); 760 | */ 761 | 762 | 763 | /* 764 | program 765 | .command('save ') 766 | .description('save an image to a tar archive') 767 | .action(utils.notImplementedAction); 768 | */ 769 | 770 | 771 | /* 772 | program 773 | .command('search ') 774 | .description('search for an image on the Hub') 775 | .action(utils.notImplementedAction); 776 | */ 777 | 778 | 779 | program 780 | .command('start ') 781 | .description('start a stopped server') 782 | // .option('-a, --attach', "attach server's STDOUT and STDERR and forward " + 783 | // 'all signals to the process') 784 | // .option('-i, --interactive', "attach server's STDIN") 785 | .option('-s, --sync', 'DEPRECATED. see --wait') 786 | .option('-w, --wait', 'synchronous start. wait for SSH to be ready') 787 | .option('-T, --timeout ', 'set all timeout values to secs') 788 | .option('--boot-timeout ', 'set the boot timeout to secs') 789 | .option('--ssh-timeout ', 'set the ssh timeout to secs') 790 | .action(function(servers, options) { 791 | var client = utils.newApi(options); 792 | 793 | if (options.sync) { 794 | console.log("The 'start --sync' option was renamed to 'start --wait'"); 795 | utils.panic("Exiting."); 796 | } 797 | 798 | var globalTimeout = utils.panicTimeout(options.timeout, 'global execution'), 799 | bootTimeout = null, 800 | sshTimeout = null; 801 | 802 | 803 | utils.searchEntities({inputs: servers, filters: { _type: 'servers' }, any: true, client: client}, function(err, entities) { 804 | utils.assert(err); 805 | 806 | _.each(entities, function(server) { 807 | if (server.length === 0) { 808 | return null; 809 | } 810 | 811 | client.post('/servers/' + server._id + '/action', { 812 | action: 'poweron' 813 | }) 814 | .then(function() { 815 | console.log(server._id); 816 | if ( options.wait) { 817 | bootTimeout = utils.panicTimeout(options.bootTimeout, 'server state is ready'); 818 | utils.waitForServerState(client, server._id, 'running', function(err, server) { 819 | clearTimeout(bootTimeout); 820 | 821 | sshTimeout = utils.panicTimeout(options.sshTimeout, 'ssh port is ready'); 822 | utils.waitPortOpen(server.public_ip.address, 22, function(err) { 823 | clearTimeout(sshTimeout); 824 | clearTimeout(globalTimeout); 825 | utils.assert(err); 826 | process.exit(0); 827 | }); 828 | debug('server state is running'); 829 | }); 830 | } 831 | }) 832 | .catch(function (err) { 833 | clearTimeout(globalTimeout); 834 | if (err.error.message !== 'server should be stopped') { 835 | utils.panic(err); 836 | } 837 | }); 838 | return null; 839 | }); 840 | }); 841 | }); 842 | 843 | 844 | program 845 | .command('stop ') 846 | .description('stop a running server') 847 | .option('-t, --terminate', 'stop and trash a server and its volumes') 848 | .action(function(servers, options) { 849 | var client = utils.newApi(options); 850 | 851 | var data = { 852 | action: 'poweroff' 853 | }; 854 | if (options.terminate) { 855 | data.action = 'terminate'; 856 | } 857 | 858 | utils.searchEntities({inputs: servers, filters: { _type: 'servers' }, any: true, client: client}, function(err, entities) { 859 | utils.assert(err); 860 | 861 | _.each(entities, function(server) { 862 | if (server.length === 0) { 863 | return null; 864 | } 865 | 866 | return client.post('/servers/' + server._id + '/action', data) 867 | .then(function() { 868 | console.log(server._id); 869 | }) 870 | .catch(function (err) { 871 | if (!_.includes([ 872 | 'server is being stopped or rebooted', 873 | 'server should be running' 874 | ], err.error.message)) { 875 | utils.error(err); 876 | } 877 | }); 878 | }); 879 | }); 880 | }); 881 | 882 | 883 | 884 | program 885 | .command('tag ') 886 | .description('tag an image into a repository') 887 | .action(function(snapshot, tagName, options) { 888 | var client = utils.newApi(options); 889 | 890 | utils.searchEntity({input: snapshot, _type: 'snapshots', client: client}, function(err, entity) { 891 | utils.assert(err); 892 | 893 | client.get('/snapshots/' + entity._id) 894 | .then(function(res) { 895 | client.post('/images', { 896 | root_volume: res.body.snapshot.id, 897 | organization: res.body.snapshot.organization, 898 | name: tagName, 899 | arch: 'arm' 900 | }) 901 | .then(function(res) { 902 | utils.saveEntity(res.body.image, 'images'); 903 | console.log(res.body.image.id); 904 | }) 905 | .catch(utils.panic); 906 | }) 907 | .catch(utils.panic); 908 | }); 909 | }); 910 | 911 | 912 | program 913 | .command('top ') 914 | .description('lookup the running processes of a server') 915 | .action(function(server, options) { 916 | var client = utils.newApi(options); 917 | 918 | utils.searchEntity({input: server, _type: 'servers', client: client}, function(err, entity) { 919 | utils.assert(err); 920 | client.get('/servers/' + entity._id) 921 | .then(function(res) { 922 | if (!res.body.server.public_ip) { 923 | utils.panic('Server ' + res.body.server.id + ' is not running'); 924 | } 925 | var ip = res.body.server.public_ip.address; 926 | 927 | utils.sshExec(ip, 'ps', {}, function(statusCode) { 928 | if (statusCode === 0) { 929 | console.log(server); 930 | } 931 | process.exit(statusCode); 932 | }); 933 | }) 934 | .catch(utils.panic); 935 | }); 936 | }); 937 | 938 | 939 | /* 940 | program 941 | .command('unpause ') 942 | .description('unpause a paused server') 943 | .action(utils.notImplementedAction); 944 | */ 945 | 946 | 947 | program._events.version = null; 948 | program 949 | .command('version') 950 | .description('show the version information') 951 | .action(function() { 952 | console.log('Client version: ' + utils.getVersion('..')); 953 | console.log('Client API version: ' + utils.getVersion('scaleway')); 954 | console.log('Node.js version (client): ' + process.version); 955 | console.log('OS/Arch (client): ' + process.platform + '/' + process.arch); 956 | // FIXME: add information about server 957 | }); 958 | 959 | 960 | 961 | program 962 | .command('wait ') 963 | .description('block until a server stops') 964 | .action(function(server, options) { 965 | var client = utils.newApi(options); 966 | 967 | utils.searchEntity({input: server, _type: 'servers', client: client}, function(err, entity) { 968 | utils.assert(err); 969 | 970 | utils.waitForServerState(client, entity._id, 'stopped', function(err) { 971 | utils.assert(err); 972 | console.log(0); 973 | }); 974 | }); 975 | }); 976 | 977 | 978 | program 979 | .command('_patch ', null, {noHelp: true}) 980 | .description('coucou') 981 | .action(function(item, _updates, options) { 982 | var updates = {}; 983 | _.each(_updates, function(item) { 984 | var entry = item.split(/=(.+)?/); 985 | updates[entry[0]] = entry[1]; 986 | }); 987 | 988 | var client = utils.newApi(options); 989 | var promises = []; 990 | 991 | var once = function(item, cb) { 992 | if (item._type) { 993 | return [client.get('/' + item._type + '/' + item._id)]; 994 | } 995 | 996 | return [ 997 | client.get('/servers/' + item._id), 998 | client.get('/images/' + item._id), 999 | client.get('/volumes/' + item._id), 1000 | client.get('/bootscripts/' + item._id), 1001 | client.get('/organizations/' + item._id), 1002 | client.get('/users/' + item._id), 1003 | client.get('/ips/' + item._id) 1004 | ]; 1005 | }; 1006 | 1007 | utils.searchEntity({input: item}, function(err, entity) { 1008 | utils.assert(err); 1009 | promises = once(entity); 1010 | 1011 | Q.allSettled(promises).then( 1012 | function(results) { 1013 | var entry = _.compact(_.pluck(_.pluck(results, 'value'), 'body'))[0]; 1014 | var itemPath = results[0].value.request.path; 1015 | 1016 | _.each(updates, function(newValue, key) { 1017 | var oldValue = utils.findKeyInDeepObject(entry, key); 1018 | if (oldValue) { 1019 | console.log('- ' + key + ': ' + oldValue + ' => ' + newValue); 1020 | } else { 1021 | console.log('- ' + key + ': (new value) ' + newValue); 1022 | } 1023 | }); 1024 | 1025 | client.patch(itemPath, updates).then(function(res) { 1026 | console.log(res.body.server.id); 1027 | }).catch(utils.panic); 1028 | }, utils.panic); 1029 | }); 1030 | }); 1031 | 1032 | 1033 | 1034 | module.exports = program; 1035 | 1036 | 1037 | module.exports.run = function() { 1038 | utils.dbInit(function() { 1039 | program.parse(process.argv); 1040 | if (!process.argv.slice(2).length) { 1041 | program.outputHelp(); 1042 | } 1043 | }); 1044 | }; 1045 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | var Api = require('scaleway'), 2 | Datastore = require('nedb'), 3 | Q = require('q'), 4 | Table = require('cli-table'), 5 | _ = require('lodash'), 6 | async = require('async'), 7 | attempt = require('attempt'), 8 | child_process = require('child_process'), 9 | debug = require('debug')('scaleway-cli:utils'), 10 | filesizeParser = require('filesize-parser'), 11 | portScanner = require('portscanner'), 12 | util = require('util'), 13 | validator = require('validator'); 14 | 15 | 16 | var db = module.exports.db = new Datastore({ 17 | filename: '/tmp/scw.db', 18 | autoload: false 19 | }); 20 | 21 | 22 | module.exports.dbInit = function(fn) { 23 | attempt( 24 | { retries: 15 }, 25 | function(attempts) { 26 | db.loadDatabase(this); 27 | }, 28 | function (err, results) { 29 | assert(err); 30 | fn(); 31 | } 32 | ); 33 | }; 34 | 35 | 36 | var prepareEntity = function(entity, category) { 37 | entity._id = entity.id; 38 | entity._type = category; 39 | entity.creation_date = entity.creation_date || new Date(1970, 1, 1); 40 | return entity; 41 | }; 42 | 43 | 44 | if (RegExp.prototype.toJSON === undefined) { 45 | RegExp.prototype.toJSON = RegExp.prototype.toString; 46 | } 47 | var inspect = function(name, obj) { 48 | debug(name, '\n' + JSON.stringify(obj, null, 4)); 49 | }; 50 | 51 | 52 | module.exports.saveEntity = function(entity, category) { 53 | entity = prepareEntity(entity, category); 54 | db.insert([entity], function(err, newDocs) { 55 | assert(err); 56 | }); 57 | }; 58 | 59 | 60 | module.exports.saveEntities = function(entities, category, fn) { 61 | entities = _.map(entities, function(entity) { 62 | return prepareEntity(entity, category); 63 | }); 64 | 65 | db.remove( 66 | { _type: category }, 67 | { multi: true }, 68 | function(err, numRemoved) { 69 | debug('saveEntities: removed ' + numRemoved + ' items'); 70 | assert(err); 71 | db.insert(entities, function(err, newDocs) { 72 | debug('saveEntities: inserted ' + newDocs.length + ' items'); 73 | assert(err); 74 | if (fn) { 75 | fn(); 76 | } 77 | }); 78 | }); 79 | return entities; 80 | }; 81 | 82 | 83 | module.exports.executeQuery = function(query, cb) { 84 | db.find(query, function(err, rows) { 85 | cb(err, rows); 86 | return (err ? null : rows); 87 | }); 88 | }; 89 | 90 | 91 | module.exports.searchEntity = function(opts, cb) { 92 | if (!opts.input) { 93 | return cb(true, null); 94 | } 95 | 96 | opts.quiet = opts.quiet || false; 97 | opts.filters = opts.filters || {}; 98 | 99 | if (opts._type !== undefined) { 100 | opts.filters._type = opts._type; 101 | delete opts._type; 102 | } 103 | 104 | inspect('searchEntity::input', opts); 105 | 106 | var queries = [ 107 | _.assign({ 108 | _id: new RegExp('^' + opts.input, 'i') 109 | }, _.clone(opts.filters || {})) 110 | ]; 111 | 112 | var nameRegex = opts.input.replace(/[_-]/g, '.*'); 113 | 114 | if (!opts.filters._type || opts.filters._type == 'servers') { 115 | queries.push( 116 | _.assign({ 117 | name: new RegExp(nameRegex, 'i'), 118 | _type: 'servers' 119 | }, _.clone(opts.filters || {})) 120 | ); 121 | } 122 | 123 | if (!opts.filters._type || opts.filters._type == 'images') { 124 | queries.push( 125 | _.assign({ 126 | name: new RegExp(nameRegex, 'i'), 127 | _type: 'images' 128 | }, _.clone(opts.filters || {})) 129 | ); 130 | if (opts.input.indexOf('user/') == 0) { 131 | queries.push( 132 | _.assign({ 133 | name: new RegExp(nameRegex.replace(/^user\//, ''), 'i'), 134 | _type: 'images' 135 | }, _.clone(opts.filters || {})) 136 | ); 137 | } 138 | } 139 | 140 | if (!opts.filters._type || opts.filters._type == 'bootscripts') { 141 | queries.push( 142 | _.assign({ 143 | title: new RegExp(nameRegex, 'i'), 144 | _type: 'bootscripts' 145 | }, _.clone(opts.filters || {})) 146 | ); 147 | } 148 | 149 | inspect('searchEntity::queries', queries); 150 | 151 | return async.concat(queries, module.exports.executeQuery, function(err, results) { 152 | if (err) { 153 | if (opts.quiet) { return cb(null, results); } 154 | return cb(err, results); 155 | } 156 | if (results.length === 1) { 157 | return cb(null, results[0]); 158 | } else if (results.length === 0) { 159 | if (validator.isUUID(opts.input)) { 160 | return cb(null, { _id: opts.input }); 161 | } else { 162 | if (!opts.no_cache_update) { 163 | return module.exports.buildCache(opts, function(err) { 164 | assert(err); 165 | opts.no_cache_update = true; 166 | module.exports.searchEntity(opts, cb); 167 | }); 168 | } 169 | if (opts.quiet) { return cb(null, results); } 170 | return cb('No such id for ' + opts.input, null); 171 | } 172 | } else { 173 | var output = 'too many candidates for ' + opts.input + ' (' + results.length + ')'; 174 | _.forEach(results, function(result) { 175 | output += '\n- ' + result._id + ' - ' + result.name; 176 | }); 177 | if (opts.quiet) { return cb(null, results); } 178 | return cb(output, results); 179 | } 180 | }); 181 | }; 182 | 183 | 184 | module.exports.buildCache = function(opts, fn) { 185 | var type = opts.filters._type; 186 | if (!type || !opts.client) { 187 | return fn(); 188 | } 189 | 190 | switch (type) { 191 | case 'snapshots': 192 | case 'servers': 193 | case 'images': 194 | case 'volumes': 195 | opts.client.get('/' + type).then(function(res) { 196 | module.exports.saveEntities(res.body[type], type, fn); 197 | }); 198 | break; 199 | default: 200 | fn(); 201 | break; 202 | } 203 | }; 204 | 205 | 206 | module.exports.waitForServerState = function(client, serverId, targetState, cb) { 207 | var latestState = 'unknown'; 208 | var latestServer = null; 209 | async.whilst( 210 | function(a) { 211 | return latestState !== targetState; 212 | }, 213 | function(whilstCb) { 214 | client.get('/servers/' + serverId) 215 | .then(function(res) { 216 | latestServer = res.body.server; 217 | latestState = res.body.server.state; 218 | if (latestState === targetState) { 219 | whilstCb(null, res.body.server); 220 | } else { 221 | setTimeout(whilstCb, 3000); 222 | } 223 | }) 224 | .catch(panic); 225 | }, 226 | function(err, server) { 227 | assert(err); 228 | cb(null, latestServer); 229 | }); 230 | }; 231 | 232 | 233 | module.exports.waitPortOpen = function(ip, port, cb) { 234 | var isPortOpen = false; 235 | async.until( 236 | function () { return isPortOpen; }, 237 | function (loopCb) { 238 | portScanner.checkPortStatus(port, ip, function(err, statusOfPort) { 239 | debug('portscanner', port, ip, err, statusOfPort); 240 | if (statusOfPort === 'open') { 241 | isPortOpen = true; 242 | cb(err); 243 | } else { 244 | setTimeout(function() { loopCb(null); }, 3000); 245 | } 246 | }); 247 | }, assert); 248 | }; 249 | 250 | 251 | module.exports.searchEntities = function(opts, cb) { 252 | var promises = _.map(opts.inputs, function(input) { 253 | return { 254 | input: input, 255 | filters: opts.filters, 256 | quiet: opts.any, 257 | client: opts.client 258 | }; 259 | }); 260 | return async.map(promises, module.exports.searchEntity, cb); 261 | }; 262 | 263 | 264 | var error = module.exports.error = function(msg) { 265 | if (msg && msg.options && msg.options.method && msg.options.url && 266 | msg.statusCode && msg.error && msg.error.message) { 267 | debug('panic', msg); 268 | console.error('> ' + msg.options.method + ' ' + msg.options.url); 269 | console.error('< ' + msg.error.message + ' (' + msg.statusCode + ')'); 270 | if (msg.error.fields) { 271 | _.forEach(msg.error.fields, function(value, key) { 272 | console.log(' - ' + key + ': ' + value.join('. ')); 273 | }); 274 | } 275 | } else { 276 | console.error(msg); 277 | } 278 | }; 279 | 280 | 281 | var panic = module.exports.panic = function(msg) { 282 | error(msg); 283 | console.error(''); 284 | console.error(' Hey ! this is probably a bug !'); 285 | console.error(' Fresh beers will be waiting for you on our next meetup'); 286 | console.error(' if you report a new issue :) 🍻'); 287 | console.error(''); 288 | console.error(' https://github.com/moul/scaleway-cli-node/issues'); 289 | console.error(''); 290 | process.exit(-1); 291 | }; 292 | 293 | 294 | var assert = module.exports.assert = function(check, err) { 295 | if (typeof(err) == 'undefined') { 296 | err = check; 297 | } 298 | if (check) { 299 | panic(err); 300 | } 301 | }; 302 | 303 | 304 | module.exports.notImplementedAction = function() { 305 | console.error("scw: Not implemented"); 306 | }; 307 | 308 | 309 | module.exports.truncateRow = function(row, limits) { 310 | for (var idx in row) { 311 | if (limits[idx] !== -1) { 312 | row[idx] = row[idx].toString().substring(0, limits[idx]); 313 | } 314 | } 315 | return row; 316 | }; 317 | 318 | 319 | module.exports.defaultConfigPath = function() { 320 | var home = process.env[( 321 | process.platform === 'win32' ? 322 | 'USERPROFILE' : 323 | 'HOME' 324 | )]; 325 | return home + '/.scwrc'; 326 | }; 327 | 328 | 329 | module.exports.newTable = function(options) { 330 | options = options || {}; 331 | options.chars = options.chars || { 332 | 'top': '', 'top-mid': '', 'top-left': '', 'top-right': '', 333 | 'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '', 334 | 'left': '', 'left-mid': '', 'mid': '', 'mid-mid': '', 335 | 'right': '', 'right-mid': '', 'middle': ' ' 336 | }; 337 | options.style = options.style || { 338 | // 'padding-left': 0, 'padding-right': 0 339 | }; 340 | return new Table(options); 341 | }; 342 | 343 | 344 | module.exports.wordify = function(str) { 345 | return str 346 | .replace(/[^a-zA-Z0-9-]/g, '_') 347 | .replace(/__+/g, '_') 348 | .replace(/^_/, '') 349 | .replace(/_$/, ''); 350 | }; 351 | 352 | 353 | module.exports.newApi = function(options) { 354 | var overrides = {}; 355 | 356 | options = options || {}; 357 | options.parent = options.parent || {}; 358 | if (options.parent.apiEndpoint) { 359 | overrides.api_endpoint = options.parent.apiEndpoint; 360 | } 361 | if (options.parent.dryRun) { 362 | overrides.dry_run = options.parent.dryRun; 363 | } 364 | var config = module.exports.rc(overrides); 365 | return new Api(config); 366 | }; 367 | 368 | 369 | module.exports.collect = function(val, memo) { 370 | memo.push(val); 371 | return memo; 372 | }; 373 | 374 | 375 | module.exports.rc = function() { 376 | return require('scaleway/node_modules/rc')('scw', { 377 | api_endpoint: 'https://api.scaleway.com/', 378 | organization: null, 379 | token: null 380 | }); 381 | }; 382 | 383 | 384 | module.exports.getVersion = function(module) { 385 | return require(module + '/package.json').version; 386 | }; 387 | 388 | 389 | module.exports.anonymizeUUID = function(uuid) { 390 | return uuid.replace(/^(.{4})(.{4})-(.{4})-(.{4})-(.{4})-(.{8})(.{4})$/, '$1-xxxx-$4-xxxx-xxxxxxxx$7'); 391 | }; 392 | 393 | 394 | module.exports.escapeShell = function(command) { 395 | if (typeof(command) !== 'string') { 396 | command = command.join(' '); 397 | } 398 | return '\'' + command.replace(/\'/g, "'\\''") + '\''; 399 | }; 400 | 401 | 402 | module.exports.sshExec = function(ip, command, options, fn) { 403 | options = options || {}; 404 | 405 | var args = []; 406 | 407 | if (!debug.enabled) { 408 | args.push('-q'); 409 | } 410 | 411 | args = args.concat.apply(args, ['-l', 'root', ip, '/bin/sh', '-e']); 412 | 413 | if (options.verbose) { 414 | args.push('-x'); 415 | } 416 | 417 | args.push('-c'); 418 | args.push(module.exports.escapeShell(command)); 419 | 420 | debug('spawn: ssh ' + args.join(' ')); 421 | var spawn = child_process.spawn( 422 | 'ssh', 423 | args, 424 | { stdio: 'inherit' } 425 | ); 426 | if (fn) { 427 | spawn.on('close', function(code) { 428 | return fn(code); 429 | }); 430 | } 431 | return spawn; 432 | }; 433 | 434 | 435 | module.exports.createServer = function(client, options) { 436 | return client.createServer(options) 437 | .then(function (res) { 438 | module.exports.saveEntity(res.body.server, 'servers'); 439 | console.log(res.body.server.id); 440 | }) 441 | .catch(panic); 442 | }; 443 | 444 | 445 | module.exports.getImageOrNewVolume = function(client, image, fn) { 446 | var ret; 447 | // Resolve image 448 | module.exports.searchEntity({input: image, _type: 'images'}, function(err, imageEntity) { 449 | if (err) { 450 | // err only means the image is not found. 451 | // when creating a server, if the image is not found we try to 452 | // create an image instead. 453 | var size; 454 | try { 455 | size = filesizeParser(image, {base: 10}); 456 | } catch (e) { 457 | size = 0; 458 | } 459 | assert(!size, err); 460 | return client.post('/volumes', { 461 | organization: client.config.organization, 462 | size: parseInt(size), 463 | name: image, 464 | volume_type: 'l_ssd' 465 | }).then(function(results) { 466 | ret = { 467 | 'volume': results.body.volume.id 468 | }; 469 | fn(ret); 470 | return ret; 471 | }).catch(panic); 472 | } else { 473 | ret = { 474 | 'image': imageEntity._id 475 | }; 476 | fn(ret); 477 | return ret; 478 | } 479 | }); 480 | }; 481 | 482 | 483 | module.exports.panicTimeout = function(delay, name) { 484 | if (!delay) { 485 | return null; 486 | } 487 | return setTimeout(function() { 488 | panic(name + '... failed: Operation timed out.'); 489 | }, delay * 1000); 490 | }; 491 | 492 | 493 | module.exports.serverExec = function(ip, command, commandArgs, options, fn) { 494 | var args = []; 495 | 496 | if (!debug.enabled) { 497 | args.push('-q'); 498 | } 499 | 500 | if (options.sshTimeout) { 501 | args = args.concat.apply(args, [ 502 | '-o', 'ConnectTimeout=' + options.sshTimeout 503 | ], commandArgs); 504 | } 505 | 506 | args = args.concat.apply(args, [ 507 | '-l', 'root', 508 | ip, '-t', '--', command], commandArgs); 509 | 510 | if (options.secure) { 511 | debug('Using secure SSH connection'); 512 | } else { 513 | args = [].concat.apply([ 514 | '-o', 'UserKnownHostsFile=/dev/null', 515 | '-o', 'StrictHostKeyChecking=no' 516 | ], args); 517 | } 518 | 519 | debug('spawn: ssh ' + args.join(' ')); 520 | 521 | if (options.parent.dryRun) { 522 | console.log('ssh ' + args.join(' ')); 523 | if (fn) { 524 | fn(); 525 | } 526 | } else { 527 | var spawn = child_process.spawn( 528 | 'ssh', 529 | args, 530 | { stdio: 'inherit' } 531 | ); 532 | if (fn) { 533 | spawn.on('close', fn); 534 | } 535 | } 536 | }; 537 | 538 | 539 | module.exports.findKeyInDeepObject = findKeyInDeepObject = function(obj, key) { 540 | if (_.has(obj, key)) { 541 | return obj[key]; 542 | } 543 | return _.flatten(_.map(obj, function(v) { 544 | return typeof v == "object" ? findKeyInDeepObject(v, key) : []; 545 | }), true)[0]; 546 | }; 547 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scaleway-cli", 3 | "version": "0.22.0", 4 | "description": "Interact with Scaleway API from commmand line", 5 | "main": "lib/program.js", 6 | "scripts": { 7 | "test": "mocha --reporter spec --ui tdd --bail --check-leaks ./test/all.js" 8 | }, 9 | "bin": { 10 | "scw": "./bin/scw" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/moul/scaleway-cli-node" 15 | }, 16 | "keywords": [ 17 | "api", 18 | "cli", 19 | "cloud", 20 | "labs", 21 | "online", 22 | "onlinelabs", 23 | "scaleway" 24 | ], 25 | "author": "Manfred Touron (@moul)", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/moul/scaleway-cli-node/issues" 29 | }, 30 | "homepage": "https://github.com/moul/scaleway-cli-node", 31 | "dependencies": { 32 | "JSONPath": "^0.10.0", 33 | "async": "^0.9.0", 34 | "attempt": "^1.0.1", 35 | "cli-table": "^0.3.1", 36 | "commander": "^2.8.0", 37 | "debug": "^2.1.3", 38 | "filesize": "^3.1.2", 39 | "filesize-parser": "^1.2.0", 40 | "lodash": "^3.6.0", 41 | "moment": "^2.10.2", 42 | "nedb": "^1.1.2", 43 | "portscanner": "^1.0.0", 44 | "q": "^1.2.0", 45 | "scaleway": "^0.6.0", 46 | "term.js-cli": "^0.3.0", 47 | "validator": "^3.37.0" 48 | }, 49 | "devDependencies": { 50 | "chai": "^2.1.2", 51 | "mocha": "^2.2.1", 52 | "test-console": "^0.7.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/all.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var chai = require('chai'), 4 | debug = require('debug')('tests'), 5 | program = require('..'), 6 | stdout = require('test-console').stdout, 7 | util = require('util'); 8 | 9 | 10 | // Initialize chai.should() 11 | chai.should(); 12 | 13 | 14 | var inspect = function(name, obj) { 15 | debug(name, util.inspect(obj, {showHidden: false, depth: null, colors: true})); 16 | }; 17 | 18 | 19 | var run = function(command) { 20 | var args = []; 21 | args = args.concat.apply(['node', 'scw'], command); 22 | var inspect = stdout.inspect(); 23 | program.parse(args); 24 | inspect.restore(); 25 | return inspect.output.join(''); 26 | }; 27 | 28 | 29 | suite("[program]", function() { 30 | test('info', function() { 31 | var output = run(['info']); 32 | (output).should.contain('User: ' + process.env['USER']); 33 | }); 34 | test('version', function() { 35 | var output = run(['version']); 36 | (output).should.contain('Client version: ' + require('../package.json').version); 37 | (output).should.contain('Client API version: ' + require('scaleway/package.json').version); 38 | }); 39 | }); 40 | --------------------------------------------------------------------------------