ruby-triton 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6ec6c7ecda93e72915f6370e0b5c8d89622a6993
4
+ data.tar.gz: ed330e27e36e6272b66360ae0a05b57c73982796
5
+ SHA512:
6
+ metadata.gz: ed8ef30ad0b7f605c6ff9c1ce5bd77b70e52a579f437c2cac38ba9f321f2bf37253306c0030114a9e9668f3ab8c93cce7ab94f36850d5194a7837a4eaf5cd241
7
+ data.tar.gz: 425d74abe4b5291e26d71db8e3172941ba824c35f7f90e953127c99537bdba995d5fda4890704134c1c1c4ef05812bf560fcec63119acc77d09c9fcd5ec044b9
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ The MIT License (MIT) Copyright (c) 2016 Les Technologies Alesium Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ ruby-triton
2
+ ===========
3
+
4
+ What's ruby-triton?
5
+ ------------------
6
+
7
+ ruby-triton is a client for communicating with cloudapi from Triton container
8
+ services to interact with the datacenter.
9
+
10
+ Acknolegement
11
+ =============
12
+
13
+ Joyent for creating Triton cloudapi
14
+ @dekobon for creating ruby-manta which was used as the base for this.
15
+
16
+
17
+ License
18
+ =======
19
+
20
+ (c) 2016 Les Technologies Alesium Inc, licensed under MIT. See LICENSE for
21
+ details, you legal geek you.
@@ -0,0 +1,17 @@
1
+ require_relative 'lib/ruby-triton'
2
+
3
+ host = ENV['SDC_URL']
4
+ account = ENV['SDC_ACCOUNT']
5
+ #subuser = ENV['SDC_USER']
6
+ priv_key = ENV['SDC_KEY' ]
7
+
8
+ priv_key_data = File.read(priv_key)
9
+
10
+ client = RubyTriton::CloudApiClient.new(host, account, priv_key_data,
11
+ :verify_ssl => false,
12
+ # :subuser => 'monte',
13
+ )
14
+
15
+ client.list_machines(:name => "lala").each do | instances |
16
+ puts instances.inspect
17
+ end
@@ -0,0 +1,5 @@
1
+ module RubyTriton
2
+ end
3
+ require_relative 'ruby-triton/cloudapi_client'
4
+
5
+ TritonClient = RubyTriton::CloudApiClient
@@ -0,0 +1,1665 @@
1
+ # Copyright (c) 2016, Les Technologies Alesium, Inc. All rights reserved.
2
+ #
3
+ # ruby-triton is a simple low-abstraction layer which communicates with Joyent's
4
+ # Triton container as a service.
5
+ #
6
+ # ruby-triton should be thread-safe, and supports pooling of keep-alive
7
+ # connections to the same server (through HTTPClient). It only relies on the
8
+ # standard library and two pure Ruby libraries, so it should work anywhere.
9
+ #
10
+ # For more information about Triton and general ruby-triton usage, please see
11
+ # README.md.
12
+
13
+
14
+
15
+ require 'openssl'
16
+ require 'net/ssh'
17
+ require 'rest-client'
18
+ require 'base64'
19
+ require 'digest'
20
+ require 'time'
21
+ require 'json'
22
+ require 'cgi'
23
+ require 'uri'
24
+
25
+ require File.expand_path('../version', __FILE__)
26
+
27
+
28
+ module RubyTriton
29
+ class CloudApiClient
30
+ DEFAULT_ATTEMPTS = 3
31
+ DEFAULT_OPEN_TIMEOUT = 20
32
+ DEFAULT_READ_TIMEOUT = 20
33
+ DEFAULT_VERIFY_SSL = OpenSSL::SSL::VERIFY_PEER
34
+
35
+ MAX_LIMIT = 1000
36
+ HTTP_AGENT = "ruby-triton/#{VERSION} (#{RUBY_PLATFORM}; #{OpenSSL::OPENSSL_VERSION}) ruby/#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"
37
+ HTTP_SIGNATURE = 'Signature keyId="/%s/keys/%s",algorithm="%s",signature="%s"'
38
+
39
+ CLOUDAPI_VERSION = 8
40
+ ## TODO: Add this check
41
+ #CLOUDAPI_PATH_REGEX = Regexp.new('^/[^/]+(?:/?/$|/keys|/users|/roles|/jobs)(?:/|$)')
42
+
43
+ ERROR_CLASSES = [ 'BadRequest', 'InternalError', 'InUseError',
44
+ 'InvalidArgument', 'InvalidCredentials', 'InvalidHeader',
45
+ 'InvalidVersion', 'MissingParameter', 'NotAuthorized',
46
+ 'RequestThrottled', 'RequestTooLarge', 'RequestMoved',
47
+ 'ResourceNotFound', 'UnknownError']
48
+
49
+
50
+
51
+
52
+ # Initialize a MantaClient instance.
53
+ #
54
+ # priv_key_data is data read directly from an SSH private key (i.e. RFC 4716
55
+ # format). The method can also accept several optional args: :connect_timeout,
56
+ # :send_timeout, :receive_timeout, :disable_ssl_verification and :attempts.
57
+ # The timeouts are in seconds, and :attempts determines the default number of
58
+ # attempts each method will make upon receiving recoverable errors.
59
+ #
60
+ # Will throw an exception if given a key whose format it doesn't understand.
61
+ def initialize(host, account, priv_key_data, opts = {})
62
+ raise ArgumentError unless host =~ /^https{0,1}:\/\/.*[^\/]/
63
+ raise ArgumentError unless account.is_a?(String) && account.size > 0
64
+
65
+ @host = host
66
+ @account = account
67
+ @subuser = opts[:subuser] ? opts[:subuser] : nil
68
+
69
+ @attempts = opts[:attempts] || DEFAULT_ATTEMPTS
70
+ raise ArgumentError unless @attempts > 0
71
+
72
+ if priv_key_data =~ /BEGIN RSA/
73
+ @digest = OpenSSL::Digest::SHA1.new
74
+ @digest_name = 'rsa-sha1'
75
+ algorithm = OpenSSL::PKey::RSA
76
+ elsif priv_key_data =~ /BEGIN DSA/
77
+ @digest = OpenSSL::Digest::DSS1.new
78
+ @digest_name = 'dsa-sha1'
79
+ algorithm = OpenSSL::PKey::DSA
80
+ else
81
+ raise UnsupportedKey
82
+ end
83
+
84
+ @priv_key = algorithm.new(priv_key_data)
85
+ @fingerprint = OpenSSL::Digest::MD5.hexdigest(@priv_key.to_blob).
86
+ scan(/../).join(':')
87
+
88
+
89
+ if opts[:verify_ssl] == false
90
+ verify_ssl = OpenSSL::SSL::VERIFY_NONE
91
+ end
92
+ @client = RestClient::Resource.new(@host,
93
+ :open_timeout => opts[:open_timeout] || DEFAULT_OPEN_TIMEOUT,
94
+ :read_timeout => opts[:read_timeout] || DEFAULT_READ_TIMEOUT,
95
+ :verify_ssl => verify_ssl || DEFAULT_VERIFY_SSL
96
+ )
97
+ end
98
+
99
+ ##
100
+ # Account
101
+ ##
102
+
103
+ # You can obtain your account details and update them through CloudAPI,
104
+ # although login cannot be changed, and password can not be retrieved.
105
+ #
106
+ # Retrieves your account details. Instead of providing your login name, you
107
+ # can also provide 'my' (i.e. GET /my).
108
+ def get_account(opts= {})
109
+ c = @client["#{@account}"]
110
+ headers = gen_headers(opts)
111
+ attempt(opts[:attempts]) do
112
+ do_get(c, headers)
113
+ end
114
+ end
115
+
116
+ # Update your account details with the given parameters.
117
+ #
118
+ # ==== Options
119
+ #
120
+ # * +:email+ - String
121
+ # * +:companyName+ - String
122
+ # * +:firstName - String
123
+ # * +:lastName - String
124
+ # * +:address - String
125
+ # * +:postalCode - String
126
+ # * +:city - String
127
+ # * +:state - String
128
+ # * +:country - String
129
+ # * +:phone - String
130
+ # * +:triton_cns_enabled - Boolean
131
+ def update_account(opts= {})
132
+ c = @client["#{@account}"]
133
+ headers = gen_headers(opts)
134
+ attempt(opts[:attempts]) do
135
+ do_post(c, headers, opts)
136
+ end
137
+ end
138
+
139
+ ##
140
+ # Keys
141
+ ##
142
+
143
+ # This part of the API is the means by which you operate on your SSH/signing
144
+ # keys. These keys are needed in order to login to instances over SSH, as
145
+ # well as signing requests to this API.
146
+ #
147
+ # Currently CloudAPI supports uploads of public keys in the OpenSSH format.
148
+ #
149
+ # Note that while it's possible to provide a name attribute for an SSH key
150
+ # in order to use it as a human-friendly alias, this attribute's presence is
151
+ # optional. When it's not provided, the ssh key fingerprint will be used as
152
+ # the name instead.
153
+ #
154
+ # For the following routes, the parameter placeholder :key can be replaced
155
+ # with with either the key's name or its fingerprint. It's strongly
156
+ # recommended to use fingerprint when possible, since the name attribute does
157
+ # not have any uniqueness constraints.
158
+ #
159
+ # Lists all public keys we have on record for the specified account.
160
+ def list_keys(opts= {})
161
+ c = @client["#{@user_path}/keys"]
162
+ headers = gen_headers(opts)
163
+ attempt(opts[:attempts]) do
164
+ do_get(c, headers)
165
+ end
166
+ end
167
+
168
+ # Retrieves the record for an individual key.
169
+ #
170
+ # ==== Attributes
171
+ #
172
+ # * +:key String - Public key in OpenSSH format
173
+ def get_key(key, opts= {})
174
+ raise ArgumentError unless key.is_a? String
175
+ c = @client["#{@user_path}/keys/#{key}"]
176
+ headers = gen_headers(opts)
177
+ attempt(opts[:attempts]) do
178
+ do_get(c, headers)
179
+ end
180
+ end
181
+
182
+ # Uploads a new OpenSSH key to Triton for use in HTTP signing and SSH.
183
+ #
184
+ # ==== Attributes
185
+ #
186
+ # * +:key String - Public key in OpenSSH format
187
+ #
188
+ # ==== Options
189
+ #
190
+ # * +:name - String Name for this key
191
+ def create_key(key, opts= {})
192
+ raise ArgumentError unless key.is_a? String
193
+ c = @client["#{@user_path}/keys"]
194
+ headers = gen_headers(opts)
195
+ opts[:key] = key
196
+ attempt(opts[:attempts]) do
197
+ do_post(c, headers, opts)
198
+ end
199
+ end
200
+
201
+ # Deletes a single SSH key, by name or fingerprint.
202
+ #
203
+ # ==== Attributes
204
+ #
205
+ # * +:key String - Public key in OpenSSH format
206
+ def delete_key(key, opts= {})
207
+ raise ArgumentError unless key.is_a? String
208
+ c = @client["#{@user_path}/keys/#{key}"]
209
+ headers = gen_headers(opts)
210
+ attempt(opts[:attempts]) do
211
+ do_delete(c, headers)
212
+ end
213
+ end
214
+
215
+ ##
216
+ # Users
217
+ ##
218
+ # These are users (also known as sub-users); additional users who are
219
+ # authorized to use the same account, but are subject to the RBAC system.
220
+
221
+ # Returns a list of an account's user objects. These have the same format
222
+ # as the main account object.
223
+ def list_users(opts= {})
224
+ c = @client["#{@account}/users"]
225
+ headers = gen_headers(opts)
226
+ attempt(opts[:attempts]) do
227
+ do_get(c, headers)
228
+ end
229
+ end
230
+
231
+ # Get one user for an account.
232
+ #
233
+ # ==== Attributes
234
+ #
235
+ # * +:user String - User name
236
+ def get_user(user, opts = {})
237
+ raise unless user.is_a? String
238
+ c = @client["#{@account}/users/#{user}"]
239
+ headers = gen_headers(opts)
240
+ attempt(opts[:attempts]) do
241
+ do_get(c, headers)
242
+ end
243
+
244
+ end
245
+
246
+ # Creates a new user under an account.
247
+ #
248
+ # ==== Attributes
249
+ #
250
+ # * +:email String - (Required) Email address
251
+ # * +:login String - (Required) Login
252
+ # * +:password String - (Required) Password
253
+ #
254
+ # ==== Options
255
+ #
256
+ # * +:companyName+ - String
257
+ # * +:firstName - String
258
+ # * +:lastName - String
259
+ # * +:address - String
260
+ # * +:postalCode - String
261
+ # * +:city - String
262
+ # * +:state - String
263
+ # * +:country - String
264
+ # * +:phone - String
265
+ def create_user(email, login, password, opts = {})
266
+ raise ArgumentError unless email.is_a? String
267
+ raise ArgumentError unless login.is_a? String
268
+ raise ArgumentError unless password.is_a? String
269
+
270
+ c = @client["#{@account}/users/#{user}"]
271
+ headers = gen_headers(opts)
272
+ opts[:email] = email
273
+ opts[:login] = login
274
+ opts[:password] = password
275
+ attempt(opts[:attempts]) do
276
+ do_post(c, headers, opts)
277
+ end
278
+
279
+ end
280
+
281
+ # Update a user's modifiable properties.
282
+ # Note: Password changes are not allowed using this endpoint; there is an
283
+ # additional methode (change_user_password) for password changes so it can
284
+ # be selectively allowed/disallowed for users using policies.
285
+ #
286
+ # ==== Attributes
287
+ #
288
+ # *+:id - String
289
+ #
290
+ # ==== Options
291
+ #
292
+ # * +:email - String
293
+ # * +:login - String
294
+ # * +:password - String
295
+ # * +:companyName+ - String
296
+ # * +:firstName - String
297
+ # * +:lastName - String
298
+ # * +:address - String
299
+ # * +:postalCode - String
300
+ # * +:city - String
301
+ # * +:state - String
302
+ # * +:country - String
303
+ # * +:phone - String
304
+ def update_user(id, opts = {})
305
+ raise ArgumentError unless id.is_a? String
306
+ c = @client["#{@account}/users/#{id}"]
307
+ headers = gen_headers(opts)
308
+ attempt(opts[:attempts]) do
309
+ do_post(c, headers, opts)
310
+ end
311
+
312
+ end
313
+
314
+ # This is a separate rule for password changes, so different policies can
315
+ # be used for an user trying to modify other data, or only their own password.
316
+ #
317
+ # ==== Attributes
318
+ #
319
+ # *+:user - String
320
+ # *+:password - String
321
+ # *+:password_confirmation - String
322
+ def change_user_password(user, password, password_confirmation, opts = {})
323
+ raise ArgumentError unless user.is_a? String
324
+ raise ArgumentError unless password.is_a? String
325
+ raise ArgumentError unless password_confirmation.is_a? String
326
+ raise InvalidArgument unless password != password_confirmation
327
+ c = @client["#{@account}/users/#{user}/change_password"]
328
+ headers = gen_headers(opts)
329
+ opts[:password] = password
330
+ opts[:password_confirmation] = password_confirmation
331
+ attempt(opts[:attempts]) do
332
+ do_post(c, headers, opts)
333
+ end
334
+
335
+ end
336
+
337
+ # Remove a user. They will no longer be able to use this API.
338
+ #
339
+ # ==== Attributes
340
+ #
341
+ # *+:user - String
342
+ def delete_user(user, opts = {})
343
+ raise ArgumentError unless user.is_a? String
344
+ c = @client["#{@account}/users/#{user}"]
345
+ headers = gen_headers(opts)
346
+ attempt(opts[:attempts]) do
347
+ do_delete(c, headers)
348
+ end
349
+
350
+ end
351
+
352
+ ##
353
+ # Roles
354
+ ##
355
+ # Roles a sub-users can adopt when attempting to access a resource. See the RBAC section for more details.
356
+
357
+ # Returns an array of account roles.
358
+ def list_roles(opts= {})
359
+ c = @client["#{@account}/roles"]
360
+ headers = gen_headers(opts)
361
+ attempt(opts[:attempts]) do
362
+ do_get(c, headers)
363
+ end
364
+ end
365
+
366
+ # Get an account role (:role) by id or name.
367
+ #
368
+ # ==== Attributes
369
+ #
370
+ # * +:role String - id or name of the role
371
+ def get_role(role, opts = {})
372
+ raise unless role.is_a? String
373
+ c = @client["#{@account}/roles/#{role}"]
374
+ headers = gen_headers(opts)
375
+ attempt(opts[:attempts]) do
376
+ do_get(c, headers)
377
+ end
378
+
379
+ end
380
+
381
+ # Create a new role for your account.
382
+ #
383
+ # ==== Attributes
384
+ #
385
+ # * +:name - String (Required) The role's name
386
+ #
387
+ # ==== Options
388
+ #
389
+ # * +:policies - Array This account's policies to be given to this role
390
+ # * +:members - Array This account's user logins to be added to this role (Optional)
391
+ # * +:default_members - Array This account's user logins to be added to this role and have it enabled by default (Optional)
392
+ def create_role(name, opts = {})
393
+ raise ArgumentError unless name.is_a? String
394
+ c = @client["#{@account}/roles"]
395
+ headers = gen_headers(opts)
396
+ attempt(opts[:attempts]) do
397
+ do_post(c, headers, opts)
398
+ end
399
+
400
+ end
401
+
402
+ # Modifies an account role. Anything but id can be modified.
403
+ #
404
+ # ==== Attributes
405
+ #
406
+ # *+:name - String role name or id
407
+ #
408
+ # ==== Options
409
+ #
410
+ # *+:policies - Array This account's policies to be given to this role (Optional)
411
+ # *+:members - Array This account's user logins to be added to this role (Optional)
412
+ # *+:default_members - Array This account's user logins to be added to this role and have it enabled by default (Optional)
413
+ def update_role(name, opts = {})
414
+ raise ArgumentError unless name.is_a? String
415
+ c = @client["#{@account}/roles/#{name}"]
416
+ headers = gen_headers(opts)
417
+ attempt(opts[:attempts]) do
418
+ do_post(c, headers, opts)
419
+ end
420
+
421
+ end
422
+
423
+ # Remove a role. :role must be the role id (a UUID).
424
+ #
425
+ # ==== Attributes
426
+ #
427
+ # *+:role - String UUID of the role
428
+ def delete_role(role, opts = {})
429
+ raise ArgumentError unless role.is_a? String
430
+ c = @client["#{@account}/roles/#{role}"]
431
+ headers = gen_headers(opts)
432
+ attempt(opts[:attempts]) do
433
+ do_delete(c, headers)
434
+ end
435
+
436
+ end
437
+
438
+ ##
439
+ # Role tags
440
+ ##
441
+
442
+ # Sets the given role tags to the provided resource path. resource_path can
443
+ # be the path to any of the CloudAPI resources described in this document:
444
+ # account, keys, users, roles, policies, user's ssh keys, datacenters,
445
+ # images, packages, instances, analytics, instrumentations, firewall rules
446
+ # and networks.
447
+ # For each of these you can set role tags either for an individual resource
448
+ # or for the whole group.
449
+ #
450
+ # ==== Attributes
451
+ #
452
+ # * +:resource_url - String (Required) The resource path to assign a role tags to.
453
+ # * +:role-tag - Array (Required) Array of tags to be assigned/modified
454
+ def create_role_tags(resource_url, role-tag, opts = {})
455
+ raise ArgumentError unless resource_url.is_a? String
456
+ raise ArgumentError unless role-tag.is_a? Array
457
+ c = @client[resource_url]
458
+ headers = gen_headers(opts)
459
+ opts['role-tag'] = role-tag
460
+ attempt(opts[:attempts]) do
461
+ do_put(c, headers, opts)
462
+ end
463
+
464
+ end
465
+
466
+ ##
467
+ # Policies
468
+ ##
469
+ # Retrieves a list of account policies.
470
+ def list_policies(opts= {})
471
+ c = @client["#{@account}/policies"]
472
+ headers = gen_headers(opts)
473
+ attempt(opts[:attempts]) do
474
+ do_get(c, headers)
475
+ end
476
+ end
477
+
478
+ # Get an account policy (:policy) by id.
479
+ #
480
+ # ==== Attributes
481
+ #
482
+ # * +:policy - String id of the policy
483
+ def get_policy(policy, opts = {})
484
+ raise ArgumentError unless policy.is_a? String
485
+ c = @client["#{@account}/policies/#{policy}"]
486
+ headers = gen_headers(opts)
487
+ attempt(opts[:attempts]) do
488
+ do_get(c, headers)
489
+ end
490
+
491
+ end
492
+
493
+ # Creates a new account policy.
494
+ #
495
+ # ==== Attributes
496
+ #
497
+ # * +:name - String The policy name
498
+ # * +:rules - One or more Aperture sentences to be added to the current policy
499
+ #
500
+ # ==== Options
501
+ #
502
+ # * +:description - String A description for this policy (Optional)
503
+ def create_policy(name, rules, opts = {})
504
+ raise ArgumentError unless name.is_a? String
505
+ raise ArgumentError unless rules.is_a? Array
506
+ c = @client["#{@account}/policies"]
507
+ headers = gen_headers(opts)
508
+ opts[:rules] = rules
509
+ attempt(opts[:attempts]) do
510
+ do_post(c, headers, opts)
511
+ end
512
+
513
+ end
514
+
515
+ # Updates an existing account policy. Everything but id can be modified.
516
+ #
517
+ # ==== Attributes
518
+ #
519
+ # * +:policy - String id of the policy
520
+ #
521
+ # ==== Options
522
+ #
523
+ # * +:name - String The policy name
524
+ # * +:rules - One or more Aperture sentences to be added to the current policy
525
+ # * +:description - String A description for this policy (Optional)
526
+ def update_policy(policy, opts = {})
527
+ raise ArgumentError unless name.is_a? String
528
+ c = @client["#{@account}/policies/#{policy}"]
529
+ headers = gen_headers(opts)
530
+ attempt(opts[:attempts]) do
531
+ do_post(c, headers, opts)
532
+ end
533
+
534
+ end
535
+
536
+ # Delete an RBAC policy. :policy must be the policy id (a UUID).
537
+ #
538
+ # ==== Attributes
539
+ #
540
+ # *+:policy - String UUID of the role
541
+ def delete_policy(policy, opts = {})
542
+ raise ArgumentError unless policy.is_a? String
543
+ c = @client["#{@account}/policies/#{policy}"]
544
+ headers = gen_headers(opts)
545
+ attempt(opts[:attempts]) do
546
+ do_delete(c, headers)
547
+ end
548
+
549
+ end
550
+
551
+ ##
552
+ # User SSH Keys
553
+ ##
554
+ # See account keys for a detailed description. Only difference is the path
555
+ # from where you can access users' keys:
556
+
557
+ # Lists all public keys we have on record for the specified account user.
558
+ # See list_keys.
559
+ def list_user_keys(opts= {})
560
+ raise unless @subuser is not nil
561
+ c = @client["#{@account}/users/#{@subuser}/keys"]
562
+ headers = gen_headers(opts)
563
+ attempt(opts[:attempts]) do
564
+ do_get(c, headers)
565
+ end
566
+ end
567
+
568
+ # Retrieves the given key record either by fingerprint or name. See get_key.
569
+ #
570
+ # ==== Attributes
571
+ #
572
+ # * +:key - String id or fingerprint of key
573
+ def get_user_key(key, opts = {})
574
+ raise InvalidCredentials unless @subuser is not nil
575
+ raise ArgumentError unless key.is_a? String
576
+ c = @client["#{@account}/users/#{@subuser}/keys/#{key}"]
577
+ headers = gen_headers(opts)
578
+ attempt(opts[:attempts]) do
579
+ do_get(c, headers)
580
+ end
581
+
582
+ end
583
+
584
+ # Creates a new key record. See create_key.
585
+ #
586
+ # ==== Attributes
587
+ #
588
+ # * +:key - String Public key in OpenSSH format
589
+ #
590
+ # ==== Options
591
+ #
592
+ # * +:name - String Name for this key
593
+ def create_user_key(key, opts = {})
594
+ raise InvalidCredentials unless @subuser is not nil
595
+ raise ArgumentError unless key.is_a? String
596
+ c = @client["#{@account}/users/#{@subuser}/keys"]
597
+ headers = gen_headers(opts)
598
+ opts[:key] = key
599
+ attempt(opts[:attempts]) do
600
+ do_post(c, headers, opts)
601
+ end
602
+
603
+ end
604
+
605
+ # Removes a key. See get_key.
606
+ #
607
+ # ==== Attributes
608
+ #
609
+ # *+:key - String id or fingerprint of key
610
+ def delete_user_key(key, opts = {})
611
+ raise InvalidCredentials unless @subuser is not nil
612
+ raise ArgumentError unless key.is_a? String
613
+ c = @client["#{@account}/users/#{@subuser}/keys/#{key}"]
614
+ headers = gen_headers(opts)
615
+ attempt(opts[:attempts]) do
616
+ do_delete(c, headers)
617
+ end
618
+
619
+ end
620
+
621
+
622
+ ##
623
+ # Config
624
+ ##
625
+ # These endpoints allow you to get and set configuration values related to
626
+ # your account.
627
+
628
+ # Outputs configuration for your account. The configuration values that are
629
+ # currently configurable are:
630
+ # default_network: the network that docker containers are provisioned on.
631
+ def get_config(opts = {})
632
+ c = @client["#{@account}/config"]
633
+ headers = gen_headers(opts)
634
+ attempt(opts[:attempts]) do
635
+ do_get(c, headers)
636
+ end
637
+
638
+ end
639
+
640
+ # Updates configuration values for your account.
641
+ #
642
+ # ==== Attributes
643
+ #
644
+ # * +:default_network - String ID of the network used for provisioning docker containers
645
+ def update_config(default_network, opts = {})
646
+ raise ArgumentError unless default_network.is_a? String
647
+ c = @client["#{@account}/config"]
648
+ headers = gen_headers(opts)
649
+ opts[:default_network] = default_network
650
+ attempt(opts[:attempts]) do
651
+ do_put(c, headers, opts)
652
+ end
653
+
654
+ end
655
+
656
+ ##
657
+ # datacenters
658
+ ##
659
+
660
+ # Provides a list of all datacenters this cloud is aware of.
661
+ def list_datacenters(opts= {})
662
+ c = @client["#{@account}/datacenters"]
663
+ headers = gen_headers(opts)
664
+ attempt(opts[:attempts]) do
665
+ do_get(c, headers)
666
+ end
667
+ end
668
+
669
+ # Gets an individual datacenter by name. Returns an HTTP redirect to
670
+ # your client, where the datacenter url is in the Location header.
671
+ #
672
+ # ==== Attributes
673
+ #
674
+ # * +:name - String datacenter name
675
+ def get_datacenter(name, opts = {})
676
+ raise ArgumentError unless name.is_a? String
677
+ c = @client["#{@account}/datacenters/#{name}"]
678
+ headers = gen_headers(opts)
679
+ attempt(opts[:attempts]) do
680
+ raise InvalidArgument unless c.is_a? RestClient::Resource
681
+ result = c.get(headers)
682
+ raise InternalError unless result.is_a? RestClient::Response
683
+
684
+ if result.code == 302
685
+ return JSON.parse(result.body)
686
+ end
687
+
688
+ raise_error(result)
689
+ end
690
+
691
+ end
692
+
693
+ ##
694
+ # Services
695
+ ##
696
+
697
+ # Provides the URL endpoints for services for this datacenter. It is a
698
+ # mapping of service name to URL endpoint.
699
+ def list_services(opts= {})
700
+ c = @client["#{@account}/services"]
701
+ headers = gen_headers(opts)
702
+ attempt(opts[:attempts]) do
703
+ do_get(c, headers)
704
+ end
705
+ end
706
+
707
+ ##
708
+ # Images
709
+ ##
710
+ # An image contains the software packages that will be available on
711
+ # newly-provisioned instance. In the case of hardware virtual machines,
712
+ # the image also includes the operating system.
713
+
714
+ # Provides a list of images available in this datacenter.
715
+ # Note: Currently, Docker images are not included in this endpoint's
716
+ # responses. You must use docker images against the docker service for this
717
+ # datacenter.
718
+ #
719
+ # ==== Options
720
+ #
721
+ # * +:name - String The "friendly" name for this image
722
+ # * +:os - String The underlying operating system for this image
723
+ # * +:version - String The version for this image
724
+ # * +:public - Boolean Filter public/private images
725
+ # * +:state - String Filter on image state. By default only active images are shown. Use ?state=all to list all images
726
+ # * +:owner - String Filter on owner UUID
727
+ # * +:type - String Filter on image type. The types changed in v8.0.0
728
+ def list_images(opts= {})
729
+ url = "#{@account}/images"
730
+ if opts.size > 0
731
+ url = url + '?' + URI.encode_www_form(opts)
732
+ end
733
+ c = @client[url]
734
+ headers = gen_headers(opts)
735
+ attempt(opts[:attempts]) do
736
+ do_get(c, headers)
737
+ end
738
+ end
739
+
740
+ # Gets an individual image by id.
741
+ #
742
+ # ==== Attributes
743
+ #
744
+ # * +:image - String id of the image
745
+ def get_image(image, opts = {})
746
+ raise ArgumentError unless image.is_a? String
747
+ c = @client["#{@account}/images/#{image}"]
748
+ headers = gen_headers(opts)
749
+ attempt(opts[:attempts]) do
750
+ do_get(c, headers)
751
+ end
752
+
753
+ end
754
+
755
+ # Delete an image. Caller must be the owner of the image to delete it.
756
+ #
757
+ # ==== Attributes
758
+ #
759
+ # *+:image - String id of the image
760
+ def delete_image(image, opts = {})
761
+ raise ArgumentError unless image.is_a? String
762
+ c = @client["#{@account}/images/#{policy}"]
763
+ headers = gen_headers(opts)
764
+ attempt(opts[:attempts]) do
765
+ do_delete(c, headers)
766
+ end
767
+
768
+ end
769
+
770
+ # Exports an image to the specified Manta path. Caller must be the owner of
771
+ # the image, and the correspondent Manta path prefix, in order to export it.
772
+ # Both the image manifest and the image file will be exported, and their
773
+ # filenames will default to the following format when the specified manta
774
+ # path is a directory
775
+ #
776
+ # ==== Attributes
777
+ #
778
+ # * +:image - String id of the image
779
+ # * +:manta_path - String Manta path prefix used when exporting the image
780
+ def export_image(image, manta_path, opts = {})
781
+ raise ArgumentError unless image.is_a? String
782
+ raise ArgumentError unless manta_path.is_a? String
783
+ c = @client["#{@account}/images/#{image}?action=export"]
784
+ headers = gen_headers(opts)
785
+ opts[:manta_path] = manta_path
786
+ attempt(opts[:attempts]) do
787
+ do_post(c, headers, opts)
788
+ end
789
+
790
+ end
791
+
792
+ # Create a new custom image from an instance. The typical process is:
793
+ #
794
+ # 1. Customize an instance the way you want it.
795
+ # 2. Call this method (create_image_from_machine) to create a new image.
796
+ # 3. Repeat from step 1 if more customizations are desired with different images.
797
+ # 4. Use the new image(s) for provisioning via create_machine.
798
+ #
799
+ # ==== Attributes
800
+ #
801
+ # * +:machine - String The prepared and stopped instance UUID from which the image is to be created
802
+ # * +:name - String The name of the custom image, e.g. "my-image". See the IMGAPI docs for details
803
+ # * +:version - String The version of the custom image, e.g. "1.0.0". See the IMGAPI docs for details
804
+ #
805
+ # ==== Options
806
+ #
807
+ # * +:description - String The image description
808
+ # * +:homepage - String The image homepage
809
+ # * +:eula - String The image eula
810
+ # * +:acl - String The image acl
811
+ # * +:tags - String The image tags
812
+ def create_image_from_machine(machine, name, version, opts = {})
813
+ raise ArgumentError unless machine.is_a? String
814
+ raise ArgumentError unless name.is_a? String
815
+ raise ArgumentError unless version.is_a? String
816
+ c = @client["#{@account}/images"]
817
+ headers = gen_headers(opts)
818
+ opts[:machine] = machine
819
+ opts[:name] = name
820
+ opts[:version] = version
821
+ attempt(opts[:attempts]) do
822
+ do_post(c, headers, opts)
823
+ end
824
+
825
+ end
826
+
827
+ # Updates metadata about an image.
828
+ #
829
+ # ==== Attributes
830
+ #
831
+ # * +:machine - String The prepared and stopped instance UUID from which the image is to be created
832
+ #
833
+ # ==== Options
834
+ #
835
+ # * +:name - String The name of the custom image, e.g. "my-image". See the IMGAPI docs for details
836
+ # * +:version - String The version of the custom image, e.g. "1.0.0". See the IMGAPI docs for details
837
+ # * +:description - String The image description
838
+ # * +:homepage - String The image homepage
839
+ # * +:eula - String The image eula
840
+ # * +:acl - String The image acl
841
+ # * +:tags - String The image tags
842
+ def update_image(machine, opts = {})
843
+ raise ArgumentError unless machine.is_a? String
844
+ c = @client["#{@account}/images/#{image}?action=update"]
845
+ headers = gen_headers(opts)
846
+ attempt(opts[:attempts]) do
847
+ do_post(c, headers, opts)
848
+ end
849
+
850
+ end
851
+
852
+ ##
853
+ # Packages
854
+ ##
855
+ # Packages are named collections of resources that are used to describe the
856
+ # dimensions of either a container or a hardware virtual machine. These
857
+ # resources include (but are not limited to) RAM size, CPUs, CPU caps,
858
+ # lightweight threads, disk space, swap size, and logical networks.
859
+
860
+ # Provides a list of packages available in this datacenter.
861
+ #
862
+ # ==== Options
863
+ #
864
+ # * +:name - String The "friendly" name for this package
865
+ # * +:memory - Number How much memory will by available (in MiB)
866
+ # * +:disk - Number How much disk space will be available (in MiB)
867
+ # * +:swap - Number How much swap space will be available (in MiB)
868
+ # * +:lwps - Number Maximum number of light-weight processes (threads) allowed
869
+ # * +:vcpus - Number Number of vCPUs for this package
870
+ # * +:version - String The version of this package
871
+ # * +:group - String The group this package belongs to
872
+ def list_packages(opts= {})
873
+ url = "#{@account}/packages"
874
+ if opts.size > 0
875
+ url = url + '?' + URI.encode_www_form(opts)
876
+ end
877
+ c = @client[url]
878
+ headers = gen_headers(opts)
879
+ attempt(opts[:attempts]) do
880
+ do_get(c, headers)
881
+ end
882
+ end
883
+
884
+ # Gets an individual image by id.
885
+ #
886
+ # ==== Attributes
887
+ #
888
+ # * +:package - String id of the package
889
+ def get_package(package, opts = {})
890
+ raise ArgumentError unless package.is_a? String
891
+ c = @client["#{@account}/packages/#{package}"]
892
+ headers = gen_headers(opts)
893
+ attempt(opts[:attempts]) do
894
+ do_get(c, headers)
895
+ end
896
+
897
+ end
898
+
899
+ ##
900
+ # Instances
901
+ ##
902
+ # Triton supports three different types of instances:
903
+ #
904
+ # Docker containers. OS-virtualized instances managed through the Docker client.
905
+ # Infrastructure containers. More traditional OS-virtualized instances
906
+ # running SmartOS or more Linux distributions.
907
+ # Hardware-virtualized machines. Hardware-virtualized instances (KVM) for
908
+ # running legacy or special-purpose operating systems.
909
+ #
910
+ # Infrastructure and Docker containers are lightweight, offering the most
911
+ # performance, observability and operational flexibility. Harware-virtualized
912
+ # machines are useful for non-SmartOS or non-Linux stacks.
913
+
914
+ # Lists all instances we have on record for your account. If you have a large
915
+ # number of instances, you can filter using the input parameters listed below.
916
+ # Note that deleted instances are returned only if the instance history has
917
+ # not been purged from Triton.
918
+ #
919
+ # You can paginate this API by passing in offset and limit. HTTP responses
920
+ # will contain the additional headers x-resource-count and x-query-limit.
921
+ # If x-resource-count is less than x-query-limit, you're done, otherwise call
922
+ # the API again with offset set to offset + limit to fetch additional instances.
923
+ #
924
+ # Note that there is a opts[:method] = :head form of this API, so you can
925
+ # retrieve the number of instances without retrieving a JSON describing the
926
+ # instances themselves.
927
+ #
928
+ # ==== Options
929
+ # *+:type - String (deprecated) The type of instance (virtualmachine or smartmachine)
930
+ # *+:brand - String (v8.0+) The type of instance (e.g. lx)
931
+ # *+:name - String Machine name to find (will make your list size 1, or 0 if nothing found)
932
+ # *+:image - String Image id; returns instances provisioned with that image
933
+ # *+:state - String The current state of the instance (e.g. running)
934
+ # *+:memory - Integer The current size of the RAM deployed for the instance (in MiB)
935
+ # *+:tombstone - Boolean Include destroyed and failed instances available in instance history
936
+ # *+:limit - Number Return a max of N instances; default is 1000 (which is also the maximum allowable result set size)
937
+ # *+:offset - Number Get a limit number of instances starting at this offset
938
+ # *+:tag.$name - String An arbitrary set of tags can be used for querying, assuming they are prefixed with "tag."
939
+ # *+:docker - Boolean Whether to only list Docker instances, or only non-Docker instances, if present. Defaults to showing all instances.
940
+ # *+:credentials -Boolean Whether to include the generated credentials for instances, if present. Defaults to false
941
+ def list_machines(opts= {})
942
+ url = "#{@account}/machines"
943
+ # TODO fix for head query
944
+ opts[:limit] = opts[:limit] ? MAX_LIMIT
945
+ raise ArgumentError unless 0 < opts[:limit] && opts[:limit] <= MAX_LIMIT
946
+ if opts.size > 0
947
+ url = url + '?' + URI.encode_www_form(opts)
948
+ end
949
+ c = @client[url]
950
+ headers = gen_headers(opts)
951
+ attempt(opts[:attempts]) do
952
+ if opts[:head]
953
+ do_head(c, headers)
954
+ else
955
+ do_get(c, headers)
956
+ end
957
+ end
958
+ end
959
+
960
+
961
+ # Gets the details for an individual instance.
962
+ #
963
+ # Deleted instances are returned only if the instance history has not been
964
+ # purged from Triton.
965
+ #
966
+ # ==== Attributes
967
+ #
968
+ # * +:instance - String id of the instance
969
+ def get_instance(instance, opts = {})
970
+ raise ArgumentError unless instance.is_a? String
971
+ c = @client["#{@account}/instances/#{instance}"]
972
+ headers = gen_headers(opts)
973
+ attempt(opts[:attempts]) do
974
+ do_get(c, headers)
975
+ end
976
+
977
+ end
978
+
979
+ # Allows you to provision an instance.
980
+ # If you do not specify a name, CloudAPI will generate a random one for you.
981
+ # If you have enabled Triton CNS on your account, this name will also be
982
+ # used in DNS to refer to the new instance (and must therefore consist of
983
+ # DNS-safe characters only).
984
+ #
985
+ # Your instance will initially be not available for login (Triton must
986
+ # provision and boot it); you can poll GetMachine for its status. When the
987
+ # state field is equal to running, you can log in. If the instance is a
988
+ # brand other than kvm, you can usually use any of the SSH keys managed
989
+ # under the keys section of CloudAPI to login as any POSIX user on the OS.
990
+ # You can add/remove keys over time, and the instance will automatically
991
+ # work with that set.
992
+ #
993
+ # If the the instance has a brand kvm, and of a UNIX-derived OS (e.g. Linux),
994
+ # you must have keys uploaded before provisioning; that entire set of keys
995
+ # will be written out to /root/.ssh/authorized_keys in the new instance,
996
+ # and you can SSH in using one of those keys. Changing the keys over time
997
+ # under your account will not affect a running hardware virtual machine in
998
+ # any way; those keys are statically written at provisioning-time only, and
999
+ # you will need to manually manage them on the instance itself.
1000
+ #
1001
+ # ==== Attributes
1002
+ #
1003
+ # * +:image - String id of the image
1004
+ # * +:package - String id of the packge
1005
+ #
1006
+ # ==== Options
1007
+ #
1008
+ # * +:name - String Friendly name for this instance; default is the first 8 characters of the machine id
1009
+ # * +:networks - Array Desired networks ids, obtained from list_networks
1010
+ # * +:locality - Object[String => Array] Optionally specify which instances the new instance should be near or far from
1011
+ # * +:metadata.$name - String An arbitrary set of metadata key/value pairs can be set at provision time, but they must be prefixed with "metadata."
1012
+ # * +:tag.$name - String An arbitrary set of tags can be set at provision time, but they must be prefixed with "tag."
1013
+ # * +:firewall_enabled - Boolean Completely enable or disable firewall for this instance. Default is false
1014
+
1015
+ def create_machine(image, package, opts= {})
1016
+ raise ArgumentError unless image.is_a? String
1017
+ raise ArgumentError unless package.is_a? String
1018
+ c = @client["#{@account}/machines"]
1019
+ headers = gen_headers(opts)
1020
+ opts[:image] = machine
1021
+ opts[:package] = name
1022
+ attempt(opts[:attempts]) do
1023
+ do_post(c, headers, opts)
1024
+ end
1025
+
1026
+ end
1027
+
1028
+ # Allows you to shut down an instance. POST to the instance name with an
1029
+ # action of stop.
1030
+ #
1031
+ # You can poll on get_machine until the state is stopped.
1032
+ # ==== Attributes
1033
+ #
1034
+ # * +:machine - String id of the machine
1035
+ def stop_machine(machine, opts= {})
1036
+ raise ArgumentError unless machine.is_a? String
1037
+ c = @client["#{@account}/machines/#{machine}?action=stop"]
1038
+ attempt(opts[:attempts]) do
1039
+ do_post(c, headers, opts)
1040
+ end
1041
+
1042
+ end
1043
+
1044
+ # Allows you to boot up an instance. POST to the instance name with an
1045
+ # action of start.
1046
+ #
1047
+ # You can poll on get_machine until the state is running.
1048
+ # ==== Attributes
1049
+ #
1050
+ # * +:machine - String id of the machine
1051
+ def start_machine(machine, opts= {})
1052
+ raise ArgumentError unless machine.is_a? String
1053
+ c = @client["#{@account}/machines/#{machine}?action=start"]
1054
+ attempt(opts[:attempts]) do
1055
+ do_post(c, headers, opts)
1056
+ end
1057
+
1058
+ end
1059
+
1060
+ # Allows you to reboot an instance. POST to the instance name with an
1061
+ # action of reboot.
1062
+ #
1063
+ # You can poll on get_machine until the state is running.
1064
+ # ==== Attributes
1065
+ #
1066
+ # * +:machine - String id of the machine
1067
+ def reboot_machine(machine, opts= {})
1068
+ raise ArgumentError unless machine.is_a? String
1069
+ c = @client["#{@account}/machines/#{machine}?action=reboot"]
1070
+ attempt(opts[:attempts]) do
1071
+ do_post(c, headers, opts)
1072
+ end
1073
+
1074
+ end
1075
+
1076
+
1077
+ # Resize an instance to a new package (a.k.a. instance type).
1078
+ #
1079
+ # Resizing is only supported for containers (instances which are not
1080
+ # hardware virtual machines -- they have brand=kvm). Hardware virtual machines
1081
+ # cannot be resized.
1082
+ #
1083
+ # Resizing is not guaranteed to work, especially when resizing upwards in
1084
+ # resources. It is best-effort, and may fail. Resizing downwards will usually
1085
+ # succeed.
1086
+ # ==== Attributes
1087
+ #
1088
+ # * +:machine - String id of the machine
1089
+ # * +:package - String A package id, as returned from list_packages
1090
+ def resize_machine(machine, package, opts= {})
1091
+ raise ArgumentError unless machine.is_a? String
1092
+ raise ArgumentError unless package.is_a? String
1093
+ c = @client["#{@account}/machines/#{machine}?action=resize"]
1094
+ opts['package'] = package
1095
+ attempt(opts[:attempts]) do
1096
+ do_post(c, headers, opts)
1097
+ end
1098
+
1099
+ end
1100
+
1101
+ # Allows you to rename an instance. POST to the instance id with an action
1102
+ # of rename. You must additionally include a new name for the instance.
1103
+ #
1104
+ # ==== Attributes
1105
+ #
1106
+ # * +:machine - String id of the machine
1107
+ # * +:name - String The new "friendly" name for this instance
1108
+ def rename_machine(machine, name, opts= {})
1109
+ raise ArgumentError unless machine.is_a? String
1110
+ raise ArgumentError unless name.is_a? String
1111
+ c = @client["#{@account}/machines/#{machine}?action=rename"]
1112
+ opts['name'] = name
1113
+ attempt(opts[:attempts]) do
1114
+ do_post(c, headers, opts)
1115
+ end
1116
+
1117
+ end
1118
+
1119
+ # Allows you to enable the firewall for an instance.
1120
+ #
1121
+ # ==== Attributes
1122
+ #
1123
+ # * +:machine - String id of the machine
1124
+ def enable_machine_firewall(machine, opts= {})
1125
+ raise ArgumentError unless machine.is_a? String
1126
+ c = @client["#{@account}/machines/#{machine}?action=enable_firewall"]
1127
+ attempt(opts[:attempts]) do
1128
+ do_post(c, headers, opts)
1129
+ end
1130
+
1131
+ end
1132
+
1133
+ # Allows you to completely disable the firewall of an instance.
1134
+ #
1135
+ # ==== Attributes
1136
+ #
1137
+ # * +:machine - String id of the machine
1138
+ def disable_machine_firewall(machine, opts= {})
1139
+ raise ArgumentError unless machine.is_a? String
1140
+ c = @client["#{@account}/machines/#{machine}?action=disable_firewall"]
1141
+ attempt(opts[:attempts]) do
1142
+ do_post(c, headers, opts)
1143
+ end
1144
+
1145
+ end
1146
+
1147
+ # Allows you to take a snapshot of an instance. Once you have one or more
1148
+ # snapshots, you can boot the instance from a previous snapshot.
1149
+ #
1150
+ # Snapshots are not usable with other instances; they are a point-in-time
1151
+ # snapshot of the current instance. Snapshots can also only be taken of
1152
+ # instances that are not of brand 'kvm'.
1153
+ #
1154
+ # Since instance instances use a copy-on-write filesystem, snapshots take up
1155
+ # increasing amounts of space as the filesystem changes over time. There is
1156
+ # a limit to how much space snapshots are allowed to take. Plan your
1157
+ # snapshots accordingly.
1158
+ #
1159
+ # You can poll on get_machine_snapshot until the state is created.
1160
+ #
1161
+ # ==== Attributes
1162
+ #
1163
+ # * +:machine - String id of the machine
1164
+ #
1165
+ # ==== Options
1166
+ #
1167
+ # * +:name - String The name to assign to the new snapshot
1168
+ def create_machine_snapshot(machine, opts= {})
1169
+ raise ArgumentError unless machine.is_a? String
1170
+ c = @client["#{@account}/machines/#{machine}/snapshots"]
1171
+ attempt(opts[:attempts]) do
1172
+ do_post(c, headers, opts)
1173
+ end
1174
+
1175
+ end
1176
+
1177
+ # If an instance is in the 'stopped' state, you can choose to start the
1178
+ # instance from the referenced snapshot. This is effectively a means to
1179
+ # roll back instance state.
1180
+ #
1181
+ # ==== Attributes
1182
+ #
1183
+ # * +:machine - String id of the machine
1184
+ # * +:snapshot - String The name of the snapshot
1185
+ #
1186
+ # ==== Options
1187
+ #
1188
+ # * +:name - String The name to assign to the new snapshot
1189
+ def start_machine_from_snapshot(machine, snapshot, opts= {})
1190
+ raise ArgumentError unless machine.is_a? String
1191
+ raise ArgumentError unless snapshot.is_a? String
1192
+ c = @client["#{@account}/machines/#{machine}/snapshots/#{snapshot}"]
1193
+ attempt(opts[:attempts]) do
1194
+ do_post(c, headers, opts)
1195
+ end
1196
+
1197
+ end
1198
+
1199
+ # Lists all snapshots taken for a given instance. There are no filtration
1200
+ # parameters for this API.
1201
+ #
1202
+ # ==== Attributes
1203
+ #
1204
+ # * +:machine - String id of the machine
1205
+ def list_machine_snapshots(machine, opts= {})
1206
+ raise ArgumentError unless machine.is_a? String
1207
+ c = @client["#{@account}/machines/#{machine}/snapshots"]
1208
+ attempt(opts[:attempts]) do
1209
+ do_get(c, headers)
1210
+ end
1211
+
1212
+ end
1213
+
1214
+ # Gets the state of the named snapshot.
1215
+ #
1216
+ # ==== Attributes
1217
+ #
1218
+ # * +:machine - String id of the machine
1219
+ # * +:snapshot - String The name of the snapshot
1220
+ def get_machine_snapshots(machine, snapshot, opts= {})
1221
+ raise ArgumentError unless machine.is_a? String
1222
+ raise ArgumentError unless snapshot.is_a? String
1223
+ c = @client["#{@account}/machines/#{machine}/snapshots/#{snapshot}"]
1224
+ attempt(opts[:attempts]) do
1225
+ do_get(c, headers)
1226
+ end
1227
+
1228
+ end
1229
+
1230
+ # Deletes the specified snapshot of an instance.
1231
+ #
1232
+ # ==== Attributes
1233
+ #
1234
+ # * +:machine - String id of the machine
1235
+ # * +:snapshot - String The name of the snapshot
1236
+ def delete_machine_snapshot(machine, snapshot, opts= {})
1237
+ raise ArgumentError unless machine.is_a? String
1238
+ raise ArgumentError unless snapshot.is_a? String
1239
+ c = @client["#{@account}/machines/#{machine}/snapshots/#{snapshot}"]
1240
+ attempt(opts[:attempts]) do
1241
+ do_delete(c, headers)
1242
+ end
1243
+
1244
+ end
1245
+
1246
+ # Allows you to update the metadata for a given instance. Note that updating
1247
+ # the metadata via CloudAPI will result in the metadata being updated in the
1248
+ # running instance.
1249
+ #
1250
+ # The semantics of this call are subtly different that the add_machine_tags
1251
+ # call -- any metadata keys passed in here are created if they do not exist,
1252
+ # and overwritten if they do.
1253
+ #
1254
+ # ==== Attributes
1255
+ #
1256
+ # * +:machine - String id of the machine
1257
+ # * +:keys - String or Json object of keys to update
1258
+ #
1259
+ # ==== Options
1260
+ #
1261
+ # * +:name - String The name to assign to the new snapshot
1262
+ def update_machine_metadata(machine, keys, opts= {})
1263
+ raise ArgumentError unless machine.is_a? String
1264
+ raise ArgumentError unless keys.is_a? String || keys.is_a? JSON
1265
+ c = @client["#{@account}/machines/#{machine}/snapshots/#{snapshot}"]
1266
+ opts['keys'] = keys
1267
+ attempt(opts[:attempts]) do
1268
+ do_post(c, headers, opts)
1269
+ end
1270
+
1271
+ end
1272
+
1273
+ # Returns the complete set of metadata associated with this instance.
1274
+ #
1275
+ # ==== Attributes
1276
+ #
1277
+ # * +:machine - String id of the machine
1278
+ #
1279
+ # ==== Options
1280
+ #
1281
+ # * +:credentials - Boolean Whether or not to return instance credentials. Defaults to false
1282
+ def list_machine_metadata(machine, opts= {})
1283
+ raise ArgumentError unless machine.is_a? String
1284
+ url = "#{@account}/machines/#{machine}/metadata"
1285
+ if opts.size > 0 && opts['credentials'].is_a? Boolean
1286
+ if opts['credentials']
1287
+ url = url + '?' + "credentials=true"
1288
+ end
1289
+ end
1290
+ c = @client[url]
1291
+ attempt(opts[:attempts]) do
1292
+ do_get(c, headers)
1293
+ end
1294
+
1295
+ end
1296
+
1297
+
1298
+ # Returns a single metadata entry associated with this instance.
1299
+ #
1300
+ # ==== Attributes
1301
+ #
1302
+ # * +:machine - String id of the machine
1303
+ # * +:key - String Name of metadata value to retrieve
1304
+ def get_machine_metadata(machine, key,opts= {})
1305
+ raise ArgumentError unless machine.is_a? String
1306
+ raise ArgumentError unless key.is_a? String
1307
+ c = @client["#{@account}/machines/#{machine}/metadata/#{key}"]
1308
+ attempt(opts[:attempts]) do
1309
+ do_get(c, headers)
1310
+ end
1311
+
1312
+ end
1313
+
1314
+ # Deletes a single metadata key from this instance.
1315
+ #
1316
+ # ==== Attributes
1317
+ #
1318
+ # * +:machine - String id of the machine
1319
+ # * +:key - String Name of metadata value to delete
1320
+ def delete_machine_metadata(machine, key,opts= {})
1321
+ raise ArgumentError unless machine.is_a? String
1322
+ raise ArgumentError unless key.is_a? String
1323
+ c = @client["#{@account}/machines/#{machine}/metadata/#{key}"]
1324
+ attempt(opts[:attempts]) do
1325
+ do_delete(c, headers)
1326
+ end
1327
+
1328
+ end
1329
+
1330
+ # Deletes all metadata keys from this instance.
1331
+ #
1332
+ # ==== Attributes
1333
+ #
1334
+ # * +:machine - String id of the machine
1335
+ def delete_all_machine_metadata(machine, opts= {})
1336
+ raise ArgumentError unless machine.is_a? String
1337
+ c = @client["#{@account}/machines/#{machine}/metadata"]
1338
+ attempt(opts[:attempts]) do
1339
+ do_delete(c, headers)
1340
+ end
1341
+
1342
+ end
1343
+
1344
+ # Set tags on the given instance. A pre-existing tag with the same name as
1345
+ # one given will be overwritten.
1346
+ # Note: This action is asynchronous. You can poll on ListMachineTags to wait
1347
+ # for the update to be complete (the triton instance tag set -w,--wait
1348
+ # option does this).
1349
+ #
1350
+ # ==== Attributes
1351
+ #
1352
+ # * +:machine - String id of the machine
1353
+ # * +:tags - Array tags to be added
1354
+ def add_machine_tags(machine, tags, opts= {})
1355
+ raise ArgumentError unless machine.is_a? String
1356
+ raise ArgumentError unless tags.is_a? Hash
1357
+ c = @client["#{@account}/machines/#{machine}/tags"]
1358
+ opts[:tags] = tags
1359
+ attempt(opts[:attempts]) do
1360
+ do_post(c, headers, opts)
1361
+ end
1362
+
1363
+ end
1364
+
1365
+ # Fully replace all tags on an instance with the given tags.
1366
+ # Note: This action is asynchronous. You can poll on ListMachineTags to wait
1367
+ # for the update to be complete (the triton instance tag set -w,--wait
1368
+ # option does this).
1369
+ #
1370
+ # ==== Attributes
1371
+ #
1372
+ # * +:machine - String id of the machine
1373
+ # * +:tags - Array tags to replace
1374
+ def replace_machine_tags(machine, tags, opts= {})
1375
+ raise ArgumentError unless machine.is_a? String
1376
+ raise ArgumentError unless tags.is_a? Hash
1377
+ c = @client["#{@account}/machines/#{machine}/tags"]
1378
+ opts[:tags] = tags
1379
+ attempt(opts[:attempts]) do
1380
+ do_put(c, headers, opts)
1381
+ end
1382
+
1383
+ end
1384
+
1385
+ # Fully replace all tags on an instance with the given tags.
1386
+ # Note: This action is asynchronous. You can poll on ListMachineTags to wait
1387
+ # for the update to be complete (the triton instance tag set -w,--wait
1388
+ # option does this).
1389
+ #
1390
+ # ==== Attributes
1391
+ #
1392
+ # * +:machine - String id of the machine
1393
+ def list_machine_tags(machine, opts= {})
1394
+ raise ArgumentError unless machine.is_a? String
1395
+ c = @client["#{@account}/machines/#{machine}/tags"]
1396
+ attempt(opts[:attempts]) do
1397
+ do_get(c, headers)
1398
+ end
1399
+
1400
+ end
1401
+
1402
+ # Returns the value for a single tag on this instance.
1403
+ #
1404
+ # Typically one calls CloudAPI endpoints with Accept: application/json.
1405
+ # This endpoint can be called that way, or alternatively with Accept:
1406
+ # text/plain to get the non-JSON value in the response.
1407
+ #
1408
+ # ==== Attributes
1409
+ #
1410
+ # * +:machine - String id of the machine
1411
+ # * +:tag - String tag to get
1412
+ def get_machine_tags(machine, tag, opts= {})
1413
+ raise ArgumentError unless machine.is_a? String
1414
+ raise ArgumentError unless tag.is_a? String
1415
+ c = @client["#{@account}/machines/#{machine}/tags/#{tag}"]
1416
+ attempt(opts[:attempts]) do
1417
+ do_get(c, headers)
1418
+ end
1419
+
1420
+ end
1421
+
1422
+ # Deletes a single tag from this instance.
1423
+ #
1424
+ # Note: This action is asynchronous. You can poll on ListMachineTags to wait
1425
+ # for the update to be complete (the triton instance tag delete -w,--wait
1426
+ # option does this).
1427
+ #
1428
+ # ==== Attributes
1429
+ #
1430
+ # * +:machine - String id of the machine
1431
+ # * +:tag - String tag to get
1432
+ def delete_machine_tag(machine, tag, opts= {})
1433
+ raise ArgumentError unless machine.is_a? String
1434
+ raise ArgumentError unless tag.is_a? String
1435
+ c = @client["#{@account}/machines/#{machine}/tags/#{tag}"]
1436
+ attempt(opts[:attempts]) do
1437
+ do_delete(c, headers)
1438
+ end
1439
+
1440
+ end
1441
+
1442
+ # Deletes all tags from an instance.
1443
+ #
1444
+ # Note: This action is asynchronous. You can poll on ListMachineTags to wait
1445
+ # for the update to be complete (the triton instance tag delete -w,--wait
1446
+ # option does this).
1447
+ #
1448
+ # ==== Attributes
1449
+ #
1450
+ # * +:machine - String id of the machine
1451
+ def delete_machine_tags(machine, opts= {})
1452
+ raise ArgumentError unless machine.is_a? String
1453
+ c = @client["#{@account}/machines/#{machine}/tags"]
1454
+ attempt(opts[:attempts]) do
1455
+ do_delete(c, headers)
1456
+ end
1457
+
1458
+ end
1459
+
1460
+ # Allows you to completely destroy an instance.
1461
+ #
1462
+ # ==== Attributes
1463
+ #
1464
+ # * +:machine - String id of the machine
1465
+ def delete_machine(machine, opts= {})
1466
+ raise ArgumentError unless machine.is_a? String
1467
+ c = @client["#{@account}/machines/#{machine}"]
1468
+ attempt(opts[:attempts]) do
1469
+ do_delete(c, headers)
1470
+ end
1471
+
1472
+ end
1473
+
1474
+ # Provides a list of an instance's accomplished actions. Results are sorted
1475
+ # from newest to oldest action.
1476
+ #
1477
+ # Note that the complete audit trail is returned only if the instance history
1478
+ # and job records have not been purged from Triton.
1479
+ #
1480
+ # ==== Attributes
1481
+ #
1482
+ # * +:machine - String id of the machine
1483
+ def machine_audit(machine, opts= {})
1484
+ raise ArgumentError unless machine.is_a? String
1485
+ c = @client["#{@account}/machines/#{machine}/audit"]
1486
+ attempt(opts[:attempts]) do
1487
+ do_get(c, headers)
1488
+ end
1489
+
1490
+ end
1491
+
1492
+
1493
+ # ---------------------------------------------------------------------------
1494
+ protected
1495
+
1496
+ # Executes a block. If there is a connection- or corruption-related exception
1497
+ # the block will be reexecuted up to the `tries' argument. It will sleep
1498
+ # for an exponentially-increasing number of seconds between retries.
1499
+ def attempt(tries, &blk)
1500
+ if tries
1501
+ raise ArgumentError unless tries > 0
1502
+ else
1503
+ tries ||= @attempts
1504
+ end
1505
+
1506
+ attempt = 1
1507
+
1508
+ while true
1509
+ begin
1510
+ return yield blk
1511
+ rescue Errno::ECONNREFUSED, RestClient::ServerBrokeConnection
1512
+ raise e if attempt == tries
1513
+ sleep 2 ** attempt
1514
+ attempt += 1
1515
+ end
1516
+ end
1517
+ end
1518
+
1519
+ # Creates a qualified user path consisting of the user and subuser if the
1520
+ # subuser is present. Otherwise, it returns the user
1521
+ def user_path
1522
+ @subuser ? "#{@account}/#{@subuser}" : @account
1523
+ end
1524
+
1525
+ # :m_some_header becomes "M-Some-Header"
1526
+ def symbol_to_header(header_symbol)
1527
+ header_symbol.to_s.split('_').map(&:capitalize).join('-')
1528
+ end
1529
+
1530
+ # Creates headers to be given to the HTTP client and sent to the Manta
1531
+ # service. The most important is the Authorization header, without which
1532
+ # none of this class would work.
1533
+ def gen_headers(opts)
1534
+ now = Time.now.httpdate
1535
+ sig = gen_signature('date: ' + now)
1536
+
1537
+ headers = { "Date" => now,
1538
+ "Authorization" => sig,
1539
+ "User-Agent" => HTTP_AGENT,
1540
+ "Api-Version" => "~#{CLOUDAPI_VERSION}"
1541
+ }
1542
+
1543
+ return headers
1544
+ end
1545
+
1546
+ # Given a chunk of data, creates an HTTP signature which the Manta service
1547
+ # understands and uses for authentication.
1548
+ def gen_signature(data)
1549
+ raise ArgumentError unless data
1550
+
1551
+ sig = @priv_key.sign(@digest, data)
1552
+ base64sig = Base64.strict_encode64(sig)
1553
+
1554
+ return HTTP_SIGNATURE % [user_path, @fingerprint, @digest_name, base64sig]
1555
+ end
1556
+
1557
+ # Returns a full URL for a given path to an object.
1558
+ def cloudapi_url(path)
1559
+ ## TODO: Add this check
1560
+ #raise ArgumentError unless path =~ CLOUDAPI_PATH_REGEX
1561
+ URI.encode(path)
1562
+ end
1563
+
1564
+ # Raises an appropriate exception given the HTTP response. If a 40* is
1565
+ # returned, attempts to look up an appropriate error class and raise,
1566
+ # otherwise raises an UnknownError.
1567
+ def raise_error(result)
1568
+ raise unless result.is_a? RestClient::Response
1569
+
1570
+ err = JSON.parse(result.body)
1571
+ klass = CloudApiClient.const_get err['code']
1572
+ raise klass, err['message']
1573
+ rescue NameError, TypeError, JSON::ParserError
1574
+ raise UnknownError, result.status.to_s + ': ' + result.body
1575
+ end
1576
+
1577
+
1578
+ #
1579
+ # do_get abstraction method to GET request
1580
+ #
1581
+ def do_get(c, headers)
1582
+ raise unless c.is_a? RestClient::Resource
1583
+ result = c.get(headers)
1584
+ raise unless result.is_a? RestClient::Response
1585
+
1586
+ if result.code == 200
1587
+ return JSON.parse(result.body)
1588
+ end
1589
+
1590
+ raise_error(result)
1591
+ end
1592
+
1593
+ #
1594
+ # do_post abstraction method to POST request
1595
+ #
1596
+ def do_post(c, headers, payload)
1597
+ raise unless c.is_a? RestClient::Resource
1598
+ result = c.post(payload.except!(:attempts), headers)
1599
+ raise unless result.is_a? RestClient::Response
1600
+
1601
+ if result.code == 200
1602
+ return JSON.parse(result.body)
1603
+ end
1604
+
1605
+ raise_error(result)
1606
+ end
1607
+
1608
+ #
1609
+ # do_put abstraction method to PUT request
1610
+ #
1611
+ def do_put(c, headers, payload)
1612
+ raise unless c.is_a? RestClient::Resource
1613
+ result = c.put(payload.except!(:attempts), headers)
1614
+ raise unless result.is_a? RestClient::Response
1615
+
1616
+ if result.code == 200
1617
+ return JSON.parse(result.body)
1618
+ end
1619
+
1620
+ raise_error(result)
1621
+ end
1622
+ #
1623
+ # do_delete abstraction method to delete request
1624
+ #
1625
+ def do_delete(c, headers)
1626
+ raise unless c.is_a? RestClient::Resource
1627
+ result = c.delete(headers)
1628
+ raise unless result.is_a? RestClient::Response
1629
+
1630
+ if result.code == 204
1631
+ return true
1632
+ end
1633
+
1634
+ raise_error(result)
1635
+ end
1636
+
1637
+ #
1638
+ # do_head abstraction method to head request
1639
+ #
1640
+ def do_head(c, headers)
1641
+ raise unless c.is_a? RestClient::Resource
1642
+ result = c.head(headers)
1643
+ raise unless result.is_a? RestClient::Response
1644
+
1645
+ if result.code == 200
1646
+ return JSON.parse(result.body)
1647
+ end
1648
+
1649
+ raise_error(result)
1650
+ end
1651
+ #
1652
+ # Validate input parameters
1653
+ # TODO: Not used yet.
1654
+ #
1655
+ def validate_parameters(query_parameters, valid_parameters, opts)
1656
+ raise unless query_parameters.is_a? Hash
1657
+ raise unless valid_parameters.is_a? Hash
1658
+ raise unless opts.is_a? Hash
1659
+ opts.each do | key, val |
1660
+ puts "key = #{key};"
1661
+ end
1662
+ end
1663
+
1664
+ end
1665
+ end