kitchen-openstack 0.5.0 → 1.0.0

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.
data/CHANGELOG.md CHANGED
@@ -1,4 +1,20 @@
1
- # ?.?.? / ????-??-??
1
+ # 1.0.0 / 2013-10-16
2
+
3
+ ### New Features
4
+
5
+ * PR [#26][] - Support image and flavor names and regexes; via [@jgawor][]
6
+ * PR [#25][] - Support specific floating IPs, in addition to named pools
7
+ * PR [#14][] - Add support for floating IP pools; via [@hufman][]
8
+
9
+ ### Improvements
10
+
11
+ * PR [#15][] - Improved SSH key support, support RSA and DSA; via [@hufman][]
12
+
13
+ ### Bug Fixes
14
+
15
+ * PR [#27][] - Prevent IP contention in TK parallel mode; via [@jgawor][]
16
+
17
+ # 0.5.0 / 2013-09-23
2
18
 
3
19
  ### Improvements
4
20
 
@@ -36,8 +52,13 @@
36
52
 
37
53
  * Initial release! Woo!
38
54
 
55
+ [#27]: https://github.com/RoboticCheese/kitchen-openstack/pull/27
56
+ [#26]: https://github.com/RoboticCheese/kitchen-openstack/pull/26
57
+ [#25]: https://github.com/RoboticCheese/kitchen-openstack/pull/25
39
58
  [#20]: https://github.com/RoboticCheese/kitchen-openstack/pull/20
40
59
  [#19]: https://github.com/RoboticCheese/kitchen-openstack/pull/19
60
+ [#15]: https://github.com/RoboticCheese/kitchen-openstack/pull/15
61
+ [#14]: https://github.com/RoboticCheese/kitchen-openstack/pull/14
41
62
  [#12]: https://github.com/RoboticCheese/kitchen-openstack/pull/12
42
63
  [#11]: https://github.com/RoboticCheese/kitchen-openstack/pull/11
43
64
  [#10]: https://github.com/RoboticCheese/kitchen-openstack/pull/10
@@ -45,5 +66,7 @@
45
66
  [#7]: https://github.com/RoboticCheese/kitchen-openstack/pull/7
46
67
  [#2]: https://github.com/RoboticCheese/kitchen-openstack/pull/2
47
68
 
69
+ [@jgawor]: https://github.com/jgawor
70
+ [@hufman]: https://github.com/hufman
48
71
  [@saketoba]: https://github.com/saketoba
49
72
  [@stevendanna]: https://github.com/stevendanna
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Build Status](https://travis-ci.org/RoboticCheese/kitchen-openstack.png?branch=master)](https://travis-ci.org/RoboticCheese/kitchen-openstack) [![Code Climate](https://codeclimate.com/github/RoboticCheese/kitchen-openstack.png)](https://codeclimate.com/github/RoboticCheese/kitchen-openstack)
1
+ [![Gem Version](https://badge.fury.io/rb/kitchen-openstack.png)](http://badge.fury.io/rb/kitchen-openstack) [![Dependency Status](https://gemnasium.com/RoboticCheese/kitchen-openstack.png)](https://gemnasium.com/RoboticCheese/kitchen-openstack) [![Build Status](https://travis-ci.org/RoboticCheese/kitchen-openstack.png?branch=master)](https://travis-ci.org/RoboticCheese/kitchen-openstack) [![Code Climate](https://codeclimate.com/github/RoboticCheese/kitchen-openstack.png)](https://codeclimate.com/github/RoboticCheese/kitchen-openstack)
2
2
 
3
3
  # Kitchen::OpenStack
4
4
 
@@ -34,12 +34,15 @@ Provide, at a minimum, the required driver options in your `.kitchen.yml` file:
34
34
  image_ref: [SERVER IMAGE ID]
35
35
  flavor_ref: [SERVER FLAVOR ID]
36
36
 
37
+ The `image_ref` and `flavor_ref` options can be specified as an exact id,
38
+ an exact name, or as a regular expression matching the name of the image or flavor.
39
+
37
40
  By default, a unique server name will be generated and the current user's SSH
38
- key will be used, though that behavior can be overridden with additional
39
- options:
41
+ key will be used (with an RSA key taking precedence over a DSA), though that
42
+ behavior can be overridden with additional options:
40
43
 
41
44
  name: [A UNIQUE SERVER NAME]
42
- ssh_key: [PATH TO YOUR PRIVATE SSH KEY]
45
+ private_key_path: [PATH TO YOUR PRIVATE SSH KEY]
43
46
  public_key_path: [PATH TO YOUR SSH PUBLIC KEY]
44
47
  username: [SSH USER]
45
48
  port: [SSH PORT]
@@ -48,9 +51,15 @@ options:
48
51
  openstack_region: [A VALID OPENSTACK REGION]
49
52
  openstack_service_name: [YOUR OPENSTACK COMPUTE SERVICE NAME]
50
53
  openstack_network_name: [YOUR OPENSTACK NETWORK NAME]
54
+ floating_ip: [A SPECIFIC FLOATING IP TO ASSIGN]
55
+ floating_ip_pool: [AN OPENSTACK POOL NAME TO ASSIGN THE NEXT IP FROM]
56
+
57
+ If a `key_name` is provided it will be used instead of any
58
+ `public_key_path` that is specified.
51
59
 
52
- If a key\_name is provided it will be used instead of any
53
- public\_key\_path that is specified.
60
+ If a `key_name` is provided without any `private_key_path`, unexpected
61
+ behavior may result if your local RSA/DSA private key doesn't match that
62
+ OpenStack key.
54
63
 
55
64
  disable_ssl_validation: true
56
65
 
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ Cane::RakeTask.new
8
8
 
9
9
  desc 'Run Tailor to lint check code'
10
10
  Tailor::RakeTask.new do |task|
11
- task.file_set '**/**/*.rb'
11
+ task.file_set '**/*.rb'
12
12
  end
13
13
 
14
14
  desc 'Display LOC stats'
@@ -29,8 +29,19 @@ module Kitchen
29
29
  #
30
30
  # @author Jonathan Hartman <j@p4nt5.com>
31
31
  class Openstack < Kitchen::Driver::SSHBase
32
+ @@ip_pool_lock = Mutex.new
33
+
32
34
  default_config :name, nil
33
- default_config :public_key_path, File.expand_path('~/.ssh/id_dsa.pub')
35
+ default_config :key_name, nil
36
+ default_config :private_key_path do |driver|
37
+ %w{id_rsa id_dsa}.collect do |k|
38
+ f = File.expand_path "~/.ssh/#{k}"
39
+ f if File.exists? f
40
+ end.compact.first
41
+ end
42
+ default_config :public_key_path do |driver|
43
+ driver[:private_key_path] + '.pub'
44
+ end
34
45
  default_config :username, 'root'
35
46
  default_config :port, '22'
36
47
  default_config :use_ipv6, false
@@ -38,6 +49,8 @@ module Kitchen
38
49
  default_config :openstack_region, nil
39
50
  default_config :openstack_service_name, nil
40
51
  default_config :openstack_network_name, nil
52
+ default_config :floating_ip_pool, nil
53
+ default_config :floating_ip, nil
41
54
 
42
55
  def create(state)
43
56
  config[:name] ||= generate_name(instance.name)
@@ -45,10 +58,21 @@ module Kitchen
45
58
  server = create_server
46
59
  state[:server_id] = server.id
47
60
  info "OpenStack instance <#{state[:server_id]}> created."
48
- server.wait_for { print '.'; ready? } ; puts "\n(server ready)"
61
+ server.wait_for { print '.'; ready? } ; info "\n(server ready)"
62
+ if config[:floating_ip_pool]
63
+ attach_ip_from_pool(server, config[:floating_ip_pool])
64
+ elsif config[:floating_ip]
65
+ attach_ip(server, config[:floating_ip])
66
+ end
49
67
  state[:hostname] = get_ip(server)
50
- wait_for_sshd(state[:hostname]) ; puts '(ssh ready)'
51
- unless config[:ssh_key] or config[:key_name]
68
+ state[:ssh_key] = config[:private_key_path]
69
+ wait_for_sshd(state[:hostname]) ; info '(ssh ready)'
70
+ if config[:key_name]
71
+ info "Using OpenStack keypair <#{config[:key_name]}>"
72
+ end
73
+ info "Using public SSH key <#{config[:public_key_path]}>"
74
+ info "Using private SSH key <#{config[:private_key_path]}>"
75
+ unless config[:key_name]
52
76
  do_ssh_setup(state, config, server)
53
77
  end
54
78
  rescue Fog::Errors::Error, Excon::Errors::Error => ex
@@ -85,10 +109,18 @@ module Kitchen
85
109
  end
86
110
 
87
111
  def create_server
112
+ image = find_matching(compute.images, config[:image_ref])
113
+ raise ActionFailed, "Image not found" if !image
114
+ debug "Selected image: #{image.id} #{image.name}"
115
+
116
+ flavor = find_matching(compute.flavors, config[:flavor_ref])
117
+ raise ActionFailed, "Flavor not found" if !flavor
118
+ debug "Selected flavor: #{flavor.id} #{flavor.name}"
119
+
88
120
  server_def = {
89
121
  :name => config[:name],
90
- :image_ref => config[:image_ref],
91
- :flavor_ref => config[:flavor_ref]
122
+ :image_ref => image.id,
123
+ :flavor_ref => flavor.id
92
124
  }
93
125
  if config[:public_key_path]
94
126
  server_def[:public_key_path] = config[:public_key_path]
@@ -121,6 +153,25 @@ module Kitchen
121
153
  pieces.join sep
122
154
  end
123
155
 
156
+ def attach_ip_from_pool(server, pool)
157
+ @@ip_pool_lock.synchronize do
158
+ info "Attaching floating IP from <#{pool}> pool"
159
+ free_addrs = compute.addresses.collect do |i|
160
+ i.ip if i.fixed_ip.nil? and i.instance_id.nil? and i.pool == pool
161
+ end.compact
162
+ if free_addrs.empty?
163
+ raise ActionFailed, "No available IPs in pool <#{pool}>"
164
+ end
165
+ attach_ip(server, free_addrs[0])
166
+ end
167
+ end
168
+
169
+ def attach_ip(server, ip)
170
+ info "Attaching floating IP <#{ip}>"
171
+ server.associate_address ip
172
+ (server.addresses['public'] ||= []) << { 'version' => 4, 'addr' => ip }
173
+ end
174
+
124
175
  def get_ip(server)
125
176
  if config[:openstack_network_name]
126
177
  debug "Using configured network: #{config[:openstack_network_name]}"
@@ -149,6 +200,7 @@ module Kitchen
149
200
  end
150
201
 
151
202
  def do_ssh_setup(state, config, server)
203
+ info "Setting up SSH access for key <#{config[:public_key_path]}>"
152
204
  ssh = Fog::SSH.new(state[:hostname], config[:username],
153
205
  { :password => server.password })
154
206
  pub_key = open(config[:public_key_path]).read
@@ -163,6 +215,28 @@ module Kitchen
163
215
  require 'excon'
164
216
  Excon.defaults[:ssl_verify_peer] = false
165
217
  end
218
+
219
+ def find_matching(collection, name)
220
+ name = name.to_s
221
+ if name.start_with?('/')
222
+ regex = eval(name)
223
+ # check for regex name match
224
+ collection.each do |single|
225
+ return single if regex =~ single.name
226
+ end
227
+ else
228
+ # check for exact id match
229
+ collection.each do |single|
230
+ return single if single.id == name
231
+ end
232
+ # check for exact name match
233
+ collection.each do |single|
234
+ return single if single.name == name
235
+ end
236
+ end
237
+ nil
238
+ end
239
+
166
240
  end
167
241
  end
168
242
  end
@@ -19,7 +19,7 @@
19
19
  module Kitchen
20
20
  module Driver
21
21
  # Version string for OpenStack Kitchen driver
22
- OPENSTACK_VERSION = '0.5.0'
22
+ OPENSTACK_VERSION = '1.0.0'
23
23
  end
24
24
  end
25
25
 
@@ -27,6 +27,8 @@ describe Kitchen::Driver::Openstack do
27
27
  let(:logger) { Logger.new(logged_output) }
28
28
  let(:config) { Hash.new }
29
29
  let(:state) { Hash.new }
30
+ let(:dsa) { File.expand_path('~/.ssh/id_dsa') }
31
+ let(:rsa) { File.expand_path('~/.ssh/id_rsa') }
30
32
 
31
33
  let(:instance) do
32
34
  double(:name => 'potatoes', :logger => logger, :to_str => 'instance')
@@ -38,32 +40,73 @@ describe Kitchen::Driver::Openstack do
38
40
  d
39
41
  end
40
42
 
43
+ before(:each) do
44
+ File.stub(:exists?).and_call_original
45
+ File.stub(:exists?).with(dsa).and_return(true)
46
+ File.stub(:exists?).with(rsa).and_return(true)
47
+ end
48
+
41
49
  describe '#initialize'do
42
50
  context 'default options' do
43
- it 'defaults to local user\'s SSH public key' do
44
- expect(driver[:public_key_path]).to eq(File.expand_path(
45
- '~/.ssh/id_dsa.pub'))
46
- end
51
+ context 'both DSA and RSA SSH keys available for the user' do
52
+ it 'prefers the local user\'s RSA private key' do
53
+ expect(driver[:private_key_path]).to eq(rsa)
54
+ end
47
55
 
48
- it 'defaults to SSH with root user on port 22' do
49
- expect(driver[:username]).to eq('root')
50
- expect(driver[:port]).to eq('22')
56
+ it 'prefers the local user\'s RSA public key' do
57
+ expect(driver[:public_key_path]).to eq(rsa + '.pub')
58
+ end
51
59
  end
52
60
 
53
- it 'defaults to no server name' do
54
- expect(driver[:name]).to eq(nil)
61
+ context 'only a DSA SSH key available for the user' do
62
+ before(:each) do
63
+ File.unstub(:exists?)
64
+ File.stub(:exists?).and_return(false)
65
+ File.stub(:exists?).with(dsa).and_return(true)
66
+ end
67
+
68
+ it 'uses the local user\'s DSA private key' do
69
+ expect(driver[:private_key_path]).to eq(dsa)
70
+ end
71
+
72
+ it 'uses the local user\'s DSA public key' do
73
+ expect(driver[:public_key_path]).to eq(dsa + '.pub')
74
+ end
55
75
  end
56
76
 
57
- it 'defaults to no tenant' do
58
- expect(driver[:openstack_tenant]).to eq(nil)
77
+ context 'only a RSA SSH key available for the user' do
78
+ before(:each) do
79
+ File.unstub(:exists?)
80
+ File.stub(:exists?).and_return(false)
81
+ File.stub(:exists?).with(rsa).and_return(true)
82
+ end
83
+
84
+ it 'uses the local user\'s RSA private key' do
85
+ expect(driver[:private_key_path]).to eq(rsa)
86
+ end
87
+
88
+ it 'uses the local user\'s RSA public key' do
89
+ expect(driver[:public_key_path]).to eq(rsa + '.pub')
90
+ end
59
91
  end
60
92
 
61
- it 'defaults to no region' do
62
- expect(driver[:openstack_region]).to eq(nil)
93
+ it 'defaults to SSH with root user on port 22' do
94
+ expect(driver[:username]).to eq('root')
95
+ expect(driver[:port]).to eq('22')
63
96
  end
64
97
 
65
- it 'defaults to no service name' do
66
- expect(driver[:openstack_service_name]).to eq(nil)
98
+ nils = [
99
+ :name,
100
+ :openstack_tenant,
101
+ :openstack_region,
102
+ :openstack_service_name,
103
+ :floating_ip_pool,
104
+ :floating_ip
105
+ ]
106
+ nils.each do |i|
107
+ it "defaults to no #{i}" do
108
+ expect(driver[i]).to eq(nil)
109
+ end
67
110
  end
68
111
  end
69
112
 
@@ -79,7 +122,9 @@ describe Kitchen::Driver::Openstack do
79
122
  :openstack_tenant => 'that_one',
80
123
  :openstack_region => 'atlantis',
81
124
  :openstack_service_name => 'the_service',
82
- :ssh_key => '/path/to/id_rsa'
125
+ :private_key_path => '/path/to/id_rsa',
126
+ :floating_ip_pool => 'swimmers',
127
+ :floating_ip => '11111'
83
128
  }
84
129
  end
85
130
 
@@ -89,10 +134,6 @@ describe Kitchen::Driver::Openstack do
89
134
  expect(drv[k]).to eq(v)
90
135
  end
91
136
  end
92
-
93
- it 'SSH with user-specified private key' do
94
- expect(driver[:ssh_key]).to eq('/path/to/id_rsa')
95
- end
96
137
  end
97
138
  end
98
139
 
@@ -263,8 +304,8 @@ describe Kitchen::Driver::Openstack do
263
304
  let(:config) do
264
305
  {
265
306
  :name => 'hello',
266
- :image_ref => 'there',
267
- :flavor_ref => 'captain',
307
+ :image_ref => '111',
308
+ :flavor_ref => '1',
268
309
  :public_key_path => 'tarpals'
269
310
  }
270
311
  end
@@ -273,7 +314,14 @@ describe Kitchen::Driver::Openstack do
273
314
  s.stub(:create) { |arg| arg }
274
315
  s
275
316
  end
276
- let(:compute) { double(:servers => servers) }
317
+ let(:ubuntu_image) { double(:id => '111', :name => 'ubuntu') }
318
+ let(:fedora_image) { double(:id => '222', :name => 'fedora') }
319
+ let(:tiny_flavor) { double(:id => '1', :name => 'tiny') }
320
+ let(:small_flavor) { double(:id => '2', :name => 'small') }
321
+ let(:compute) do
322
+ double(:servers => servers, :images => [ubuntu_image, fedora_image],
323
+ :flavors => [tiny_flavor, small_flavor])
324
+ end
277
325
  let(:driver) do
278
326
  d = Kitchen::Driver::Openstack.new(config)
279
327
  d.instance = instance
@@ -293,8 +341,8 @@ describe Kitchen::Driver::Openstack do
293
341
  let(:config) do
294
342
  {
295
343
  :name => 'hello',
296
- :image_ref => 'there',
297
- :flavor_ref => 'captain',
344
+ :image_ref => '111',
345
+ :flavor_ref => '1',
298
346
  :public_key_path => 'tarpals'
299
347
  }
300
348
  end
@@ -309,8 +357,8 @@ describe Kitchen::Driver::Openstack do
309
357
  let(:config) do
310
358
  {
311
359
  :name => 'hello',
312
- :image_ref => 'there',
313
- :flavor_ref => 'captain',
360
+ :image_ref => '111',
361
+ :flavor_ref => '1',
314
362
  :public_key_path => 'montgomery',
315
363
  :key_name => 'tarpals'
316
364
  }
@@ -321,6 +369,61 @@ describe Kitchen::Driver::Openstack do
321
369
  expect(driver.send(:create_server)).to eq(@config)
322
370
  end
323
371
  end
372
+
373
+ context 'image/flavor specifies id' do
374
+ let(:config) do
375
+ {
376
+ :name => 'hello',
377
+ :image_ref => '111',
378
+ :flavor_ref => '1',
379
+ :public_key_path => 'tarpals'
380
+ }
381
+ end
382
+
383
+ it 'exact id match' do
384
+ servers.should_receive(:create).with(:name => 'hello',
385
+ :image_ref => '111', :flavor_ref => '1',
386
+ :public_key_path => 'tarpals')
387
+ driver.send(:create_server)
388
+ end
389
+ end
390
+
391
+ context 'image/flavor specifies name' do
392
+ let(:config) do
393
+ {
394
+ :name => 'hello',
395
+ :image_ref => 'fedora',
396
+ :flavor_ref => 'small',
397
+ :public_key_path => 'tarpals'
398
+ }
399
+ end
400
+
401
+ it 'exact name match' do
402
+ servers.should_receive(:create).with(:name => 'hello',
403
+ :image_ref => '222', :flavor_ref => '2',
404
+ :public_key_path => 'tarpals')
405
+ driver.send(:create_server)
406
+ end
407
+ end
408
+
409
+ context 'image/flavor specifies regex' do
410
+ let(:config) do
411
+ {
412
+ :name => 'hello',
413
+ # pass regex as string as yml returns string values
414
+ :image_ref => '/edo/',
415
+ :flavor_ref => '/in/',
416
+ :public_key_path => 'tarpals'
417
+ }
418
+ end
419
+
420
+ it 'regex name match' do
421
+ servers.should_receive(:create).with(:name => 'hello',
422
+ :image_ref => '222', :flavor_ref => '1',
423
+ :public_key_path => 'tarpals')
424
+ driver.send(:create_server)
425
+ end
426
+ end
324
427
  end
325
428
 
326
429
  describe '#generate_name' do
@@ -346,6 +449,50 @@ describe Kitchen::Driver::Openstack do
346
449
  end
347
450
  end
348
451
 
452
+ describe '#attach_ip_from_pool' do
453
+ let(:server) { nil }
454
+ let(:pool) { 'swimmers' }
455
+ let(:ip) { '1.1.1.1' }
456
+ let(:address) { double(:ip => ip, :fixed_ip => nil, :instance_id => nil,
457
+ :pool => pool) }
458
+ let(:compute) { double(:addresses => [address]) }
459
+
460
+ before(:each) do
461
+ driver.stub(:attach_ip).with(server, ip).and_return('bing!')
462
+ driver.stub(:compute).and_return(compute)
463
+ end
464
+
465
+ it 'determines an IP to attempt to attach' do
466
+ expect(driver.send(:attach_ip_from_pool, server, pool)).to eq('bing!')
467
+ end
468
+
469
+ context 'no free addresses in the specified pool' do
470
+ let(:address) { double(:ip => ip, :fixed_ip => nil, :instance_id => nil,
471
+ :pool => 'some_other_pool') }
472
+
473
+ it 'raises an exception' do
474
+ expect { driver.send(:attach_ip_from_pool, server, pool) }.to \
475
+ raise_error
476
+ end
477
+ end
478
+ end
479
+
480
+ describe '#attach_ip' do
481
+ let(:ip) { '1.1.1.1' }
482
+ let(:addresses) { {} }
483
+ let(:server) do
484
+ s = double('server')
485
+ s.should_receive(:associate_address).with(ip).and_return(true)
486
+ s.stub(:addresses).and_return(addresses)
487
+ s
488
+ end
489
+
490
+ it 'associates the IP address with the server' do
491
+ expect(driver.send(:attach_ip, server, ip)).to eq(
492
+ [{ 'version' => 4, 'addr' => ip }])
493
+ end
494
+ end
495
+
349
496
  describe '#get_ip' do
350
497
  let(:addresses) { nil }
351
498
  let(:public_ip_addresses) { nil }
@@ -543,6 +690,7 @@ describe Kitchen::Driver::Openstack do
543
690
  end
544
691
 
545
692
  describe '#do_ssh_setup' do
693
+ let(:config) { { :public_key_path => '/pub_key' } }
546
694
  let(:server) { double(:password => 'aloha') }
547
695
  let(:state) { { :hostname => 'host' } }
548
696
  let(:read) { double(:read => 'a_key') }
@@ -555,8 +703,7 @@ describe Kitchen::Driver::Openstack do
555
703
  it 'opens an SSH session to the server' do
556
704
  Fog::SSH.stub(:new).with('host', 'root',
557
705
  { :password => 'aloha' }).and_return(ssh)
558
- driver.stub(:open).with(File.expand_path(
559
- '~/.ssh/id_dsa.pub')).and_return(read)
706
+ driver.stub(:open).with('/pub_key').and_return(read)
560
707
  read.stub(:read).and_return('a_key')
561
708
  res = driver.send(:do_ssh_setup, state, config, server)
562
709
  expected = [
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-openstack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 1.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-09-23 00:00:00.000000000 Z
12
+ date: 2013-10-16 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: test-kitchen