Skip to content

Commit

Permalink
Allow booting from volume
Browse files Browse the repository at this point in the history
issue #44
  • Loading branch information
ggiamarchi committed Sep 14, 2014
1 parent bae62ba commit ebb0fd8
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 13 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ cloud.
* Automatic SSH key generation and Nova public key provisioning
* Automatic floating IP allocation and association
* Provision the instances with any built-in Vagrant provisioner
* Boot instance from volume
* Attach Cinder volumes to the instances
* Minimal synced folder support via `rsync`
* Custom sub-commands within Vagrant CLI to query Openstack objects
Expand Down Expand Up @@ -187,6 +188,8 @@ os.volumes = [
end
```

* `volume_boot` - Volume to boot the VM from. When booting from an existing volume, `image` is not necessary and must not be provided.

### 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
53 changes: 41 additions & 12 deletions source/lib/vagrant-openstack-provider/action/create_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,16 @@ def call(env)
options = {
flavor: resolve_flavor(env),
image: resolve_image(env),
volume_boot: resolve_volume_boot(env),
networks: resolve_networks(env),
volumes: resolve_volumes(env),
keypair_name: resolve_keypair(env),
availability_zone: env[:machine].provider_config.availability_zone
}

fail Errors::MissingBootOption if options[:image].nil? && options[:volume_boot].nil?
fail Errors::ConflictBootOption unless options[:image].nil? || options[:volume_boot].nil?

server_id = create_server(env, options)

# Store the ID right away so we can track it
Expand Down Expand Up @@ -123,6 +128,7 @@ def resolve_flavor(env)
def resolve_image(env)
@logger.info 'Resolving image'
config = env[:machine].provider_config
return nil if config.image.nil?
nova = env[:openstack_client].nova
env[:ui].info(I18n.t('vagrant_openstack.finding_image'))
images = nova.get_all_images(env)
Expand Down Expand Up @@ -159,6 +165,22 @@ def resolve_networks(env)
networks
end

def resolve_volume_boot(env)
@logger.info 'Resolving image'
config = env[:machine].provider_config
return nil if config.volume_boot.nil?

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

@logger.debug(volume_list)

volume = resolve_volume(config.volume_boot, volume_list, volume_ids)
device = volume[:device].nil? ? 'vda' : volume[:device]

{ id: volume[:id], device: device }
end

def resolve_volumes(env)
@logger.info 'Resolving volume(s)'
config = env[:machine].provider_config
Expand All @@ -172,19 +194,18 @@ def resolve_volumes(env)

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
volumes << resolve_volume(volume, volume_list, volume_ids)
end
@logger.debug("Resolved volumes : #{volumes.to_json}")
volumes
end

def resolve_volume(volume, volume_list, volume_ids)
return resolve_volume_from_string(volume, volume_list) if volume.is_a? String
return resolve_volume_from_hash(volume, volume_list, volume_ids) if volume.is_a? Hash
fail Errors::InvalidVolumeObject, volume: volume
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?
Expand Down Expand Up @@ -221,8 +242,11 @@ def create_server(env, options)
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}")
unless options[:image].nil?
env[:ui].info(" -- Image : #{options[:image].name}")
env[:ui].info(" -- ImageRef : #{options[:image].id}")
end
env[:ui].info(" -- Boot volume : #{options[:volume_boot][:id]} (#{options[:volume_boot][:device]})") unless options[:volume_boot].nil?
env[:ui].info(" -- KeyPair : #{options[:keypair_name]}")

unless options[:networks].empty?
Expand All @@ -243,14 +267,19 @@ def create_server(env, options)

log = "Lauching server '#{server_name}' in project '#{config.tenant_name}' "
log << "with flavor '#{options[:flavor].name}' (#{options[:flavor].id}), "
log << "image '#{options[:image].name}' (#{options[:image].id}) "
unless options[:image].nil?
log << "image '#{options[:image].name}' (#{options[:image].id}) "
end
log << "and keypair '#{options[:keypair_name]}'"

@logger.info(log)

image_ref = options[:image].id unless options[:image].nil?

create_opts = {
name: server_name,
image_ref: options[:image].id,
image_ref: image_ref,
volume_boot: options[:volume_boot],
flavor_ref: options[:flavor].id,
keypair: options[:keypair_name],
availability_zone: options[:availability_zone],
Expand Down
6 changes: 5 additions & 1 deletion source/lib/vagrant-openstack-provider/client/nova.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ def get_all_images(env)
def create_server(env, options)
server = {}.tap do |s|
s['name'] = options[:name]
s['imageRef'] = options[:image_ref]
if options[:image_ref].nil?
s['block_device_mapping'] = [{ volume_id: options[:volume_boot][:id], device_name: options[:volume_boot][:device] }]
else
s['imageRef'] = options[:image_ref]
end
s['flavorRef'] = options[:flavor_ref]
s['key_name'] = options[:keypair]
s['availability_zone'] = options[:availability_zone] unless options[:availability_zone].nil?
Expand Down
6 changes: 6 additions & 0 deletions source/lib/vagrant-openstack-provider/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class Config < Vagrant.plugin('2', :config)
# expression to partially match a name.
attr_accessor :image

# Volume to boot the vm from
#
attr_accessor :volume_boot

#
# The name of the openstack project on witch the vm will be created.
#
Expand Down Expand Up @@ -120,6 +124,7 @@ def initialize
@openstack_auth_url = UNSET_VALUE
@flavor = UNSET_VALUE
@image = UNSET_VALUE
@volume_boot = UNSET_VALUE
@tenant_name = UNSET_VALUE
@server_name = UNSET_VALUE
@username = UNSET_VALUE
Expand All @@ -146,6 +151,7 @@ def finalize!
@openstack_auth_url = nil if @openstack_auth_url == UNSET_VALUE
@flavor = nil if @flavor == UNSET_VALUE
@image = nil if @image == UNSET_VALUE
@volume_boot = nil if @volume_boot == UNSET_VALUE
@tenant_name = nil if @tenant_name == UNSET_VALUE
@server_name = nil if @server_name == UNSET_VALUE
@username = nil if @username == UNSET_VALUE
Expand Down
8 changes: 8 additions & 0 deletions source/lib/vagrant-openstack-provider/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ class ConflictVolumeNameId < VagrantOpenstackError
class MultipleVolumeName < VagrantOpenstackError
error_key(:multiple_volume_name)
end

class MissingBootOption < VagrantOpenstackError
error_key(:missing_boot_option)
end

class ConflictBootOption < VagrantOpenstackError
error_key(:conflict_boot_option)
end
end
end
end
4 changes: 4 additions & 0 deletions source/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ en:
One (and only one) of 'id' or 'name' must be specified in volume definition : %{volume}
multiple_volume_name: |-
More than one volume exists with name '%{name}'. In the case you can't use name in volume definition. Please, use id instead.
missing_boot_option: |-
Either 'image' or 'volume_boot' configuration must be provided
conflict_boot_option: |-
Only one of 'image' and 'volume_boot' configuration must be provided
states:
short_active: |-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
name: 'testName',
flavor_ref: flavor.id,
image_ref: image.id,
volume_boot: nil,
networks: ['test-networks'],
keypair: 'test-keypair',
availability_zone: 'test-az') do '1234'
Expand Down Expand Up @@ -290,6 +291,92 @@

end

describe 'resolve_volume_boot' do
context 'with string volume id' do
it 'returns normalized volume' do
config.stub(:volume_boot) { '001' }
expect(@action.resolve_volume_boot(env)).to eq id: '001', device: 'vda'
end
end

context 'with string volume name' do
it 'returns normalized volume' do
config.stub(:volume_boot) { 'vol-01' }
expect(@action.resolve_volume_boot(env)).to eq id: '001', device: 'vda'
end
end

context 'with hash volume id' do
it 'returns normalized volume' do
config.stub(:volume_boot) { { id: '001' } }
expect(@action.resolve_volume_boot(env)).to eq id: '001', device: 'vda'
end
end

context 'with hash volume name' do
it 'returns normalized volume' do
config.stub(:volume_boot) { { name: 'vol-01' } }
expect(@action.resolve_volume_boot(env)).to eq id: '001', device: 'vda'
end
end

context 'with hash volume id and device' do
it 'returns normalized volume' do
config.stub(:volume_boot) { { id: '001', device: 'vdb' } }
expect(@action.resolve_volume_boot(env)).to eq id: '001', device: 'vdb'
end
end

context 'with hash volume name and device' do
it 'returns normalized volume' do
config.stub(:volume_boot) { { name: 'vol-01', device: 'vdb' } }
expect(@action.resolve_volume_boot(env)).to eq id: '001', device: 'vdb'
end
end

context 'with empty hash' do
it 'raises an error' do
config.stub(:volume_boot) { {} }
expect { @action.resolve_volume_boot(env) }.to raise_error(Errors::ConflictVolumeNameId)
end
end

context 'with invalid volume object' do
it 'raises an error' do
config.stub(:volume_boot) { 1 }
expect { @action.resolve_volume_boot(env) }.to raise_error(Errors::InvalidVolumeObject)
end
end

context 'with hash containing a bad id' do
it 'raises an error' do
config.stub(:volume_boot) { { id: 'not-exist' } }
expect { @action.resolve_volume_boot(env) }.to raise_error(Errors::UnresolvedVolumeId)
end
end

context 'with hash containing a bad name' do
it 'raises an error' do
config.stub(:volume_boot) { { name: 'not-exist' } }
expect { @action.resolve_volume_boot(env) }.to raise_error(Errors::UnresolvedVolumeName)
end
end

context 'with hash containing both id and name' do
it 'raises an error' do
config.stub(:volume_boot) { { id: '001', name: 'vol-01' } }
expect { @action.resolve_volume_boot(env) }.to raise_error(Errors::ConflictVolumeNameId)
end
end

context 'with hash containing a name matching more than one volume' do
it 'raises an error' do
config.stub(:volume_boot) { { name: 'vol-07-08' } }
expect { @action.resolve_volume_boot(env) }.to raise_error(Errors::MultipleVolumeName)
end
end
end

describe 'resolve_volumes' do
context 'with volume attached in all possible ways' do
it 'returns normalized volume list' do
Expand Down
19 changes: 19 additions & 0 deletions source/spec/vagrant-openstack-provider/client/nova_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,25 @@
end
end

context 'with volume_boot' do
it 'returns new instance id' do

stub_request(:post, 'http://nova/a1b2c3/servers')
.with(
body: '{"server":{"name":"inst","block_device_mapping":[{"volume_id":"vol","device_name":"vda"}],"flavorRef":"flav","key_name":"key"}}',
headers:
{
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'X-Auth-Token' => '123456'
})
.to_return(status: 202, body: '{ "server": { "id": "o1o2o3" } }')

instance_id = @nova_client.create_server(env, name: 'inst', volume_boot: { id: 'vol', device: 'vda' }, flavor_ref: 'flav', keypair: 'key')

expect(instance_id).to eq('o1o2o3')
end
end
end
end

Expand Down

0 comments on commit ebb0fd8

Please sign in to comment.