knife-cloudstack 0.0.11

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,604 @@
1
+ #
2
+ # Author:: Ryan Holmes (<rholmes@edmunds.com>)
3
+ # Author:: KC Braunschweig (<kbraunschweig@edmunds.com>)
4
+ # Copyright:: Copyright (c) 2011 Edmunds, Inc.
5
+ # License:: Apache License, Version 2.0
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require 'rubygems'
21
+ require 'base64'
22
+ require 'openssl'
23
+ require 'uri'
24
+ require 'cgi'
25
+ require 'net/http'
26
+ require 'json'
27
+
28
+ module CloudstackClient
29
+ class Connection
30
+
31
+ ASYNC_POLL_INTERVAL = 2.0
32
+ ASYNC_TIMEOUT = 300
33
+
34
+ def initialize(api_url, api_key, secret_key)
35
+ @api_url = api_url
36
+ @api_key = api_key
37
+ @secret_key = secret_key
38
+ end
39
+
40
+ ##
41
+ # Finds the server with the specified name.
42
+
43
+ def get_server(name)
44
+ params = {
45
+ 'command' => 'listVirtualMachines',
46
+ 'name' => name
47
+ }
48
+ json = send_request(params)
49
+ machines = json['virtualmachine']
50
+
51
+ if !machines || machines.empty? then
52
+ return nil
53
+ end
54
+
55
+ machines.first
56
+ end
57
+
58
+ ##
59
+ # Finds the public ip for a server
60
+
61
+ def get_server_public_ip(server, cached_rules=nil)
62
+ return nil unless server
63
+
64
+ # find the public ip
65
+ nic = get_server_default_nic(server) || {}
66
+ if nic['type'] == 'Virtual' then
67
+ ssh_rule = get_ssh_port_forwarding_rule(server, cached_rules)
68
+ ssh_rule ? ssh_rule['ipaddress'] : nil
69
+ else
70
+ nic['ipaddress']
71
+ end
72
+ end
73
+
74
+ ##
75
+ # Returns the fully qualified domain name for a server.
76
+
77
+ def get_server_fqdn(server)
78
+ return nil unless server
79
+
80
+ nic = get_server_default_nic(server) || {}
81
+ networks = list_networks || {}
82
+
83
+ id = nic['networkid']
84
+ network = networks.select { |net|
85
+ net['id'] == id
86
+ }.first
87
+ return nil unless network
88
+
89
+ "#{server['name']}.#{network['networkdomain']}"
90
+ end
91
+
92
+ def get_server_default_nic(server)
93
+ server['nic'].each do |nic|
94
+ return nic if nic['isdefault']
95
+ end
96
+ end
97
+
98
+ ##
99
+ # Lists all the servers in your account.
100
+
101
+ def list_servers
102
+ params = {
103
+ 'command' => 'listVirtualMachines'
104
+ }
105
+ json = send_request(params)
106
+ json['virtualmachine'] || []
107
+ end
108
+
109
+ ##
110
+ # Deploys a new server using the specified parameters.
111
+
112
+ def create_server(host_name, service_name, template_name, zone_name=nil, network_names=[])
113
+
114
+ if host_name then
115
+ if get_server(host_name) then
116
+ puts "Error: Server '#{host_name}' already exists."
117
+ exit 1
118
+ end
119
+ end
120
+
121
+ service = get_service_offering(service_name)
122
+ if !service then
123
+ puts "Error: Service offering '#{service_name}' is invalid"
124
+ exit 1
125
+ end
126
+
127
+ template = get_template(template_name)
128
+ if !template then
129
+ puts "Error: Template '#{template_name}' is invalid"
130
+ exit 1
131
+ end
132
+
133
+ zone = zone_name ? get_zone(zone_name) : get_default_zone
134
+ if !zone then
135
+ msg = zone_name ? "Zone '#{zone_name}' is invalid" : "No default zone found"
136
+ puts "Error: #{msg}"
137
+ exit 1
138
+ end
139
+
140
+ networks = []
141
+ network_names.each do |name|
142
+ network = get_network(name)
143
+ if !network then
144
+ puts "Error: Network '#{name}' not found"
145
+ exit 1
146
+ end
147
+ networks << get_network(name)
148
+ end
149
+ if networks.empty? then
150
+ networks << get_default_network
151
+ end
152
+ if networks.empty? then
153
+ puts "No default network found"
154
+ exit 1
155
+ end
156
+ network_ids = networks.map { |network|
157
+ network['id']
158
+ }
159
+
160
+ params = {
161
+ 'command' => 'deployVirtualMachine',
162
+ 'serviceOfferingId' => service['id'],
163
+ 'templateId' => template['id'],
164
+ 'zoneId' => zone['id'],
165
+ 'networkids' => network_ids.join(',')
166
+ }
167
+ params['name'] = host_name if host_name
168
+
169
+ json = send_async_request(params)
170
+ json['virtualmachine']
171
+ end
172
+
173
+ ##
174
+ # Deletes the server with the specified name.
175
+ #
176
+
177
+ def delete_server(name)
178
+ server = get_server(name)
179
+ if !server || !server['id'] then
180
+ puts "Error: Virtual machine '#{name}' does not exist"
181
+ exit 1
182
+ end
183
+
184
+ params = {
185
+ 'command' => 'destroyVirtualMachine',
186
+ 'id' => server['id']
187
+ }
188
+
189
+ json = send_async_request(params)
190
+ json['virtualmachine']
191
+ end
192
+
193
+ ##
194
+ # Stops the server with the specified name.
195
+ #
196
+
197
+ def stop_server(name, forced=nil)
198
+ server = get_server(name)
199
+ if !server || !server['id'] then
200
+ puts "Error: Virtual machine '#{name}' does not exist"
201
+ exit 1
202
+ end
203
+
204
+ params = {
205
+ 'command' => 'stopVirtualMachine',
206
+ 'id' => server['id']
207
+ }
208
+ params['forced'] = true if forced
209
+
210
+ json = send_async_request(params)
211
+ json['virtualmachine']
212
+ end
213
+
214
+ ##
215
+ # Start the server with the specified name.
216
+ #
217
+
218
+ def start_server(name)
219
+ server = get_server(name)
220
+ if !server || !server['id'] then
221
+ puts "Error: Virtual machine '#{name}' does not exist"
222
+ exit 1
223
+ end
224
+
225
+ params = {
226
+ 'command' => 'startVirtualMachine',
227
+ 'id' => server['id']
228
+ }
229
+
230
+ json = send_async_request(params)
231
+ json['virtualmachine']
232
+ end
233
+
234
+ ##
235
+ # Reboot the server with the specified name.
236
+ #
237
+
238
+ def reboot_server(name)
239
+ server = get_server(name)
240
+ if !server || !server['id'] then
241
+ puts "Error: Virtual machine '#{name}' does not exist"
242
+ exit 1
243
+ end
244
+
245
+ params = {
246
+ 'command' => 'rebootVirtualMachine',
247
+ 'id' => server['id']
248
+ }
249
+
250
+ json = send_async_request(params)
251
+ json['virtualmachine']
252
+ end
253
+
254
+ ##
255
+ # Finds the service offering with the specified name.
256
+
257
+ def get_service_offering(name)
258
+
259
+ # TODO: use name parameter
260
+ # listServiceOfferings in CloudStack 2.2 doesn't seem to work
261
+ # when the name parameter is specified. When this is fixed,
262
+ # the name parameter should be added to the request.
263
+ params = {
264
+ 'command' => 'listServiceOfferings'
265
+ }
266
+ json = send_request(params)
267
+
268
+ services = json['serviceoffering']
269
+ return nil unless services
270
+
271
+ services.each { |s|
272
+ if s['name'] == name then
273
+ return s
274
+ end
275
+ }
276
+
277
+ nil
278
+ end
279
+
280
+ ##
281
+ # Lists all available service offerings.
282
+
283
+ def list_service_offerings
284
+ params = {
285
+ 'command' => 'listServiceOfferings'
286
+ }
287
+ json = send_request(params)
288
+ json['serviceoffering'] || []
289
+ end
290
+
291
+ ##
292
+ # Finds the template with the specified name.
293
+
294
+ def get_template(name)
295
+
296
+ # TODO: use name parameter
297
+ # listTemplates in CloudStack 2.2 doesn't seem to work
298
+ # when the name parameter is specified. When this is fixed,
299
+ # the name parameter should be added to the request.
300
+ params = {
301
+ 'command' => 'listTemplates',
302
+ 'templateFilter' => 'executable'
303
+ }
304
+ json = send_request(params)
305
+
306
+ templates = json['template']
307
+ if !templates then
308
+ return nil
309
+ end
310
+
311
+ templates.each { |t|
312
+ if t['name'] == name then
313
+ return t
314
+ end
315
+ }
316
+
317
+ nil
318
+ end
319
+
320
+ ##
321
+ # Lists all templates that match the specified filter.
322
+ #
323
+ # Allowable filter values are:
324
+ #
325
+ # * featured - templates that are featured and are public
326
+ # * self - templates that have been registered/created by the owner
327
+ # * self-executable - templates that have been registered/created by the owner that can be used to deploy a new VM
328
+ # * executable - all templates that can be used to deploy a new VM
329
+ # * community - templates that are public
330
+
331
+ def list_templates(filter)
332
+ filter ||= 'featured'
333
+ params = {
334
+ 'command' => 'listTemplates',
335
+ 'templateFilter' => filter
336
+ }
337
+ json = send_request(params)
338
+ json['template'] || []
339
+ end
340
+
341
+ ##
342
+ # Finds the network with the specified name.
343
+
344
+ def get_network(name)
345
+ params = {
346
+ 'command' => 'listNetworks'
347
+ }
348
+ json = send_request(params)
349
+
350
+ networks = json['network']
351
+ return nil unless networks
352
+
353
+ networks.each { |n|
354
+ if n['name'] == name then
355
+ return n
356
+ end
357
+ }
358
+
359
+ nil
360
+ end
361
+
362
+ ##
363
+ # Finds the default network.
364
+
365
+ def get_default_network
366
+ params = {
367
+ 'command' => 'listNetworks',
368
+ 'isDefault' => true
369
+ }
370
+ json = send_request(params)
371
+
372
+ networks = json['network']
373
+ return nil if !networks || networks.empty?
374
+
375
+ default = networks.first
376
+ return default if networks.length == 1
377
+
378
+ networks.each { |n|
379
+ if n['type'] == 'Direct' then
380
+ default = n
381
+ break
382
+ end
383
+ }
384
+
385
+ default
386
+ end
387
+
388
+ ##
389
+ # Lists all available networks.
390
+
391
+ def list_networks
392
+ params = {
393
+ 'command' => 'listNetworks'
394
+ }
395
+ json = send_request(params)
396
+ json['network'] || []
397
+ end
398
+
399
+ ##
400
+ # Finds the zone with the specified name.
401
+
402
+ def get_zone(name)
403
+ params = {
404
+ 'command' => 'listZones',
405
+ 'available' => 'true'
406
+ }
407
+ json = send_request(params)
408
+
409
+ networks = json['zone']
410
+ return nil unless networks
411
+
412
+ networks.each { |z|
413
+ if z['name'] == name then
414
+ return z
415
+ end
416
+ }
417
+
418
+ nil
419
+ end
420
+
421
+ ##
422
+ # Finds the default zone for your account.
423
+
424
+ def get_default_zone
425
+ params = {
426
+ 'command' => 'listZones',
427
+ 'available' => 'true'
428
+ }
429
+ json = send_request(params)
430
+
431
+ zones = json['zone']
432
+ return nil unless zones
433
+
434
+ zones.first
435
+ end
436
+
437
+ ##
438
+ # Lists all available zones.
439
+
440
+ def list_zones
441
+ params = {
442
+ 'command' => 'listZones',
443
+ 'available' => 'true'
444
+ }
445
+ json = send_request(params)
446
+ json['zone'] || []
447
+ end
448
+
449
+ ##
450
+ # Finds the public ip address for a given ip address string.
451
+
452
+ def get_public_ip_address(ip_address)
453
+ params = {
454
+ 'command' => 'listPublicIpAddresses',
455
+ 'ipaddress' => ip_address
456
+ }
457
+ json = send_request(params)
458
+ json['publicipaddress'].first
459
+ end
460
+
461
+
462
+ ##
463
+ # Acquires and associates a public IP to an account.
464
+
465
+ def associate_ip_address(zone_id)
466
+ params = {
467
+ 'command' => 'associateIpAddress',
468
+ 'zoneId' => zone_id
469
+ }
470
+
471
+ json = send_async_request(params)
472
+ json['ipaddress']
473
+ end
474
+
475
+ ##
476
+ # Disassociates an ip address from the account.
477
+ #
478
+ # Returns true if successful, false otherwise.
479
+
480
+ def disassociate_ip_address(id)
481
+ params = {
482
+ 'command' => 'disassociateIpAddress',
483
+ 'id' => id
484
+ }
485
+ json = send_async_request(params)
486
+ json['success']
487
+ end
488
+
489
+ ##
490
+ # Lists all port forwarding rules.
491
+
492
+ def list_port_forwarding_rules(ip_address_id=nil)
493
+ params = {
494
+ 'command' => 'listPortForwardingRules'
495
+ }
496
+ params['ipAddressId'] = ip_address_id if ip_address_id
497
+ json = send_request(params)
498
+ json['portforwardingrule']
499
+ end
500
+
501
+ ##
502
+ # Gets the SSH port forwarding rule for the specified server.
503
+
504
+ def get_ssh_port_forwarding_rule(server, cached_rules=nil)
505
+ rules = cached_rules || list_port_forwarding_rules || []
506
+ rules.find_all { |r|
507
+ r['virtualmachineid'] == server['id'] &&
508
+ r['privateport'] == '22'&&
509
+ r['publicport'] == '22'
510
+ }.first
511
+ end
512
+
513
+ ##
514
+ # Creates a port forwarding rule.
515
+
516
+ def create_port_forwarding_rule(ip_address_id, private_port, protocol, public_port, virtual_machine_id)
517
+ params = {
518
+ 'command' => 'createPortForwardingRule',
519
+ 'ipAddressId' => ip_address_id,
520
+ 'privatePort' => private_port,
521
+ 'protocol' => protocol,
522
+ 'publicPort' => public_port,
523
+ 'virtualMachineId' => virtual_machine_id
524
+ }
525
+ json = send_async_request(params)
526
+ json['portforwardingrule']
527
+ end
528
+
529
+ ##
530
+ # Sends a synchronous request to the CloudStack API and returns the response as a Hash.
531
+ #
532
+ # The wrapper element of the response (e.g. mycommandresponse) is discarded and the
533
+ # contents of that element are returned.
534
+
535
+ def send_request(params)
536
+ params['response'] = 'json'
537
+ params['apiKey'] = @api_key
538
+
539
+ params_arr = []
540
+ params.sort.each { |elem|
541
+ params_arr << elem[0].to_s + '=' + elem[1].to_s
542
+ }
543
+ data = params_arr.join('&')
544
+ encoded_data = URI.encode(data.downcase).gsub('+', '%20').gsub(',', '%2c')
545
+ signature = OpenSSL::HMAC.digest('sha1', @secret_key, encoded_data)
546
+ signature = Base64.encode64(signature).chomp
547
+ signature = CGI.escape(signature)
548
+
549
+ url = "#{@api_url}?#{data}&signature=#{signature}"
550
+
551
+ response = Net::HTTP.get_response(URI.parse(url))
552
+
553
+ if !response.is_a?(Net::HTTPOK) then
554
+ puts "Error #{response.code}: #{response.message}"
555
+ puts JSON.pretty_generate(JSON.parse(response.body))
556
+ puts "URL: #{url}"
557
+ exit 1
558
+ end
559
+
560
+ json = JSON.parse(response.body)
561
+ json[params['command'].downcase + 'response']
562
+ end
563
+
564
+ ##
565
+ # Sends an asynchronous request and waits for the response.
566
+ #
567
+ # The contents of the 'jobresult' element are returned upon completion of the command.
568
+
569
+ def send_async_request(params)
570
+
571
+ json = send_request(params)
572
+
573
+ params = {
574
+ 'command' => 'queryAsyncJobResult',
575
+ 'jobId' => json['jobid']
576
+ }
577
+
578
+ max_tries = (ASYNC_TIMEOUT / ASYNC_POLL_INTERVAL).round
579
+ max_tries.times do
580
+ json = send_request(params)
581
+ status = json['jobstatus']
582
+
583
+ print "."
584
+
585
+ if status == 1 then
586
+ return json['jobresult']
587
+ elsif status == 2 then
588
+ print "\n"
589
+ puts "Request failed (#{json['jobresultcode']}): #{json['jobresult']}"
590
+ exit 1
591
+ end
592
+
593
+ STDOUT.flush
594
+ sleep ASYNC_POLL_INTERVAL
595
+ end
596
+
597
+ print "\n"
598
+ puts "Error: Asynchronous request timed out"
599
+ exit 1
600
+ end
601
+
602
+ end # class
603
+ end
604
+
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: knife-cloudstack
3
+ version: !ruby/object:Gem::Version
4
+ hash: 9
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 11
10
+ version: 0.0.11
11
+ platform: ruby
12
+ authors:
13
+ - Ryan Holmes
14
+ - KC Braunschweig
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2011-11-03 00:00:00 Z
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: chef
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 55
30
+ segments:
31
+ - 0
32
+ - 10
33
+ - 0
34
+ version: 0.10.0
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ description: A Knife plugin to create, list and manage CloudStack servers
38
+ email:
39
+ - rholmes@edmunds.com
40
+ - kbraunschweig@edmunds.com
41
+ executables: []
42
+
43
+ extensions: []
44
+
45
+ extra_rdoc_files:
46
+ - README.rdoc
47
+ - CHANGES.rdoc
48
+ - LICENSE
49
+ files:
50
+ - CHANGES.rdoc
51
+ - README.rdoc
52
+ - LICENSE
53
+ - lib/knife-cloudstack/connection.rb
54
+ - lib/chef/knife/cs_stack_delete.rb
55
+ - lib/chef/knife/cs_server_list.rb
56
+ - lib/chef/knife/cs_network_list.rb
57
+ - lib/chef/knife/cs_server_delete.rb
58
+ - lib/chef/knife/cs_template_list.rb
59
+ - lib/chef/knife/cs_server_reboot.rb
60
+ - lib/chef/knife/cs_server_start.rb
61
+ - lib/chef/knife/cs_service_list.rb
62
+ - lib/chef/knife/cs_zone_list.rb
63
+ - lib/chef/knife/cs_server_stop.rb
64
+ - lib/chef/knife/cs_hosts.rb
65
+ - lib/chef/knife/cs_stack_create.rb
66
+ - lib/chef/knife/cs_server_create.rb
67
+ homepage: http://www.edmunds.com/
68
+ licenses: []
69
+
70
+ post_install_message:
71
+ rdoc_options: []
72
+
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ hash: 3
81
+ segments:
82
+ - 0
83
+ version: "0"
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ hash: 3
90
+ segments:
91
+ - 0
92
+ version: "0"
93
+ requirements: []
94
+
95
+ rubyforge_project:
96
+ rubygems_version: 1.7.2
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: A knife plugin for the CloudStack API
100
+ test_files: []
101
+