bosh_openstack_cpi 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -19,7 +19,7 @@ Cloud Foundry is the leading open source platform-as-a-service (PaaS) offering w
19
19
  Cloud Foundry BOSH is an open source tool chain for release engineering, deployment and lifecycle management of large scale distributed services. In this manual we describe the architecture, topology, configuration, and use of BOSH, as well as the structure and conventions used in packaging and deployment.
20
20
 
21
21
  * BOSH Source Code: https://github.com/cloudfoundry/bosh
22
- * BOSH Documentation: https://github.com/cloudfoundry/oss-docs/blob/master/bosh/documentation/documentation.md
22
+ * BOSH Documentation: https://github.com/cloudfoundry/oss-docs/blob/master/bosh/documentation.md
23
23
 
24
24
  ## OpenStack and Cloud Foundry, Together using BOSH
25
25
 
@@ -1,22 +1,23 @@
1
1
  # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
2
 
3
3
  module Bosh::OpenStackCloud
4
-
4
+ ##
5
+ # BOSH OpenStack CPI
5
6
  class Cloud < Bosh::Cloud
6
7
  include Helpers
7
8
 
8
- DEFAULT_AVAILABILITY_ZONE = "nova"
9
- DEVICE_POLL_TIMEOUT = 60 # seconds
10
- METADATA_TIMEOUT = 5 # seconds
11
-
12
9
  attr_reader :openstack
13
10
  attr_reader :registry
14
11
  attr_reader :glance
12
+ attr_accessor :logger
15
13
 
16
14
  ##
17
- # Initialize BOSH OpenStack CPI
18
- # @param [Hash] options CPI options
15
+ # Creates a new BOSH OpenStack CPI
19
16
  #
17
+ # @param [Hash] options CPI options
18
+ # @option options [Hash] openstack OpenStack specific options
19
+ # @option options [Hash] agent agent options
20
+ # @option options [Hash] registry agent options
20
21
  def initialize(options)
21
22
  @options = options.dup
22
23
 
@@ -36,10 +37,19 @@ module Bosh::OpenStackCloud
36
37
  :openstack_auth_url => @openstack_properties["auth_url"],
37
38
  :openstack_username => @openstack_properties["username"],
38
39
  :openstack_api_key => @openstack_properties["api_key"],
39
- :openstack_tenant => @openstack_properties["tenant"]
40
+ :openstack_tenant => @openstack_properties["tenant"],
41
+ :openstack_region => @openstack_properties["region"]
40
42
  }
41
43
  @openstack = Fog::Compute.new(openstack_params)
42
- @glance = Fog::Image.new(openstack_params)
44
+
45
+ glance_params = {
46
+ :provider => "OpenStack",
47
+ :openstack_auth_url => @openstack_properties["auth_url"],
48
+ :openstack_username => @openstack_properties["username"],
49
+ :openstack_api_key => @openstack_properties["api_key"],
50
+ :openstack_tenant => @openstack_properties["tenant"]
51
+ }
52
+ @glance = Fog::Image.new(glance_params)
43
53
 
44
54
  registry_endpoint = @registry_properties["endpoint"]
45
55
  registry_user = @registry_properties["user"]
@@ -52,31 +62,49 @@ module Bosh::OpenStackCloud
52
62
  end
53
63
 
54
64
  ##
55
- # Creates a new OpenStack Image using stemcell image.
56
- # @param [String] image_path local filesystem path to a stemcell image
65
+ # Creates a new OpenStack Image using stemcell image. It requires access
66
+ # to the OpenStack Glance service.
67
+ #
68
+ # @param [String] image_path Local filesystem path to a stemcell image
57
69
  # @param [Hash] cloud_properties CPI-specific properties
70
+ # @option cloud_properties [String] name Stemcell name
71
+ # @option cloud_properties [String] version Stemcell version
72
+ # @option cloud_properties [String] infrastructure Stemcell infraestructure
73
+ # @option cloud_properties [String] disk_format Image disk format
74
+ # @option cloud_properties [String] container_format Image container format
75
+ # @option cloud_properties [optional, String] kernel_id UUID of the kernel
76
+ # image stored at OpenStack
77
+ # @option cloud_properties [optional, String] kernel_file Name of the
78
+ # kernel image file provided at the stemcell archive
79
+ # @option cloud_properties [optional, String] ramdisk_id UUID of the
80
+ # ramdisk image stored at OpenStack
81
+ # @option cloud_properties [optional, String] ramdisk_file Name of the
82
+ # ramdisk image file provided at the stemcell archive
83
+ # @return [String] OpenStack image UUID of the stemcell
58
84
  def create_stemcell(image_path, cloud_properties)
59
85
  with_thread_name("create_stemcell(#{image_path}...)") do
60
86
  begin
61
87
  Dir.mktmpdir do |tmp_dir|
62
- @logger.info("Extracting stemcell to `#{tmp_dir}'")
88
+ @logger.info("Extracting stemcell to `#{tmp_dir}'...")
63
89
  image_name = "BOSH-#{generate_unique_name}"
64
90
 
65
91
  # 1. Unpack image to temp directory
66
92
  unpack_image(tmp_dir, image_path)
67
93
  root_image = File.join(tmp_dir, "root.img")
68
94
 
69
- # 2. If image contains a kernel file, upload it
95
+ # 2. If image contains a kernel file, upload it to glance service
70
96
  kernel_id = nil
71
97
  if cloud_properties["kernel_id"]
72
98
  kernel_id = cloud_properties["kernel_id"]
73
99
  elsif cloud_properties["kernel_file"]
74
100
  kernel_image = File.join(tmp_dir, cloud_properties["kernel_file"])
75
101
  unless File.exists?(kernel_image)
76
- cloud_error("Kernel image is missing from stemcell archive")
102
+ cloud_error("Kernel image " \
103
+ "#{cloud_properties['kernel_file']} " \
104
+ "is missing from stemcell archive")
77
105
  end
78
106
  kernel_params = {
79
- :name => "AKI-#{image_name}",
107
+ :name => "#{image_name}-AKI",
80
108
  :disk_format => "aki",
81
109
  :container_format => "aki",
82
110
  :location => kernel_image,
@@ -84,20 +112,23 @@ module Bosh::OpenStackCloud
84
112
  :stemcell => image_name
85
113
  }
86
114
  }
115
+ @logger.info("Uploading kernel image...")
87
116
  kernel_id = upload_image(kernel_params)
88
117
  end
89
118
 
90
- # 3. If image contains a ramdisk file, upload it
119
+ # 3. If image contains a ramdisk file, upload it to glance service
91
120
  ramdisk_id = nil
92
121
  if cloud_properties["ramdisk_id"]
93
122
  ramdisk_id = cloud_properties["ramdisk_id"]
94
123
  elsif cloud_properties["ramdisk_file"]
95
124
  ramdisk_image = File.join(tmp_dir, cloud_properties["ramdisk_file"])
96
125
  unless File.exists?(kernel_image)
97
- cloud_error("Ramdisk image is missing from stemcell archive")
126
+ cloud_error("Ramdisk image " \
127
+ "#{cloud_properties['ramdisk_file']} " \
128
+ "is missing from stemcell archive")
98
129
  end
99
130
  ramdisk_params = {
100
- :name => "ARI-#{image_name}",
131
+ :name => "#{image_name}-ARI",
101
132
  :disk_format => "ari",
102
133
  :container_format => "ari",
103
134
  :location => ramdisk_image,
@@ -105,6 +136,7 @@ module Bosh::OpenStackCloud
105
136
  :stemcell => image_name
106
137
  }
107
138
  }
139
+ @logger.info("Uploading ramdisk image...")
108
140
  ramdisk_id = upload_image(ramdisk_params)
109
141
  end
110
142
 
@@ -119,8 +151,16 @@ module Bosh::OpenStackCloud
119
151
  image_properties = {}
120
152
  image_properties[:kernel_id] = kernel_id if kernel_id
121
153
  image_properties[:ramdisk_id] = ramdisk_id if ramdisk_id
122
- image_params[:properties] = image_properties unless image_properties.empty?
123
-
154
+ if cloud_properties["name"]
155
+ image_properties[:stemcell_name] = cloud_properties["name"]
156
+ end
157
+ if cloud_properties["version"]
158
+ image_properties[:stemcell_version] = cloud_properties["version"]
159
+ end
160
+ unless image_properties.empty?
161
+ image_params[:properties] = image_properties
162
+ end
163
+ @logger.info("Uploading image...")
124
164
  upload_image(image_params)
125
165
  end
126
166
  rescue => e
@@ -132,56 +172,72 @@ module Bosh::OpenStackCloud
132
172
 
133
173
  ##
134
174
  # Deletes a stemcell
135
- # @param [String] stemcell stemcell id that was once returned by {#create_stemcell}
175
+ #
176
+ # @param [String] stemcell_id OpenStack image UUID of the stemcell to be
177
+ # deleted
178
+ # @return [void]
136
179
  def delete_stemcell(stemcell_id)
137
180
  with_thread_name("delete_stemcell(#{stemcell_id})") do
138
- @logger.info("Deleting `#{stemcell_id}' stemcell")
181
+ @logger.info("Deleting stemcell `#{stemcell_id}'...")
139
182
  image = @glance.images.find_by_id(stemcell_id)
140
-
141
- kernel_id = image.properties["kernel_id"]
142
- if kernel_id
143
- kernel = @glance.images.find_by_id(kernel_id)
144
- if kernel.properties["stemcell"]
145
- if kernel.properties["stemcell"] == image.name
146
- @logger.info("Deleting `#{stemcell_id}' stemcell kernel")
147
- kernel.destroy
183
+ if image
184
+ kernel_id = image.properties["kernel_id"]
185
+ if kernel_id
186
+ kernel = @glance.images.find_by_id(kernel_id)
187
+ if kernel && kernel.properties["stemcell"]
188
+ if kernel.properties["stemcell"] == image.name
189
+ @logger.info("Deleting kernel `#{kernel_id}'...")
190
+ kernel.destroy
191
+ @logger.info("Kernel `#{kernel_id}' is now deleted")
192
+ end
148
193
  end
149
194
  end
150
- end
151
195
 
152
- ramdisk_id = image.properties["ramdisk_id"]
153
- if ramdisk_id
154
- ramdisk = @glance.images.find_by_id(ramdisk_id)
155
- if ramdisk.properties["stemcell"]
156
- if ramdisk.properties["stemcell"] == image.name
157
- @logger.info("Deleting `#{stemcell_id}' stemcell ramdisk")
158
- ramdisk.destroy
196
+ ramdisk_id = image.properties["ramdisk_id"]
197
+ if ramdisk_id
198
+ ramdisk = @glance.images.find_by_id(ramdisk_id)
199
+ if ramdisk && ramdisk.properties["stemcell"]
200
+ if ramdisk.properties["stemcell"] == image.name
201
+ @logger.info("Deleting ramdisk `#{ramdisk_id}'...")
202
+ ramdisk.destroy
203
+ @logger.info("Ramdisk `#{ramdisk_id}' is now deleted")
204
+ end
159
205
  end
160
206
  end
161
- end
162
207
 
163
- image.destroy
208
+ image.destroy
209
+ @logger.info("Stemcell `#{stemcell_id}' is now deleted")
210
+ else
211
+ @logger.info("Stemcell `#{stemcell_id}' not found. Skipping.")
212
+ end
164
213
  end
165
214
  end
166
215
 
167
216
  ##
168
217
  # Creates an OpenStack server and waits until it's in running state
169
- # @param [String] agent_id Agent id associated with new VM
170
- # @param [String] stemcell_id AMI id that will be used to power on new server
171
- # @param [Hash] resource_pool Resource pool specification
172
- # @param [Hash] network_spec Network specification, if it contains security groups they must be existing
173
- # @param [optional, Array] disk_locality List of disks that might be attached to this server in the future,
174
- # can be used as a placement hint (i.e. server will only be created if resource pool availability zone is
175
- # the same as disk availability zone)
218
+ #
219
+ # @param [String] agent_id UUID for the agent that will be used later on by
220
+ # the director to locate and talk to the agent
221
+ # @param [String] stemcell_id OpenStack image UUID that will be used to
222
+ # power on new server
223
+ # @param [Hash] resource_pool cloud specific properties describing the
224
+ # resources needed for this VM
225
+ # @param [Hash] networks list of networks and their settings needed for
226
+ # this VM
227
+ # @param [optional, Array] disk_locality List of disks that might be
228
+ # attached to this server in the future, can be used as a placement
229
+ # hint (i.e. server will only be created if resource pool availability
230
+ # zone is the same as disk availability zone)
176
231
  # @param [optional, Hash] environment Data to be merged into agent settings
177
- # @return [String] created server id
232
+ # @return [String] OpenStack server UUID
178
233
  def create_vm(agent_id, stemcell_id, resource_pool,
179
234
  network_spec = nil, disk_locality = nil, environment = nil)
180
235
  with_thread_name("create_vm(#{agent_id}, ...)") do
236
+ @logger.info("Creating new server...")
181
237
  network_configurator = NetworkConfigurator.new(network_spec)
182
238
 
183
239
  server_name = "vm-#{generate_unique_name}"
184
- metadata = {
240
+ user_data = {
185
241
  "registry" => {
186
242
  "endpoint" => @registry.endpoint
187
243
  },
@@ -190,23 +246,22 @@ module Bosh::OpenStackCloud
190
246
  }
191
247
  }
192
248
 
193
- if disk_locality
194
- # TODO: use as hint for availability zones
195
- @logger.debug("Disk locality is ignored by OpenStack CPI")
196
- end
197
-
198
- security_groups = network_configurator.security_groups(@default_security_groups)
199
- @logger.debug("using security groups: #{security_groups.join(', ')}")
249
+ security_groups =
250
+ network_configurator.security_groups(@default_security_groups)
251
+ @logger.debug("Using security groups: `#{security_groups.join(', ')}'")
200
252
 
201
253
  image = @openstack.images.find { |i| i.id == stemcell_id }
202
254
  if image.nil?
203
- cloud_error("OpenStack CPI: image #{stemcell_id} not found")
255
+ cloud_error("Image `#{stemcell_id}' not found")
204
256
  end
257
+ @logger.debug("Using image: `#{stemcell_id}'")
205
258
 
206
- flavor = @openstack.flavors.find { |f| f.name == resource_pool["instance_type"] }
259
+ flavor = @openstack.flavors.find { |f|
260
+ f.name == resource_pool["instance_type"] }
207
261
  if flavor.nil?
208
- cloud_error("OpenStack CPI: flavor #{resource_pool["instance_type"]} not found")
262
+ cloud_error("Flavor `#{resource_pool["instance_type"]}' not found")
209
263
  end
264
+ @logger.debug("Using flavor: `#{resource_pool["instance_type"]}'")
210
265
 
211
266
  server_params = {
212
267
  :name => server_name,
@@ -214,26 +269,26 @@ module Bosh::OpenStackCloud
214
269
  :flavor_ref => flavor.id,
215
270
  :key_name => resource_pool["key_name"] || @default_key_name,
216
271
  :security_groups => security_groups,
217
- :user_data => Yajl::Encoder.encode(metadata)
272
+ :user_data => Yajl::Encoder.encode(user_data)
218
273
  }
219
274
 
220
- availability_zone = resource_pool["availability_zone"]
275
+ availability_zone = select_availability_zone(disk_locality,
276
+ resource_pool["availability_zone"])
221
277
  if availability_zone
222
278
  server_params[:availability_zone] = availability_zone
223
279
  end
224
280
 
225
- @logger.info("Creating new server...")
226
281
  server = @openstack.servers.create(server_params)
227
- state = server.state
228
282
 
229
- @logger.info("Creating new server `#{server.id}', state is `#{state}'")
230
- wait_resource(server, state, :active, :state)
283
+ @logger.info("Creating new server `#{server.id}'...")
284
+ wait_resource(server, :active, :state)
231
285
 
232
- @logger.info("Configuring network for `#{server.id}'")
286
+ @logger.info("Configuring network for server `#{server.id}'...")
233
287
  network_configurator.configure(@openstack, server)
234
288
 
235
- @logger.info("Updating server settings for `#{server.id}'")
236
- settings = initial_agent_settings(server_name, agent_id, network_spec, environment)
289
+ @logger.info("Updating settings for server `#{server.id}'...")
290
+ settings = initial_agent_settings(server_name, agent_id, network_spec,
291
+ environment)
237
292
  @registry.update_settings(server.name, settings)
238
293
 
239
294
  server.id.to_s
@@ -242,45 +297,68 @@ module Bosh::OpenStackCloud
242
297
 
243
298
  ##
244
299
  # Terminates an OpenStack server and waits until it reports as terminated
245
- # @param [String] server_id Running OpenStack server id
300
+ #
301
+ # @param [String] server_id OpenStack server UUID
302
+ # @return [void]
246
303
  def delete_vm(server_id)
247
304
  with_thread_name("delete_vm(#{server_id})") do
305
+ @logger.info("Deleting server `#{server_id}'...")
248
306
  server = @openstack.servers.get(server_id)
249
- @logger.info("Deleting server `#{server_id}'")
250
307
  if server
251
- state = server.state
252
-
253
- @logger.info("Deleting server `#{server.id}', state is `#{state}'")
254
308
  server.destroy
255
- wait_resource(server, state, :terminated, :state)
309
+ wait_resource(server, :terminated, :state, true)
256
310
 
257
- @logger.info("Deleting server settings for `#{server.id}'")
311
+ @logger.info("Deleting settings for server `#{server.id}'...")
258
312
  @registry.delete_settings(server.name)
313
+ else
314
+ @logger.info("Server `#{server_id}' not found. Skipping.")
259
315
  end
260
316
  end
261
317
  end
262
318
 
263
319
  ##
264
320
  # Reboots an OpenStack Server
265
- # @param [String] server_id Running OpenStack server id
321
+ #
322
+ # @param [String] server_id OpenStack server UUID
323
+ # @return [void]
266
324
  def reboot_vm(server_id)
267
325
  with_thread_name("reboot_vm(#{server_id})") do
268
326
  server = @openstack.servers.get(server_id)
327
+ unless server
328
+ cloud_error("Server `#{server_id}' not found")
329
+ end
269
330
  soft_reboot(server)
270
331
  end
271
332
  end
272
333
 
273
334
  ##
274
335
  # Configures networking on existing OpenStack server
275
- # @param [String] server_id Running OpenStack server id
276
- # @param [Hash] network_spec raw network spec passed by director
336
+ #
337
+ # @param [String] server_id OpenStack server UUID
338
+ # @param [Hash] network_spec Raw network spec passed by director
339
+ # @return [void]
340
+ # @raise [Bosh::Clouds:NotSupported] if the security groups change
277
341
  def configure_networks(server_id, network_spec)
278
342
  with_thread_name("configure_networks(#{server_id}, ...)") do
279
343
  @logger.info("Configuring `#{server_id}' to use the following " \
280
344
  "network settings: #{network_spec.pretty_inspect}")
281
345
 
282
- server = @openstack.servers.get(server_id)
283
346
  network_configurator = NetworkConfigurator.new(network_spec)
347
+ server = @openstack.servers.get(server_id)
348
+
349
+ sg = @openstack.list_security_groups(server_id).body["security_groups"]
350
+ actual = sg.collect { |s| s["name"] }.sort
351
+ new = network_configurator.security_groups(@default_security_groups)
352
+
353
+ # If the security groups change, we need to recreate the VM
354
+ # as you can't change the security group of a running server,
355
+ # we need to send the InstanceUpdater a request to do it for us
356
+ unless actual == new
357
+ raise Bosh::Clouds::NotSupported,
358
+ "security groups change requires VM recreation: %s to %s" %
359
+ [actual.join(", "), new.join(", ")]
360
+ end
361
+
284
362
  network_configurator.configure(@openstack, server)
285
363
 
286
364
  update_agent_settings(server) do |settings|
@@ -291,43 +369,43 @@ module Bosh::OpenStackCloud
291
369
 
292
370
  ##
293
371
  # Creates a new OpenStack volume
372
+ #
294
373
  # @param [Integer] size disk size in MiB
295
- # @param [optional, String] server_id vm id of the VM that this disk will be attached to
296
- # @return [String] created OpenStack volume id
374
+ # @param [optional, String] server_id OpenStack server UUID of the VM that
375
+ # this disk will be attached to
376
+ # @return [String] OpenStack volume UUID
297
377
  def create_disk(size, server_id = nil)
298
378
  with_thread_name("create_disk(#{size}, #{server_id})") do
299
379
  unless size.kind_of?(Integer)
300
- raise ArgumentError, "disk size needs to be an integer"
380
+ raise ArgumentError, "Disk size needs to be an integer"
301
381
  end
302
382
 
303
383
  if (size < 1024)
304
- cloud_error("OpenStack CPI minimum disk size is 1 GiB")
384
+ cloud_error("Minimum disk size is 1 GiB")
305
385
  end
306
386
 
307
387
  if (size > 1024 * 1000)
308
- cloud_error("OpenStack CPI maximum disk size is 1 TiB")
309
- end
310
-
311
- if server_id
312
- server = @openstack.servers.get(server_id)
313
- availability_zone = server.availability_zone
314
- else
315
- availability_zone = DEFAULT_AVAILABILITY_ZONE
388
+ cloud_error("Maximum disk size is 1 TiB")
316
389
  end
317
390
 
318
391
  volume_params = {
319
392
  :name => "volume-#{generate_unique_name}",
320
393
  :description => "",
321
- :size => (size / 1024.0).ceil,
322
- :availability_zone => availability_zone
394
+ :size => (size / 1024.0).ceil
323
395
  }
324
396
 
397
+ if server_id
398
+ server = @openstack.servers.get(server_id)
399
+ if server && server.availability_zone
400
+ volume_params[:availability_zone] = server.availability_zone
401
+ end
402
+ end
403
+
325
404
  @logger.info("Creating new volume...")
326
405
  volume = @openstack.volumes.create(volume_params)
327
- state = volume.status
328
406
 
329
- @logger.info("Creating new volume `#{volume.id}', state is `#{state}'")
330
- wait_resource(volume, state, :available)
407
+ @logger.info("Creating new volume `#{volume.id}'...")
408
+ wait_resource(volume, :available)
331
409
 
332
410
  volume.id.to_s
333
411
  end
@@ -335,28 +413,44 @@ module Bosh::OpenStackCloud
335
413
 
336
414
  ##
337
415
  # Deletes an OpenStack volume
338
- # @param [String] disk_id volume id
416
+ #
417
+ # @param [String] disk_id OpenStack volume UUID
418
+ # @return [void]
419
+ # @raise [Bosh::Clouds::CloudError] if disk is not in available state
339
420
  def delete_disk(disk_id)
340
421
  with_thread_name("delete_disk(#{disk_id})") do
422
+ @logger.info("Deleting volume `#{disk_id}'...")
341
423
  volume = @openstack.volumes.get(disk_id)
342
- state = volume.status
343
-
344
- cloud_error("Cannot delete volume `#{disk_id}', state is #{state}") if state.to_sym != :available
424
+ if volume
425
+ state = volume.status
426
+ if state.to_sym != :available
427
+ cloud_error("Cannot delete volume `#{disk_id}', state is #{state}")
428
+ end
345
429
 
346
- @logger.info("Deleting volume `#{disk_id}', state is `#{state}'")
347
- volume.destroy
348
- wait_resource(volume, state, :deleted)
430
+ volume.destroy
431
+ wait_resource(volume, :deleted, :status, true)
432
+ else
433
+ @logger.info("Volume `#{disk_id}' not found. Skipping.")
434
+ end
349
435
  end
350
436
  end
351
437
 
352
438
  ##
353
439
  # Attaches an OpenStack volume to an OpenStack server
354
- # @param [String] server_id Running OpenStack server id
355
- # @param [String] disk_id volume id
440
+ #
441
+ # @param [String] server_id OpenStack server UUID
442
+ # @param [String] disk_id OpenStack volume UUID
443
+ # @return [void]
356
444
  def attach_disk(server_id, disk_id)
357
445
  with_thread_name("attach_disk(#{server_id}, #{disk_id})") do
358
446
  server = @openstack.servers.get(server_id)
447
+ unless server
448
+ cloud_error("Server `#{server_id}' not found")
449
+ end
359
450
  volume = @openstack.volumes.get(disk_id)
451
+ unless server
452
+ cloud_error("Volume `#{disk_id}' not found")
453
+ end
360
454
 
361
455
  device_name = attach_volume(server, volume)
362
456
 
@@ -370,12 +464,20 @@ module Bosh::OpenStackCloud
370
464
 
371
465
  ##
372
466
  # Detaches an OpenStack volume from an OpenStack server
373
- # @param [String] server_id Running OpenStack server id
374
- # @param [String] disk_id volume id
467
+ #
468
+ # @param [String] server_id OpenStack server UUID
469
+ # @param [String] disk_id OpenStack volume UUID
470
+ # @return [void]
375
471
  def detach_disk(server_id, disk_id)
376
472
  with_thread_name("detach_disk(#{server_id}, #{disk_id})") do
377
473
  server = @openstack.servers.get(server_id)
474
+ unless server
475
+ cloud_error("Server `#{server_id}' not found")
476
+ end
378
477
  volume = @openstack.volumes.get(disk_id)
478
+ unless server
479
+ cloud_error("Volume `#{disk_id}' not found")
480
+ end
379
481
 
380
482
  detach_volume(server, volume)
381
483
 
@@ -389,26 +491,74 @@ module Bosh::OpenStackCloud
389
491
 
390
492
  ##
391
493
  # Validates the deployment
392
- # @api not_yet_used
494
+ #
495
+ # @note Not implemented in the OpenStack CPI
393
496
  def validate_deployment(old_manifest, new_manifest)
394
497
  not_implemented(:validate_deployment)
395
498
  end
396
499
 
500
+ ##
501
+ # Selects the availability zone to use from a list of disk volumes,
502
+ # resource pool availability zone (if any) and the default availability
503
+ # zone.
504
+ #
505
+ # @param [Array] volumes OpenStack volume UUIDs to attach to the vm
506
+ # @param [String] resource_pool_az availability zone specified in
507
+ # the resource pool (may be nil)
508
+ # @return [String] availability zone to use or nil
509
+ # @note this is a private method that is public to make it easier to test
510
+ def select_availability_zone(volumes, resource_pool_az)
511
+ if volumes && !volumes.empty?
512
+ disks = volumes.map { |vid| @openstack.volumes.get(vid) }
513
+ ensure_same_availability_zone(disks, resource_pool_az)
514
+ disks.first.availability_zone
515
+ else
516
+ resource_pool_az
517
+ end
518
+ end
519
+
520
+ ##
521
+ # Ensure all supplied availability zones are the same
522
+ #
523
+ # @param [Array] disks OpenStack volumes
524
+ # @param [String] default availability zone specified in
525
+ # the resource pool (may be nil)
526
+ # @return [String] availability zone to use or nil
527
+ # @note this is a private method that is public to make it easier to test
528
+ def ensure_same_availability_zone(disks, default)
529
+ zones = disks.map { |disk| disk.availability_zone }
530
+ zones << default if default
531
+ zones.uniq!
532
+ cloud_error "can't use multiple availability zones: %s" %
533
+ zones.join(", ") unless zones.size == 1 || zones.empty?
534
+ end
535
+
397
536
  private
398
537
 
538
+ ##
539
+ # Generates an unique name
540
+ #
541
+ # @return [String] Unique name
542
+ def generate_unique_name
543
+ UUIDTools::UUID.random_create.to_s
544
+ end
545
+
399
546
  ##
400
547
  # Generates initial agent settings. These settings will be read by agent
401
548
  # from OpenStack registry (also a BOSH component) on a target server. Disk
402
549
  # conventions for OpenStack are:
403
550
  # system disk: /dev/vda
404
- # OpenStack volumes can be configured to map to other device names later (vdc
405
- # through vdz, also some kernels will remap vd* to xvd*).
551
+ # ephemeral disk: /dev/ vdb
552
+ # OpenStack volumes can be configured to map to other device names later
553
+ # (vdc through vdz, also some kernels will remap vd* to xvd*).
406
554
  #
555
+ # @param [String] server_name Name of the OpenStack server (will be picked
556
+ # up by agent to fetch registry settings)
407
557
  # @param [String] agent_id Agent id (will be picked up by agent to
408
558
  # assume its identity
409
559
  # @param [Hash] network_spec Agent network spec
410
- # @param [Hash] environment
411
- # @return [Hash]
560
+ # @param [Hash] environment Environment settings
561
+ # @return [Hash] Agent settings
412
562
  def initial_agent_settings(server_name, agent_id, network_spec, environment)
413
563
  settings = {
414
564
  "vm" => {
@@ -427,63 +577,65 @@ module Bosh::OpenStackCloud
427
577
  settings.merge(@agent_properties)
428
578
  end
429
579
 
580
+ ##
581
+ # Updates the agent settings
582
+ #
583
+ # @param [Fog::Compute::OpenStack::Server] server OpenStack server
430
584
  def update_agent_settings(server)
431
585
  unless block_given?
432
- raise ArgumentError, "block is not provided"
586
+ raise ArgumentError, "Block is not provided"
433
587
  end
434
588
 
435
- # TODO uncomment to test registry
436
- @logger.info("Updating server settings for `#{server.id}'")
589
+ @logger.info("Updating settings for server `#{server.id}'...")
437
590
  settings = @registry.read_settings(server.name)
438
591
  yield settings
439
592
  @registry.update_settings(server.name, settings)
440
593
  end
441
594
 
442
- def generate_unique_name
443
- UUIDTools::UUID.random_create.to_s
444
- end
445
-
446
595
  ##
447
596
  # Soft reboots an OpenStack server
597
+ #
448
598
  # @param [Fog::Compute::OpenStack::Server] server OpenStack server
599
+ # @return [void]
449
600
  def soft_reboot(server)
450
- state = server.state
451
-
452
- @logger.info("Soft rebooting server `#{server.id}', state is `#{state}'")
601
+ @logger.info("Soft rebooting server `#{server.id}'...")
453
602
  server.reboot
454
- wait_resource(server, state, :active, :state)
603
+ wait_resource(server, :active, :state)
455
604
  end
456
605
 
457
606
  ##
458
607
  # Hard reboots an OpenStack server
608
+ #
459
609
  # @param [Fog::Compute::OpenStack::Server] server OpenStack server
610
+ # @return [void]
460
611
  def hard_reboot(server)
461
- state = server.state
462
-
463
- @logger.info("Hard rebooting server `#{server.id}', state is `#{state}'")
612
+ @logger.info("Hard rebooting server `#{server.id}'...")
464
613
  server.reboot(type = 'HARD')
465
- wait_resource(server, state, :active, :state)
614
+ wait_resource(server, :active, :state)
466
615
  end
467
616
 
468
617
  ##
469
618
  # Attaches an OpenStack volume to an OpenStack server
619
+ #
470
620
  # @param [Fog::Compute::OpenStack::Server] server OpenStack server
471
621
  # @param [Fog::Compute::OpenStack::Volume] volume OpenStack volume
622
+ # @return [String] Device name
472
623
  def attach_volume(server, volume)
473
- volume_attachments = @openstack.get_server_volumes(server.id).body['volumeAttachments']
474
- device_names = Set.new(volume_attachments.collect! {|v| v["device"] })
475
- new_attachment = nil
624
+ volume_attachments = @openstack.get_server_volumes(server.id).
625
+ body['volumeAttachments']
626
+ device_names = Set.new(volume_attachments.collect! { |v| v["device"] })
476
627
 
628
+ new_attachment = nil
477
629
  ("c".."z").each do |char|
478
630
  dev_name = "/dev/vd#{char}"
479
631
  if device_names.include?(dev_name)
480
632
  @logger.warn("`#{dev_name}' on `#{server.id}' is taken")
481
633
  next
482
634
  end
483
- @logger.info("Attaching volume `#{volume.id}' to `#{server.id}', device name is `#{dev_name}'")
635
+ @logger.info("Attaching volume `#{volume.id}' to `#{server.id}', " \
636
+ "device name is `#{dev_name}'")
484
637
  if volume.attach(server.id, dev_name)
485
- state = volume.status
486
- wait_resource(volume, state, :"in-use")
638
+ wait_resource(volume, :"in-use")
487
639
  new_attachment = dev_name
488
640
  end
489
641
  break
@@ -498,89 +650,46 @@ module Bosh::OpenStackCloud
498
650
 
499
651
  ##
500
652
  # Detaches an OpenStack volume from an OpenStack server
653
+ #
501
654
  # @param [Fog::Compute::OpenStack::Server] server OpenStack server
502
655
  # @param [Fog::Compute::OpenStack::Volume] volume OpenStack volume
656
+ # @return [void]
503
657
  def detach_volume(server, volume)
504
- volume_attachments = @openstack.get_server_volumes(server.id).body['volumeAttachments']
505
- device_map = volume_attachments.collect! {|v| v["volumeId"] }
658
+ volume_attachments = @openstack.get_server_volumes(server.id).
659
+ body['volumeAttachments']
660
+ device_map = volume_attachments.collect! { |v| v["volumeId"] }
506
661
 
507
- if !device_map.include?(volume.id)
508
- cloud_error("Disk `#{volume.id}' is not attached to server `#{server.id}'")
662
+ unless device_map.include?(volume.id)
663
+ cloud_error("Disk `#{volume.id}' is not attached to " \
664
+ "server `#{server.id}'")
509
665
  end
510
666
 
511
- state = volume.status
512
- @logger.info("Detaching volume `#{volume.id}' from `#{server.id}', state is `#{state}'")
667
+ @logger.info("Detaching volume `#{volume.id}' from `#{server.id}'...")
513
668
  volume.detach(server.id, volume.id)
514
- wait_resource(volume, state, :available)
669
+ wait_resource(volume, :available)
515
670
  end
516
671
 
517
672
  ##
518
673
  # Uploads a new image to OpenStack via Glance
674
+ #
519
675
  # @param [Hash] image_params Image params
676
+ # @return [String] OpenStack image UUID
520
677
  def upload_image(image_params)
521
678
  @logger.info("Creating new image...")
679
+ started_at = Time.now
522
680
  image = @glance.images.create(image_params)
523
- state = image.status
524
-
525
- @logger.info("Creating new image `#{image.id}', state is `#{state}'")
526
- wait_resource(image, state, :active)
681
+ total = Time.now - started_at
682
+ @logger.info("Created new image `#{image.id}', took #{total}s")
527
683
 
528
684
  image.id.to_s
529
685
  end
530
686
 
531
687
  ##
532
- # Reads current server id from OpenStack metadata. We are assuming
533
- # server id cannot change while current process is running
534
- # and thus memoizing it.
535
- def current_server_id
536
- @metadata_lock.synchronize do
537
- return @current_server_id if @current_server_id
538
-
539
- client = HTTPClient.new
540
- client.connect_timeout = METADATA_TIMEOUT
541
- # Using 169.254.169.254 is an OpenStack convention for getting
542
- # server metadata
543
- uri = "http://169.254.169.254/latest/user-data"
544
-
545
- headers = {"Accept" => "application/json"}
546
- response = client.get(uri, {}, headers)
547
- unless response.status == 200
548
- cloud_error("Server metadata endpoint returned HTTP #{response.status}")
549
- end
550
-
551
- user_data = Yajl::Parser.parse(response.body)
552
- unless user_data.is_a?(Hash)
553
- cloud_error("Invalid response from #{uri} , Hash expected, " \
554
- "got #{response.body.class}: #{response.body}")
555
- end
556
-
557
- unless user_data.has_key?("server") &&
558
- user_data["server"].has_key?("name")
559
- cloud_error("Cannot parse user data for endpoint #{user_data.inspect}")
560
- end
561
- @current_server_id = user_data["server"]["name"]
562
- end
563
-
564
- rescue HTTPClient::TimeoutError
565
- cloud_error("Timed out reading server metadata, " \
566
- "please make sure CPI is running on an OpenStack server")
567
- end
568
-
569
- def find_device(vd_name)
570
- xvd_name = vd_name.gsub(/^\/dev\/vd/, "/dev/xvd")
571
-
572
- DEVICE_POLL_TIMEOUT.times do
573
- if File.blockdev?(vd_name)
574
- return vd_name
575
- elsif File.blockdev?(xvd_name)
576
- return xvd_name
577
- end
578
- sleep(1)
579
- end
580
-
581
- cloud_error("Cannot find OpenStack volume on current server")
582
- end
583
-
688
+ # Unpacks a stemcell archive
689
+ #
690
+ # @param [String] tmp_dir Temporary directory
691
+ # @param [String] image_path Local filesystem path to a stemcell image
692
+ # @return [void]
584
693
  def unpack_image(tmp_dir, image_path)
585
694
  output = `tar -C #{tmp_dir} -xzf #{image_path} 2>&1`
586
695
  if $?.exitstatus != 0
@@ -598,6 +707,8 @@ module Bosh::OpenStackCloud
598
707
  # Checks if options passed to CPI are valid and can actually
599
708
  # be used to create all required data structures etc.
600
709
  #
710
+ # @return [void]
711
+ # @raise [ArgumentError] if options are not valid
601
712
  def validate_options
602
713
  unless @options.has_key?("openstack") &&
603
714
  @options["openstack"].is_a?(Hash) &&
@@ -617,6 +728,9 @@ module Bosh::OpenStackCloud
617
728
  end
618
729
  end
619
730
 
620
- end
731
+ def task_checkpoint
732
+ Bosh::Clouds::Config.task_checkpoint
733
+ end
621
734
 
735
+ end
622
736
  end