bosh_openstack_cpi 0.0.7 → 1.5.0.pre.1113

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,7 +1,5 @@
1
1
  # OpenStack BOSH Cloud Provider Interface
2
2
 
3
- [![Build Status](https://secure.travis-ci.org/piston/openstack-bosh-cpi.png)](http://travis-ci.org/piston/openstack-bosh-cpi)
4
-
5
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
6
4
 
7
5
  This repo contains software designed to manage the deployment of Cloud Foundry on top of OpenStack, using Cloud Foundry BOSH. Say what?
@@ -18,9 +16,6 @@ Cloud Foundry is the leading open source platform-as-a-service (PaaS) offering w
18
16
 
19
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.
20
18
 
21
- * BOSH Source Code: https://github.com/cloudfoundry/bosh
22
- * BOSH Documentation: https://github.com/cloudfoundry/oss-docs/blob/master/bosh/documentation.md
23
-
24
19
  ## OpenStack and Cloud Foundry, Together using BOSH
25
20
 
26
21
  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.
data/USAGE.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # BOSH OpenStack Cloud Provider Interface
2
+ # Copyright (c) 2009-2013 VMware, Inc.
2
3
  # Copyright (c) 2012 Piston Cloud Computing, Inc.
3
4
 
4
5
  For online documentation see: http://rubydoc.info/gems/bosh_openstack_cpi/
@@ -9,6 +10,8 @@ These options are passed to the OpenStack CPI when it is instantiated.
9
10
 
10
11
  ### OpenStack options
11
12
 
13
+ The registry options are passed to the Openstack CPI by the BOSH director based on the settings in `director.yml`:
14
+
12
15
  * `auth_url` (required)
13
16
  URL of the OpenStack Identity endpoint to connect to
14
17
  * `username` (required)
@@ -20,45 +23,55 @@ These options are passed to the OpenStack CPI when it is instantiated.
20
23
  * `region` (optional)
21
24
  OpenStack region
22
25
  * `endpoint_type` (optional)
23
- OpenStack endpoint type for Glance
26
+ OpenStack endpoint type (publicURL (default), adminURL, internalURL)
27
+ * `state_timeout` (optional)
28
+ Timeout (in seconds) for OpenStack resources desired state (by default 300)
29
+ * `stemcell_public_visibility` (optional)
30
+ Set public visibility for stemcells (true or false (default))
24
31
  * `default_key_name` (required)
25
32
  default OpenStack ssh key name to assign to created virtual machines
26
33
  * `default_security_group` (required)
27
34
  default OpenStack security group to assign to created virtual machines
28
- * `private_key` (required)
29
- local path to the ssh private key, must match `default_key_name`
30
35
 
31
36
  ### Registry options
32
37
 
33
- The registry options are passed to the Openstack CPI by the BOSH director based on the settings in `director.yml`, but can be overridden if needed.
38
+ The registry options are passed to the Openstack CPI by the BOSH director based on the settings in `director.yml`:
34
39
 
35
40
  * `endpoint` (required)
36
41
  OpenStack registry URL
37
42
  * `user` (required)
38
43
  OpenStack registry user
39
44
  * `password` (required)
40
- rOpenStack egistry password
45
+ OpenStack registry password
41
46
 
42
47
  ### Agent options
43
48
 
44
- Agent options are passed to the OpenStack CPI by the BOSH director based on the settings in `director.yml`, but can be overridden if needed.
49
+ The agent options are passed to the OpenStack CPI by the BOSH director based on the settings in `director.yml`:
50
+
51
+ ## Network options
52
+
53
+ The OpenStack CPI supports these networks types:
54
+
55
+ * `type` (required)
56
+ can be `dynamic` for a DHCP assigned IP by OpenStack, `manual` for a static IP assigned manually at the BOSH deployment manifest or `vip` to use a Floating IP (which needs to be already allocated)
45
57
 
46
- ### Resource pool options
58
+ These options are specified under `cloud_properties` in the `networks` section of a BOSH deployment manifest:
47
59
 
48
- These options are specified under `cloud_options` in the `resource_pools` section of a BOSH deployment manifest.
60
+ * `security_groups` (optional)
61
+ the OpenStack security groups to assign to VMs. If not specified, it'll use the default security groups set at the OpenStack options
62
+
63
+ * `net_id` (required for `manual` networks)
64
+ the OpenStack Quantum network UUID to attach as a NIC to VMs.
65
+
66
+ ## Resource pool options
67
+
68
+ These options are specified under `cloud_properties` in the `resource_pools` section of a BOSH deployment manifest:
49
69
 
50
70
  * `instance_type` (required)
51
71
  which type of instance (OpenStack flavor) the VMs should belong to
52
72
  * `availability_zone` (optional)
53
73
  the OpenStack availability zone the VMs should be created in
54
74
 
55
- ### Network options
56
-
57
- These options are specified under `cloud_options` in the `networks` section of a BOSH deployment manifest.
58
-
59
- * `type` (required)
60
- can be either `dynamic` for a DHCP assigned IP by OpenStack, or `vip` to use a Floating IP (which needs to be already allocated)
61
-
62
75
  ## Example
63
76
 
64
77
  This is a sample of how OpenStack specific properties are used in a BOSH deployment manifest:
@@ -70,36 +83,51 @@ This is a sample of how OpenStack specific properties are used in a BOSH deploym
70
83
  ...
71
84
 
72
85
  networks:
73
- - name: nginx_network
74
- type: vip
75
- cloud_properties: {}
76
86
  - name: default
77
87
  type: dynamic
78
88
  cloud_properties:
79
89
  security_groups:
80
- - default
81
-
90
+ - default
91
+ net_id: 2438bca2-24fa-450f-ae7b-ec2e53b51984
92
+ - name: static
93
+ type: manual
94
+ subnets:
95
+ - name: private
96
+ range: 10.0.1.0/24
97
+ gateway: 10.0.1.1
98
+ reserved:
99
+ - 10.0.1.2 - 10.0.1.9
100
+ static:
101
+ - 10.0.1.10 - 10.0.1.20
102
+ cloud_properties:
103
+ security_groups:
104
+ - default
105
+ net_id: 8d8b84b4-faa6-4605-9fbf-c179bdae4282
106
+ - name: floating
107
+ type: vip
108
+ cloud_properties: {}
82
109
  ...
83
110
 
84
111
  resource_pools:
85
112
  - name: common
86
113
  network: default
87
- size: 3
114
+ size: 1
88
115
  stemcell:
89
- name: bosh-stemcell
90
- version: 0.6.7
116
+ name: bosh-openstack-kvm-ubuntu
117
+ version: latest
91
118
  cloud_properties:
92
119
  instance_type: m1.small
120
+ availability_zone:
93
121
 
94
122
  ...
95
123
 
96
124
  properties:
97
125
  openstack:
98
- auth_url: http://pistoncloud.com/:5000/v2.0/tokens
126
+ auth_url: http://pistoncloud.com/:5000/v2.0
99
127
  username: christopher
100
128
  api_key: QRoqsenPsNGX6
101
129
  tenant: Bosh
102
- region: us-west
130
+ region: RegionOne
131
+ endpoint_type: publicURL
103
132
  default_key_name: bosh
104
- default_security_groups: ["bosh"]
105
- private_key: /home/bosh/.ssh/bosh.pem
133
+ default_security_groups: ["bosh"]
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ # Copyright (c) 2009-2013 VMware, Inc.
3
4
  # Copyright (c) 2012 Piston Cloud Computing, Inc.
4
5
 
5
6
  # Usage example:
@@ -10,15 +11,6 @@
10
11
  # [],
11
12
  # {"foo" =>"bar"})
12
13
 
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
14
  require "bosh_openstack_cpi"
23
15
  require "irb"
24
16
  require "irb/completion"
@@ -38,7 +30,7 @@ unless config_file
38
30
  exit(1)
39
31
  end
40
32
 
41
- @config = YAML.load_file(config_file)
33
+ @config = Psych.load_file(config_file)
42
34
 
43
35
  module ConsoleHelpers
44
36
  def cpi
@@ -1,3 +1,4 @@
1
+ # Copyright (c) 2009-2013 VMware, Inc.
1
2
  # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
3
 
3
4
  require "cloud/openstack"
@@ -1,3 +1,4 @@
1
+ # Copyright (c) 2009-2013 VMware, Inc.
1
2
  # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
3
 
3
4
  module Bosh
@@ -6,24 +7,28 @@ end
6
7
 
7
8
  require "fog"
8
9
  require "httpclient"
10
+ require "json"
9
11
  require "pp"
10
12
  require "set"
11
13
  require "tmpdir"
12
- require "uuidtools"
14
+ require "securerandom"
13
15
  require "yajl"
14
16
 
17
+ require "common/exec"
15
18
  require "common/thread_pool"
16
19
  require "common/thread_formatter"
17
20
 
21
+ require 'bosh/registry/client'
18
22
  require "cloud"
19
23
  require "cloud/openstack/helpers"
20
24
  require "cloud/openstack/cloud"
21
- require "cloud/openstack/registry_client"
25
+ require "cloud/openstack/tag_manager"
22
26
  require "cloud/openstack/version"
23
27
 
24
28
  require "cloud/openstack/network_configurator"
25
29
  require "cloud/openstack/network"
26
30
  require "cloud/openstack/dynamic_network"
31
+ require "cloud/openstack/manual_network"
27
32
  require "cloud/openstack/vip_network"
28
33
 
29
34
  module Bosh
@@ -1,3 +1,4 @@
1
+ # Copyright (c) 2009-2013 VMware, Inc.
1
2
  # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
3
 
3
4
  module Bosh::OpenStackCloud
@@ -6,6 +7,9 @@ module Bosh::OpenStackCloud
6
7
  class Cloud < Bosh::Cloud
7
8
  include Helpers
8
9
 
10
+ BOSH_APP_DIR = "/var/vcap/bosh"
11
+ FIRST_DEVICE_NAME_LETTER = "b"
12
+
9
13
  attr_reader :openstack
10
14
  attr_reader :registry
11
15
  attr_reader :glance
@@ -22,15 +26,21 @@ module Bosh::OpenStackCloud
22
26
  @options = options.dup
23
27
 
24
28
  validate_options
29
+ initialize_registry
25
30
 
26
31
  @logger = Bosh::Clouds::Config.logger
27
32
 
28
33
  @agent_properties = @options["agent"] || {}
29
34
  @openstack_properties = @options["openstack"]
30
- @registry_properties = @options["registry"]
31
35
 
32
36
  @default_key_name = @openstack_properties["default_key_name"]
33
37
  @default_security_groups = @openstack_properties["default_security_groups"]
38
+ @state_timeout = @openstack_properties["state_timeout"]
39
+ @stemcell_public_visibility = @openstack_properties["stemcell_public_visibility"]
40
+
41
+ unless @openstack_properties["auth_url"].match(/\/tokens$/)
42
+ @openstack_properties["auth_url"] = @openstack_properties["auth_url"] + "/tokens"
43
+ end
34
44
 
35
45
  openstack_params = {
36
46
  :provider => "OpenStack",
@@ -38,9 +48,15 @@ module Bosh::OpenStackCloud
38
48
  :openstack_username => @openstack_properties["username"],
39
49
  :openstack_api_key => @openstack_properties["api_key"],
40
50
  :openstack_tenant => @openstack_properties["tenant"],
41
- :openstack_region => @openstack_properties["region"]
51
+ :openstack_region => @openstack_properties["region"],
52
+ :openstack_endpoint_type => @openstack_properties["endpoint_type"]
42
53
  }
43
- @openstack = Fog::Compute.new(openstack_params)
54
+ begin
55
+ @openstack = Fog::Compute.new(openstack_params)
56
+ rescue Exception => e
57
+ @logger.error(e)
58
+ cloud_error("Unable to connect to the OpenStack Compute API. Check task debug log for details.")
59
+ end
44
60
 
45
61
  glance_params = {
46
62
  :provider => "OpenStack",
@@ -49,17 +65,14 @@ module Bosh::OpenStackCloud
49
65
  :openstack_api_key => @openstack_properties["api_key"],
50
66
  :openstack_tenant => @openstack_properties["tenant"],
51
67
  :openstack_region => @openstack_properties["region"],
52
- :openstack_endpoint_type => @openstack_properties["endpoint_type"] ||
53
- "publicURL"
68
+ :openstack_endpoint_type => @openstack_properties["endpoint_type"]
54
69
  }
55
- @glance = Fog::Image.new(glance_params)
56
-
57
- registry_endpoint = @registry_properties["endpoint"]
58
- registry_user = @registry_properties["user"]
59
- registry_password = @registry_properties["password"]
60
- @registry = RegistryClient.new(registry_endpoint,
61
- registry_user,
62
- registry_password)
70
+ begin
71
+ @glance = Fog::Image.new(glance_params)
72
+ rescue Exception => e
73
+ @logger.error(e)
74
+ cloud_error("Unable to connect to the OpenStack Image Service API. Check task debug log for details.")
75
+ end
63
76
 
64
77
  @metadata_lock = Mutex.new
65
78
  end
@@ -75,12 +88,8 @@ module Bosh::OpenStackCloud
75
88
  # @option cloud_properties [String] infrastructure Stemcell infraestructure
76
89
  # @option cloud_properties [String] disk_format Image disk format
77
90
  # @option cloud_properties [String] container_format Image container format
78
- # @option cloud_properties [optional, String] kernel_id UUID of the kernel
79
- # image stored at OpenStack
80
91
  # @option cloud_properties [optional, String] kernel_file Name of the
81
92
  # kernel image file provided at the stemcell archive
82
- # @option cloud_properties [optional, String] ramdisk_id UUID of the
83
- # ramdisk image stored at OpenStack
84
93
  # @option cloud_properties [optional, String] ramdisk_file Name of the
85
94
  # ramdisk image file provided at the stemcell archive
86
95
  # @return [String] OpenStack image UUID of the stemcell
@@ -88,83 +97,41 @@ module Bosh::OpenStackCloud
88
97
  with_thread_name("create_stemcell(#{image_path}...)") do
89
98
  begin
90
99
  Dir.mktmpdir do |tmp_dir|
91
- @logger.info("Extracting stemcell to `#{tmp_dir}'...")
92
- image_name = "BOSH-#{generate_unique_name}"
93
-
94
- # 1. Unpack image to temp directory
95
- unpack_image(tmp_dir, image_path)
96
- root_image = File.join(tmp_dir, "root.img")
97
-
98
- # 2. If image contains a kernel file, upload it to glance service
99
- kernel_id = nil
100
- if cloud_properties["kernel_id"]
101
- kernel_id = cloud_properties["kernel_id"]
102
- elsif cloud_properties["kernel_file"]
103
- kernel_image = File.join(tmp_dir, cloud_properties["kernel_file"])
104
- unless File.exists?(kernel_image)
105
- cloud_error("Kernel image " \
106
- "#{cloud_properties['kernel_file']} " \
107
- "is missing from stemcell archive")
108
- end
109
- kernel_params = {
110
- :name => "#{image_name}-AKI",
111
- :disk_format => "aki",
112
- :container_format => "aki",
113
- :location => kernel_image,
114
- :properties => {
115
- :stemcell => image_name
116
- }
117
- }
118
- @logger.info("Uploading kernel image...")
119
- kernel_id = upload_image(kernel_params)
120
- end
121
-
122
- # 3. If image contains a ramdisk file, upload it to glance service
123
- ramdisk_id = nil
124
- if cloud_properties["ramdisk_id"]
125
- ramdisk_id = cloud_properties["ramdisk_id"]
126
- elsif cloud_properties["ramdisk_file"]
127
- ramdisk_image = File.join(tmp_dir, cloud_properties["ramdisk_file"])
128
- unless File.exists?(ramdisk_image)
129
- cloud_error("Ramdisk image " \
130
- "#{cloud_properties['ramdisk_file']} " \
131
- "is missing from stemcell archive")
132
- end
133
- ramdisk_params = {
134
- :name => "#{image_name}-ARI",
135
- :disk_format => "ari",
136
- :container_format => "ari",
137
- :location => ramdisk_image,
138
- :properties => {
139
- :stemcell => image_name
140
- }
141
- }
142
- @logger.info("Uploading ramdisk image...")
143
- ramdisk_id = upload_image(ramdisk_params)
144
- end
145
-
146
- # 4. Upload image using Glance service
100
+ @logger.info("Creating new image...")
147
101
  image_params = {
148
- :name => image_name,
102
+ :name => "BOSH-#{generate_unique_name}",
149
103
  :disk_format => cloud_properties["disk_format"],
150
104
  :container_format => cloud_properties["container_format"],
151
- :location => root_image,
152
- :is_public => true
105
+ :is_public => @stemcell_public_visibility.nil? ? false : @stemcell_public_visibility,
153
106
  }
107
+
154
108
  image_properties = {}
155
- image_properties[:kernel_id] = kernel_id if kernel_id
156
- image_properties[:ramdisk_id] = ramdisk_id if ramdisk_id
157
- if cloud_properties["name"]
158
- image_properties[:stemcell_name] = cloud_properties["name"]
159
- end
160
- if cloud_properties["version"]
161
- image_properties[:stemcell_version] = cloud_properties["version"]
162
- end
163
- unless image_properties.empty?
164
- image_params[:properties] = image_properties
109
+ vanilla_options = ["name", "version", "os_type", "os_distro", "architecture", "auto_disk_config"]
110
+ vanilla_options.reject{ |o| cloud_properties[o].nil? }.each do |key|
111
+ image_properties[key.to_sym] = cloud_properties[key]
112
+ end
113
+ image_params[:properties] = image_properties unless image_properties.empty?
114
+
115
+ # If image_location is set in cloud properties, then pass the copy-from parm. Then Glance will fetch it
116
+ # from the remote location on a background job and store it in its repository.
117
+ # Otherwise, unpack image to temp directory and upload to Glance the root image.
118
+ if cloud_properties["image_location"]
119
+ @logger.info("Using remote image from `#{cloud_properties["image_location"]}'...")
120
+ image_params[:copy_from] = cloud_properties["image_location"]
121
+ else
122
+ @logger.info("Extracting stemcell file to `#{tmp_dir}'...")
123
+ unpack_image(tmp_dir, image_path)
124
+ image_params[:location] = File.join(tmp_dir, "root.img")
165
125
  end
166
- @logger.info("Uploading image...")
167
- upload_image(image_params)
126
+
127
+ # Upload image using Glance service
128
+ @logger.debug("Using image parms: `#{image_params.inspect}'")
129
+ image = with_openstack { @glance.images.create(image_params) }
130
+
131
+ @logger.info("Creating new image `#{image.id}'...")
132
+ wait_resource(image, :active)
133
+
134
+ image.id.to_s
168
135
  end
169
136
  rescue => e
170
137
  @logger.error(e)
@@ -182,33 +149,9 @@ module Bosh::OpenStackCloud
182
149
  def delete_stemcell(stemcell_id)
183
150
  with_thread_name("delete_stemcell(#{stemcell_id})") do
184
151
  @logger.info("Deleting stemcell `#{stemcell_id}'...")
185
- image = @glance.images.find_by_id(stemcell_id)
152
+ image = with_openstack { @glance.images.find_by_id(stemcell_id) }
186
153
  if image
187
- kernel_id = image.properties["kernel_id"]
188
- if kernel_id
189
- kernel = @glance.images.find_by_id(kernel_id)
190
- if kernel && kernel.properties["stemcell"]
191
- if kernel.properties["stemcell"] == image.name
192
- @logger.info("Deleting kernel `#{kernel_id}'...")
193
- kernel.destroy
194
- @logger.info("Kernel `#{kernel_id}' is now deleted")
195
- end
196
- end
197
- end
198
-
199
- ramdisk_id = image.properties["ramdisk_id"]
200
- if ramdisk_id
201
- ramdisk = @glance.images.find_by_id(ramdisk_id)
202
- if ramdisk && ramdisk.properties["stemcell"]
203
- if ramdisk.properties["stemcell"] == image.name
204
- @logger.info("Deleting ramdisk `#{ramdisk_id}'...")
205
- ramdisk.destroy
206
- @logger.info("Ramdisk `#{ramdisk_id}' is now deleted")
207
- end
208
- end
209
- end
210
-
211
- image.destroy
154
+ with_openstack { image.destroy }
212
155
  @logger.info("Stemcell `#{stemcell_id}' is now deleted")
213
156
  else
214
157
  @logger.info("Stemcell `#{stemcell_id}' not found. Skipping.")
@@ -240,50 +183,76 @@ module Bosh::OpenStackCloud
240
183
  server_name = "vm-#{generate_unique_name}"
241
184
 
242
185
  network_configurator = NetworkConfigurator.new(network_spec)
243
- security_groups =
244
- network_configurator.security_groups(@default_security_groups)
245
- @logger.debug("Using security groups: `#{security_groups.join(', ')}'")
246
186
 
247
- image = @openstack.images.find { |i| i.id == stemcell_id }
248
- if image.nil?
249
- cloud_error("Image `#{stemcell_id}' not found")
187
+ openstack_security_groups = with_openstack { @openstack.security_groups }.collect { |sg| sg.name }
188
+ security_groups = network_configurator.security_groups(@default_security_groups)
189
+ security_groups.each do |sg|
190
+ cloud_error("Security group `#{sg}' not found") unless openstack_security_groups.include?(sg)
250
191
  end
192
+ @logger.debug("Using security groups: `#{security_groups.join(', ')}'")
193
+
194
+ nics = network_configurator.nics
195
+ @logger.debug("Using NICs: `#{nics.join(', ')}'")
196
+
197
+ image = with_openstack { @openstack.images.find { |i| i.id == stemcell_id } }
198
+ cloud_error("Image `#{stemcell_id}' not found") if image.nil?
251
199
  @logger.debug("Using image: `#{stemcell_id}'")
252
200
 
253
- flavor = @openstack.flavors.find { |f|
254
- f.name == resource_pool["instance_type"] }
255
- if flavor.nil?
256
- cloud_error("Flavor `#{resource_pool["instance_type"]}' not found")
201
+ flavor = with_openstack { @openstack.flavors.find { |f| f.name == resource_pool["instance_type"] } }
202
+ cloud_error("Flavor `#{resource_pool["instance_type"]}' not found") if flavor.nil?
203
+ if flavor_has_ephemeral_disk?(flavor)
204
+ if flavor.ram
205
+ # Ephemeral disk size should be at least the double of the vm total memory size, as agent will need:
206
+ # - vm total memory size for swapon,
207
+ # - the rest for /vcar/vcap/data
208
+ min_ephemeral_size = (flavor.ram / 1024) * 2
209
+ if flavor.ephemeral < min_ephemeral_size
210
+ cloud_error("Flavor `#{resource_pool["instance_type"]}' should have at least #{min_ephemeral_size}Gb " +
211
+ "of ephemeral disk")
212
+ end
213
+ end
257
214
  end
258
215
  @logger.debug("Using flavor: `#{resource_pool["instance_type"]}'")
259
216
 
217
+ keyname = resource_pool["key_name"] || @default_key_name
218
+ keypair = with_openstack { @openstack.key_pairs.find { |k| k.name == keyname } }
219
+ cloud_error("Key-pair `#{keyname}' not found") if keypair.nil?
220
+ @logger.debug("Using key-pair: `#{keypair.name}' (#{keypair.fingerprint})")
221
+
260
222
  server_params = {
261
223
  :name => server_name,
262
224
  :image_ref => image.id,
263
225
  :flavor_ref => flavor.id,
264
- :key_name => resource_pool["key_name"] || @default_key_name,
226
+ :key_name => keypair.name,
265
227
  :security_groups => security_groups,
266
- :user_data => Yajl::Encoder.encode(user_data(server_name,
267
- network_spec))
228
+ :nics => nics,
229
+ :user_data => Yajl::Encoder.encode(user_data(server_name, network_spec)),
230
+ :personality => [{
231
+ "path" => "#{BOSH_APP_DIR}/user_data.json",
232
+ "contents" => Yajl::Encoder.encode(user_data(server_name, network_spec, keypair.public_key))
233
+ }]
268
234
  }
269
235
 
270
- availability_zone = select_availability_zone(disk_locality,
271
- resource_pool["availability_zone"])
272
- if availability_zone
273
- server_params[:availability_zone] = availability_zone
274
- end
236
+ availability_zone = select_availability_zone(disk_locality, resource_pool["availability_zone"])
237
+ server_params[:availability_zone] = availability_zone if availability_zone
275
238
 
276
- server = @openstack.servers.create(server_params)
239
+ @logger.debug("Using boot parms: `#{server_params.inspect}'")
240
+ server = with_openstack { @openstack.servers.create(server_params) }
277
241
 
278
242
  @logger.info("Creating new server `#{server.id}'...")
279
- wait_resource(server, :active, :state)
243
+ begin
244
+ wait_resource(server, :active, :state)
245
+ rescue Bosh::Clouds::CloudError => e
246
+ @logger.warn("Failed to create server: #{e.message}")
247
+ raise Bosh::Clouds::VMCreationFailed.new(true)
248
+ end
280
249
 
281
250
  @logger.info("Configuring network for server `#{server.id}'...")
282
251
  network_configurator.configure(@openstack, server)
283
252
 
284
253
  @logger.info("Updating settings for server `#{server.id}'...")
285
- settings = initial_agent_settings(server_name, agent_id, network_spec,
286
- environment)
254
+ settings = initial_agent_settings(server_name, agent_id, network_spec, environment,
255
+ flavor_has_ephemeral_disk?(flavor))
287
256
  @registry.update_settings(server.name, settings)
288
257
 
289
258
  server.id.to_s
@@ -298,10 +267,10 @@ module Bosh::OpenStackCloud
298
267
  def delete_vm(server_id)
299
268
  with_thread_name("delete_vm(#{server_id})") do
300
269
  @logger.info("Deleting server `#{server_id}'...")
301
- server = @openstack.servers.get(server_id)
270
+ server = with_openstack { @openstack.servers.get(server_id) }
302
271
  if server
303
- server.destroy
304
- wait_resource(server, :terminated, :state, true)
272
+ with_openstack { server.destroy }
273
+ wait_resource(server, [:terminated, :deleted], :state, true)
305
274
 
306
275
  @logger.info("Deleting settings for server `#{server.id}'...")
307
276
  @registry.delete_settings(server.name)
@@ -311,6 +280,18 @@ module Bosh::OpenStackCloud
311
280
  end
312
281
  end
313
282
 
283
+ ##
284
+ # Checks if an OpenStack server exists
285
+ #
286
+ # @param [String] server_id OpenStack server UUID
287
+ # @return [Boolean] True if the vm exists
288
+ def has_vm?(server_id)
289
+ with_thread_name("has_vm?(#{server_id})") do
290
+ server = with_openstack { @openstack.servers.get(server_id) }
291
+ !server.nil? && ![:terminated, :deleted].include?(server.state.downcase.to_sym)
292
+ end
293
+ end
294
+
314
295
  ##
315
296
  # Reboots an OpenStack Server
316
297
  #
@@ -318,10 +299,9 @@ module Bosh::OpenStackCloud
318
299
  # @return [void]
319
300
  def reboot_vm(server_id)
320
301
  with_thread_name("reboot_vm(#{server_id})") do
321
- server = @openstack.servers.get(server_id)
322
- unless server
323
- cloud_error("Server `#{server_id}' not found")
324
- end
302
+ server = with_openstack { @openstack.servers.get(server_id) }
303
+ cloud_error("Server `#{server_id}' not found") unless server
304
+
325
305
  soft_reboot(server)
326
306
  end
327
307
  end
@@ -332,28 +312,20 @@ module Bosh::OpenStackCloud
332
312
  # @param [String] server_id OpenStack server UUID
333
313
  # @param [Hash] network_spec Raw network spec passed by director
334
314
  # @return [void]
335
- # @raise [Bosh::Clouds:NotSupported] if the security groups change
315
+ # @raise [Bosh::Clouds:NotSupported] If there's a network change that requires the recreation of the VM
336
316
  def configure_networks(server_id, network_spec)
337
317
  with_thread_name("configure_networks(#{server_id}, ...)") do
338
318
  @logger.info("Configuring `#{server_id}' to use the following " \
339
319
  "network settings: #{network_spec.pretty_inspect}")
340
-
341
320
  network_configurator = NetworkConfigurator.new(network_spec)
342
- server = @openstack.servers.get(server_id)
343
-
344
- sg = @openstack.list_security_groups(server_id).body["security_groups"]
345
- actual = sg.collect { |s| s["name"] }.sort
346
- new = network_configurator.security_groups(@default_security_groups)
347
-
348
- # If the security groups change, we need to recreate the VM
349
- # as you can't change the security group of a running server,
350
- # we need to send the InstanceUpdater a request to do it for us
351
- unless actual == new
352
- raise Bosh::Clouds::NotSupported,
353
- "security groups change requires VM recreation: %s to %s" %
354
- [actual.join(", "), new.join(", ")]
355
- end
356
321
 
322
+ server = with_openstack { @openstack.servers.get(server_id) }
323
+ cloud_error("Server `#{server_id}' not found") unless server
324
+
325
+ compare_security_groups(server, network_configurator.security_groups(@default_security_groups))
326
+
327
+ compare_private_ip_addresses(server, network_configurator.private_ip)
328
+
357
329
  network_configurator.configure(@openstack, server)
358
330
 
359
331
  update_agent_settings(server) do |settings|
@@ -371,17 +343,9 @@ module Bosh::OpenStackCloud
371
343
  # @return [String] OpenStack volume UUID
372
344
  def create_disk(size, server_id = nil)
373
345
  with_thread_name("create_disk(#{size}, #{server_id})") do
374
- unless size.kind_of?(Integer)
375
- raise ArgumentError, "Disk size needs to be an integer"
376
- end
377
-
378
- if (size < 1024)
379
- cloud_error("Minimum disk size is 1 GiB")
380
- end
381
-
382
- if (size > 1024 * 1000)
383
- cloud_error("Maximum disk size is 1 TiB")
384
- end
346
+ raise ArgumentError, "Disk size needs to be an integer" unless size.kind_of?(Integer)
347
+ cloud_error("Minimum disk size is 1 GiB") if (size < 1024)
348
+ cloud_error("Maximum disk size is 1 TiB") if (size > 1024 * 1000)
385
349
 
386
350
  volume_params = {
387
351
  :name => "volume-#{generate_unique_name}",
@@ -390,14 +354,14 @@ module Bosh::OpenStackCloud
390
354
  }
391
355
 
392
356
  if server_id
393
- server = @openstack.servers.get(server_id)
357
+ server = with_openstack { @openstack.servers.get(server_id) }
394
358
  if server && server.availability_zone
395
359
  volume_params[:availability_zone] = server.availability_zone
396
360
  end
397
361
  end
398
362
 
399
363
  @logger.info("Creating new volume...")
400
- volume = @openstack.volumes.create(volume_params)
364
+ volume = with_openstack { @openstack.volumes.create(volume_params) }
401
365
 
402
366
  @logger.info("Creating new volume `#{volume.id}'...")
403
367
  wait_resource(volume, :available)
@@ -415,14 +379,14 @@ module Bosh::OpenStackCloud
415
379
  def delete_disk(disk_id)
416
380
  with_thread_name("delete_disk(#{disk_id})") do
417
381
  @logger.info("Deleting volume `#{disk_id}'...")
418
- volume = @openstack.volumes.get(disk_id)
382
+ volume = with_openstack { @openstack.volumes.get(disk_id) }
419
383
  if volume
420
384
  state = volume.status
421
385
  if state.to_sym != :available
422
386
  cloud_error("Cannot delete volume `#{disk_id}', state is #{state}")
423
387
  end
424
388
 
425
- volume.destroy
389
+ with_openstack { volume.destroy }
426
390
  wait_resource(volume, :deleted, :status, true)
427
391
  else
428
392
  @logger.info("Volume `#{disk_id}' not found. Skipping.")
@@ -438,14 +402,11 @@ module Bosh::OpenStackCloud
438
402
  # @return [void]
439
403
  def attach_disk(server_id, disk_id)
440
404
  with_thread_name("attach_disk(#{server_id}, #{disk_id})") do
441
- server = @openstack.servers.get(server_id)
442
- unless server
443
- cloud_error("Server `#{server_id}' not found")
444
- end
445
- volume = @openstack.volumes.get(disk_id)
446
- unless server
447
- cloud_error("Volume `#{disk_id}' not found")
448
- end
405
+ server = with_openstack { @openstack.servers.get(server_id) }
406
+ cloud_error("Server `#{server_id}' not found") unless server
407
+
408
+ volume = with_openstack { @openstack.volumes.get(disk_id) }
409
+ cloud_error("Volume `#{disk_id}' not found") unless volume
449
410
 
450
411
  device_name = attach_volume(server, volume)
451
412
 
@@ -465,14 +426,11 @@ module Bosh::OpenStackCloud
465
426
  # @return [void]
466
427
  def detach_disk(server_id, disk_id)
467
428
  with_thread_name("detach_disk(#{server_id}, #{disk_id})") do
468
- server = @openstack.servers.get(server_id)
469
- unless server
470
- cloud_error("Server `#{server_id}' not found")
471
- end
472
- volume = @openstack.volumes.get(disk_id)
473
- unless server
474
- cloud_error("Volume `#{disk_id}' not found")
475
- end
429
+ server = with_openstack { @openstack.servers.get(server_id) }
430
+ cloud_error("Server `#{server_id}' not found") unless server
431
+
432
+ volume = with_openstack { @openstack.volumes.get(disk_id) }
433
+ cloud_error("Volume `#{disk_id}' not found") unless volume
476
434
 
477
435
  detach_volume(server, volume)
478
436
 
@@ -484,6 +442,64 @@ module Bosh::OpenStackCloud
484
442
  end
485
443
  end
486
444
 
445
+ ##
446
+ # Takes a snapshot of an OpenStack volume
447
+ #
448
+ # @param [String] disk_id OpenStack volume UUID
449
+ # @param [Hash] metadata Metadata key/value pairs to add to snapshot
450
+ # @return [String] OpenStack snapshot UUID
451
+ # @raise [Bosh::Clouds::CloudError] if volume is not found
452
+ def snapshot_disk(disk_id, metadata)
453
+ with_thread_name("snapshot_disk(#{disk_id})") do
454
+ volume = with_openstack { @openstack.volumes.get(disk_id) }
455
+ cloud_error("Volume `#{disk_id}' not found") unless volume
456
+
457
+ devices = []
458
+ volume.attachments.each { |attachment| devices << attachment["device"] unless attachment.empty? }
459
+
460
+ description = [:deployment, :job, :index].collect { |key| metadata[key] }
461
+ description << devices.first.split('/').last unless devices.empty?
462
+ snapshot_params = {
463
+ :name => "snapshot-#{generate_unique_name}",
464
+ :description => description.join('/'),
465
+ :volume_id => volume.id
466
+ }
467
+
468
+ @logger.info("Creating new snapshot for volume `#{disk_id}'...")
469
+ snapshot = @openstack.snapshots.new(snapshot_params)
470
+ with_openstack { snapshot.save(true) }
471
+
472
+ @logger.info("Creating new snapshot `#{snapshot.id}' for volume `#{disk_id}'...")
473
+ wait_resource(snapshot, :available)
474
+
475
+ snapshot.id.to_s
476
+ end
477
+ end
478
+
479
+ ##
480
+ # Deletes an OpenStack volume snapshot
481
+ #
482
+ # @param [String] snapshot_id OpenStack snapshot UUID
483
+ # @return [void]
484
+ # @raise [Bosh::Clouds::CloudError] if snapshot is not in available state
485
+ def delete_snapshot(snapshot_id)
486
+ with_thread_name("delete_snapshot(#{snapshot_id})") do
487
+ @logger.info("Deleting snapshot `#{snapshot_id}'...")
488
+ snapshot = with_openstack { @openstack.snapshots.get(snapshot_id) }
489
+ if snapshot
490
+ state = snapshot.status
491
+ if state.to_sym != :available
492
+ cloud_error("Cannot delete snapshot `#{snapshot_id}', state is #{state}")
493
+ end
494
+
495
+ with_openstack { snapshot.destroy }
496
+ wait_resource(snapshot, :deleted, :status, true)
497
+ else
498
+ @logger.info("Snapshot `#{snapshot_id}' not found. Skipping.")
499
+ end
500
+ end
501
+ end
502
+
487
503
  ##
488
504
  # Set metadata for an OpenStack server
489
505
  #
@@ -492,14 +508,13 @@ module Bosh::OpenStackCloud
492
508
  # @return [void]
493
509
  def set_vm_metadata(server_id, metadata)
494
510
  with_thread_name("set_vm_metadata(#{server_id}, ...)") do
495
- server = @openstack.servers.get(server_id)
496
- unless server
497
- cloud_error("Server `#{server_id}' not found")
498
- end
511
+ with_openstack do
512
+ server = @openstack.servers.get(server_id)
513
+ cloud_error("Server `#{server_id}' not found") unless server
499
514
 
500
- metadata.each do |name, value|
501
- value = "" if value.nil? # value is required
502
- server.metadata.update(name => value)
515
+ metadata.each do |name, value|
516
+ TagManager.tag(server, name, value)
517
+ end
503
518
  end
504
519
  end
505
520
  end
@@ -524,7 +539,7 @@ module Bosh::OpenStackCloud
524
539
  # @note this is a private method that is public to make it easier to test
525
540
  def select_availability_zone(volumes, resource_pool_az)
526
541
  if volumes && !volumes.empty?
527
- disks = volumes.map { |vid| @openstack.volumes.get(vid) }
542
+ disks = volumes.map { |vid| with_openstack { @openstack.volumes.get(vid) } }
528
543
  ensure_same_availability_zone(disks, resource_pool_az)
529
544
  disks.first.availability_zone
530
545
  else
@@ -555,7 +570,7 @@ module Bosh::OpenStackCloud
555
570
  #
556
571
  # @return [String] Unique name
557
572
  def generate_unique_name
558
- UUIDTools::UUID.random_create.to_s
573
+ SecureRandom.uuid
559
574
  end
560
575
 
561
576
  ##
@@ -564,11 +579,12 @@ module Bosh::OpenStackCloud
564
579
  # @param [String] server_name server name
565
580
  # @param [Hash] network_spec network specification
566
581
  # @return [Hash] server user data
567
- def user_data(server_name, network_spec)
582
+ def user_data(server_name, network_spec, public_key = nil)
568
583
  data = {}
569
584
 
570
585
  data["registry"] = { "endpoint" => @registry.endpoint }
571
586
  data["server"] = { "name" => server_name }
587
+ data["openssh"] = { "public_key" => public_key } if public_key
572
588
 
573
589
  with_dns(network_spec) do |servers|
574
590
  data["dns"] = { "nameserver" => servers }
@@ -592,13 +608,12 @@ module Bosh::OpenStackCloud
592
608
  end
593
609
 
594
610
  ##
595
- # Generates initial agent settings. These settings will be read by agent
596
- # from OpenStack registry (also a BOSH component) on a target server. Disk
597
- # conventions for OpenStack are:
598
- # system disk: /dev/vda
599
- # ephemeral disk: /dev/ vdb
600
- # OpenStack volumes can be configured to map to other device names later
601
- # (vdc through vdz, also some kernels will remap vd* to xvd*).
611
+ # Generates initial agent settings. These settings will be read by Bosh Agent from Bosh Registry on a target
612
+ # server. Disk conventions in Bosh Agent for OpenStack are:
613
+ # - system disk: /dev/sda
614
+ # - ephemeral disk: /dev/sdb
615
+ # - persistent disks: /dev/sdc through /dev/sdz
616
+ # As some kernels remap device names (from sd* to vd* or xvd*), Bosh Agent will lookup for the proper device name
602
617
  #
603
618
  # @param [String] server_name Name of the OpenStack server (will be picked
604
619
  # up by agent to fetch registry settings)
@@ -606,8 +621,9 @@ module Bosh::OpenStackCloud
606
621
  # assume its identity
607
622
  # @param [Hash] network_spec Agent network spec
608
623
  # @param [Hash] environment Environment settings
624
+ # @param [Boolean] has_ephemeral Has Ephemeral disk?
609
625
  # @return [Hash] Agent settings
610
- def initial_agent_settings(server_name, agent_id, network_spec, environment)
626
+ def initial_agent_settings(server_name, agent_id, network_spec, environment, has_ephemeral)
611
627
  settings = {
612
628
  "vm" => {
613
629
  "name" => server_name
@@ -615,12 +631,12 @@ module Bosh::OpenStackCloud
615
631
  "agent_id" => agent_id,
616
632
  "networks" => network_spec,
617
633
  "disks" => {
618
- "system" => "/dev/vda",
619
- "ephemeral" => "/dev/vdb",
634
+ "system" => "/dev/sda",
620
635
  "persistent" => {}
621
636
  }
622
637
  }
623
638
 
639
+ settings["disks"]["ephemeral"] = has_ephemeral ? "/dev/sdb" : nil
624
640
  settings["env"] = environment if environment
625
641
  settings.merge(@agent_properties)
626
642
  end
@@ -630,9 +646,7 @@ module Bosh::OpenStackCloud
630
646
  #
631
647
  # @param [Fog::Compute::OpenStack::Server] server OpenStack server
632
648
  def update_agent_settings(server)
633
- unless block_given?
634
- raise ArgumentError, "Block is not provided"
635
- end
649
+ raise ArgumentError, "Block is not provided" unless block_given?
636
650
 
637
651
  @logger.info("Updating settings for server `#{server.id}'...")
638
652
  settings = @registry.read_settings(server.name)
@@ -647,7 +661,7 @@ module Bosh::OpenStackCloud
647
661
  # @return [void]
648
662
  def soft_reboot(server)
649
663
  @logger.info("Soft rebooting server `#{server.id}'...")
650
- server.reboot
664
+ with_openstack { server.reboot }
651
665
  wait_resource(server, :active, :state)
652
666
  end
653
667
 
@@ -658,7 +672,7 @@ module Bosh::OpenStackCloud
658
672
  # @return [void]
659
673
  def hard_reboot(server)
660
674
  @logger.info("Hard rebooting server `#{server.id}'...")
661
- server.reboot(type = 'HARD')
675
+ with_openstack { server.reboot(type = 'HARD') }
662
676
  wait_resource(server, :active, :state)
663
677
  end
664
678
 
@@ -669,31 +683,58 @@ module Bosh::OpenStackCloud
669
683
  # @param [Fog::Compute::OpenStack::Volume] volume OpenStack volume
670
684
  # @return [String] Device name
671
685
  def attach_volume(server, volume)
672
- volume_attachments = @openstack.get_server_volumes(server.id).
673
- body['volumeAttachments']
674
- device_names = Set.new(volume_attachments.collect! { |v| v["device"] })
675
-
676
- new_attachment = nil
677
- ("c".."z").each do |char|
678
- dev_name = "/dev/vd#{char}"
679
- if device_names.include?(dev_name)
680
- @logger.warn("`#{dev_name}' on `#{server.id}' is taken")
681
- next
682
- end
683
- @logger.info("Attaching volume `#{volume.id}' to `#{server.id}', " \
684
- "device name is `#{dev_name}'")
685
- if volume.attach(server.id, dev_name)
686
- wait_resource(volume, :"in-use")
687
- new_attachment = dev_name
688
- end
689
- break
686
+ @logger.info("Attaching volume `#{volume.id}' to server `#{server.id}'...")
687
+ volume_attachments = with_openstack { server.volume_attachments }
688
+ device = volume_attachments.find { |a| a["volumeId"] == volume.id }
689
+
690
+ if device.nil?
691
+ device_name = select_device_name(volume_attachments, first_device_name_letter(server))
692
+ cloud_error("Server has too many disks attached") if device_name.nil?
693
+
694
+ @logger.info("Attaching volume `#{volume.id}' to server `#{server.id}', device name is `#{device_name}'")
695
+ with_openstack { volume.attach(server.id, device_name) }
696
+ wait_resource(volume, :"in-use")
697
+ else
698
+ device_name = device["device"]
699
+ @logger.info("Volume `#{volume.id}' is already attached to server `#{server.id}' in `#{device_name}'. Skipping.")
690
700
  end
691
701
 
692
- if new_attachment.nil?
693
- cloud_error("Server has too many disks attached")
702
+ device_name
703
+ end
704
+
705
+ ##
706
+ # Select the first available device name
707
+ #
708
+ # @param [Array] volume_attachments Volume attachments
709
+ # @param [String] first_device_name_letter First available letter for device names
710
+ # @return [String] First available device name or nil is none is available
711
+ def select_device_name(volume_attachments, first_device_name_letter)
712
+ (first_device_name_letter.."z").each do |char|
713
+ # Some kernels remap device names (from sd* to vd* or xvd*).
714
+ device_names = ["/dev/sd#{char}", "/dev/vd#{char}", "/dev/xvd#{char}"]
715
+ # Bosh Agent will lookup for the proper device name if we set it initially to sd*.
716
+ return "/dev/sd#{char}" if volume_attachments.select { |v| device_names.include?( v["device"]) }.empty?
717
+ @logger.warn("`/dev/sd#{char}' is already taken")
694
718
  end
695
719
 
696
- new_attachment
720
+ nil
721
+ end
722
+
723
+ ##
724
+ # Returns the first letter to be used on device names
725
+ #
726
+ # @param [Fog::Compute::OpenStack::Server] server OpenStack server
727
+ # @return [String] First available letter
728
+ def first_device_name_letter(server)
729
+ letter = "#{FIRST_DEVICE_NAME_LETTER}"
730
+ return letter if server.flavor.nil?
731
+ return letter unless server.flavor.has_key?('id')
732
+ flavor = with_openstack { @openstack.flavors.find { |f| f.id == server.flavor['id'] } }
733
+ return letter if flavor.nil?
734
+
735
+ letter.succ! if flavor_has_ephemeral_disk?(flavor)
736
+ letter.succ! if flavor_has_swap_disk?(flavor)
737
+ letter
697
738
  end
698
739
 
699
740
  ##
@@ -703,33 +744,68 @@ module Bosh::OpenStackCloud
703
744
  # @param [Fog::Compute::OpenStack::Volume] volume OpenStack volume
704
745
  # @return [void]
705
746
  def detach_volume(server, volume)
706
- volume_attachments = @openstack.get_server_volumes(server.id).
707
- body['volumeAttachments']
708
- device_map = volume_attachments.collect! { |v| v["volumeId"] }
747
+ @logger.info("Detaching volume `#{volume.id}' from `#{server.id}'...")
748
+ volume_attachments = with_openstack { server.volume_attachments }
749
+ if volume_attachments.find { |a| a["volumeId"] == volume.id }
750
+ with_openstack { volume.detach(server.id, volume.id) }
751
+ wait_resource(volume, :available)
752
+ else
753
+ @logger.info("Disk `#{volume.id}' is not attached to server `#{server.id}'. Skipping.")
754
+ end
755
+ end
709
756
 
710
- unless device_map.include?(volume.id)
711
- cloud_error("Disk `#{volume.id}' is not attached to " \
712
- "server `#{server.id}'")
757
+ ##
758
+ # Compares actual server security groups with those specified at the network spec
759
+ #
760
+ # @param [Fog::Compute::OpenStack::Server] server OpenStack server
761
+ # @param [Array] specified_sg_names Security groups specified at the network spec
762
+ # @return [void]
763
+ # @raise [Bosh::Clouds:NotSupported] If the security groups change, we need to recreate the VM as you can't
764
+ # change the security group of a running server, so we need to send the InstanceUpdater a request to do it for us
765
+ def compare_security_groups(server, specified_sg_names)
766
+ actual_sg_names = with_openstack { server.security_groups }.collect { |sg| sg.name }
767
+
768
+ unless actual_sg_names.sort == specified_sg_names.sort
769
+ raise Bosh::Clouds::NotSupported,
770
+ "security groups change requires VM recreation: %s to %s" %
771
+ [actual_sg_names.join(", "), specified_sg_names.join(", ")]
713
772
  end
773
+ end
714
774
 
715
- @logger.info("Detaching volume `#{volume.id}' from `#{server.id}'...")
716
- volume.detach(server.id, volume.id)
717
- wait_resource(volume, :available)
775
+ ##
776
+ # Compares actual server private IP addresses with the IP address specified at the network spec
777
+ #
778
+ # @param [Fog::Compute::OpenStack::Server] server OpenStack server
779
+ # @param [String] specified_ip_address IP address specified at the network spec (if Manual network)
780
+ # @return [void]
781
+ # @raise [Bosh::Clouds:NotSupported] If the IP address change, we need to recreate the VM as you can't
782
+ # change the IP address of a running server, so we need to send the InstanceUpdater a request to do it for us
783
+ def compare_private_ip_addresses(server, specified_ip_address)
784
+ actual_ip_addresses = with_openstack { server.private_ip_addresses }
785
+
786
+ unless specified_ip_address.nil? || actual_ip_addresses.include?(specified_ip_address)
787
+ raise Bosh::Clouds::NotSupported,
788
+ "IP address change requires VM recreation: %s to %s" %
789
+ [actual_ip_addresses.join(", "), specified_ip_address]
790
+ end
718
791
  end
719
792
 
720
793
  ##
721
- # Uploads a new image to OpenStack via Glance
794
+ # Checks if the OpenStack flavor has ephemeral disk
722
795
  #
723
- # @param [Hash] image_params Image params
724
- # @return [String] OpenStack image UUID
725
- def upload_image(image_params)
726
- @logger.info("Creating new image...")
727
- started_at = Time.now
728
- image = @glance.images.create(image_params)
729
- total = Time.now - started_at
730
- @logger.info("Created new image `#{image.id}', took #{total}s")
796
+ # @param [Fog::Compute::OpenStack::Flavor] OpenStack flavor
797
+ # @return [Boolean] true if flavor has ephemeral disk, false otherwise
798
+ def flavor_has_ephemeral_disk?(flavor)
799
+ flavor.ephemeral.nil? || flavor.ephemeral.to_i <= 0 ? false : true
800
+ end
731
801
 
732
- image.id.to_s
802
+ ##
803
+ # Checks if the OpenStack flavor has swap disk
804
+ #
805
+ # @param [Fog::Compute::OpenStack::Flavor] OpenStack flavor
806
+ # @return [Boolean] true if flavor has swap disk, false otherwise
807
+ def flavor_has_swap_disk?(flavor)
808
+ flavor.swap.nil? || flavor.swap.to_i <= 0 ? false : true
733
809
  end
734
810
 
735
811
  ##
@@ -739,10 +815,11 @@ module Bosh::OpenStackCloud
739
815
  # @param [String] image_path Local filesystem path to a stemcell image
740
816
  # @return [void]
741
817
  def unpack_image(tmp_dir, image_path)
742
- output = `tar -C #{tmp_dir} -xzf #{image_path} 2>&1`
743
- if $?.exitstatus != 0
744
- cloud_error("Failed to unpack stemcell root image" \
745
- "tar exit status #{$?.exitstatus}: #{output}")
818
+ result = Bosh::Exec.sh("tar -C #{tmp_dir} -xzf #{image_path} 2>&1", :on_error => :return)
819
+ if result.failed?
820
+ @logger.error("Extracting stemcell root image failed in dir #{tmp_dir}, " +
821
+ "tar returned #{result.exit_status}, output: #{result.output}")
822
+ cloud_error("Extracting stemcell root image failed. Check task debug log for details.")
746
823
  end
747
824
  root_image = File.join(tmp_dir, "root.img")
748
825
  unless File.exists?(root_image)
@@ -775,8 +852,15 @@ module Bosh::OpenStackCloud
775
852
  end
776
853
  end
777
854
 
778
- def task_checkpoint
779
- Bosh::Clouds::Config.task_checkpoint
855
+ def initialize_registry
856
+ registry_properties = @options.fetch('registry')
857
+ registry_endpoint = registry_properties.fetch('endpoint')
858
+ registry_user = registry_properties.fetch('user')
859
+ registry_password = registry_properties.fetch('password')
860
+
861
+ @registry = Bosh::Registry::Client.new(registry_endpoint,
862
+ registry_user,
863
+ registry_password)
780
864
  end
781
865
 
782
866
  end