bosh_openstack_cpi 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,40 @@
1
+ # OpenStack BOSH Cloud Provider Interface
2
+
3
+ ## Bringing the world’s most popular open source platform-as-a-service to the world’s most popular open source infrastructure-as-a-service platform
4
+
5
+ This repo contains software designed to manage the deployment of Cloud Foundry on top of OpenStack, using Cloud Foundry BOSH. Say what?
6
+
7
+ ## OpenStack
8
+
9
+ OpenStack is a collection of interrelated open source projects that, together, form a pluggable framework for building massively-scalable infrastructure as a service clouds. OpenStack represents the world's largest and fastest-growing open cloud community, a global collaboration of over 150 leading companies.
10
+
11
+ ## Cloud Foundry
12
+
13
+ Cloud Foundry is the leading open source platform-as-a-service (PaaS) offering with a fast growing ecosystem and strong enterprise demand.
14
+
15
+ ## BOSH
16
+
17
+ 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.
18
+
19
+ * BOSH Source Code: https://github.com/cloudfoundry/bosh
20
+ * BOSH Documentation: https://github.com/cloudfoundry/oss-docs/blob/master/bosh/documentation/documentation.md
21
+
22
+ ## OpenStack and Cloud Foundry, Together using BOSH
23
+
24
+ Cloud Foundry BOSH defines a Cloud Provider Interface API that enables platform-as-a-service deployment across multiple cloud providers - initially VMWare's vSphere and AWS. Piston Cloud has partnered with VMWare to provide a CPI for OpenStack, opening up Cloud Foundry deployment to an entire ecosystem of public and private OpenStack deployments.
25
+
26
+ Using a popular cloud-services client written in Ruby, the OpenStack CPI manages the deployment of a set of virtual machines and enables applications to be deployed dynamically using Cloud Foundry. A common image, called a stem-cell, allows Cloud Foundry BOSH to rapidly build new virtual machines enabling rapid scale-out.
27
+
28
+ We've partnered with VMWare to deliver this project, because the leading open-source platform-as-a-service offering should work seamlessly with deployments of the leading open-source infrastructure-as-a-service project. The work being done to develop this CPI, will enable customers of any OpenStack cloud to use Cloud Foundry to accelerate development of cloud applications and drive value by working against a common service API.
29
+
30
+ ## Piston Cloud Computing, Inc.
31
+
32
+ Piston Cloud Computing, Inc. is the enterprise OpenStack™ company. Founded in early 2011 by technical team leads from NASA and Rackspace®, Piston Cloud is built around OpenStack, the fastest-growing, massively scalable cloud framework. Piston Enterprise OS™ (pentOS™) is the first fully- automated bare-metal cloud operating system built on OpenStack and the first OpenStack distribution specifically focused on security and easy operation of enterprise private clouds for the enterprise.
33
+
34
+ ## Legal Stuff
35
+
36
+ This project, as well as OpenStack and Cloud Foundry, are Apache2-licensed Open Source.
37
+
38
+ VMware and Cloud Foundry are registered trademarks or trademarks of VMware, Inc. in the United States and/or other jurisdictions.
39
+
40
+ OpenStack is a registered trademark of OpenStack, LLC.
@@ -0,0 +1,50 @@
1
+ # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
+
3
+ $:.unshift(File.expand_path("../../rake", __FILE__))
4
+
5
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __FILE__)
6
+
7
+ require "rubygems"
8
+ require "bundler"
9
+ Bundler.setup(:default, :test)
10
+
11
+ require "rake"
12
+ begin
13
+ require "rspec/core/rake_task"
14
+ rescue LoadError
15
+ end
16
+
17
+ require "../bosh/rake/bundler_task"
18
+ require "../bosh/rake/ci_task"
19
+
20
+ gem_helper = Bundler::GemHelper.new(Dir.pwd)
21
+
22
+ desc "Build CPI gem into the pkg directory"
23
+ task "build" do
24
+ gem_helper.build_gem
25
+ end
26
+
27
+ desc "Build and install CPI into system gems"
28
+ task "install" do
29
+ Rake::Task["bundler:install"].invoke
30
+ gem_helper.install_gem
31
+ end
32
+
33
+ BundlerTask.new
34
+
35
+ if defined?(RSpec)
36
+ namespace :spec do
37
+ desc "Run Unit Tests"
38
+ rspec_task = RSpec::Core::RakeTask.new(:unit) do |t|
39
+ t.pattern = "spec/unit/**/*_spec.rb"
40
+ t.rspec_opts = %w(--format progress --colour)
41
+ end
42
+
43
+ CiTask.new do |task|
44
+ task.rspec_task = rspec_task
45
+ end
46
+ end
47
+
48
+ desc "Run tests"
49
+ task :spec => %w(spec:unit)
50
+ end
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Copyright (c) 2012 Piston Cloud Computing, Inc.
4
+
5
+ # Usage example:
6
+ # irb(main):001:0> cpi.create_vm("test",
7
+ # "natty-server-cloudimg-amd64",
8
+ # {"instance_type" => "m1.tiny"},
9
+ # {"default" => {"type" => "dynamic", "cloud_properties" => {"security_groups" => ["name" => "default"]}}},
10
+ # [],
11
+ # {"foo" =>"bar"})
12
+
13
+ gemfile = File.expand_path("../../Gemfile", __FILE__)
14
+
15
+ if File.exists?(gemfile)
16
+ ENV["BUNDLE_GEMFILE"] = gemfile
17
+ require "rubygems"
18
+ require "bundler/setup"
19
+ end
20
+
21
+ $:.unshift(File.expand_path("../../lib", __FILE__))
22
+ require "bosh_openstack_cpi"
23
+ require "irb"
24
+ require "irb/completion"
25
+ require "ostruct"
26
+ require "optparse"
27
+
28
+ config_file = nil
29
+
30
+ opts_parser = OptionParser.new do |opts|
31
+ opts.on("-c", "--config FILE") { |file| config_file = file }
32
+ end
33
+
34
+ opts_parser.parse!
35
+
36
+ unless config_file
37
+ puts opts_parser
38
+ exit(1)
39
+ end
40
+
41
+ @config = YAML.load_file(config_file)
42
+
43
+ module ConsoleHelpers
44
+ def cpi
45
+ @cpi ||= Bosh::OpenStackCloud::Cloud.new(@config)
46
+ end
47
+
48
+ def openstack
49
+ cpi.openstack
50
+ end
51
+
52
+ def registry
53
+ cpi.registry
54
+ end
55
+ end
56
+
57
+ cloud_config = OpenStruct.new(:logger => Logger.new(STDOUT))
58
+
59
+ Bosh::Clouds::Config.configure(cloud_config)
60
+
61
+ include ConsoleHelpers
62
+
63
+ begin
64
+ require 'ruby-debug'
65
+ puts "=> Debugger enabled"
66
+ rescue LoadError
67
+ puts "=> ruby-debug not found, debugger disabled"
68
+ end
69
+
70
+ puts "=> Welcome to BOSH OpenStack CPI console"
71
+ puts "You can use 'cpi' to access CPI methods"
72
+ puts "You can use 'openstack' to access Fog::Compute::OpenStack methods"
73
+
74
+ IRB.start
@@ -0,0 +1,3 @@
1
+ # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
+
3
+ require "cloud/openstack"
@@ -0,0 +1,34 @@
1
+ # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
+
3
+ module Bosh
4
+ module OpenStackCloud; end
5
+ end
6
+
7
+ require "fog"
8
+ require "httpclient"
9
+ require "pp"
10
+ require "set"
11
+ require "tmpdir"
12
+ require "uuidtools"
13
+ require "yajl"
14
+
15
+ require "common/thread_pool"
16
+ require "common/thread_formatter"
17
+
18
+ require "cloud"
19
+ require "cloud/openstack/helpers"
20
+ require "cloud/openstack/cloud"
21
+ require "cloud/openstack/registry_client"
22
+ require "cloud/openstack/version"
23
+
24
+ require "cloud/openstack/network_configurator"
25
+ require "cloud/openstack/network"
26
+ require "cloud/openstack/dynamic_network"
27
+ require "cloud/openstack/vip_network"
28
+
29
+ module Bosh
30
+ module Clouds
31
+ OpenStack = Bosh::OpenStackCloud::Cloud
32
+ Openstack = OpenStack # Alias needed for Bosh::Clouds::Provider.create method
33
+ end
34
+ end
@@ -0,0 +1,550 @@
1
+ # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
+
3
+ module Bosh::OpenStackCloud
4
+
5
+ class Cloud < Bosh::Cloud
6
+ include Helpers
7
+
8
+ DEFAULT_AVAILABILITY_ZONE = "nova"
9
+ DEVICE_POLL_TIMEOUT = 60 # seconds
10
+ METADATA_TIMEOUT = 5 # seconds
11
+
12
+ attr_reader :openstack
13
+ attr_reader :registry
14
+ attr_reader :glance
15
+
16
+ ##
17
+ # Initialize BOSH OpenStack CPI
18
+ # @param [Hash] options CPI options
19
+ #
20
+ def initialize(options)
21
+ @options = options.dup
22
+
23
+ validate_options
24
+
25
+ @logger = Bosh::Clouds::Config.logger
26
+
27
+ @agent_properties = @options["agent"] || {}
28
+ @openstack_properties = @options["openstack"]
29
+ @registry_properties = @options["registry"]
30
+
31
+ @default_key_name = @openstack_properties["default_key_name"]
32
+ @default_security_groups = @openstack_properties["default_security_groups"]
33
+
34
+ openstack_params = {
35
+ :provider => "OpenStack",
36
+ :openstack_auth_url => @openstack_properties["auth_url"],
37
+ :openstack_username => @openstack_properties["username"],
38
+ :openstack_api_key => @openstack_properties["api_key"],
39
+ :openstack_tenant => @openstack_properties["tenant"]
40
+ }
41
+ @openstack = Fog::Compute.new(openstack_params)
42
+ @glance = Fog::Image.new(openstack_params)
43
+
44
+ registry_endpoint = @registry_properties["endpoint"]
45
+ registry_user = @registry_properties["user"]
46
+ registry_password = @registry_properties["password"]
47
+ @registry = RegistryClient.new(registry_endpoint,
48
+ registry_user,
49
+ registry_password)
50
+
51
+ @metadata_lock = Mutex.new
52
+ end
53
+
54
+ ##
55
+ # Creates a new OpenStack Image using stemcell image.
56
+ # @param [String] image_path local filesystem path to a stemcell image
57
+ # @param [Hash] cloud_properties CPI-specific properties
58
+ def create_stemcell(image_path, cloud_properties)
59
+ # TODO: refactor into several smaller methods
60
+ with_thread_name("create_stemcell(#{image_path}...)") do
61
+ begin
62
+ Dir.mktmpdir do |tmp_dir|
63
+ @logger.info("Extracting stemcell to `#{tmp_dir}'")
64
+
65
+ # 1. Unpack image to temp directory
66
+ unpack_image(tmp_dir, image_path)
67
+ root_image = File.join(tmp_dir, "root.img")
68
+
69
+ # 2. Upload image using Glance service
70
+ image_params = {
71
+ :name => "BOSH-#{generate_unique_name}",
72
+ :disk_format => cloud_properties["disk_format"],
73
+ :container_format => cloud_properties["container_format"],
74
+ :properties => {
75
+ :kernel_id => cloud_properties["kernel_id"],
76
+ :ramdisk_id => cloud_properties["ramdisk_id"],
77
+ },
78
+ :location => root_image,
79
+ :is_public => true
80
+ }
81
+
82
+ @logger.info("Creating new image...")
83
+ image = @glance.images.create(image_params)
84
+ state = image.status
85
+
86
+ @logger.info("Creating new image `#{image.id}', state is `#{state}'")
87
+ wait_resource(image, state, :active)
88
+
89
+ image.id.to_s
90
+ end
91
+ rescue => e
92
+ @logger.error(e)
93
+ raise e
94
+ end
95
+ end
96
+ end
97
+
98
+ ##
99
+ # Deletes a stemcell
100
+ # @param [String] stemcell stemcell id that was once returned by {#create_stemcell}
101
+ def delete_stemcell(stemcell_id)
102
+ with_thread_name("delete_stemcell(#{stemcell_id})") do
103
+ @logger.info("Deleting `#{stemcell_id}' stemcell")
104
+ image = @openstack.images.get(stemcell_id)
105
+ image.destroy
106
+ end
107
+ end
108
+
109
+ ##
110
+ # Creates an OpenStack server and waits until it's in running state
111
+ # @param [String] agent_id Agent id associated with new VM
112
+ # @param [String] stemcell_id AMI id that will be used to power on new server
113
+ # @param [Hash] resource_pool Resource pool specification
114
+ # @param [Hash] network_spec Network specification, if it contains security groups they must be existing
115
+ # @param [optional, Array] disk_locality List of disks that might be attached to this server in the future,
116
+ # can be used as a placement hint (i.e. server will only be created if resource pool availability zone is
117
+ # the same as disk availability zone)
118
+ # @param [optional, Hash] environment Data to be merged into agent settings
119
+ # @return [String] created server id
120
+ def create_vm(agent_id, stemcell_id, resource_pool,
121
+ network_spec = nil, disk_locality = nil, environment = nil)
122
+ with_thread_name("create_vm(#{agent_id}, ...)") do
123
+ network_configurator = NetworkConfigurator.new(network_spec)
124
+
125
+ server_name = "vm-#{generate_unique_name}"
126
+ metadata = {
127
+ "registry" => {
128
+ "endpoint" => @registry.endpoint
129
+ },
130
+ "server" => {
131
+ "name" => server_name
132
+ }
133
+ }
134
+
135
+ if disk_locality
136
+ # TODO: use as hint for availability zones
137
+ @logger.debug("Disk locality is ignored by OpenStack CPI")
138
+ end
139
+
140
+ security_groups = network_configurator.security_groups(@default_security_groups)
141
+ @logger.debug("using security groups: #{security_groups.join(', ')}")
142
+
143
+ image = @openstack.images.find { |i| i.id == stemcell_id }
144
+ if image.nil?
145
+ cloud_error("OpenStack CPI: image #{stemcell_id} not found")
146
+ end
147
+
148
+ flavor = @openstack.flavors.find { |f| f.name == resource_pool["instance_type"] }
149
+ if flavor.nil?
150
+ cloud_error("OpenStack CPI: flavor #{resource_pool["instance_type"]} not found")
151
+ end
152
+
153
+ server_params = {
154
+ :name => server_name,
155
+ :image_ref => image.id,
156
+ :flavor_ref => flavor.id,
157
+ :key_name => resource_pool["key_name"] || @default_key_name,
158
+ :security_groups => security_groups.map { |secgrp| {:name => secgrp} },
159
+ :user_data => Yajl::Encoder.encode(metadata)
160
+ }
161
+
162
+ availability_zone = resource_pool["availability_zone"]
163
+ if availability_zone
164
+ server_params[:availability_zone] = availability_zone
165
+ end
166
+
167
+ @logger.info("Creating new server...")
168
+ server = @openstack.servers.create(server_params)
169
+ state = server.state
170
+
171
+ @logger.info("Creating new server `#{server.id}', state is `#{state}'")
172
+ wait_resource(server, state, :active, :state)
173
+
174
+ @logger.info("Configuring network for `#{server.id}'")
175
+ network_configurator.configure(@openstack, server)
176
+
177
+ @logger.info("Updating server settings for `#{server.id}'")
178
+ settings = initial_agent_settings(server_name, agent_id, network_spec, environment)
179
+ @registry.update_settings(server.name, settings)
180
+
181
+ server.id.to_s
182
+ end
183
+ end
184
+
185
+ ##
186
+ # Terminates an OpenStack server and waits until it reports as terminated
187
+ # @param [String] server_id Running OpenStack server id
188
+ def delete_vm(server_id)
189
+ with_thread_name("delete_vm(#{server_id})") do
190
+ server = @openstack.servers.get(server_id)
191
+ @logger.info("Deleting server `#{server_id}'")
192
+ if server
193
+ state = server.state
194
+
195
+ @logger.info("Deleting server `#{server.id}', state is `#{state}'")
196
+ server.destroy
197
+ wait_resource(server, state, :terminated, :state)
198
+
199
+ @logger.info("Deleting server settings for `#{server.id}'")
200
+ @registry.delete_settings(server.name)
201
+ end
202
+ end
203
+ end
204
+
205
+ ##
206
+ # Reboots an OpenStack Server
207
+ # @param [String] server_id Running OpenStack server id
208
+ def reboot_vm(server_id)
209
+ with_thread_name("reboot_vm(#{server_id})") do
210
+ server = @openstack.servers.get(server_id)
211
+ soft_reboot(server)
212
+ end
213
+ end
214
+
215
+ ##
216
+ # Configures networking on existing OpenStack server
217
+ # @param [String] server_id Running OpenStack server id
218
+ # @param [Hash] network_spec raw network spec passed by director
219
+ def configure_networks(server_id, network_spec)
220
+ with_thread_name("configure_networks(#{server_id}, ...)") do
221
+ @logger.info("Configuring `#{server_id}' to use the following " \
222
+ "network settings: #{network_spec.pretty_inspect}")
223
+
224
+ server = @openstack.servers.get(server_id)
225
+ network_configurator = NetworkConfigurator.new(network_spec)
226
+ network_configurator.configure(@openstack, server)
227
+
228
+ update_agent_settings(server) do |settings|
229
+ settings["networks"] = network_spec
230
+ end
231
+ end
232
+ end
233
+
234
+ ##
235
+ # Creates a new OpenStack volume
236
+ # @param [Integer] size disk size in MiB
237
+ # @param [optional, String] server_id vm id of the VM that this disk will be attached to
238
+ # @return [String] created OpenStack volume id
239
+ def create_disk(size, server_id = nil)
240
+ with_thread_name("create_disk(#{size}, #{server_id})") do
241
+ unless size.kind_of?(Integer)
242
+ raise ArgumentError, "disk size needs to be an integer"
243
+ end
244
+
245
+ if (size < 1024)
246
+ cloud_error("OpenStack CPI minimum disk size is 1 GiB")
247
+ end
248
+
249
+ if (size > 1024 * 1000)
250
+ cloud_error("OpenStack CPI maximum disk size is 1 TiB")
251
+ end
252
+
253
+ if server_id
254
+ server = @openstack.servers.get(server_id)
255
+ availability_zone = server.availability_zone
256
+ else
257
+ availability_zone = DEFAULT_AVAILABILITY_ZONE
258
+ end
259
+
260
+ volume_params = {
261
+ :name => "volume-#{generate_unique_name}",
262
+ :description => "",
263
+ :size => (size / 1024.0).ceil,
264
+ :availability_zone => availability_zone
265
+ }
266
+
267
+ @logger.info("Creating new volume...")
268
+ volume = @openstack.volumes.create(volume_params)
269
+ state = volume.status
270
+
271
+ @logger.info("Creating new volume `#{volume.id}', state is `#{state}'")
272
+ wait_resource(volume, state, :available)
273
+
274
+ volume.id.to_s
275
+ end
276
+ end
277
+
278
+ ##
279
+ # Deletes an OpenStack volume
280
+ # @param [String] disk_id volume id
281
+ def delete_disk(disk_id)
282
+ with_thread_name("delete_disk(#{disk_id})") do
283
+ volume = @openstack.volumes.get(disk_id)
284
+ state = volume.status
285
+
286
+ cloud_error("Cannot delete volume `#{disk_id}', state is #{state}") if state.to_sym != :available
287
+
288
+ @logger.info("Deleting volume `#{disk_id}', state is `#{state}'")
289
+ volume.destroy
290
+ wait_resource(volume, state, :deleted)
291
+ end
292
+ end
293
+
294
+ ##
295
+ # Attaches an OpenStack volume to an OpenStack server
296
+ # @param [String] server_id Running OpenStack server id
297
+ # @param [String] disk_id volume id
298
+ def attach_disk(server_id, disk_id)
299
+ with_thread_name("attach_disk(#{server_id}, #{disk_id})") do
300
+ server = @openstack.servers.get(server_id)
301
+ volume = @openstack.volumes.get(disk_id)
302
+
303
+ device_name = attach_volume(server, volume)
304
+
305
+ update_agent_settings(server) do |settings|
306
+ settings["disks"] ||= {}
307
+ settings["disks"]["persistent"] ||= {}
308
+ settings["disks"]["persistent"][disk_id] = device_name
309
+ end
310
+ end
311
+ end
312
+
313
+ ##
314
+ # Detaches an OpenStack volume from an OpenStack server
315
+ # @param [String] server_id Running OpenStack server id
316
+ # @param [String] disk_id volume id
317
+ def detach_disk(server_id, disk_id)
318
+ with_thread_name("detach_disk(#{server_id}, #{disk_id})") do
319
+ server = @openstack.servers.get(server_id)
320
+ volume = @openstack.volumes.get(disk_id)
321
+
322
+ detach_volume(server, volume)
323
+
324
+ update_agent_settings(server) do |settings|
325
+ settings["disks"] ||= {}
326
+ settings["disks"]["persistent"] ||= {}
327
+ settings["disks"]["persistent"].delete(disk_id)
328
+ end
329
+ end
330
+ end
331
+
332
+ ##
333
+ # Validates the deployment
334
+ # @api not_yet_used
335
+ def validate_deployment(old_manifest, new_manifest)
336
+ not_implemented(:validate_deployment)
337
+ end
338
+
339
+ private
340
+
341
+ ##
342
+ # Generates initial agent settings. These settings will be read by agent
343
+ # from OpenStack registry (also a BOSH component) on a target server. Disk
344
+ # conventions for OpenStack are:
345
+ # system disk: /dev/vda
346
+ # OpenStack volumes can be configured to map to other device names later (vdc
347
+ # through vdz, also some kernels will remap vd* to xvd*).
348
+ #
349
+ # @param [String] agent_id Agent id (will be picked up by agent to
350
+ # assume its identity
351
+ # @param [Hash] network_spec Agent network spec
352
+ # @param [Hash] environment
353
+ # @return [Hash]
354
+ def initial_agent_settings(server_name, agent_id, network_spec, environment)
355
+ settings = {
356
+ "vm" => {
357
+ "name" => server_name
358
+ },
359
+ "agent_id" => agent_id,
360
+ "networks" => network_spec,
361
+ "disks" => {
362
+ "system" => "/dev/vda",
363
+ "ephemeral" => "/dev/vdb",
364
+ "persistent" => {}
365
+ }
366
+ }
367
+
368
+ settings["env"] = environment if environment
369
+ settings.merge(@agent_properties)
370
+ end
371
+
372
+ def update_agent_settings(server)
373
+ unless block_given?
374
+ raise ArgumentError, "block is not provided"
375
+ end
376
+
377
+ # TODO uncomment to test registry
378
+ @logger.info("Updating server settings for `#{server.id}'")
379
+ settings = @registry.read_settings(server.name)
380
+ yield settings
381
+ @registry.update_settings(server.name, settings)
382
+ end
383
+
384
+ def generate_unique_name
385
+ UUIDTools::UUID.random_create.to_s
386
+ end
387
+
388
+ ##
389
+ # Soft reboots an OpenStack server
390
+ # @param [Fog::Compute::OpenStack::Server] server OpenStack server
391
+ def soft_reboot(server)
392
+ state = server.state
393
+
394
+ @logger.info("Soft rebooting server `#{server.id}', state is `#{state}'")
395
+ server.reboot
396
+ wait_resource(server, state, :active, :state)
397
+ end
398
+
399
+ ##
400
+ # Hard reboots an OpenStack server
401
+ # @param [Fog::Compute::OpenStack::Server] server OpenStack server
402
+ def hard_reboot(server)
403
+ state = server.state
404
+
405
+ @logger.info("Hard rebooting server `#{server.id}', state is `#{state}'")
406
+ server.reboot(type = 'HARD')
407
+ wait_resource(server, state, :active, :state)
408
+ end
409
+
410
+ ##
411
+ # Attaches an OpenStack volume to an OpenStack server
412
+ # @param [Fog::Compute::OpenStack::Server] server OpenStack server
413
+ # @param [Fog::Compute::OpenStack::Volume] volume OpenStack volume
414
+ def attach_volume(server, volume)
415
+ volume_attachments = @openstack.get_server_volumes(server.id).body['volumeAttachments']
416
+ device_names = Set.new(volume_attachments.collect! {|v| v["device"] })
417
+ new_attachment = nil
418
+
419
+ ("c".."z").each do |char|
420
+ dev_name = "/dev/vd#{char}"
421
+ if device_names.include?(dev_name)
422
+ @logger.warn("`#{dev_name}' on `#{server.id}' is taken")
423
+ next
424
+ end
425
+ @logger.info("Attaching volume `#{volume.id}' to `#{server.id}', device name is `#{dev_name}'")
426
+ if volume.attach(server.id, dev_name)
427
+ state = volume.status
428
+ wait_resource(volume, state, :"in-use")
429
+ new_attachment = dev_name
430
+ end
431
+ break
432
+ end
433
+
434
+ if new_attachment.nil?
435
+ cloud_error("Server has too many disks attached")
436
+ end
437
+
438
+ new_attachment
439
+ end
440
+
441
+ ##
442
+ # Detaches an OpenStack volume from an OpenStack server
443
+ # @param [Fog::Compute::OpenStack::Server] server OpenStack server
444
+ # @param [Fog::Compute::OpenStack::Volume] volume OpenStack volume
445
+ def detach_volume(server, volume)
446
+ volume_attachments = @openstack.get_server_volumes(server.id).body['volumeAttachments']
447
+ device_map = volume_attachments.collect! {|v| v["volumeId"] }
448
+
449
+ if !device_map.include?(volume.id)
450
+ cloud_error("Disk `#{volume.id}' is not attached to server `#{server.id}'")
451
+ end
452
+
453
+ state = volume.status
454
+ @logger.info("Detaching volume `#{volume.id}' from `#{server.id}', state is `#{state}'")
455
+ volume.detach(server.id, volume.id)
456
+ wait_resource(volume, state, :available)
457
+ end
458
+
459
+ ##
460
+ # Reads current server id from OpenStack metadata. We are assuming
461
+ # server id cannot change while current process is running
462
+ # and thus memoizing it.
463
+ def current_server_id
464
+ @metadata_lock.synchronize do
465
+ return @current_server_id if @current_server_id
466
+
467
+ client = HTTPClient.new
468
+ client.connect_timeout = METADATA_TIMEOUT
469
+ # Using 169.254.169.254 is an OpenStack convention for getting
470
+ # server metadata
471
+ uri = "http://169.254.169.254/1.0/user-data"
472
+
473
+ headers = {"Accept" => "application/json"}
474
+ response = client.get(uri, {}, headers)
475
+ unless response.status == 200
476
+ cloud_error("Server metadata endpoint returned HTTP #{response.status}")
477
+ end
478
+
479
+ user_data = Yajl::Parser.parse(response.body)
480
+ unless user_data.is_a?(Hash)
481
+ cloud_error("Invalid response from #{uri} , Hash expected, " \
482
+ "got #{response.body.class}: #{response.body}")
483
+ end
484
+
485
+ unless user_data.has_key?("server") &&
486
+ user_data["server"].has_key?("name")
487
+ cloud_error("Cannot parse user data for endpoint #{user_data.inspect}")
488
+ end
489
+ @current_server_id = user_data["server"]["name"]
490
+ end
491
+
492
+ rescue HTTPClient::TimeoutError
493
+ cloud_error("Timed out reading server metadata, " \
494
+ "please make sure CPI is running on an OpenStack server")
495
+ end
496
+
497
+ def find_device(vd_name)
498
+ xvd_name = vd_name.gsub(/^\/dev\/vd/, "/dev/xvd")
499
+
500
+ DEVICE_POLL_TIMEOUT.times do
501
+ if File.blockdev?(vd_name)
502
+ return vd_name
503
+ elsif File.blockdev?(xvd_name)
504
+ return xvd_name
505
+ end
506
+ sleep(1)
507
+ end
508
+
509
+ cloud_error("Cannot find OpenStack volume on current server")
510
+ end
511
+
512
+ def unpack_image(tmp_dir, image_path)
513
+ output = `tar -C #{tmp_dir} -xzf #{image_path} 2>&1`
514
+ if $?.exitstatus != 0
515
+ cloud_error("Failed to unpack stemcell root image" \
516
+ "tar exit status #{$?.exitstatus}: #{output}")
517
+ end
518
+
519
+ root_image = File.join(tmp_dir, "root.img")
520
+ unless File.exists?(root_image)
521
+ cloud_error("Root image is missing from stemcell archive")
522
+ end
523
+ end
524
+
525
+ ##
526
+ # Checks if options passed to CPI are valid and can actually
527
+ # be used to create all required data structures etc.
528
+ #
529
+ def validate_options
530
+ unless @options.has_key?("openstack") &&
531
+ @options["openstack"].is_a?(Hash) &&
532
+ @options["openstack"]["auth_url"] &&
533
+ @options["openstack"]["username"] &&
534
+ @options["openstack"]["api_key"] &&
535
+ @options["openstack"]["tenant"]
536
+ raise ArgumentError, "Invalid OpenStack configuration parameters"
537
+ end
538
+
539
+ unless @options.has_key?("registry") &&
540
+ @options["registry"].is_a?(Hash) &&
541
+ @options["registry"]["endpoint"] &&
542
+ @options["registry"]["user"] &&
543
+ @options["registry"]["password"]
544
+ raise ArgumentError, "Invalid registry configuration parameters"
545
+ end
546
+ end
547
+
548
+ end
549
+
550
+ end