├── lib ├── beaker-openstack │ └── version.rb └── beaker │ └── hypervisor │ └── openstack.rb ├── .simplecov ├── Gemfile ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── test.yml │ └── release.yml ├── spec ├── spec_helper.rb └── beaker │ └── hypervisor │ └── openstack_spec.rb ├── bin └── beaker-openstack ├── beaker-openstack.gemspec ├── CHANGELOG.md ├── openstack.md ├── Rakefile ├── README.md └── LICENSE /lib/beaker-openstack/version.rb: -------------------------------------------------------------------------------- 1 | module BeakerOpenstack 2 | VERSION = '2.0.0' 3 | end 4 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | SimpleCov.configure do 2 | add_filter 'spec/' 3 | add_filter 'vendor/' 4 | add_filter do |file| 5 | file.lines_of_code < 10 6 | end 7 | end 8 | 9 | SimpleCov.start if ENV['BEAKER_OPENSTACK_COVERAGE'] 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source ENV['GEM_SOURCE'] || "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :release, optional: true do 6 | gem 'faraday-retry', '~> 2.1', require: false 7 | gem 'github_changelog_generator','~> 1.16', '>= 1.16.4', require: false 8 | end 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | log/* 3 | !.gitignore 4 | junit 5 | acceptance-tests 6 | pkg 7 | Gemfile.lock 8 | options.rb 9 | test.cfg 10 | .yardoc 11 | coverage 12 | .bundle 13 | .vendor 14 | _vendor 15 | tmp/ 16 | doc 17 | # JetBrains IDEA 18 | *.iml 19 | .idea/ 20 | # rbenv file 21 | .ruby-version 22 | .ruby-gemset 23 | # Vagrant folder 24 | .vagrant/ 25 | .vagrant_files/ 26 | sut-files.tgz 27 | vendor/ 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # raise PRs for gem updates 4 | - package-ecosystem: bundler 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | time: "13:00" 9 | open-pull-requests-limit: 10 10 | 11 | # Maintain dependencies for GitHub Actions 12 | - package-ecosystem: github-actions 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | time: "13:00" 17 | open-pull-requests-limit: 10 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | - pull_request 5 | - push 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | ruby: 14 | - "2.7" 15 | - "3.0" 16 | steps: 17 | - uses: actions/checkout@v6 18 | - name: Install Ruby ${{ matrix.ruby }} 19 | uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: ${{ matrix.ruby }} 22 | bundler-cache: true 23 | - name: Run tests 24 | run: bundle exec rake spec 25 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'rspec/its' 3 | require 'beaker' 4 | 5 | Dir.glob(Dir.pwd + '/lib/beaker/hypervisor/*.rb') {|file| require file} 6 | 7 | # setup & require beaker's spec_helper.rb 8 | beaker_gem_spec = Gem::Specification.find_by_name('beaker') 9 | beaker_gem_dir = beaker_gem_spec.gem_dir 10 | beaker_spec_path = File.join(beaker_gem_dir, 'spec') 11 | $LOAD_PATH << beaker_spec_path 12 | require File.join(beaker_spec_path, 'spec_helper.rb') 13 | 14 | RSpec.configure do |config| 15 | config.include TestFileHelpers 16 | config.include HostHelpers 17 | end 18 | -------------------------------------------------------------------------------- /bin/beaker-openstack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' unless defined?(Gem) 4 | require 'beaker-openstack' 5 | 6 | VERSION_STRING = 7 | " 8 | _ .--. 9 | ( ` ) 10 | beaker-openstack .-' `--, 11 | _..----.. ( )`-. 12 | .'_|` _|` _|( .__, ) 13 | /_| _| _| _( (_, .-' 14 | ;| _| _| _| '-'__,--'`--' 15 | | _| _| _| _| | 16 | _ || _| _| _| _| %s 17 | _( `--.\\_| _| _| _|/ 18 | .-' )--,| _| _|.` 19 | (__, (_ ) )_| _| / 20 | `-.__.\\ _,--'\\|__|__/ 21 | ;____; 22 | \\YT/ 23 | || 24 | |\"\"| 25 | '==' 26 | " 27 | 28 | 29 | 30 | puts BeakerOpenstack::VERSION 31 | 32 | exit 0 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | create: 5 | ref_type: tag 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | if: github.repository == 'voxpupuli/beaker-openstack' 11 | steps: 12 | - uses: actions/checkout@v6 13 | - name: Install Ruby 3.0 14 | uses: ruby/setup-ruby@v1 15 | with: 16 | ruby-version: '3.0' 17 | env: 18 | BUNDLE_WITHOUT: release 19 | - name: Build gem 20 | run: gem build *.gemspec 21 | - name: Publish gem to rubygems.org 22 | run: gem push *.gem 23 | env: 24 | GEM_HOST_API_KEY: '${{ secrets.RUBYGEMS_AUTH_TOKEN }}' 25 | - name: Setup GitHub packages access 26 | run: | 27 | mkdir -p ~/.gem 28 | echo ":github: Bearer ${{ secrets.GITHUB_TOKEN }}" >> ~/.gem/credentials 29 | chmod 0600 ~/.gem/credentials 30 | - name: Publish gem to GitHub packages 31 | run: gem push --key github --host https://rubygems.pkg.github.com/voxpupuli *.gem 32 | -------------------------------------------------------------------------------- /beaker-openstack.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $LOAD_PATH.unshift File.expand_path("../lib", __FILE__) 3 | require 'beaker-openstack/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "beaker-openstack" 7 | s.version = BeakerOpenstack::VERSION 8 | s.authors = 'Vox Pupuli' 9 | s.email = 'voxpupuli@groups.io' 10 | s.homepage = 'https://github.com/voxpupuli/beaker-openstack' 11 | s.summary = %q{Beaker DSL Extension Helpers!} 12 | s.description = %q{For use for the Beaker acceptance testing tool} 13 | s.license = 'Apache-2.0' 14 | 15 | s.files = `git ls-files`.split("\n") 16 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 17 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 18 | s.require_paths = ["lib"] 19 | 20 | s.required_ruby_version = '>= 2.7', '< 4' 21 | 22 | # Testing dependencies 23 | s.add_development_dependency 'rspec', '~> 3.0' 24 | s.add_development_dependency 'rspec-its' 25 | s.add_development_dependency 'fakefs', '~> 2.4' 26 | s.add_development_dependency 'rake', '>= 12.3.3' 27 | s.add_development_dependency 'simplecov' 28 | s.add_development_dependency 'pry', '~> 0.10' 29 | 30 | # Documentation dependencies 31 | s.add_development_dependency 'yard' 32 | s.add_development_dependency 'markdown' 33 | s.add_development_dependency 'thin' 34 | 35 | # Run time dependencies 36 | s.add_runtime_dependency 'stringify-hash', '~> 0.0.0' 37 | s.add_runtime_dependency 'fog-openstack', '~> 1.0.0' 38 | s.add_runtime_dependency 'beaker', '~> 5.6' 39 | end 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [2.0.0](https://github.com/voxpupuli/beaker-openstack/tree/2.0.0) (2024-02-03) 6 | 7 | [Full Changelog](https://github.com/voxpupuli/beaker-openstack/compare/1.0.0...2.0.0) 8 | 9 | **Breaking changes:** 10 | 11 | - Drop EoL Ruby 2.4/2.5/2.6 support [\#22](https://github.com/voxpupuli/beaker-openstack/pull/22) ([bastelfreak](https://github.com/bastelfreak)) 12 | 13 | **Implemented enhancements:** 14 | 15 | - Add parallel instance creation opt-in option [\#20](https://github.com/voxpupuli/beaker-openstack/pull/20) ([canihavethisone](https://github.com/canihavethisone)) 16 | 17 | **Merged pull requests:** 18 | 19 | - gemspec: Fix typo in Apache-2.0 license name [\#27](https://github.com/voxpupuli/beaker-openstack/pull/27) ([bastelfreak](https://github.com/bastelfreak)) 20 | - Apply best practices for our changelog generator [\#26](https://github.com/voxpupuli/beaker-openstack/pull/26) ([bastelfreak](https://github.com/bastelfreak)) 21 | - Cleanup Gemfile, add beaker as runtime dependency [\#24](https://github.com/voxpupuli/beaker-openstack/pull/24) ([bastelfreak](https://github.com/bastelfreak)) 22 | - dependabot: check for github actions and bundler [\#21](https://github.com/voxpupuli/beaker-openstack/pull/21) ([bastelfreak](https://github.com/bastelfreak)) 23 | - Update fakefs requirement from ~\> 1.3 to ~\> 2.4 [\#19](https://github.com/voxpupuli/beaker-openstack/pull/19) ([dependabot[bot]](https://github.com/apps/dependabot)) 24 | 25 | ## [1.0.0](https://github.com/voxpupuli/beaker-openstack/tree/1.0.0) (2021-07-09) 26 | 27 | [Full Changelog](https://github.com/voxpupuli/beaker-openstack/compare/0.3.0...1.0.0) 28 | 29 | **Merged pull requests:** 30 | 31 | - Cleanup README/gemspec/GitHub actions [\#13](https://github.com/voxpupuli/beaker-openstack/pull/13) ([bastelfreak](https://github.com/bastelfreak)) 32 | - Update fakefs requirement from ~\> 0.6 to ~\> 1.3 [\#11](https://github.com/voxpupuli/beaker-openstack/pull/11) ([dependabot[bot]](https://github.com/apps/dependabot)) 33 | - Add Vox Pupuli's CI bits [\#10](https://github.com/voxpupuli/beaker-openstack/pull/10) ([genebean](https://github.com/genebean)) 34 | - Add openstack\_project\_id logic etc, expand example nodeset in readme [\#9](https://github.com/voxpupuli/beaker-openstack/pull/9) ([canihavethisone](https://github.com/canihavethisone)) 35 | 36 | ## [0.3.0](https://github.com/voxpupuli/beaker-openstack/tree/0.3.0) (2019-06-17) 37 | 38 | [Full Changelog](https://github.com/voxpupuli/beaker-openstack/compare/0.2.0...0.3.0) 39 | 40 | **Merged pull requests:** 41 | 42 | - hypervisor/openstack: fix usage with V3 API [\#5](https://github.com/voxpupuli/beaker-openstack/pull/5) ([GiedriusS](https://github.com/GiedriusS)) 43 | - \(BKR-1509\) Hypervisor usage instructions for Beaker 4.0 [\#4](https://github.com/voxpupuli/beaker-openstack/pull/4) ([Dakta](https://github.com/Dakta)) 44 | 45 | ## [0.2.0](https://github.com/voxpupuli/beaker-openstack/tree/0.2.0) (2018-01-09) 46 | 47 | [Full Changelog](https://github.com/voxpupuli/beaker-openstack/compare/0.1.0...0.2.0) 48 | 49 | **Merged pull requests:** 50 | 51 | - \(BKR-1270\) Use fog-openstack instead of fog [\#3](https://github.com/voxpupuli/beaker-openstack/pull/3) ([Sharpie](https://github.com/Sharpie)) 52 | 53 | ## [0.1.0](https://github.com/voxpupuli/beaker-openstack/tree/0.1.0) (2017-07-28) 54 | 55 | [Full Changelog](https://github.com/voxpupuli/beaker-openstack/compare/9fc506a9a93f9985fb3f36132a91805026f19d79...0.1.0) 56 | 57 | **Merged pull requests:** 58 | 59 | - \(MAINT\) Update docs [\#2](https://github.com/voxpupuli/beaker-openstack/pull/2) ([rishijavia](https://github.com/rishijavia)) 60 | - \(MAINT\) Update env variable to correct hypervisor [\#1](https://github.com/voxpupuli/beaker-openstack/pull/1) ([rishijavia](https://github.com/rishijavia)) 61 | 62 | 63 | 64 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 65 | -------------------------------------------------------------------------------- /openstack.md: -------------------------------------------------------------------------------- 1 | # Openstack 2 | 3 | OpenStack is a free and open-source software platform for cloud computing. [Their Site](http://www.openstack.org/). 4 | 5 | Considered **EXPERIMENTAL**, may break without notice. 6 | 7 | # Getting Started 8 | 9 | ### Requirements 10 | 11 | Get openstack Access & Security credentials: 12 | 13 | - "openstack_api_key" 14 | - "openstack_auth_url" 15 | - "openstack_username" 16 | - "openstack_tenant" 17 | - "openstack_network" 18 | - "openstack_keyname" 19 | 20 | If you are using [OpenStack Dashboard "Horizon"](https://wiki.openstack.org/wiki/Horizon) 21 | you can find these keys in next places: 22 | 23 | 1. login to "Horizon dashboard" -> "project" -> "Compute" -> "Access & Security" -> tab "API Access" -> "Download OpenStack RC File": 24 | * "openstack_auth_url" == OS_AUTH_URL + "/tokens" 25 | * "openstack_username" == OS_USERNAME 26 | * "openstack_tenant" == OS_TENANT_NAME 27 | 2. "openstack_network": in "project" -> "Networks" 28 | 3. "openstack_keyname": in "project" -> "Compute" -> "Access & Security" -> tab "Key Pairs" 29 | 4. "openstack_api_key": Your user Password 30 | 31 | ### Setup a Openstack Hosts File 32 | 33 | An Openstack hosts file looks like a typical hosts file, 34 | except that there are a number of required properties that need to be added to every host 35 | in order for the Openstack hypervisor to provision hosts properly. 36 | 37 | **Basic Openstack hosts file** 38 | 39 | HOSTS: 40 | centos-6-master: 41 | roles: 42 | - master 43 | - agent 44 | - database 45 | - dashboard 46 | platform: el-6-x86_64 47 | hypervisor: openstack 48 | image: centos-6-x86_64-nocm 49 | flavor: m1.large 50 | 51 | CONFIG: 52 | nfs_server: none 53 | consoleport: 443 54 | openstack_api_key: Pas$w0rd 55 | openstack_username: user 56 | openstack_auth_url: http://10.10.10.10:5000/v2.0/tokens 57 | openstack_tenant: testing 58 | openstack_network : testing 59 | openstack_keyname : nopass 60 | 61 | The `image` - image name. 62 | 63 | The `flavor` - templates for VMs, defining sizes for RAM, disk, number of cores, and so on. 64 | 65 | 66 | # Openstack-Specific Hosts File Settings 67 | 68 | ### user-data 69 | 70 | "user data" - a blob of data that the user can specify when they launch an instance. 71 | The instance can access this data through the metadata service or config drive with one of the next requests: 72 | 73 | - curl http://169.254.169.254/2009-04-04/user-data 74 | - curl http://169.254.169.254/openstack/2012-08-10/user_data 75 | 76 | 77 | Examples of `user_data` you can find here: http://cloudinit.readthedocs.io/en/latest/topics/examples.html 78 | 79 | Also if you plan use `user-data` make sure that 'cloud-init' package installed in your VM `image` and 'cloud-init' service is running. 80 | 81 | **Example Openstack hosts file with user_data** 82 | 83 | HOSTS: 84 | centos-6-master: 85 | roles: 86 | - master 87 | - agent 88 | - database 89 | - dashboard 90 | platform: el-6-x86_64 91 | image: centos-6-x86_64-nocm 92 | flavor: m1.large 93 | hypervisor: openstack 94 | user_data: | 95 | #cloud-config 96 | bootcmd: 97 | - echo 123 > /tmp/test.txt 98 | CONFIG: 99 | nfs_server: none 100 | consoleport: 443 101 | openstack_api_key: P1as$w0rd 102 | openstack_username: user 103 | openstack_auth_url: http://10.10.10.10:5000/v2.0/tokens 104 | openstack_tenant: testing 105 | openstack_network : testing 106 | openstack_keyname : nopass 107 | 108 | ### Security groups 109 | 110 | A security group is a set of rules for incoming and outgoing traffic to 111 | an instance. You can associate a host with one or many security groups 112 | in the `CONFIG` section of your hosts file: 113 | 114 | security_group: ['my_sg', 'default'] 115 | 116 | This is an optional config parameter. 117 | 118 | ### Floating IP Pool 119 | 120 | The name of the floating IP pool that a VM can grab IPs from. This is useful 121 | if your organization doesn't have a public pool of floating IPs, or give each 122 | user their own pool. It's used in allocating new IPs. It's an options 123 | parameter in the CONFIG section of the host file: 124 | 125 | floating_ip_pool: 'my_pool_name' 126 | 127 | ### Volumes 128 | 129 | Attaching volumes to a VM is supported via the Cinder service. All versions are transparently 130 | supported to cater for differences in the APIs. To create and attach volumes simply add hash 131 | called 'volumes' to a host in the HOSTS section. Each key is the name given to the volume upon 132 | resource creation. The value is a hash with a single integer parameter 'size' which defines the 133 | volume size in MB. 134 | 135 | **Example OpenStack hosts file with volumes** 136 | 137 | HOSTS: 138 | ceph: 139 | roles: 140 | - master 141 | platform: ubuntu-16.04-amd64 142 | hypervisor: openstack 143 | flavor: m1.large 144 | image: xenial-server-cloudimg-amd64-scsi 145 | user: ubuntu 146 | volumes: 147 | osd0: 148 | size: 10000 149 | osd1: 150 | size: 10000 151 | osd2: 152 | size: 10000 153 | journal: 154 | size: 1000 155 | 156 | 157 | In the event you're using an OpenStack instance that does not deploy the volume service you can disable that functionality to prevent beaker runs from failing. Either using an ENV variable or setting the following value in the `CONFIG` section of your hosts file(valid values are `true` or `false`): 158 | 159 | ``` 160 | openstack_volume_support: false 161 | ``` 162 | 163 | You can also configure this setting via an environment variable: 164 | 165 | ``` 166 | export OS_VOLUME_SUPPORT=false 167 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | namespace :test do 4 | 5 | namespace :spec do 6 | 7 | desc "Run spec tests" 8 | RSpec::Core::RakeTask.new(:run) do |t| 9 | t.rspec_opts = ['--color'] 10 | t.pattern = 'spec/' 11 | end 12 | 13 | desc "Run spec tests with coverage" 14 | RSpec::Core::RakeTask.new(:coverage) do |t| 15 | ENV['BEAKER_OPENSTACK_COVERAGE'] = 'y' 16 | t.rspec_opts = ['--color'] 17 | t.pattern = 'spec/' 18 | end 19 | 20 | end 21 | 22 | namespace :acceptance do 23 | 24 | desc <<-EOS 25 | A quick acceptance test, named because it has no pre-suites to run 26 | EOS 27 | task :quick do 28 | 29 | # setup & load_path of beaker's acceptance base and lib directory 30 | beaker_gem_spec = Gem::Specification.find_by_name('beaker') 31 | beaker_gem_dir = beaker_gem_spec.gem_dir 32 | beaker_test_base_dir = File.join(beaker_gem_dir, 'acceptance/tests/base') 33 | load_path_option = File.join(beaker_gem_dir, 'acceptance/lib') 34 | 35 | unless ENV['OPENSTACK_HOSTS'] and ENV['OPENSTACK_KEY'] 36 | raise(ArgumentError, "Please set OPENSTACK_HOSTS and OPENSTACK_KEY environment variables") 37 | end 38 | 39 | sh("beaker", 40 | "--hosts", ENV['OPENSTACK_HOSTS'], 41 | "--tests", beaker_test_base_dir, 42 | "--log-level", "debug", 43 | "--load-path", load_path_option, 44 | "--key", ENV['OPENSTACK_KEY'], 45 | "--debug") 46 | end 47 | 48 | end 49 | 50 | end 51 | 52 | # namespace-named default tasks. 53 | # these are the default tasks invoked when only the namespace is referenced. 54 | # they're needed because `task :default` in those blocks doesn't work as expected. 55 | task 'test:spec' => 'test:spec:run' 56 | task 'test:acceptance' => 'test:acceptance:quick' 57 | 58 | # global defaults 59 | task :test => 'test:spec' 60 | task :default => :test 61 | task :spec => 'test:spec' 62 | 63 | ########################################################### 64 | # 65 | # Documentation Tasks 66 | # 67 | ########################################################### 68 | DOCS_DAEMON = "yard server --reload --daemon --server thin" 69 | FOREGROUND_SERVER = 'bundle exec yard server --reload --verbose --server thin lib/beaker' 70 | 71 | def running?( cmdline ) 72 | ps = `ps -ef` 73 | found = ps.lines.grep( /#{Regexp.quote( cmdline )}/ ) 74 | if found.length > 1 75 | raise StandardError, "Found multiple YARD Servers. Don't know what to do." 76 | end 77 | 78 | yes = found.empty? ? false : true 79 | return yes, found.first 80 | end 81 | 82 | def pid_from( output ) 83 | output.squeeze(' ').strip.split(' ')[1] 84 | end 85 | 86 | desc 'Start the documentation server in the foreground' 87 | task :docs => 'docs:clear' do 88 | original_dir = Dir.pwd 89 | Dir.chdir( File.expand_path(File.dirname(__FILE__)) ) 90 | sh FOREGROUND_SERVER 91 | Dir.chdir( original_dir ) 92 | end 93 | 94 | namespace :docs do 95 | 96 | desc 'Clear the generated documentation cache' 97 | task :clear do 98 | original_dir = Dir.pwd 99 | Dir.chdir( File.expand_path(File.dirname(__FILE__)) ) 100 | sh 'rm -rf docs' 101 | Dir.chdir( original_dir ) 102 | end 103 | 104 | desc 'Generate static documentation' 105 | task :gen => 'docs:clear' do 106 | original_dir = Dir.pwd 107 | Dir.chdir( File.expand_path(File.dirname(__FILE__)) ) 108 | output = `bundle exec yard doc` 109 | puts output 110 | if output =~ /\[warn\]|\[error\]/ 111 | fail "Errors/Warnings during yard documentation generation" 112 | end 113 | Dir.chdir( original_dir ) 114 | end 115 | 116 | desc 'Run the documentation server in the background, alias `bg`' 117 | task :background => 'docs:clear' do 118 | yes, output = running?( DOCS_DAEMON ) 119 | if yes 120 | puts "Not starting a new YARD Server..." 121 | puts "Found one running with pid #{pid_from( output )}." 122 | else 123 | original_dir = Dir.pwd 124 | Dir.chdir( File.expand_path(File.dirname(__FILE__)) ) 125 | sh "bundle exec #{DOCS_DAEMON}" 126 | Dir.chdir( original_dir ) 127 | end 128 | end 129 | 130 | task(:bg) { Rake::Task['docs:background'].invoke } 131 | 132 | desc 'Check the status of the documentation server' 133 | task :status do 134 | yes, output = running?( DOCS_DAEMON ) 135 | if yes 136 | pid = pid_from( output ) 137 | puts "Found a YARD Server running with pid #{pid}" 138 | else 139 | puts "Could not find a running YARD Server." 140 | end 141 | end 142 | 143 | desc "Stop a running YARD Server" 144 | task :stop do 145 | yes, output = running?( DOCS_DAEMON ) 146 | if yes 147 | pid = pid_from( output ) 148 | puts "Found a YARD Server running with pid #{pid}" 149 | `kill #{pid}` 150 | puts "Stopping..." 151 | yes, output = running?( DOCS_DAEMON ) 152 | if yes 153 | `kill -9 #{pid}` 154 | yes, output = running?( DOCS_DAEMON ) 155 | if yes 156 | puts "Could not Stop Server!" 157 | else 158 | puts "Server stopped." 159 | end 160 | else 161 | puts "Server stopped." 162 | end 163 | else 164 | puts "Could not find a running YARD Server" 165 | end 166 | end 167 | end 168 | 169 | begin 170 | require 'rubygems' 171 | require 'github_changelog_generator/task' 172 | rescue LoadError 173 | # github_changelog_generator is an optional group 174 | else 175 | GitHubChangelogGenerator::RakeTask.new :changelog do |config| 176 | config.header = "# Changelog\n\nAll notable changes to this project will be documented in this file." 177 | config.exclude_labels = %w[duplicate question invalid wontfix wont-fix skip-changelog github_actions] 178 | config.user = 'voxpupuli' 179 | config.project = 'beaker-openstack' 180 | config.future_release = Gem::Specification.load("#{config.project}.gemspec").version 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # beaker-openstack 2 | 3 | [![License](https://img.shields.io/github/license/voxpupuli/beaker-openstack.svg)](https://github.com/voxpupuli/beaker-openstack/blob/master/LICENSE) 4 | [![Test](https://github.com/voxpupuli/beaker-openstack/actions/workflows/test.yml/badge.svg)](https://github.com/voxpupuli/beaker-openstack/actions/workflows/test.yml) 5 | [![Release](https://github.com/voxpupuli/beaker-openstack/actions/workflows/release.yml/badge.svg)](https://github.com/voxpupuli/beaker-openstack/actions/workflows/release.yml) 6 | [![RubyGem Version](https://img.shields.io/gem/v/beaker-openstack.svg)](https://rubygems.org/gems/beaker-openstack) 7 | [![RubyGem Downloads](https://img.shields.io/gem/dt/beaker-openstack.svg)](https://rubygems.org/gems/beaker-openstack) 8 | [![Donated by Puppet Inc](https://img.shields.io/badge/donated%20by-Puppet%20Inc-fb7047.svg)](#transfer-notice) 9 | 10 | Beaker library to use openstack hypervisor 11 | 12 | # How to use this wizardry 13 | 14 | This gem that allows you to use hosts with [openstack](openstack.md) hypervisor with [beaker](https://github.com/puppetlabs/beaker). 15 | 16 | Beaker will automatically load the appropriate hypervisors for any given hosts file, so as long as your project dependencies are satisfied there's nothing else to do. No need to `require` this library in your tests. 17 | 18 | ## With Beaker 3.x 19 | 20 | This library is included as a dependency of Beaker 3.x versions, so there's nothing to do. 21 | 22 | ## With Beaker 4.x 23 | 24 | As of Beaker 4.0, all hypervisor and DSL extension libraries have been removed and are no longer dependencies. In order to use a specific hypervisor or DSL extension library in your project, you will need to include them alongside Beaker in your Gemfile or project.gemspec. E.g. 25 | 26 | ~~~ruby 27 | # Gemfile 28 | gem 'beaker', '~>4.0' 29 | gem 'beaker-aws' 30 | # project.gemspec 31 | s.add_runtime_dependency 'beaker', '~>4.0' 32 | s.add_runtime_dependency 'beaker-aws' 33 | ~~~ 34 | 35 | # Spec tests 36 | 37 | Spec test live under the `spec` folder. There are the default rake task and therefore can run with a simple command: 38 | ```bash 39 | bundle exec rake test:spec 40 | ``` 41 | 42 | # Acceptance tests 43 | 44 | We run beaker's base acceptance tests with this library to see if the hypervisor is working with beaker. Please check our [openstack docs](openstack.md) to create host file to run acceptance tests. You need to set two environment variables before running acceptance tests: 45 | 46 | 1. `OPENSTACK_HOSTS` - Path to hostfile with hosts using openstack hypervisor 47 | 48 | 2. `OPENSTACK_KEY` - Path to private key that is used to SSH into Openstack VMs 49 | 50 | You will need at least two hosts defined in a nodeset file. An example comprehensive nodeset is below (note that not all parameters are required): 51 | 52 | ```yaml 53 | 54 | HOSTS: 55 | master: 56 | roles: 57 | - agent 58 | - master 59 | - dashboard 60 | - database 61 | hypervisor: openstack 62 | platform: 63 | user: 64 | image: 65 | flavor: 66 | ssh: 67 | user: cloud-user 68 | password: 69 | auth_methods: 70 | - password 71 | - publickey 72 | keys: 73 | - 74 | user_data: | 75 | #cloud-config 76 | output: {all: '| tee -a /var/log/cloud-init-output.log'} 77 | disable_root: 78 | ssh_pwauth: 79 | chpasswd: 80 | list: | 81 | root: 82 | cloud-user: 83 | expire: False 84 | runcmd: 85 | - 86 | 87 | agent_1: 88 | roles: 89 | - agent 90 | - default 91 | hypervisor: openstack 92 | platform: 93 | user: 94 | image: 95 | flavor: 96 | ssh: 97 | user: cloud-user 98 | password: 99 | auth_methods: 100 | - publickey 101 | keys: 102 | - 103 | number_of_password_prompts: 0 104 | keepalive: true 105 | keepalive_interval: 5 106 | user_data: | 107 | #cloud-config 108 | output: {all: '| tee -a /var/log/cloud-init-output.log'} 109 | disable_root: 110 | ssh_pwauth: 111 | chpasswd: 112 | list: | 113 | root: 114 | cloud-user: 115 | expire: False 116 | runcmd: 117 | - 118 | 119 | CONFIG: 120 | log_level: 121 | trace_limit: 50 122 | timesync: 123 | nfs_server: none 124 | consoleport: 443 125 | openstack_username: 126 | openstack_api_key: 127 | # openstack_project_name: # alternatively use openstack_project_id 128 | openstack_project_id: 129 | # openstack_user_domain: # alternatively use openstack_user_domain_id 130 | openstack_user_domain_id: 131 | # openstack_project_domain: # alternatively use openstack_project_domain_id 132 | openstack_project_domain_id: 133 | openstack_auth_url: http://:5000/v3/ 134 | openstack_network: 135 | openstack_keyname: 136 | openstack_floating_ip: 137 | openstack_volume_support: 138 | security_group: ['default'] 139 | preserve_hosts: 140 | create_in_parallel: true 141 | run_in_parallel: ['configure', 'install'] 142 | type: 143 | ``` 144 | 145 | Note that when using _id parameters, you must also match the parameter type across the following when domain is specified: 146 | - openstack_project_id 147 | - openstack_user_domain_id 148 | - openstack_project_domain_id 149 | 150 | Further, you can opt to use a static master by setting the master's hypervisor to none, and identifying its location thus: 151 | ```yaml 152 | hypervisor: none 153 | hostname: 154 | vmhostname: 155 | ip: 156 | ``` 157 | 158 | Additionally, you can set instance creation to occur in parallel instead of sequentially via this CONFIG entry: 159 | ```yaml 160 | create_in_parallel: true 161 | ``` 162 | 163 | Additional parameter information is available at https://github.com/voxpupuli/beaker/blob/master/docs/concepts/argument_processing_and_precedence.md 164 | 165 | There is a simple rake task to invoke acceptance test for the library once the two environment variables are set: 166 | ```bash 167 | bundle exec rake test:acceptance 168 | ``` 169 | 170 | # Contributing 171 | 172 | Please refer to puppetlabs/beaker's [contributing](https://github.com/puppetlabs/beaker/blob/master/CONTRIBUTING.md) guide. 173 | -------------------------------------------------------------------------------- /spec/beaker/hypervisor/openstack_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fog/openstack' 3 | 4 | module Beaker 5 | describe Openstack do 6 | 7 | let(:options) { make_opts.merge({'logger' => double().as_null_object, 'openstack_floating_ip' => true}) } 8 | 9 | let(:openstack) { 10 | Openstack.new(@hosts, options) 11 | } 12 | 13 | before :each do 14 | @hosts = make_hosts() 15 | 16 | @compute_client = double().as_null_object 17 | @network_client = double().as_null_object 18 | 19 | allow( Fog::Compute ).to receive( :new ).and_return( @compute_client ) 20 | allow( Fog::Network ).to receive( :new ).and_return( @network_client ) 21 | end 22 | 23 | context 'keystone version support' do 24 | it 'supports keystone v2' do 25 | credentials = openstack.instance_eval('@credentials') 26 | expect(credentials[:openstack_user_domain]).to be_nil 27 | expect(credentials[:openstack_project_domain]).to be_nil 28 | end 29 | 30 | it 'supports keystone v3 with implicit arguments' do 31 | v3_options = options 32 | v3_options[:openstack_auth_url] = 'https://example.com/identity/v3/auth' 33 | v3_options[:openstack_project_name] = 'TeamTest_ab_c' 34 | v3_options[:openstack_tenant] = nil 35 | 36 | credentials = Openstack.new(@hosts, v3_options).instance_eval('@credentials') 37 | expect(credentials[:openstack_user_domain]).to eq('Default') 38 | expect(credentials[:openstack_project_domain]).to eq('Default') 39 | expect(credentials[:openstack_project_name]).to eq('TeamTest_ab_c') 40 | expect(credentials[:openstack_tenant]).to be_nil 41 | end 42 | 43 | it 'supports keystone v3 with explicit arguments' do 44 | v3_options = options 45 | v3_options[:openstack_auth_url] = 'https://example.com/identity/v3/auth' 46 | v3_options[:openstack_user_domain] = 'acme.com' 47 | v3_options[:openstack_project_domain] = 'R&D' 48 | v3_options[:openstack_project_name] = 'Team_test_abc' 49 | v3_options[:openstack_tenant] = nil 50 | 51 | credentials = Openstack.new(@hosts, v3_options).instance_eval('@credentials') 52 | expect(credentials[:openstack_user_domain]).to eq('acme.com') 53 | expect(credentials[:openstack_project_domain]).to eq('R&D') 54 | expect(credentials[:openstack_project_name]).to eq('Team_test_abc') 55 | expect(credentials[:openstack_tenant]).to be_nil 56 | end 57 | end 58 | 59 | describe '#provision' do 60 | 61 | it 'check openstack options during initialization' do 62 | options = openstack.instance_eval('@options') 63 | expect(options['openstack_api_key']).to eq('P1as$w0rd') 64 | expect(options['openstack_username']).to eq('user') 65 | expect(options['openstack_auth_url']).to eq('http://openstack_hypervisor.labs.net:5000/v2.0/tokens') 66 | expect(options['openstack_tenant']).to eq('testing') 67 | expect(options['openstack_network']).to eq('testing') 68 | expect(options['openstack_keyname']).to eq('nopass') 69 | expect(options['security_group']).to eq(['my_sg', 'default']) 70 | expect(options['floating_ip_pool']).to eq('my_pool') 71 | end 72 | 73 | it 'check hosts options during initialization' do 74 | @hosts.each do |host| 75 | expect(host['image']).to eq('default_image') 76 | expect(host['flavor']).to eq('m1.large') 77 | expect(host['user_data']).to eq('#cloud-config\nmanage_etc_hosts: true\nfinal_message: "The host is finally up!"') 78 | end 79 | end 80 | 81 | it 'check host options during server creation' do 82 | 83 | mock_flavor = Object.new 84 | allow( mock_flavor ).to receive( :id ).and_return( 12345 ) 85 | allow( openstack ).to receive( :flavor ).and_return( mock_flavor ) 86 | expect( openstack ).to receive( :flavor ).with( 'm1.large' ) 87 | 88 | mock_image = Object.new 89 | allow( mock_image ).to receive( :id ).and_return( 54321 ) 90 | allow( openstack ).to receive( :image ).and_return( mock_image ) 91 | expect( openstack ).to receive( :image ).with( 'default_image' ) 92 | 93 | mock_servers = double().as_null_object 94 | allow( @compute_client ).to receive( :servers ).and_return( mock_servers ) 95 | 96 | expect(mock_servers).to receive(:create).with(hash_including( 97 | :user_data => '#cloud-config\nmanage_etc_hosts: true\nfinal_message: "The host is finally up!"', 98 | :flavor_ref => 12345, 99 | :image_ref => 54321) 100 | ) 101 | 102 | @hosts.each do |host| 103 | allow(host).to receive(:wait_for_port).and_return(true) 104 | end 105 | 106 | openstack.provision 107 | end 108 | 109 | it 'generates valid keynames during server creation' do 110 | # Simulate getting a dynamic IP from OpenStack to test key generation code 111 | # after provisioning. See _validate_new_key_pair in openstack/nova for reference 112 | mock_ip = double().as_null_object 113 | allow( openstack ).to receive( :get_floating_ip ).and_return( mock_ip ) 114 | allow( mock_ip ).to receive( :ip ).and_return( '172.16.0.1' ) 115 | openstack.instance_eval('@options')['openstack_keyname'] = nil 116 | 117 | @hosts.each do |host| 118 | allow(host).to receive(:wait_for_port).and_return(true) 119 | end 120 | 121 | openstack.provision 122 | 123 | @hosts.each do |host| 124 | expect(host[:keyname]).to match(/^[_\-0-9a-zA-Z]+$/) 125 | end 126 | end 127 | 128 | it 'get_floating_ip always allocates a new floatingip' do 129 | # Assume beaker is being executed in parallel N times by travis (or similar). 130 | # IPs are allocated (but not associated) before an instance is created; it is 131 | # hightly possible the first instance will allocate a new IP and create an ssh 132 | # key. While the instance is being created the other N-1 instances come along, 133 | # find the unused IP and try to use it as well which causes keyname clashes 134 | # and other IP related shenannigans. Ensure we allocate a new IP each and every 135 | # time 136 | mock_addresses = double().as_null_object 137 | mock_ip = double().as_null_object 138 | allow(@compute_client).to receive(:addresses).and_return(mock_addresses) 139 | allow(mock_addresses).to receive(:create).and_return(mock_ip) 140 | expect(mock_addresses).to receive(:create).exactly(3).times 141 | (1..3).each { openstack.get_floating_ip } 142 | end 143 | 144 | context 'volume creation option' do 145 | it 'provisions volume by default' do 146 | mock_flavor = Object.new 147 | allow( mock_flavor ).to receive( :id ).and_return( 12345 ) 148 | allow( openstack ).to receive( :flavor ).and_return( mock_flavor ) 149 | mock_image = Object.new 150 | allow( mock_image ).to receive( :id ).and_return( 54321 ) 151 | allow( openstack ).to receive( :image ).and_return( mock_image ) 152 | mock_servers = double().as_null_object 153 | allow( @compute_client ).to receive( :servers ).and_return( mock_servers ) 154 | 155 | @hosts.each do |host| 156 | allow(host).to receive(:wait_for_port).and_return(true) 157 | expect(openstack).to receive(:provision_storage) 158 | end 159 | 160 | openstack.provision 161 | end 162 | 163 | it 'skips provisioning when disabled' do 164 | mock_flavor = Object.new 165 | allow( mock_flavor ).to receive( :id ).and_return( 12345 ) 166 | allow( openstack ).to receive( :flavor ).and_return( mock_flavor ) 167 | mock_image = Object.new 168 | allow( mock_image ).to receive( :id ).and_return( 54321 ) 169 | allow( openstack ).to receive( :image ).and_return( mock_image ) 170 | mock_servers = double().as_null_object 171 | allow( @compute_client ).to receive( :servers ).and_return( mock_servers ) 172 | 173 | openstack.instance_eval('@options')['openstack_volume_support'] = false 174 | 175 | @hosts.each do |host| 176 | allow(host).to receive(:wait_for_port).and_return(true) 177 | expect(openstack).not_to receive(:provision_storage) 178 | end 179 | 180 | openstack.provision 181 | end 182 | end 183 | end 184 | 185 | describe '#provision_storage' do 186 | 187 | it 'creates volumes with cinder v1' do 188 | # Mock a volume 189 | allow(openstack).to receive(:get_volumes).and_return({'volume1' => {'size' => 1000000 }}) 190 | 191 | # Stub out the call to create the client and hard code the return value 192 | allow(openstack).to receive(:volume_client_create).and_return(nil) 193 | client = double().as_null_object 194 | openstack.instance_variable_set(:@volume_client, client) 195 | allow(openstack).to receive(:get_volume_api_version).and_return(1) 196 | 197 | # Check the parameters are valid, correct 'name' parameter and correct size conversion 198 | mock_volume = double().as_null_object 199 | expect(client).to receive(:create).with(:display_name => 'volume1', 200 | :description => 'Beaker volume: host=alan volume=volume1', 201 | :size => 1000 202 | ).and_return(mock_volume) 203 | allow(mock_volume).to receive(:wait_for).and_return(nil) 204 | 205 | # Perform the test! 206 | mock_vm = double().as_null_object 207 | allow(mock_volume).to receive(:id).and_return('Fake ID') 208 | expect(mock_vm).to receive(:attach_volume).with('Fake ID', '/dev/vdb') 209 | 210 | mock_host = double().as_null_object 211 | allow(mock_host).to receive(:name).and_return('alan') 212 | 213 | openstack.provision_storage mock_host, mock_vm 214 | end 215 | 216 | it 'creates volumes with cinder v2' do 217 | # Mock a volume 218 | allow(openstack).to receive(:get_volumes).and_return({'volume1' => {'size' => 1000000 }}) 219 | 220 | # Stub out the call to create the client and hard code the return value 221 | allow(openstack).to receive(:volume_client_create).and_return(nil) 222 | client = double().as_null_object 223 | openstack.instance_variable_set(:@volume_client, client) 224 | allow(openstack).to receive(:get_volume_api_version).and_return(-1) 225 | 226 | # Check the parameters are valid, correct 'name' parameter and correct size conversion 227 | mock_volume = double().as_null_object 228 | expect(client).to receive(:create).with(:name => 'volume1', 229 | :description => 'Beaker volume: host=alan volume=volume1', 230 | :size => 1000 231 | ).and_return(mock_volume) 232 | allow(mock_volume).to receive(:wait_for).and_return(nil) 233 | 234 | # Perform the test! 235 | mock_vm = double().as_null_object 236 | allow(mock_volume).to receive(:id).and_return('Fake ID') 237 | expect(mock_vm).to receive(:attach_volume).with('Fake ID', '/dev/vdb') 238 | 239 | mock_host = double().as_null_object 240 | allow(mock_host).to receive(:name).and_return('alan') 241 | 242 | openstack.provision_storage mock_host, mock_vm 243 | end 244 | end 245 | end 246 | end 247 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /lib/beaker/hypervisor/openstack.rb: -------------------------------------------------------------------------------- 1 | module Beaker 2 | #Beaker support for OpenStack 3 | #This code is EXPERIMENTAL! 4 | #Please file any issues/concerns at https://github.com/puppetlabs/beaker/issues 5 | class Openstack < Beaker::Hypervisor 6 | 7 | SLEEPWAIT = 5 8 | 9 | #Create a new instance of the OpenStack hypervisor object 10 | #@param [] openstack_hosts The array of OpenStack hosts to provision 11 | #@param [Hash{Symbol=>String}] options The options hash containing configuration values 12 | #@option options [String] :openstack_api_key The key to access the OpenStack instance with (required) 13 | #@option options [String] :openstack_username The username to access the OpenStack instance with (required) 14 | #@option options [String] :openstack_auth_url The URL to access the OpenStack instance with (required) 15 | #@option options [String] :openstack_tenant The tenant to access the OpenStack instance with (either this or openstack_project_name is required) 16 | #@option options [String] :openstack_project_name The project name to access the OpenStack instance with (either this or openstack_tenant is required) 17 | #@option options [String] :openstack_project_id The project id to access the OpenStack instance with (alternative to openstack_project_name) 18 | #@option options [String] :openstack_user_domain The user domain name to access the OpenStack instance with 19 | #@option options [String] :openstack_user_domain_id The user domain id to access the OpenStack instance with (alternative to openstack_user_domain) 20 | #@option options [String] :openstack_project_domain The project domain to access the OpenStack instance with 21 | #@option options [String] :openstack_project_domain_id The project domain id to access the OpenStack instance with (alternative to openstack_project_domain) 22 | #@option options [String] :openstack_region The region that each OpenStack instance should be provisioned on (optional) 23 | #@option options [String] :openstack_network The network that each OpenStack instance should be contacted through (required) 24 | #@option options [Bool] :openstack_floating_ip Whether a floating IP should be allocated (required) 25 | #@option options [String] :openstack_keyname The name of an existing key pair that should be auto-loaded onto each 26 | #@option options [Hash] :security_group An array of security groups to associate with the instance 27 | # OpenStack instance (optional) 28 | #@option options [String] :jenkins_build_url Added as metadata to each OpenStack instance 29 | #@option options [String] :department Added as metadata to each OpenStack instance 30 | #@option options [String] :project Added as metadata to each OpenStack instance 31 | #@option options [Integer] :timeout The amount of time to attempt execution before quiting and exiting with failure 32 | def initialize(openstack_hosts, options) 33 | require 'fog/openstack' 34 | @options = options 35 | @logger = options[:logger] 36 | @hosts = openstack_hosts 37 | @vms = [] 38 | 39 | raise 'You must specify an Openstack API key (:openstack_api_key) for OpenStack instances!' unless @options[:openstack_api_key] 40 | raise 'You must specify an Openstack username (:openstack_username) for OpenStack instances!' unless @options[:openstack_username] 41 | raise 'You must specify an Openstack auth URL (:openstack_auth_url) for OpenStack instances!' unless @options[:openstack_auth_url] 42 | raise 'You must specify an Openstack network (:openstack_network) for OpenStack instances!' unless @options[:openstack_network] 43 | raise 'You must specify whether a floating IP (:openstack_floating_ip) should be used for OpenStack instances!' unless !@options[:openstack_floating_ip].nil? 44 | 45 | is_v3 = @options[:openstack_auth_url].include?('/v3/') 46 | raise 'You must specify an Openstack tenant (:openstack_tenant) for OpenStack instances!' if !is_v3 and !@options[:openstack_tenant] 47 | raise 'You must specify an Openstack project name (:openstack_project_name) or Openstack project id (:openstack_project_id) for OpenStack instances!' if is_v3 and (!@options[:openstack_project_name] and !@options[:openstack_project_id]) 48 | raise 'You must specify either Openstack project name (:openstack_project_name) or Openstack project id (:openstack_project_id) not both!' if is_v3 and (@options[:openstack_project_name] and @options[:openstack_project_id]) 49 | raise 'You may specify either Openstack user domain (:openstack_user_domain) or Openstack user domain id (:openstack_user_domain_id) not both!' if is_v3 and (@options[:openstack_user_domain] and @options[:openstack_user_domain_id]) 50 | raise 'You may specify either Openstack project domain (:openstack_project_domain) or Openstack project domain id (:openstack_project_domain_id) not both!' if is_v3 and (@options[:openstack_project_domain] and @options[:openstack_project_domain_id]) 51 | raise 'Invalid option specified: v3 API expects :openstack_project_name or :openstack_project_id, not :openstack_tenant for OpenStack instances!' if is_v3 and @options[:openstack_tenant] 52 | raise 'Invalid option specified: v2 API expects :openstack_tenant, not :openstack_project_name or :openstack_project_id for OpenStack instances!' if !is_v3 and (@options[:openstack_project_name] or @options[:openstack_project_id]) 53 | # Ensure that _id and non _id params are not mixed (due to bug in fog-openstack) 54 | raise 'You must not mix _id values non _id (name) values. Please use the same type for (:openstack_project_), (:openstack_user_domain) and (:openstack_project_domain)!' if is_v3 and (@options[:openstack_project_name] or @options[:openstack_user_domain] or @options[:openstack_project_domain]) and (@options[:openstack_project_id] or @options[:openstack_user_domain_id] or @options[:openstack_project_domain_id]) 55 | 56 | # Keystone version 3 changed the parameter names 57 | if !is_v3 58 | extra_credentials = {:openstack_tenant => @options[:openstack_tenant]} 59 | else 60 | if @options[:openstack_project_id] 61 | extra_credentials = {:openstack_project_id => @options[:openstack_project_id]} 62 | else 63 | extra_credentials = {:openstack_project_name => @options[:openstack_project_name]} 64 | end 65 | end 66 | 67 | # Common keystone authentication credentials 68 | @credentials = { 69 | :provider => :openstack, 70 | :openstack_auth_url => @options[:openstack_auth_url], 71 | :openstack_api_key => @options[:openstack_api_key], 72 | :openstack_username => @options[:openstack_username], 73 | :openstack_tenant => @options[:openstack_tenant], 74 | :openstack_region => @options[:openstack_region], 75 | }.merge(extra_credentials) 76 | 77 | # Keystone version 3 requires users and projects to be scoped 78 | if is_v3 79 | if @options[:openstack_user_domain_id] 80 | @credentials[:openstack_user_domain_id] = @options[:openstack_user_domain_id] 81 | else 82 | @credentials[:openstack_user_domain] = @options[:openstack_user_domain] || 'Default' 83 | end 84 | if @options[:openstack_project_domain_id] 85 | @credentials[:openstack_project_domain_id] = @options[:openstack_project_domain_id] 86 | else 87 | @credentials[:openstack_project_domain] = @options[:openstack_project_domain] || 'Default' 88 | end 89 | end 90 | 91 | @compute_client ||= Fog::Compute.new(@credentials) 92 | 93 | if not @compute_client 94 | raise "Unable to create OpenStack Compute instance (api key: #{@options[:openstack_api_key]}, username: #{@options[:openstack_username]}, auth_url: #{@options[:openstack_auth_url]}, tenant: #{@options[:openstack_tenant]}, project_name: #{@options[:openstack_project_name]})" 95 | end 96 | 97 | @network_client ||= Fog::Network.new(@credentials) 98 | 99 | if not @network_client 100 | raise "Unable to create OpenStack Network instance (api key: #{@options[:openstack_api_key]}, username: #{@options[:openstack_username]}, auth_url: #{@options[:openstack_auth_url]}, tenant: #{@options[:openstack_tenant]}, project_name: #{@options[:openstack_project_name]})" 101 | end 102 | 103 | # Validate openstack_volume_support setting value, reset to boolean if passed via ENV value string 104 | @options[:openstack_volume_support] = true if @options[:openstack_volume_support].to_s.match(/\btrue\b/i) 105 | @options[:openstack_volume_support] = false if @options[:openstack_volume_support].to_s.match(/\bfalse\b/i) 106 | [true,false].include? @options[:openstack_volume_support] or raise "Invalid openstack_volume_support setting, current: @options[:openstack_volume_support]" 107 | 108 | end 109 | 110 | #Provided a flavor name return the OpenStack id for that flavor 111 | #@param [String] f The flavor name 112 | #@return [String] Openstack id for provided flavor name 113 | def flavor f 114 | @logger.debug "OpenStack: Looking up flavor '#{f}'" 115 | @compute_client.flavors.find { |x| x.name == f } || raise("Couldn't find flavor: #{f}") 116 | end 117 | 118 | #Provided an image name return the OpenStack id for that image 119 | #@param [String] i The image name 120 | #@return [String] Openstack id for provided image name 121 | def image i 122 | @logger.debug "OpenStack: Looking up image '#{i}'" 123 | @compute_client.images.find { |x| x.name == i } || raise("Couldn't find image: #{i}") 124 | end 125 | 126 | #Provided a network name return the OpenStack id for that network 127 | #@param [String] n The network name 128 | #@return [String] Openstack id for provided network name 129 | def network n 130 | @logger.debug "OpenStack: Looking up network '#{n}'" 131 | @network_client.networks.find { |x| x.name == n } || raise("Couldn't find network: #{n}") 132 | end 133 | 134 | #Provided an array of security groups return that array if all 135 | #security groups are present 136 | #@param [Array] sgs The array of security group names 137 | #@return [Array] The array of security group names 138 | def security_groups sgs 139 | for sg in sgs 140 | @logger.debug "Openstack: Looking up security group '#{sg}'" 141 | @compute_client.security_groups.find { |x| x.name == sg } || raise("Couldn't find security group: #{sg}") 142 | sgs 143 | end 144 | end 145 | 146 | # Create a volume client on request 147 | # @return [Fog::OpenStack::Volume] OpenStack volume client 148 | def volume_client_create 149 | @volume_client ||= Fog::Volume.new(@credentials) 150 | unless @volume_client 151 | raise "Unable to create OpenStack Volume instance"\ 152 | " (api_key: #{@options[:openstack_api_key]},"\ 153 | " username: #{@options[:openstack_username]},"\ 154 | " auth_url: #{@options[:openstack_auth_url]},"\ 155 | " tenant: #{@options[:openstack_tenant]})" 156 | end 157 | end 158 | 159 | # Get a hash of volumes from the host 160 | def get_volumes host 161 | return host['volumes'] if host['volumes'] 162 | {} 163 | end 164 | 165 | # Get the API version 166 | def get_volume_api_version 167 | case @volume_client 168 | when Fog::Volume::OpenStack::V1 169 | 1 170 | else 171 | -1 172 | end 173 | end 174 | 175 | # Create and attach dynamic volumes 176 | # 177 | # Creates an array of volumes and attaches them to the current host. 178 | # The host bus type is determined by the image type, so by default 179 | # devices appear as /dev/vdb, /dev/vdc etc. Setting the glance 180 | # properties hw_disk_bus=scsi, hw_scsi_model=virtio-scsi will present 181 | # them as /dev/sdb, /dev/sdc (or 2:0:0:1, 2:0:0:2 in SCSI addresses) 182 | # 183 | # @param host [Hash] thet current host defined in the nodeset 184 | # @param vm [Fog::Compute::OpenStack::Server] the server to attach to 185 | def provision_storage host, vm 186 | volumes = get_volumes(host) 187 | if !volumes.empty? 188 | # Lazily create the volume client if needed 189 | volume_client_create 190 | volumes.keys.each_with_index do |volume, index| 191 | @logger.debug "Creating volume #{volume} for OpenStack host #{host.name}" 192 | 193 | # The node defintion file defines volume sizes in MB (due to precedent 194 | # with the vagrant virtualbox implementation) however OpenStack requires 195 | # this translating into GB 196 | openstack_size = volumes[volume]['size'].to_i / 1000 197 | 198 | # Set up the volume creation arguments 199 | args = { 200 | :size => openstack_size, 201 | :description => "Beaker volume: host=#{host.name} volume=#{volume}", 202 | } 203 | 204 | # Between version 1 and subsequent versions the API was updated to 205 | # rename 'display_name' to just 'name' for better consistency 206 | if get_volume_api_version == 1 207 | args[:display_name] = volume 208 | else 209 | args[:name] = volume 210 | end 211 | 212 | # Create the volume and wait for it to become available 213 | vol = @volume_client.volumes.create(**args) 214 | vol.wait_for { ready? } 215 | 216 | # Fog needs a device name to attach as, so invent one. The guest 217 | # doesn't pay any attention to this 218 | device = "/dev/vd#{('b'.ord + index).chr}" 219 | vm.attach_volume(vol.id, device) 220 | end 221 | end 222 | end 223 | 224 | # Detach and delete guest volumes 225 | # @param vm [Fog::Compute::OpenStack::Server] the server to detach from 226 | def cleanup_storage vm 227 | vm.volumes.each do |vol| 228 | @logger.debug "Deleting volume #{vol.name} for OpenStack host #{vm.name}" 229 | vm.detach_volume(vol.id) 230 | vol.wait_for { ready? } 231 | vol.destroy 232 | end 233 | end 234 | 235 | # Get a floating IP address to associate with the instance, try 236 | # to allocate a new one from the specified pool if none are available 237 | # 238 | # TODO(GiedriusS): convert to use @network_client. This API will be turned off 239 | # completely very soon. 240 | def get_floating_ip 241 | begin 242 | @logger.debug "Creating IP" 243 | ip = @compute_client.addresses.create 244 | rescue Fog::OpenStack::Compute::NotFound 245 | # If there are no more floating IP addresses, allocate a 246 | # new one and try again. 247 | @compute_client.allocate_address(@options[:floating_ip_pool]) 248 | ip = @compute_client.addresses.find { |ip| ip.instance_id.nil? } 249 | end 250 | ip 251 | end 252 | 253 | # Create new instances in OpenStack, depending on if create_in_parallel is true or not 254 | def provision 255 | if @options[:create_in_parallel] 256 | # Enable abort on exception for threads 257 | Thread.abort_on_exception = true 258 | @logger.notify "Provisioning OpenStack in parallel" 259 | provision_parallel 260 | else 261 | @logger.notify "Provisioning OpenStack sequentially" 262 | provision_sequential 263 | end 264 | hack_etc_hosts @hosts, @options 265 | end 266 | 267 | # Parallel creation wrapper 268 | def provision_parallel 269 | # Array to store threads 270 | threads = @hosts.map do |host| 271 | Thread.new do 272 | create_instance_resources(host) 273 | end 274 | end 275 | # Wait for all threads to finish 276 | threads.each(&:join) 277 | end 278 | 279 | # Sequential creation wrapper 280 | def provision_sequential 281 | @hosts.each do |host| 282 | create_instance_resources(host) 283 | end 284 | end 285 | 286 | # Create the actual instance resources 287 | def create_instance_resources(host) 288 | @logger.notify "Provisioning OpenStack" 289 | if @options[:openstack_floating_ip] 290 | ip = get_floating_ip 291 | hostname = ip.ip.gsub('.', '-') 292 | host[:vmhostname] = hostname + '.rfc1918.puppetlabs.net' 293 | else 294 | hostname = ('a'..'z').to_a.shuffle[0, 10].join 295 | host[:vmhostname] = hostname 296 | end 297 | 298 | create_or_associate_keypair(host, hostname) 299 | @logger.debug "Provisioning #{host.name} (#{host[:vmhostname]})" 300 | options = { 301 | :flavor_ref => flavor(host[:flavor]).id, 302 | :image_ref => image(host[:image]).id, 303 | :nics => [{'net_id' => network(@options[:openstack_network]).id}], 304 | :name => host[:vmhostname], 305 | :hostname => host[:vmhostname], 306 | :user_data => host[:user_data] || "#cloud-config\nmanage_etc_hosts: true\n", 307 | :key_name => host[:keyname], 308 | } 309 | options[:security_groups] = security_groups(@options[:security_group]) unless @options[:security_group].nil? 310 | vm = @compute_client.servers.create(options) 311 | 312 | # Wait for the new instance to start up 313 | try = 1 314 | attempts = @options[:timeout].to_i / SLEEPWAIT 315 | 316 | while try <= attempts 317 | begin 318 | vm.wait_for(5) { ready? } 319 | break 320 | rescue Fog::Errors::TimeoutError => e 321 | if try >= attempts 322 | @logger.debug "Failed to connect to new OpenStack instance #{host.name} (#{host[:vmhostname]})" 323 | raise e 324 | end 325 | @logger.debug "Timeout connecting to instance #{host.name} (#{host[:vmhostname]}), trying again..." 326 | end 327 | sleep SLEEPWAIT 328 | try += 1 329 | end 330 | 331 | if @options[:openstack_floating_ip] 332 | # Associate a public IP to the VM 333 | ip.server = vm 334 | host[:ip] = ip.ip 335 | else 336 | # Get the first address of the VM that was just created just like in the 337 | # OpenStack UI 338 | host[:ip] = vm.addresses.first[1][0]["addr"] 339 | end 340 | 341 | @logger.debug "OpenStack host #{host.name} (#{host[:vmhostname]}) assigned ip: #{host[:ip]}" 342 | 343 | # Set metadata 344 | vm.metadata.update({:jenkins_build_url => @options[:jenkins_build_url].to_s, 345 | :department => @options[:department].to_s, 346 | :project => @options[:project].to_s }) 347 | @vms << vm 348 | 349 | # Wait for the host to accept SSH logins 350 | host.wait_for_port(22) 351 | 352 | # Enable root if the user is not root 353 | enable_root(host) 354 | 355 | provision_storage(host, vm) if @options[:openstack_volume_support] 356 | @logger.notify "OpenStack Volume Support Disabled, can't provision volumes" if not @options[:openstack_volume_support] 357 | 358 | # Handle exceptions in the thread 359 | rescue => e 360 | @logger.error "Thread #{host} failed with error: #{e.message}" 361 | # Call cleanup function to delete orphaned hosts 362 | cleanup 363 | # Pass the error to the main thread to terminate all threads 364 | Thread.main.raise(e) 365 | # Terminate the current thread (to prevent hack_etc_hosts trying to run after error raised) 366 | Thread.kill(Thread.current) 367 | end 368 | 369 | # Destroy any OpenStack instances 370 | def cleanup 371 | @logger.notify "Cleaning up OpenStack" 372 | @vms.each do |vm| 373 | cleanup_storage(vm) if @options[:openstack_volume_support] 374 | @logger.debug "Release floating IPs for OpenStack host #{vm.name}" 375 | floating_ips = vm.all_addresses # fetch and release its floating IPs 376 | floating_ips.each do |address| 377 | @compute_client.disassociate_address(vm.id, address['ip']) 378 | @compute_client.release_address(address['id']) 379 | end 380 | @logger.debug "Destroying OpenStack host #{vm.name}" 381 | vm.destroy 382 | if @options[:openstack_keyname].nil? 383 | @logger.debug "Deleting random keypair" 384 | @compute_client.delete_key_pair vm.key_name 385 | end 386 | end 387 | end 388 | 389 | # Enables root access for a host when username is not root 390 | # This method ripped from the aws_sdk implementation and is probably wrong 391 | # because it iterates on a collection when there's no guarantee the collection 392 | # has all been brought up in openstack yet and will thus explode 393 | # @return [void] 394 | # @api private 395 | def enable_root_on_hosts 396 | @hosts.each do |host| 397 | enable_root(host) 398 | end 399 | end 400 | 401 | # Enable root on a single host (the current one presumably) but only 402 | # if the username isn't 'root' 403 | def enable_root(host) 404 | if host['user'] != 'root' 405 | copy_ssh_to_root(host, @options) 406 | enable_root_login(host, @options) 407 | host['user'] = 'root' 408 | host.close 409 | end 410 | end 411 | 412 | #Get key_name from options or generate a new rsa key and add it to 413 | #OpenStack keypairs 414 | # 415 | #@param [Host] host The OpenStack host to provision 416 | #@api private 417 | def create_or_associate_keypair(host, keyname) 418 | if @options[:openstack_keyname] 419 | @logger.debug "Adding optional key_name #{@options[:openstack_keyname]} to #{host.name} (#{host[:vmhostname]})" 420 | keyname = @options[:openstack_keyname] 421 | else 422 | @logger.debug "Generate a new rsa key" 423 | 424 | # There is apparently an error that can occur when generating RSA keys, probably 425 | # due to some timing issue, probably similar to the issue described here: 426 | # https://github.com/negativecode/vines/issues/34 427 | # In order to mitigate this error, we will simply try again up to three times, and 428 | # then fail if we continue to error out. 429 | begin 430 | retries ||= 0 431 | key = OpenSSL::PKey::RSA.new 2048 432 | rescue OpenSSL::PKey::RSAError => e 433 | retries += 1 434 | if retries > 2 435 | @logger.notify "error generating RSA key #{retries} times, exiting" 436 | raise e 437 | end 438 | retry 439 | end 440 | 441 | type = key.ssh_type 442 | data = [ key.to_blob ].pack('m0') 443 | @logger.debug "Creating Openstack keypair '#{keyname}' for public key '#{type} #{data}'" 444 | @compute_client.create_key_pair keyname, "#{type} #{data}" 445 | host['ssh'][:key_data] = [ key.to_pem ] 446 | end 447 | 448 | host[:keyname] = keyname 449 | end 450 | end 451 | end 452 | --------------------------------------------------------------------------------