Skip to content

Commit

Permalink
Add attached volumes support
Browse files Browse the repository at this point in the history
issue #24
  • Loading branch information
ggiamarchi committed Sep 5, 2014
1 parent 9f2ca07 commit 82bf82a
Show file tree
Hide file tree
Showing 13 changed files with 503 additions and 17 deletions.
39 changes: 36 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ This provider exposes quite a few provider-specific configuration options:
* `openstack_auth_url` - The endpoint to authentication against. By default, vagrant will use the global
openstack authentication endpoint for all regions with the exception of :lon. IF :lon region is specified
vagrant will authenticate against the UK authentication endpoint.
* `openstack_compute_url` - The compute URL to hit. This is good for custom endpoints. If not provided, vagrant will try to get it from catalog endpoint.
* `openstack_network_url` - The network URL to hit. This is good for custom endpoints. If not provided, vagrant will try to get it from catalog endpoint.
* `openstack_compute_url` - The compute service URL to hit. This is good for custom endpoints. If not provided, vagrant will try to get it from catalog endpoint.
* `openstack_network_url` - The network service URL to hit. This is good for custom endpoints. If not provided, vagrant will try to get it from catalog endpoint.
* `openstack_volume_url` - The block storage URL to hit. This is good for custom endpoints. If not provided, vagrant will try to get it from catalog endpoint.

### VM Configuration

Expand All @@ -135,7 +136,7 @@ supported with `vagrant-openstack`, currently. If any of these are
specified, Vagrant will emit a warning, but will otherwise boot
the Openstack server.

You can provide network id or name. However, in Openstack the network name is not unique, thus if there is two networks with
You can provide network id or name. However, in Openstack a network name is not unique, thus if there is two networks with
the same name in your project the plugin will fail. If so, you have to use only ids.

Here's an example which adds two Cloud Networks. The first by id and the second by name.
Expand All @@ -148,6 +149,38 @@ config.vm.provider :openstack do |os|
end
```

#### Volumes

* `volumes` - Volume list that have to be attached to the server. You can provide volume id or name. However, in Openstack
a volume name is not unique, thus if there is two volumes with the same name in your project the plugin will fail. If so,
you have to use only ids. Optionally, you can specify the device that will be assigned to the volume.

Here comes an example that show six volumes attached to a server :

```ruby
config.vm.provider :openstack do |os|
...
os.volumes = [
'619e027c-f4a9-493d-8c15-c89de81cb949',
'vol-name-02',
{
id: '410096ff-ef71-4ca4-8006-e5bd9e99239a',
device: '/dev/vdc'
},
{
name: 'vol-name-04',
device: '/dev/vde'
},
{
name: 'vol-name-05'
},
{
id: '9e419e91-8f66-4803-bc45-4600182cfd8d'
}
]
end
```

### SSH-key authentication

* `keypair_name` - The name of the key pair register in nova to associate with the VM. The public key should
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def override_endpoint_catalog_with_user_config(env)
config = env[:machine].provider_config
client.session.endpoints[:compute] = config.openstack_compute_url unless config.openstack_compute_url.nil?
client.session.endpoints[:network] = config.openstack_network_url unless config.openstack_network_url.nil?
client.session.endpoints[:volume] = config.openstack_volume_url unless config.openstack_volume_url.nil?
end

def log_endpoint_catalog(env)
Expand Down
93 changes: 83 additions & 10 deletions source/lib/vagrant-openstack-provider/action/create_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def call(env)
flavor: resolve_flavor(env),
image: resolve_image(env),
networks: resolve_networks(env),
volumes: resolve_volumes(env),
keypair_name: resolve_keypair(env),
availability_zone: env[:machine].provider_config.availability_zone
}
Expand All @@ -42,6 +43,8 @@ def call(env)
nova.add_floating_ip(env, server_id, floating_ip)
end

attach_volumes(env, server_id, options[:volumes]) unless options[:volumes].empty?

unless env[:interrupted]
# Clear the line one more time so the progress is removed
env[:ui].clear_line
Expand Down Expand Up @@ -156,24 +159,85 @@ def resolve_networks(env)
networks
end

def resolve_volumes(env)
@logger.info 'Resolving volume(s)'
config = env[:machine].provider_config
return [] if config.volumes.nil? || config.volumes.empty?
env[:ui].info(I18n.t('vagrant_openstack.finding_volumes'))

volume_list = env[:openstack_client].cinder.get_all_volumes(env)
volume_ids = volume_list.map { |v| v.id }

@logger.debug(volume_list)

volumes = []
config.volumes.each do |volume|
case volume
when String
volumes << resolve_volume_from_string(volume, volume_list)
when Hash
volumes << resolve_volume_from_hash(volume, volume_list, volume_ids)
else
fail Errors::InvalidVolumeObject, volume: volume
end
end
@logger.debug("Resolved volumes : #{volumes.to_json}")
volumes
end

def resolve_volume_from_string(volume, volume_list)
found_volume = find_matching(volume_list, volume)
fail Errors::UnresolvedVolume, volume: volume if found_volume.nil?
{ id: found_volume.id, device: nil }
end

def resolve_volume_from_hash(volume, volume_list, volume_ids)
device = nil
device = volume[:device] if volume.key?(:device)
if volume.key?(:id)
fail Errors::ConflictVolumeNameId, volume: volume if volume.key?(:name)
volume_id = volume[:id]
fail Errors::UnresolvedVolumeId, id: volume_id unless volume_ids.include? volume_id
elsif volume.key?(:name)
volume_list.each do |v|
next unless v.name.eql? volume[:name]
fail Errors::MultipleVolumeName, name: volume[:name] unless volume_id.nil?
volume_id = v.id
end
fail Errors::UnresolvedVolumeName, name: volume[:name] unless volume_ids.include? volume_id
else
fail Errors::ConflictVolumeNameId, volume: volume
end
{ id: volume_id, device: device }
end

def create_server(env, options)
config = env[:machine].provider_config
nova = env[:openstack_client].nova
server_name = config.server_name || env[:machine].name

env[:ui].info(I18n.t('vagrant_openstack.launching_server'))
env[:ui].info(" -- Tenant : #{config.tenant_name}")
env[:ui].info(" -- Name : #{server_name}")
env[:ui].info(" -- Flavor : #{options[:flavor].name}")
env[:ui].info(" -- FlavorRef : #{options[:flavor].id}")
env[:ui].info(" -- Image : #{options[:image].name}")
env[:ui].info(" -- ImageRef : #{options[:image].id}")
env[:ui].info(" -- KeyPair : #{options[:keypair_name]}")
env[:ui].info(" -- Tenant : #{config.tenant_name}")
env[:ui].info(" -- Name : #{server_name}")
env[:ui].info(" -- Flavor : #{options[:flavor].name}")
env[:ui].info(" -- FlavorRef : #{options[:flavor].id}")
env[:ui].info(" -- Image : #{options[:image].name}")
env[:ui].info(" -- ImageRef : #{options[:image].id}")
env[:ui].info(" -- KeyPair : #{options[:keypair_name]}")

unless options[:networks].empty?
if options[:networks].size == 1
env[:ui].info(" -- Network : #{options[:networks][0]}")
env[:ui].info(" -- Network : #{options[:networks][0]}")
else
env[:ui].info(" -- Networks : #{options[:networks]}")
env[:ui].info(" -- Networks : #{options[:networks]}")
end
end

unless options[:volumes].empty?
options[:volumes].each do |volume|
device = volume[:device]
device = :auto if device.nil?
env[:ui].info(" -- Volume attached : #{volume[:id]} => #{device}")
end
end

Expand Down Expand Up @@ -208,6 +272,15 @@ def waiting_for_server_to_be_build(env, server_id)
end
end

def attach_volumes(env, server_id, volumes)
@logger.info("Attaching volumes #{volumes} to server #{server_id}")
nova = env[:openstack_client].nova
volumes.each do |volume|
@logger.debug("Attaching volumes #{volume}")
nova.attach_volume(env, server_id, volume[:id], volume[:device])
end
end

def port_open?(env, ip, port, timeout)
start_time = Time.now
current_time = start_time
Expand Down Expand Up @@ -241,7 +314,7 @@ def find_matching(collection, name)
return single if single.name == name
return single if name.is_a?(Regexp) && name =~ single.name
end
@logger.error "Flavor '#{name}' not found"
@logger.error "Element '#{name}' not found in collection #{collection}"
nil
end
end
Expand Down
30 changes: 30 additions & 0 deletions source/lib/vagrant-openstack-provider/client/cinder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require 'log4r'
require 'restclient'
require 'json'

require 'vagrant-openstack-provider/client/http_utils'
require 'vagrant-openstack-provider/client/domain'

module VagrantPlugins
module Openstack
class CinderClient
include Singleton
include VagrantPlugins::Openstack::HttpUtils
include VagrantPlugins::Openstack::Domain

def initialize
@logger = Log4r::Logger.new('vagrant_openstack::cinder')
@session = VagrantPlugins::Openstack.session
end

def get_all_volumes(env)
volumes_json = get(env, "#{@session.endpoints[:volume]}/volumes/detail")
JSON.parse(volumes_json)['volumes'].map do |v|
name = v['display_name']
name = v['name'] if name.nil? # To be compatible with cinder api v1 and v2
Volume.new(v['id'], name, v['size'], v['status'], v['bootable'], v['instance_id'], v['device'])
end
end
end
end
end
68 changes: 64 additions & 4 deletions source/lib/vagrant-openstack-provider/client/domain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ def initialize(id, name)
@id = id
@name = name
end

def ==(other)
other.class == self.class && other.state == state
end

def state
[@id, @name]
end
end

class Flavor < Item
Expand All @@ -36,10 +44,6 @@ def initialize(id, name, vcpus, ram, disk)
super(id, name)
end

def ==(other)
other.class == self.class && other.state == state
end

protected

def state
Expand All @@ -55,6 +59,62 @@ def initialize(ip, pool, instance_id)
@instance_id = instance_id
end
end

class Volume < Item
#
# Size in Gigaoctet
#
attr_accessor :size

#
# Status (e.g. 'Available', 'In-use')
#
attr_accessor :status

#
# Whether volume is bootable or not
#
attr_accessor :bootable

#
# instance id volume is attached to
#
attr_accessor :instance_id

#
# device (e.g. /dev/sdb) if attached
#
attr_accessor :device

# rubocop:disable Style/ParameterLists
def initialize(id, name, size, status, bootable, instance_id, device)
@size = size
@status = status
@bootable = bootable
@instance_id = instance_id
@device = device
super(id, name)
end
# rubocop:enable Style/ParameterLists

def to_s
{
id: @id,
name: @name,
size: @size,
status: @status,
bootable: @bootable,
instance_id: @instance_id,
device: @device
}
end

protected

def state
[@id, @name, @size, @status, @bootable, @instance_id, @device]
end
end
end
end
end
11 changes: 11 additions & 0 deletions source/lib/vagrant-openstack-provider/client/nova.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,17 @@ def get_floating_ips(env)
JSON.parse(floating_ips)['floating_ips']
end

def attach_volume(env, server_id, volume_id, device = nil)
attachment = post(env, "#{@session.endpoints[:compute]}/servers/#{server_id}/os-volume_attachments",
{
volumeAttachment: {
volumeId: volume_id,
device: device
}
}.to_json)
JSON.parse(attachment)['volumeAttachment']
end

private

VM_STATES =
Expand Down
5 changes: 5 additions & 0 deletions source/lib/vagrant-openstack-provider/client/openstack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require 'vagrant-openstack-provider/client/keystone'
require 'vagrant-openstack-provider/client/nova'
require 'vagrant-openstack-provider/client/neutron'
require 'vagrant-openstack-provider/client/cinder'

module VagrantPlugins
module Openstack
Expand Down Expand Up @@ -41,5 +42,9 @@ def self.nova
def self.neutron
Openstack::NeutronClient.instance
end

def self.cinder
Openstack::CinderClient.instance
end
end
end
Loading

0 comments on commit 82bf82a

Please sign in to comment.