knife-cloudstack 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+