bosh_openstack_cpi 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +40 -0
- data/Rakefile +50 -0
- data/bin/bosh_openstack_console +74 -0
- data/lib/bosh_openstack_cpi.rb +3 -0
- data/lib/cloud/openstack.rb +34 -0
- data/lib/cloud/openstack/cloud.rb +550 -0
- data/lib/cloud/openstack/dynamic_network.rb +26 -0
- data/lib/cloud/openstack/helpers.rb +49 -0
- data/lib/cloud/openstack/network.rb +37 -0
- data/lib/cloud/openstack/network_configurator.rb +113 -0
- data/lib/cloud/openstack/registry_client.rb +109 -0
- data/lib/cloud/openstack/version.rb +7 -0
- data/lib/cloud/openstack/vip_network.rb +49 -0
- data/spec/spec_helper.rb +137 -0
- data/spec/unit/attach_disk_spec.rb +95 -0
- data/spec/unit/cloud_spec.rb +18 -0
- data/spec/unit/configure_networks_spec.rb +83 -0
- data/spec/unit/create_disk_spec.rb +82 -0
- data/spec/unit/create_stemcell_spec.rb +50 -0
- data/spec/unit/create_vm_spec.rb +142 -0
- data/spec/unit/delete_disk_spec.rb +35 -0
- data/spec/unit/delete_stemcell_spec.rb +19 -0
- data/spec/unit/delete_vm_spec.rb +26 -0
- data/spec/unit/detach_disk_spec.rb +67 -0
- data/spec/unit/helpers_spec.rb +34 -0
- data/spec/unit/network_configurator_spec.rb +57 -0
- data/spec/unit/reboot_vm_spec.rb +34 -0
- data/spec/unit/validate_deployment_spec.rb +16 -0
- metadata +190 -0
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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,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
|