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 +1 -1
- data/lib/cloud/openstack/cloud.rb +311 -197
- data/lib/cloud/openstack/dynamic_network.rb +4 -2
- data/lib/cloud/openstack/helpers.rb +43 -13
- data/lib/cloud/openstack/network.rb +1 -1
- data/lib/cloud/openstack/network_configurator.rb +23 -15
- data/lib/cloud/openstack/registry_client.rb +29 -11
- data/lib/cloud/openstack/version.rb +1 -1
- data/lib/cloud/openstack/vip_network.rb +19 -20
- data/spec/assets/sample_config.yml +14 -0
- data/spec/integration/cpi_test.rb +119 -0
- data/spec/spec_helper.rb +6 -1
- data/spec/unit/attach_disk_spec.rb +37 -21
- data/spec/unit/configure_networks_spec.rb +49 -26
- data/spec/unit/create_disk_spec.rb +17 -17
- data/spec/unit/create_stemcell_spec.rb +78 -20
- data/spec/unit/create_vm_spec.rb +80 -16
- data/spec/unit/delete_disk_spec.rb +5 -3
- data/spec/unit/delete_stemcell_spec.rb +18 -11
- data/spec/unit/delete_vm_spec.rb +1 -2
- data/spec/unit/detach_disk_spec.rb +23 -12
- data/spec/unit/helpers_spec.rb +46 -6
- data/spec/unit/network_configurator_spec.rb +8 -8
- data/spec/unit/reboot_vm_spec.rb +2 -4
- data/spec/unit/validate_deployment_spec.rb +2 -1
- metadata +9 -5
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
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
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
|
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 => "
|
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
|
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 => "
|
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
|
-
|
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
|
-
#
|
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}'
|
181
|
+
@logger.info("Deleting stemcell `#{stemcell_id}'...")
|
139
182
|
image = @glance.images.find_by_id(stemcell_id)
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
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
|
-
#
|
170
|
-
# @param [String]
|
171
|
-
#
|
172
|
-
# @param [
|
173
|
-
#
|
174
|
-
#
|
175
|
-
#
|
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]
|
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
|
-
|
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
|
-
|
194
|
-
|
195
|
-
|
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("
|
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|
|
259
|
+
flavor = @openstack.flavors.find { |f|
|
260
|
+
f.name == resource_pool["instance_type"] }
|
207
261
|
if flavor.nil?
|
208
|
-
cloud_error("
|
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(
|
272
|
+
:user_data => Yajl::Encoder.encode(user_data)
|
218
273
|
}
|
219
274
|
|
220
|
-
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}'
|
230
|
-
wait_resource(server,
|
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
|
236
|
-
settings = initial_agent_settings(server_name, agent_id, network_spec,
|
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
|
-
#
|
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,
|
309
|
+
wait_resource(server, :terminated, :state, true)
|
256
310
|
|
257
|
-
@logger.info("Deleting
|
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
|
-
#
|
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
|
-
#
|
276
|
-
# @param [
|
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
|
296
|
-
#
|
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, "
|
380
|
+
raise ArgumentError, "Disk size needs to be an integer"
|
301
381
|
end
|
302
382
|
|
303
383
|
if (size < 1024)
|
304
|
-
cloud_error("
|
384
|
+
cloud_error("Minimum disk size is 1 GiB")
|
305
385
|
end
|
306
386
|
|
307
387
|
if (size > 1024 * 1000)
|
308
|
-
cloud_error("
|
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}'
|
330
|
-
wait_resource(volume,
|
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
|
-
#
|
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
|
-
|
343
|
-
|
344
|
-
|
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
|
-
|
347
|
-
|
348
|
-
|
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
|
-
#
|
355
|
-
# @param [String]
|
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
|
-
#
|
374
|
-
# @param [String]
|
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
|
-
#
|
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
|
-
#
|
405
|
-
#
|
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, "
|
586
|
+
raise ArgumentError, "Block is not provided"
|
433
587
|
end
|
434
588
|
|
435
|
-
|
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
|
-
|
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,
|
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
|
-
|
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,
|
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).
|
474
|
-
|
475
|
-
|
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}',
|
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
|
-
|
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).
|
505
|
-
|
658
|
+
volume_attachments = @openstack.get_server_volumes(server.id).
|
659
|
+
body['volumeAttachments']
|
660
|
+
device_map = volume_attachments.collect! { |v| v["volumeId"] }
|
506
661
|
|
507
|
-
|
508
|
-
cloud_error("Disk `#{volume.id}' is not attached to
|
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
|
-
|
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,
|
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
|
-
|
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
|
-
#
|
533
|
-
#
|
534
|
-
#
|
535
|
-
|
536
|
-
|
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
|
-
|
731
|
+
def task_checkpoint
|
732
|
+
Bosh::Clouds::Config.task_checkpoint
|
733
|
+
end
|
621
734
|
|
735
|
+
end
|
622
736
|
end
|