Skip to content

Commit

Permalink
Automatic generation of SSH keys
Browse files Browse the repository at this point in the history
Fix #68
  • Loading branch information
julienvey committed Aug 29, 2014
1 parent 3702411 commit 98e91c8
Show file tree
Hide file tree
Showing 15 changed files with 127 additions and 32 deletions.
1 change: 1 addition & 0 deletions run/Vagrantfile
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ install() {
runTests() {
run "" cd /vagrant-openstack
run "Run 'bundle update'" bundle update
run "Run 'bundle install'" bundle install
run "Run 'bundle exec rake" bundle exec rake
#run "Run 'appraisal install'" appraisal install
Expand Down
2 changes: 2 additions & 0 deletions source/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ gem 'webmock', '~> 1.18.0', group: [:test]
gem 'rubocop', '0.23.0', require: false
gem 'vagrant', git: 'git://github.com/mitchellh/vagrant.git', tag: 'v1.4.3'
gem 'fakefs', '~> 0.5.2', group: [:test]
gem 'sshkey', '~> 1.6.1'
gem 'colorize', '~> 0.7.3'

group :development do
# We depend on Vagrant for development, but we don't add it as a
Expand Down
6 changes: 0 additions & 6 deletions source/Vagrantfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ Vagrant.configure('2') do |config|
config.vm.box = 'dummy-openstack'
config.vm.box_url = 'https://github.com/ggiamarchi/vagrant-openstack/raw/master/source/dummy.box'

config.ssh.private_key_path = '/home/vagrant/.ssh/id_rsa'
config.ssh.shell = 'sh'

config.vm.provider :openstack do |os|

os.server_name = 'vagrant-os-plugin-test'
os.username = ENV['OS_USERNAME']
os.floating_ip_pool = ENV['OS_FLOATING_IP_POOL']
os.password = ENV['OS_PASSWORD']
Expand All @@ -19,6 +14,5 @@ Vagrant.configure('2') do |config|
os.openstack_auth_url = ENV['OS_AUTH_URL']
os.tenant_name = ENV['OS_TENANT_NAME']
os.ssh_username = 'stack'
os.public_key_path = '/home/vagrant/.ssh/id_rsa.pub'
end
end
2 changes: 2 additions & 0 deletions source/gemfiles/latest_stable.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ gem 'webmock', '~> 1.18.0', group: [:test]
gem 'rubocop', '0.23.0', require: false
gem 'vagrant', git: 'git://github.com/mitchellh/vagrant.git', tag: 'v1.6.3'
gem 'fakefs', '~> 0.5.2', group: [:test]
gem 'sshkey', '~> 1.6.1'
gem 'colorize', '~> 0.7.3'

group :development do
gem 'coveralls', require: false
Expand Down
2 changes: 2 additions & 0 deletions source/gemfiles/oldest_current.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ gem 'webmock', '~> 1.18.0', group: [:test]
gem 'rubocop', '0.23.0', require: false
gem 'vagrant', git: 'git://github.com/mitchellh/vagrant.git', tag: 'v1.5.4'
gem 'fakefs', '~> 0.5.2', group: [:test]
gem 'sshkey', '~> 1.6.1'
gem 'colorize', '~> 0.7.3'

group :development do
gem 'coveralls', require: false
Expand Down
2 changes: 2 additions & 0 deletions source/gemfiles/previous_release.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ gem 'webmock', '~> 1.18.0', group: [:test]
gem 'rubocop', '0.23.0', require: false
gem 'vagrant', git: 'git://github.com/mitchellh/vagrant.git', tag: 'v1.4.3'
gem 'fakefs', '~> 0.5.2', group: [:test]
gem 'sshkey', '~> 1.6.1'
gem 'colorize', '~> 0.7.3'

group :development do
gem 'coveralls', require: false
Expand Down
13 changes: 11 additions & 2 deletions source/lib/vagrant-openstack-provider/action/create_server.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'log4r'
require 'socket'
require 'timeout'
require 'sshkey'

require 'vagrant/util/retryable'

Expand Down Expand Up @@ -92,8 +93,16 @@ def resolve_keypair(env)
config = env[:machine].provider_config
nova = env[:openstack_client].nova
return config.keypair_name if config.keypair_name
return nova.import_keypair(env, config.public_key_path) if config.public_key_path
fail Errors::UnableToResolveSSHKey
return nova.import_keypair_from_file(env, config.public_key_path) if config.public_key_path
generate_keypair(env)
end

def generate_keypair(env)
key = SSHKey.generate
nova = env[:openstack_client].nova
generated_keyname = nova.import_keypair(env, key.ssh_public_key)
File.write("#{env[:machine].data_dir}/#{generated_keyname}", key.private_key)
generated_keyname
end

def resolve_flavor(env)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ def call(env)

def read_ssh_info(env)
config = env[:machine].provider_config
{
hash = {
host: get_ip_address(env),
port: 22,
username: config.ssh_username
}
hash[:private_key_path] = "#{env[:machine].data_dir}/#{get_keypair_name(env)}" unless config.keypair_name || config.public_key_path
hash
end

def get_ip_address(env)
Expand All @@ -39,6 +41,10 @@ def get_ip_address(env)
end
fail Errors::UnableToResolveIP
end

def get_keypair_name(env)
env[:openstack_client].nova.get_server_details(env, env[:machine].id)['key_name']
end
end
end
end
Expand Down
13 changes: 8 additions & 5 deletions source/lib/vagrant-openstack-provider/client/nova.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,22 +97,25 @@ def add_floating_ip(env, server_id, floating_ip)
{ addFloatingIp: { address: floating_ip } }.to_json)
end

def import_keypair(env, public_key_path)
fail "File specified in public_key_path #{public_key_path} doesn't exist" unless File.exist?(public_key_path)
file = File.open(public_key_path)
contents = file.read
def import_keypair(env, public_key)
keyname = "vagrant-generated-#{Kernel.rand(36**8).to_s(36)}"

key_details = post(env, "#{@session.endpoints[:compute]}/os-keypairs",
{ keypair:
{
name: keyname,
public_key: contents
public_key: public_key
}
}.to_json)
JSON.parse(key_details)['keypair']['name']
end

def import_keypair_from_file(env, public_key_path)
fail "File specified in public_key_path #{public_key_path} doesn't exist" unless File.exist?(public_key_path)
file = File.open(public_key_path)
import_keypair(env, file.read)
end

def delete_keypair_if_vagrant(env, server_id)
keyname = get_server_details(env, server_id)['key_name']
delete(env, "#{@session.endpoints[:compute]}/os-keypairs/#{keyname}") if keyname.start_with?('vagrant-generated-')
Expand Down
11 changes: 9 additions & 2 deletions source/lib/vagrant-openstack-provider/config.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'vagrant'
require 'colorize'

module VagrantPlugins
module Openstack
Expand Down Expand Up @@ -148,12 +149,18 @@ def rsync_include(inc)
@rsync_includes << inc
end

def validate(_machine)
def validate(machine)
errors = _detected_errors

errors << I18n.t('vagrant_openstack.config.password_required') unless @password
errors << I18n.t('vagrant_openstack.config.username_required') unless @username
errors << I18n.t('vagrant_openstack.config.keypair_name_required') unless @keypair_name || @public_key_path

if machine.config.ssh.private_key_path
# Waiting for https://github.com/mitchellh/vagrant/issues/4388 to improve this
puts I18n.t('vagrant_openstack.config.keypair_name_required').yellow unless @keypair_name || @public_key_path
else
errors << I18n.t('vagrant_openstack.config.private_key_missing') if @keypair_name || @public_key_path
end

{
openstack_compute_url: @openstack_compute_url,
Expand Down
8 changes: 7 additions & 1 deletion source/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,13 @@ en:
metadata_must_be_hash: |-
Metadata must be a hash.
keypair_name_required: |-
keypair_name or a public_key_path are required.
Warning! You have specified ssh.private_key_path in your Vagrant configuration.
but nor keypair_name neither public_key_path are present. The openstack provider
will automatically generate a new keypair and your configuration option
ssh.private_key_path will be overriden
private_key_missing: |-
config.ssh.private_key_path is required when either keypair_name or
public_key_path is set in Vagrantfile
errors:
default: |-
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
require 'vagrant-openstack-provider/spec_helper'
require 'sshkey'

include VagrantPlugins::Openstack::Action
include VagrantPlugins::Openstack::HttpUtils
include VagrantPlugins::Openstack::Domain

describe VagrantPlugins::Openstack::Action::CreateServer do
include FakeFS::SpecHelpers

let(:config) do
double('config').tap do |config|
Expand All @@ -31,6 +33,13 @@
end
end

let(:ssh_key) do
double('ssh_key').tap do |key|
key.stub(:ssh_public_key) { 'ssh public key' }
key.stub(:private_key) { 'private key' }
end
end

let(:flavor) do
double('flavor').tap do |flavor|
flavor.stub(:name) { 'flavor_name' }
Expand Down Expand Up @@ -60,6 +69,7 @@
env[:ui].stub(:info).with(anything)
env[:machine] = double('machine')
env[:machine].stub(:provider_config) { config }
env[:machine].stub(:data_dir) { '/data/dir' }
env[:openstack_client] = double('openstack_client')
env[:openstack_client].stub(:neutron) { neutron }
env[:openstack_client].stub(:nova) { nova }
Expand Down Expand Up @@ -170,20 +180,30 @@
context 'with public_key_path provided' do
it 'return the keypair_name created into nova' do
config.stub(:public_key_path) { '/path/to/key' }
nova.stub(:import_keypair).with(env, '/path/to/key') { 'my-keypair-imported' }
nova.stub(:import_keypair_from_file).with(env, '/path/to/key') { 'my-keypair-imported' }
@action.resolve_keypair(env).should eq('my-keypair-imported')
end
end

context 'with no keypair_name and no public_key_path provided' do
it 'raises an error' do
it 'generates a new keypair and return the keypair name imported into nova' do
config.stub(:keypair_name) { nil }
config.stub(:public_key_path) { nil }
expect { @action.resolve_keypair(env) }.to raise_error
@action.stub(:generate_keypair) { 'my-keypair-imported' }
@action.resolve_keypair(env).should eq('my-keypair-imported')
end
end
end

describe 'generate_keypair' do
it 'returns a generated keypair name imported into nova' do
nova.stub(:import_keypair) { 'my-keypair-imported' }
SSHKey.stub(:generate) { ssh_key }
File.should_receive(:write).with('/data/dir/my-keypair-imported', 'private key')
@action.generate_keypair(env).should eq('my-keypair-imported')
end
end

describe 'resolve_networks' do

context 'with only ids of existing networks' do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
env[:machine] = double('machine')
env[:machine].stub(:provider_config) { config }
env[:machine].stub(:id) { '1234' }
env[:machine].stub(:data_dir) { '/data/dir' }
env[:openstack_client] = double('openstack_client')
env[:openstack_client].stub(:neutron) { neutron }
env[:openstack_client].stub(:nova) { nova }
Expand All @@ -56,9 +57,31 @@

describe 'read_ssh_info' do
context 'with config.floating_ip specified' do
it 'return the specified floating ip' do
config.stub(:floating_ip) { '80.80.80.80' }
@action.read_ssh_info(env).should eq(host: '80.80.80.80', port: 22, username: 'sshuser')
context 'with keypair_name specified' do
it 'returns the specified floating ip' do
config.stub(:floating_ip) { '80.80.80.80' }
config.stub(:keypair_name) { 'my_keypair' }
@action.read_ssh_info(env).should eq(host: '80.80.80.80', port: 22, username: 'sshuser')
end
end

context 'with public_key_path specified' do
it 'returns the specified floating ip' do
config.stub(:floating_ip) { '80.80.80.80' }
config.stub(:keypair_name) { nil }
config.stub(:public_key_path) { '/public/key/path' }
@action.read_ssh_info(env).should eq(host: '80.80.80.80', port: 22, username: 'sshuser')
end
end

context 'with neither keypair_name nor public_key_path specified' do
it 'returns the specified floating ip ' do
config.stub(:floating_ip) { '80.80.80.80' }
config.stub(:keypair_name) { nil }
config.stub(:public_key_path) { nil }
@action.stub(:get_keypair_name) { 'my_keypair_name' }
@action.read_ssh_info(env).should eq(host: '80.80.80.80', port: 22, username: 'sshuser', private_key_path: '/data/dir/my_keypair_name')
end
end
end
end
Expand All @@ -78,6 +101,7 @@
}
}
end
config.stub(:keypair_name) { 'my_keypair' }
@action.read_ssh_info(env).should eq(host: '12.12.12.12', port: 22, username: 'sshuser')
end
end
Expand Down
13 changes: 10 additions & 3 deletions source/spec/vagrant-openstack-provider/client/nova_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
end
end

let(:file) do
double('file').tap do |file|
file.stub(:read) { ssh_key_content }
end
end

let(:env) do
Hash.new.tap do |env|
env[:ui] = double('ui')
Expand Down Expand Up @@ -167,10 +173,11 @@
end
end

describe 'import_keypair' do
describe 'import_keypair_from_file' do
context 'with token and project_id acquainted' do
it 'returns newly created keypair name' do
File.open(filename, 'w') { |f| f.write ssh_key_content }
File.should_receive(:exist?).with(filename).and_return(true)
File.should_receive(:open).with(filename).and_return(file)
Kernel.stub!(:rand).and_return(2_036_069_739_008)

stub_request(:post, 'http://nova/a1b2c3/os-keypairs')
Expand All @@ -187,7 +194,7 @@
}
}')

@nova_client.import_keypair(env, filename)
@nova_client.import_keypair_from_file(env, filename)

end
end
Expand Down
22 changes: 16 additions & 6 deletions source/spec/vagrant-openstack-provider/config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,15 @@
let(:validation_errors) { subject.validate(machine)['Openstack Provider'] }
let(:error_message) { double('error message') }

let(:config) { double('config') }
let(:ssh) { double('ssh') }

before(:each) do
error_message.stub(:yellow) { 'Yellowed Error message ' }
machine.stub_chain(:env, :root_path).and_return '/'
ssh.stub(:private_key_path) { 'private key path' }
config.stub(:ssh) { ssh }
machine.stub(:config) { config }
subject.username = 'foo'
subject.password = 'bar'
subject.keypair_name = 'keypair'
Expand All @@ -78,18 +85,21 @@
validation_errors.first.should == error_message
end
end

context 'with good values' do
it 'should validate' do
validation_errors.should be_empty
end
end

context 'the keypair name and public_key_path' do
it 'should error if not given' do
subject.keypair_name = nil
subject.public_key_path = nil
I18n.should_receive(:t).with('vagrant_openstack.config.keypair_name_required').and_return error_message
validation_errors.first.should == error_message
context 'private_key_path is not set' do
context 'keypair_name or public_key_path is set' do
it 'should error if not given' do
ssh.stub(:private_key_path) { nil }
subject.public_key_path = 'public_key'
I18n.should_receive(:t).with('vagrant_openstack.config.private_key_missing').and_return error_message
validation_errors.first.should == error_message
end
end
end

Expand Down

0 comments on commit 98e91c8

Please sign in to comment.