├── .envrc ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── NOTICE ├── README.md ├── bin └── cf-openstack-validator ├── ci ├── assets │ └── config_renderer │ │ ├── populate.rb │ │ ├── populate_spec.rb │ │ └── render ├── docker │ ├── Dockerfile │ └── build-image.sh ├── pipeline.yml ├── ruby_scripts │ └── influxdb-post │ │ ├── Gemfile │ │ ├── Gemfile.lock │ │ ├── lib │ │ └── parser.rb │ │ ├── spec │ │ ├── assets │ │ │ ├── small-stats.log │ │ │ └── stats.log │ │ └── unit │ │ │ └── parser_spec.rb │ │ └── upload-stats.rb ├── tasks │ ├── bump-openstack-cpi.sh │ ├── bump-openstack-cpi.yml │ ├── cleanup.sh │ ├── cleanup.yml │ ├── test.sh │ ├── test.yml │ ├── utils.sh │ ├── validate.sh │ └── validate.yml └── terraform │ ├── terraform.tfvars.template │ ├── validator.tf │ └── vars.tf ├── docs ├── extensions.md ├── list_of_executed_tests.md └── openstack_configurations.md ├── extensions ├── auto_anti_affinity │ ├── README.md │ └── auto_anti_affinity_spec.rb ├── external_endpoints │ ├── README.md │ └── external_endpoints_spec.rb ├── flavors │ ├── README.md │ └── flavors_spec.rb ├── object_storage │ ├── README.md │ └── cloud_controller_blobstore_spec.rb └── quotas │ ├── README.md │ └── quotas_spec.rb ├── git-hooks └── pre-commit ├── lib ├── validator.rb └── validator │ ├── api.rb │ ├── api │ ├── configuration.rb │ ├── cpi_helpers.rb │ ├── fog_openstack.rb │ ├── helpers.rb │ ├── resource_tracker.rb │ └── validator_error.rb │ ├── cli.rb │ ├── cli │ ├── cf_openstack_validator.rb │ ├── context.rb │ ├── error_with_log_details.rb │ └── untar.rb │ ├── config_validator.rb │ ├── converter.rb │ ├── extensions.rb │ ├── external_cpi.rb │ ├── formatter.rb │ ├── instrumentor.rb │ ├── network_helper.rb │ ├── redactor.rb │ ├── resources.rb │ └── stats_log.rb ├── sample_extensions └── dummy_extension_spec.rb ├── scripts └── rubocop-staged ├── spec ├── assets │ ├── broken-cpi-release │ │ └── packages │ │ │ └── broken_package │ │ │ └── packaging │ ├── cpi-release.tgz │ ├── cpi-release │ │ └── packages │ │ │ └── dummy_package │ │ │ └── packaging │ ├── dummy.tgz │ ├── expected_cpi.json │ ├── expected_cpi_keystone_v2.json │ ├── validator.yml │ └── validator_keystone_v2.yml ├── integration │ └── cli_spec.rb └── unit │ ├── spec_helper.rb │ └── validator │ ├── api │ ├── configuration_spec.rb │ ├── fog_openstack_spec.rb │ └── resource_tracker_spec.rb │ ├── api_spec.rb │ ├── cli │ ├── cf_openstack_validator_spec.rb │ ├── context_spec.rb │ ├── error_with_log_details_spec.rb │ └── untar_spec.rb │ ├── config_validator_spec.rb │ ├── converter_spec.rb │ ├── extensions_spec.rb │ ├── external_cpi_spec.rb │ ├── formatter_spec.rb │ ├── instrumentor_spec.rb │ ├── network_helper_spec.rb │ ├── redactor_spec.rb │ └── resources_spec.rb ├── src └── specs │ ├── cpi_lifecycle_spec.rb │ ├── extension_spec.rb │ ├── openstack_configuration_spec.rb │ ├── spec_helper.rb │ └── support │ └── resource_tracker.rb ├── validate ├── validator.template.yml ├── vendor └── package │ ├── builder-3.2.3.gem │ ├── diff-lcs-1.3.gem │ ├── excon-0.60.0.gem │ ├── fog-core-1.45.0.gem │ ├── fog-json-1.0.2.gem │ ├── fog-openstack-0.1.24.gem │ ├── formatador-0.2.5.gem │ ├── ipaddress-0.8.3.gem │ ├── membrane-1.1.0.gem │ ├── mime-types-3.1.gem │ ├── mime-types-data-3.2016.0521.gem │ ├── multi_json-1.13.1.gem │ ├── rspec-3.5.0.gem │ ├── rspec-core-3.5.4.gem │ ├── rspec-expectations-3.5.0.gem │ ├── rspec-mocks-3.5.0.gem │ └── rspec-support-3.5.0.gem └── vendor_gems.sh /.envrc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f $PWD/.git/hooks/pre-commit ]; then 4 | ln -s $PWD/git-hooks/pre-commit $PWD/.git/hooks/pre-commit 5 | fi 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.rsa_id* 3 | cpi.json 4 | /*.yml 5 | !/validator.template.yml 6 | vendor/cache/ 7 | tags 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'fog-openstack', '~>1.0.2' 4 | gem 'excon', '~>0.49' 5 | gem 'rspec', '~> 3.5.0' 6 | gem 'membrane', '~> 1.1.0' 7 | gem 'mime-types', '~> 3.1' 8 | 9 | group :development, :test do 10 | gem 'rubocop-git' 11 | end 12 | 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.0) 5 | builder (3.2.3) 6 | diff-lcs (1.3) 7 | excon (0.60.0) 8 | fog-core (2.1.2) 9 | builder 10 | excon (~> 0.58) 11 | formatador (~> 0.2) 12 | mime-types 13 | fog-json (1.2.0) 14 | fog-core 15 | multi_json (~> 1.10) 16 | fog-openstack (1.0.6) 17 | fog-core (~> 2.1) 18 | fog-json (>= 1.0) 19 | ipaddress (>= 0.8) 20 | formatador (0.2.5) 21 | ipaddress (0.8.3) 22 | jaro_winkler (1.5.2) 23 | membrane (1.1.0) 24 | mime-types (3.1) 25 | mime-types-data (~> 3.2015) 26 | mime-types-data (3.2016.0521) 27 | multi_json (1.13.1) 28 | parallel (1.13.0) 29 | parser (2.6.0.0) 30 | ast (~> 2.4.0) 31 | powerpack (0.1.2) 32 | rainbow (3.0.0) 33 | rspec (3.5.0) 34 | rspec-core (~> 3.5.0) 35 | rspec-expectations (~> 3.5.0) 36 | rspec-mocks (~> 3.5.0) 37 | rspec-core (3.5.4) 38 | rspec-support (~> 3.5.0) 39 | rspec-expectations (3.5.0) 40 | diff-lcs (>= 1.2.0, < 2.0) 41 | rspec-support (~> 3.5.0) 42 | rspec-mocks (3.5.0) 43 | diff-lcs (>= 1.2.0, < 2.0) 44 | rspec-support (~> 3.5.0) 45 | rspec-support (3.5.0) 46 | rubocop (0.63.1) 47 | jaro_winkler (~> 1.5.1) 48 | parallel (~> 1.10) 49 | parser (>= 2.5, != 2.5.1.1) 50 | powerpack (~> 0.1) 51 | rainbow (>= 2.2.2, < 4.0) 52 | ruby-progressbar (~> 1.7) 53 | unicode-display_width (~> 1.4.0) 54 | rubocop-git (0.1.3) 55 | rubocop (>= 0.24.1) 56 | ruby-progressbar (1.10.0) 57 | unicode-display_width (1.4.1) 58 | 59 | PLATFORMS 60 | ruby 61 | 62 | DEPENDENCIES 63 | excon (~> 0.49) 64 | fog-openstack (~> 1.0.2) 65 | membrane (~> 1.1.0) 66 | mime-types (~> 3.1) 67 | rspec (~> 3.5.0) 68 | rubocop-git 69 | 70 | BUNDLED WITH 71 | 1.17.3 72 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | cf-openstack-validator 2 | 3 | Copyright (c) 2013-Present CloudFoundry.org Foundation, Inc. All Rights Reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CF OpenStack Validator 2 | 3 | Is your OpenStack installation ready to run BOSH and install Cloud Foundry? Run this validator to find out. 4 | 5 | * Roadmap: [Pivotal Tracker](https://www.pivotaltracker.com/epic/show/2156200) (click on Add/View Stories) 6 | * Slack: `#openstack` on cloudfoundry.slack.com ([get your invite here](https://slack.cloudfoundry.org/)) 7 | * [List of executed tests](docs/list_of_executed_tests.md) 8 | 9 | ## Prerequisites 10 | 11 | ### OpenStack 12 | 13 | * Keystone v2/v3 14 | * Create an OpenStack project/tenant 15 | * Create a user with access to the previously created project/tenant (ideally you don't want to run as admin) 16 | * Create a network 17 | * Connect the network with a router to your external network 18 | * Allocate a floating IP 19 | * Allow ssh access in the `default` security group 20 | * Create a key pair by executing 21 | ```bash 22 | $ ssh-keygen -t rsa -b 4096 -N "" -f cf-validator.rsa_id 23 | ``` 24 | * Upload the generated public key to OpenStack as `cf-validator` 25 | 26 | * A public image available in glance 27 | * If your OpenStack installation doesn't yet provide any image, you can upload a [CirrOS test image](http://docs.openstack.org/image-guide/obtain-images.html#cirros-test) 28 | 29 | ### Environment 30 | 31 | The validator runs on Mac and Linux. Please ensure that the following packages are installed on your system: 32 | 33 | **Linux Requirements** 34 | 35 | * ruby 2.4.x or newer 36 | * make 37 | * gcc 38 | * zlib1g-dev 39 | * libssl-dev 40 | * ssh 41 | 42 | **Mac Requirements** 43 | 44 | * xcode command line tools 45 | 46 | **Running the validator** 47 | 48 | The intended place to run the validator is a VM within your OpenStack. If you are executing the tests from a machine outside your OpenStack, you need to set `validator.use_external_ip` to `true`. 49 | 50 | ## Usage 51 | 52 | * `git clone https://github.com/cloudfoundry-incubator/cf-openstack-validator.git` 53 | * `cd cf-openstack-validator` 54 | * Copy the generated private key into the `cf-openstack-validator` folder. 55 | * Copy [validator.template.yml](validator.template.yml) to `validator.yml` and replace occurrences of `` with appropriate values (see prerequisites) 56 | * If using Keystone v3, ensure there are values for `domain` and `project` 57 | * If using Keystone v2, remove `domain` and `project`, and ensure there is a value for `tenant`. Also use the Keystone v2 URL as `auth_url`. 58 | ```bash 59 | $ cp validator.template.yml validator.yml 60 | ``` 61 | * Download a stemcell from [OpenStack stemcells bosh.io](https://bosh.io/stemcells/bosh-openstack-kvm-ubuntu-xenial-go_agent) 62 | ``` 63 | $ wget --content-disposition https://bosh.io/d/stemcells/bosh-openstack-kvm-ubuntu-xenial-go_agent 64 | ``` 65 | * Install dependencies 66 | ```bash 67 | $ gem install bundler 68 | $ bundle install 69 | ``` 70 | * Start validation 71 | ```bash 72 | $ ./validate --stemcell bosh-stemcell--openstack-kvm-ubuntu-xenial-go_agent.tgz --config validator.yml 73 | ``` 74 | 75 | ## Configure CPI used by validator 76 | 77 | Validator downloads CPI release from the URL specified in the validator configuration. You can override this by specifying the `--cpi-release` command line option with the path to a CPI release tarball. 78 | 79 | If you already have a CPI compiled, you can specify the path to the executable in the environment variable `OPENSTACK_CPI_BIN`. This is used when no CPI release is specified on the command line. It overrides the setting in the validator configuration file. 80 | 81 | ## Command line help 82 | 83 | To learn about available options run 84 | ```bash 85 | $ ./validate --help 86 | ``` 87 | 88 | ## Extensions 89 | 90 | You can extend the validator with custom tests. For a detailed description and examples, please have a look at the [extension documentation](./docs/extensions.md). 91 | 92 | This repository already contains some [extensions](./extensions). Each extension has its own documentation which can be found in the corresponding extension folder. 93 | 94 | ## Troubleshooting 95 | The validator does not run on your OpenStack? See [additional OpenStack related configuration options](docs/openstack_configurations.md) for possible solutions. 96 | -------------------------------------------------------------------------------- /bin/cf-openstack-validator: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'optparse' 4 | require_relative '../lib/validator/cli' 5 | 6 | cli_options = {} 7 | working_dir = "#{ENV['HOME']}/.cf-openstack-validator" 8 | required_options = {stemcell: '--stemcell', config_path: '--config'} 9 | option_parser = OptionParser.new do |parser| 10 | parser.banner = 'Usage: cf-openstack-validator [options]' 11 | 12 | parser.on('-h', '--help', 'Prints this help') do 13 | puts parser 14 | exit 15 | end 16 | 17 | parser.on('-r', '--cpi-release RELEASE', 'CPI release .tgz path. Latest version will be downloaded if not specified (optional)') do |release| 18 | cli_options[:cpi_release] = release 19 | end 20 | 21 | parser.on('-s', "#{required_options[:stemcell]} STEMCELL", 'Stemcell path') do |stemcell| 22 | cli_options[:stemcell] = stemcell 23 | end 24 | 25 | parser.on('-c', "#{required_options[:config_path]} CONFIG_FILE", 'Configuration YAML file path') do |config_path| 26 | cli_options[:config_path] = config_path 27 | end 28 | 29 | parser.on('-t', '--tag TAG', 'Run tests that match a specified RSpec tag. To run only CPI API tests use "cpi_api" as the tag (optional)') do |tag| 30 | cli_options[:tag] = tag 31 | end 32 | 33 | parser.on('-k', '--skip-cleanup', 'Skip cleanup of OpenStack resources (optional)') do 34 | cli_options[:skip_cleanup] = true 35 | end 36 | 37 | parser.on('-v', '--verbose', 'Print more output for failing tests (optional)') do 38 | cli_options[:verbose] = true 39 | end 40 | 41 | parser.on('-f', '--fail-fast', 'Stop execution after the first test failure (optional)') do 42 | cli_options[:fail_fast] = true 43 | end 44 | 45 | parser.on('-w', "--working-dir WORKING_DIR", 'Working directory for running the tests (optional)') do |workdir| 46 | cli_options[:working_dir] = workdir 47 | end 48 | end 49 | option_parser.parse! 50 | 51 | missing_required_options = required_options.keys.select do |required_option| 52 | !cli_options.include?(required_option) 53 | end 54 | 55 | unless missing_required_options.empty? 56 | STDERR.puts("Required options are missing: #{missing_required_options.map { |o| required_options[o] }.join(", ")}") 57 | puts option_parser 58 | exit 1 59 | end 60 | 61 | context = Validator::Cli::Context.new(cli_options) 62 | validator = Validator::Cli::CfOpenstackValidator.create(context) 63 | validator.run 64 | -------------------------------------------------------------------------------- /ci/assets/config_renderer/populate.rb: -------------------------------------------------------------------------------- 1 | def populate(working_directory, config, context) 2 | if check(context['AUTH_URL']) 3 | config['openstack']['auth_url'] = context['AUTH_URL'] 4 | end 5 | 6 | if check(context['USERNAME']) 7 | config['openstack']['username'] = context['USERNAME'] 8 | end 9 | 10 | if check(context['API_KEY']) 11 | config['openstack']['password'] = context['API_KEY'] 12 | end 13 | 14 | if check(context['DOMAIN']) 15 | config['openstack']['domain'] = context['DOMAIN'] 16 | end 17 | 18 | if check(context['PROJECT']) 19 | config['openstack']['project'] = context['PROJECT'] 20 | end 21 | 22 | if check(context['DEFAULT_KEY_NAME']) 23 | config['openstack']['default_key_name'] = context['DEFAULT_KEY_NAME'] 24 | end 25 | 26 | if check(context['BOOT_FROM_VOLUME']) 27 | config['openstack']['boot_from_volume'] = to_bool(context['BOOT_FROM_VOLUME']) 28 | end 29 | 30 | if check(context['CONFIG_DRIVE']) 31 | config['openstack']['config_drive'] = context['CONFIG_DRIVE'] 32 | end 33 | 34 | if check(context['WAIT_FOR_SWIFT']) 35 | config['openstack']['wait_for_swift'] = context['WAIT_FOR_SWIFT'] 36 | end 37 | 38 | if check(context['NETWORK_ID']) 39 | config['validator']['network_id'] = context['NETWORK_ID'] 40 | end 41 | 42 | if check(context['FLOATING_IP']) 43 | config['validator']['floating_ip'] = context['FLOATING_IP'] 44 | end 45 | 46 | if check(context['STATIC_IP']) 47 | config['validator']['static_ip'] = context['STATIC_IP'] 48 | end 49 | 50 | if check(context['NTP_SERVER']) 51 | config['validator']['ntp'] = to_array(context['NTP_SERVER']) 52 | end 53 | 54 | if check(context['INSTANCE_TYPE']) 55 | config['cloud_config']['vm_types'][0]['cloud_properties']['instance_type'] = context['INSTANCE_TYPE'] 56 | end 57 | 58 | if check(context['AVAILABILITY_ZONE']) 59 | config['cloud_config']['vm_types'][0]['cloud_properties']['availability_zone'] = context['AVAILABILITY_ZONE'] 60 | end 61 | 62 | if check(context['CA_CERT']) 63 | config['openstack']['connection_options']['ca_cert'] = context['CA_CERT'] 64 | end 65 | 66 | if to_bool(context['OBJECT_STORAGE']) && check(context['OBJECT_STORAGE_TEMP_URL_KEY']) 67 | config['extensions']['paths'].unshift('./extensions/object_storage/') 68 | config['extensions']['config']['object_storage'] = { 69 | 'openstack' => { 70 | 'openstack_temp_url_key' => context['OBJECT_STORAGE_TEMP_URL_KEY'] 71 | } 72 | } 73 | end 74 | 75 | config['openstack']['default_security_groups'] = to_array('validator') 76 | 77 | config['extensions']['config']['custom-config-key'] = 'custom-config-value' 78 | 79 | config['extensions']['paths'].unshift('./sample_extensions/') 80 | 81 | if check(context['EXPECTED_FLAVORS']) 82 | config['extensions']['paths'].unshift('./extensions/flavors') 83 | File.write(File.join(working_directory, 'flavors.yml'), context['EXPECTED_FLAVORS']) 84 | config['extensions']['config']['flavors'] = { 85 | 'expected_flavors' => File.join(working_directory, 'flavors.yml') 86 | } 87 | end 88 | 89 | if check(context['EXPECTED_QUOTAS']) 90 | config['extensions']['paths'].unshift('./extensions/quotas') 91 | File.write(File.join(working_directory, 'quotas.yml'), context['EXPECTED_QUOTAS']) 92 | config['extensions']['config']['quotas'] = { 93 | 'project_id' => context['PROJECT_ID'], 94 | 'expected_quotas' => File.join(working_directory, 'quotas.yml') 95 | } 96 | end 97 | 98 | if check(context['EXPECTED_ENDPOINTS']) 99 | config['extensions']['paths'].unshift('./extensions/external_endpoints') 100 | File.write(File.join(working_directory, 'endpoints.yml'), context['EXPECTED_ENDPOINTS']) 101 | config['extensions']['config']['external_endpoints'] = { 102 | 'expected_endpoints' => File.join(working_directory, 'endpoints.yml') 103 | } 104 | end 105 | 106 | if to_bool(context['AUTO_ANTI_AFFINITY']) && check(context['PROJECT_ID']) 107 | config['extensions']['paths'].unshift('./extensions/auto_anti_affinity') 108 | config['extensions']['config']['auto_anti_affinity'] = { 109 | 'project_id' => context['PROJECT_ID'], 110 | } 111 | end 112 | 113 | config['validator']['use_external_ip'] = true 114 | 115 | config 116 | end 117 | 118 | def check(value) 119 | !value.nil? && !value.empty? 120 | end 121 | 122 | def to_bool(value) 123 | value == 'true' 124 | end 125 | 126 | def to_array(value) 127 | value.split(',').map(&:strip) 128 | end 129 | -------------------------------------------------------------------------------- /ci/assets/config_renderer/populate_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'populate' 2 | require 'yaml' 3 | require 'tmpdir' 4 | 5 | describe 'populate' do 6 | 7 | let(:template) { 8 | { 9 | 'openstack' => { 10 | 'auth_url' => '', 11 | 'username' => '', 12 | 'password' => '', 13 | 'domain' => '', 14 | 'project' => '', 15 | 'default_key_name' => 'cf-validator', 16 | 'default_security_groups' => ['default'], 17 | 'boot_from_volume' => false, 18 | 'config_drive' => nil, 19 | 'wait_for_swift' => '', 20 | 'connection_options' => { 21 | 'ssl_verify_peer' => true, 22 | 'ca_cert' => nil 23 | } 24 | }, 25 | 'validator' => { 26 | 'network_id' => '', 27 | 'floating_ip' => '', 28 | 'static_ip' => '', 29 | 'private_key_path' => 'cf-validator.rsa_id', 30 | 'ntp' => ['0.pool.ntp.org', '1.pool.ntp.org'], 31 | 'releases' => [{ 32 | 'name' => 'bosh-openstack-cpi', 33 | 'url' => 'https://bosh.io/d/github.com/cloudfoundry/bosh-openstack-cpi-release?v=28', 34 | 'sha1' => '5fb85572f3a1bfebcccd6b0b75b0afea9f6df1ea' 35 | }] 36 | }, 37 | 'cloud_config' => { 38 | 'vm_types' => [{ 39 | 'name' => 'default', 40 | 'cloud_properties' => { 41 | 'instance_type' => '' 42 | } 43 | }] 44 | }, 45 | 'extensions' => { 46 | 'paths' => [], 47 | 'config' => {} 48 | } 49 | } 50 | } 51 | 52 | let(:context) { 53 | { 54 | 'AUTH_URL' => 'AUTH_URL', 55 | 'USERNAME' => 'USERNAME', 56 | 'API_KEY' => 'API_KEY', 57 | 'DOMAIN' => 'DOMAIN', 58 | 'PROJECT' => 'PROJECT', 59 | 'PROJECT_ID' => 'PROJECT_ID', 60 | 'DEFAULT_KEY_NAME' => 'DEFAULT_KEY_NAME', 61 | 'BOOT_FROM_VOLUME' => 'true', 62 | 'CONFIG_DRIVE' => 'CONFIG_DRIVE', 63 | 'WAIT_FOR_SWIFT' => 'WAIT_FOR_SWIFT', 64 | 'NETWORK_ID' => 'NETWORK_ID', 65 | 'FLOATING_IP' => 'FLOATING_IP', 66 | 'STATIC_IP' => 'STATIC_IP', 67 | 'PRIVATE_KEY' => 'PRIVATE_KEY', 68 | 'INSTANCE_TYPE' => 'INSTANCE_TYPE', 69 | 'NTP_SERVER' => 'NTP_SERVER1,NTP_SERVER2, NTP_SERVER3', 70 | 'CA_CERT' => 'CA_CERT', 71 | 'AVAILABILITY_ZONE' => 'AVAILABILITY_ZONE', 72 | 'EXPECTED_FLAVORS' => YAML.dump([ 73 | { 74 | 'name' => 'm1.medium', 75 | 'vcpus' => 2, 76 | 'ram' => 4096, 77 | 'disk' => 40 78 | } 79 | ]), 80 | 'EXPECTED_QUOTAS' => YAML.dump({ 81 | 'compute' => { 82 | 'ram' => 20 83 | } 84 | }), 85 | 'EXPECTED_ENDPOINTS' => YAML.dump([ 86 | { 87 | 'host' => 'host', 88 | 'port' => 20 89 | } 90 | ]), 91 | 'AUTO_ANTI_AFFINITY' => 'true' 92 | } 93 | } 94 | 95 | before(:each) do 96 | @tmpdir = Dir.mktmpdir 97 | end 98 | 99 | after(:each) do 100 | FileUtils.rm_rf(@tmpdir) 101 | end 102 | 103 | it 'returns' do 104 | populated_config = populate(@tmpdir, template, context) 105 | 106 | expect(populated_config).to eq({ 107 | 'openstack' => { 108 | 'auth_url' => 'AUTH_URL', 109 | 'username' => 'USERNAME', 110 | 'password' => 'API_KEY', 111 | 'domain' => 'DOMAIN', 112 | 'project' => 'PROJECT', 113 | 'default_key_name' => 'DEFAULT_KEY_NAME', 114 | 'default_security_groups' => ['validator'], 115 | 'boot_from_volume' => true, 116 | 'config_drive' => 'CONFIG_DRIVE', 117 | 'wait_for_swift' => 'WAIT_FOR_SWIFT', 118 | 'connection_options' => { 119 | 'ssl_verify_peer' => true, 120 | 'ca_cert' => 'CA_CERT' 121 | } 122 | }, 123 | 'validator' => { 124 | 'network_id' => 'NETWORK_ID', 125 | 'floating_ip' => 'FLOATING_IP', 126 | 'static_ip' => 'STATIC_IP', 127 | 'private_key_path' => 'cf-validator.rsa_id', 128 | 'ntp' => ['NTP_SERVER1', 'NTP_SERVER2', 'NTP_SERVER3'], 129 | 'releases' => [{ 130 | 'name' => 'bosh-openstack-cpi', 131 | 'url' => 'https://bosh.io/d/github.com/cloudfoundry/bosh-openstack-cpi-release?v=28', 132 | 'sha1' => '5fb85572f3a1bfebcccd6b0b75b0afea9f6df1ea' 133 | }], 134 | 'use_external_ip' => true 135 | }, 136 | 'cloud_config' => { 137 | 'vm_types' => [{ 138 | 'name' => 'default', 139 | 'cloud_properties' => { 140 | 'instance_type' => 'INSTANCE_TYPE', 141 | 'availability_zone' => 'AVAILABILITY_ZONE' 142 | } 143 | }] 144 | }, 145 | 'extensions' => { 146 | 'paths' => ['./extensions/auto_anti_affinity', './extensions/external_endpoints', './extensions/quotas', './extensions/flavors', './sample_extensions/'], 147 | 'config' => { 148 | 'custom-config-key' => 'custom-config-value', 149 | 'flavors' => { 150 | 'expected_flavors' => File.join(@tmpdir, 'flavors.yml') 151 | }, 152 | 'quotas' => { 153 | 'project_id' => 'PROJECT_ID', 154 | 'expected_quotas' => File.join(@tmpdir, 'quotas.yml') 155 | }, 156 | 'external_endpoints' => { 157 | 'expected_endpoints' => File.join(@tmpdir, 'endpoints.yml') 158 | }, 159 | 'auto_anti_affinity' => { 160 | 'project_id' => 'PROJECT_ID' 161 | } 162 | } 163 | } 164 | }) 165 | expect(File.read(File.join(@tmpdir, 'flavors.yml'))).to eq(context['EXPECTED_FLAVORS']) 166 | expect(File.read(File.join(@tmpdir, 'quotas.yml'))).to eq(context['EXPECTED_QUOTAS']) 167 | expect(File.read(File.join(@tmpdir, 'endpoints.yml'))).to eq(context['EXPECTED_ENDPOINTS']) 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /ci/assets/config_renderer/render: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'yaml' 3 | require_relative 'populate' 4 | require 'fileutils' 5 | 6 | validator_config = YAML.load_file(ARGV.shift) 7 | populated_validator_config = populate(FileUtils.pwd, validator_config, ENV) 8 | YAML.dump(populated_validator_config, STDOUT) -------------------------------------------------------------------------------- /ci/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | RUN apt-get -y update && apt-get install -y locales && locale-gen en_US.UTF-8 4 | RUN update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 5 | 6 | RUN apt-get update; apt-get -y upgrade; apt-get clean 7 | RUN apt-get install -y sudo ssh curl wget make libssl-dev jq; apt-get clean 8 | 9 | # install newest git CLI 10 | RUN apt-get install software-properties-common -y; \ 11 | add-apt-repository ppa:git-core/ppa -y; \ 12 | apt-get update; \ 13 | apt-get install apt-utils git -y 14 | 15 | 16 | RUN mkdir /tmp/ruby-install && \ 17 | cd /tmp && \ 18 | curl https://codeload.github.com/postmodern/ruby-install/tar.gz/v0.6.1 | tar -xz && \ 19 | cd /tmp/ruby-install-0.6.1 && \ 20 | make install && \ 21 | rm -rf /tmp/ruby-install 22 | 23 | RUN ruby-install --system ruby 2.4.6 24 | 25 | RUN ["/bin/bash", "-l", "-c", "gem install bundler --no-ri --no-rdoc"] 26 | 27 | RUN useradd -ms /bin/bash -G sudo validator-ci 28 | RUN echo "%sudo ALL = NOPASSWD: ALL" >> /etc/sudoers.d/sudo_group 29 | 30 | USER validator-ci 31 | -------------------------------------------------------------------------------- /ci/docker/build-image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | DOCKER_IMAGE=${DOCKER_IMAGE:-boshcpi/cf-openstack-validator-ci} 6 | 7 | docker login 8 | 9 | echo "Download latest docker image..." 10 | docker pull $DOCKER_IMAGE 11 | 12 | echo "Tagging 'latest' to 'previous'..." 13 | docker tag $DOCKER_IMAGE $DOCKER_IMAGE:previous 14 | 15 | echo "Building docker image..." 16 | docker build -t $DOCKER_IMAGE . 17 | 18 | echo "Pushing docker images to '$DOCKER_IMAGE'..." 19 | docker push $DOCKER_IMAGE 20 | -------------------------------------------------------------------------------- /ci/ruby_scripts/influxdb-post/Gemfile: -------------------------------------------------------------------------------- 1 | 2 | source 'https://rubygems.org' 3 | 4 | gem 'rspec' 5 | -------------------------------------------------------------------------------- /ci/ruby_scripts/influxdb-post/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | diff-lcs (1.3) 5 | rspec (3.6.0) 6 | rspec-core (~> 3.6.0) 7 | rspec-expectations (~> 3.6.0) 8 | rspec-mocks (~> 3.6.0) 9 | rspec-core (3.6.0) 10 | rspec-support (~> 3.6.0) 11 | rspec-expectations (3.6.0) 12 | diff-lcs (>= 1.2.0, < 2.0) 13 | rspec-support (~> 3.6.0) 14 | rspec-mocks (3.6.0) 15 | diff-lcs (>= 1.2.0, < 2.0) 16 | rspec-support (~> 3.6.0) 17 | rspec-support (3.6.0) 18 | 19 | PLATFORMS 20 | ruby 21 | 22 | DEPENDENCIES 23 | rspec 24 | 25 | BUNDLED WITH 26 | 1.15.1 27 | -------------------------------------------------------------------------------- /ci/ruby_scripts/influxdb-post/lib/parser.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | class Parser 4 | 5 | attr_accessor :data 6 | 7 | def initialize(file_path) 8 | @data = initialize_json(file_path) 9 | end 10 | 11 | def to_influx(args={}) 12 | raise ArgumentError unless args.class == Hash 13 | 14 | additional_tags = args.map { |key, value| ",#{key.to_s}=#{value}"}.join('') 15 | 16 | data.map do |line| 17 | "cpi_duration,method=#{line['request']['method']}#{additional_tags} value=#{line['duration']} #{Parser.current_time_in_influx_format}" 18 | end.join("\n") 19 | end 20 | 21 | def self.current_time_in_influx_format 22 | Time.now.getutc.to_f.to_i * 1000000000 23 | end 24 | 25 | private 26 | 27 | def initialize_json(file_path) 28 | content = [] 29 | File.read(file_path).each_line do |line| 30 | content << JSON.parse(line) 31 | end 32 | content 33 | end 34 | end -------------------------------------------------------------------------------- /ci/ruby_scripts/influxdb-post/spec/assets/small-stats.log: -------------------------------------------------------------------------------- 1 | {"request":{"method":"create_stemcell","arguments":["/image-path",{}],"context":{}},"duration":15.95} 2 | {"request":{"method":"something-else"},"duration":5.0} 3 | -------------------------------------------------------------------------------- /ci/ruby_scripts/influxdb-post/spec/assets/stats.log: -------------------------------------------------------------------------------- 1 | {"request":{"method":"create_stemcell","arguments":["/home/validator-ci/.cf-openstack-validator/stemcell/image",{"name":"bosh-openstack-kvm-ubuntu-trusty-go_agent","version":"3262.9","infrastructure":"openstack","hypervisor":"kvm","disk":3072,"disk_format":"qcow2","container_format":"bare","os_type":"linux","os_distro":"ubuntu","architecture":"x86_64","auto_disk_config":true}],"context":{"director_uuid":"validator","request_id":"333116"}},"duration":15.952004153979942} 2 | {"request":{"method":"create_vm","arguments":["agent-id","327aaed3-019d-478e-98d7-a4ffbf073b30",{"instance_type":"m1.small"},{"default":{"type":"dynamic","cloud_properties":{"net_id":"dea3ed37-54ac-440f-9580-3cfab97f4455"}}},[],{"bosh":{}}],"context":{"director_uuid":"validator","request_id":"440905"}},"duration":47.69668699498288} 3 | {"request":{"method":"has_vm","arguments":["585ae823-c5e7-4d2a-879e-6c41041d7c00"],"context":{"director_uuid":"validator","request_id":"442072"}},"duration":3.1757962880656123} 4 | {"request":{"method":"create_disk","arguments":[2048,{},"585ae823-c5e7-4d2a-879e-6c41041d7c00"],"context":{"director_uuid":"validator","request_id":"460415"}},"duration":9.76784961600788} 5 | {"request":{"method":"has_disk","arguments":["815d6238-4613-4746-9bfa-33a024485315"],"context":{"director_uuid":"validator","request_id":"291851"}},"duration":2.7761002259794623} 6 | {"request":{"method":"attach_disk","arguments":["585ae823-c5e7-4d2a-879e-6c41041d7c00","815d6238-4613-4746-9bfa-33a024485315"],"context":{"director_uuid":"validator","request_id":"711683"}},"duration":17.73711083002854} 7 | {"request":{"method":"detach_disk","arguments":["585ae823-c5e7-4d2a-879e-6c41041d7c00","815d6238-4613-4746-9bfa-33a024485315"],"context":{"director_uuid":"validator","request_id":"903172"}},"duration":10.681931533967145} 8 | {"request":{"method":"snapshot_disk","arguments":["815d6238-4613-4746-9bfa-33a024485315",{}],"context":{"director_uuid":"validator","request_id":"499043"}},"duration":8.268562825978734} 9 | {"request":{"method":"delete_snapshot","arguments":["683a7a3a-59ed-4664-b9ac-6d90d7a93a53"],"context":{"director_uuid":"validator","request_id":"748981"}},"duration":8.05206748296041} 10 | {"request":{"method":"delete_disk","arguments":["815d6238-4613-4746-9bfa-33a024485315"],"context":{"director_uuid":"validator","request_id":"975309"}},"duration":8.960609065950848} 11 | {"request":{"method":"delete_vm","arguments":["585ae823-c5e7-4d2a-879e-6c41041d7c00"],"context":{"director_uuid":"validator","request_id":"548106"}},"duration":10.001859965035692} 12 | {"request":{"method":"create_vm","arguments":["agent-id","327aaed3-019d-478e-98d7-a4ffbf073b30",{"instance_type":"m1.small"},{"default":{"type":"dynamic","cloud_properties":{"net_id":"dea3ed37-54ac-440f-9580-3cfab97f4455"}},"vip":{"type":"vip","ip":"192.168.131.142"}},[],{"bosh":{}}],"context":{"director_uuid":"validator","request_id":"565035"}},"duration":28.617990975035354} 13 | {"request":{"method":"create_vm","arguments":["agent-id","327aaed3-019d-478e-98d7-a4ffbf073b30",{"instance_type":"m1.small"},{"default":{"type":"dynamic","cloud_properties":{"net_id":"dea3ed37-54ac-440f-9580-3cfab97f4455"}}},[],{"bosh":{}}],"context":{"director_uuid":"validator","request_id":"547571"}},"duration":31.57161706604529} 14 | {"request":{"method":"create_vm","arguments":["agent-id","327aaed3-019d-478e-98d7-a4ffbf073b30",{"instance_type":"m1.small"},{"default":{"type":"manual","ip":"10.0.1.100","cloud_properties":{"net_id":"dea3ed37-54ac-440f-9580-3cfab97f4455"}}},[],{"bosh":{}}],"context":{"director_uuid":"validator","request_id":"523651"}},"duration":41.51552606606856} 15 | {"request":{"method":"create_disk","arguments":[30720,{}],"context":{"director_uuid":"validator","request_id":"162286"}},"duration":8.341544687049463} 16 | {"request":{"method":"delete_disk","arguments":["26c29394-37ce-45d3-8b50-2f2e155c2166"],"context":{"director_uuid":"validator","request_id":"471961"}},"duration":8.424850589944981} 17 | {"request":{"method":"delete_stemcell","arguments":["327aaed3-019d-478e-98d7-a4ffbf073b30"],"context":{"director_uuid":"validator","request_id":"164775"}},"duration":4.950443882960826} 18 | {"request":{"method":"create_stemcell","arguments":["/home/validator-ci/.cf-openstack-validator/stemcell/image",{"name":"bosh-openstack-kvm-ubuntu-trusty-go_agent","version":"3262.9","infrastructure":"openstack","hypervisor":"kvm","disk":3072,"disk_format":"qcow2","container_format":"bare","os_type":"linux","os_distro":"ubuntu","architecture":"x86_64","auto_disk_config":true}],"context":{"director_uuid":"validator","request_id":"167436"}},"duration":16.137015793006867} 19 | {"request":{"method":"create_vm","arguments":["agent-id","04549272-507c-439e-bc8d-7d928c85871f",{"instance_type":"m1.small"},{"default":{"type":"dynamic","cloud_properties":{"net_id":"dea3ed37-54ac-440f-9580-3cfab97f4455"}},"vip":{"type":"vip","ip":"192.168.131.142"}},[],{"bosh":{}}],"context":{"director_uuid":"validator","request_id":"210201"}},"duration":52.68185120099224} 20 | {"request":{"method":"delete_vm","arguments":["ab0fdca9-38f5-479b-8302-c1fddbff13dc"],"context":{"director_uuid":"validator","request_id":"602033"}},"duration":10.43807760195341} 21 | -------------------------------------------------------------------------------- /ci/ruby_scripts/influxdb-post/spec/unit/parser_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../lib/parser' 2 | 3 | describe Parser do 4 | subject { Parser.new(File.join('spec', 'assets', 'small-stats.log')) } 5 | 6 | describe '#new' do 7 | it 'stores the json data into a ruby hash' do 8 | expect(subject.data).to eq([{'request' => {'method' => 'create_stemcell', 'arguments' => ['/image-path',{}], 'context' =>{}}, 'duration' => 15.95},{'request' => {'method' => 'something-else'}, 'duration' => 5.0}]) 9 | end 10 | end 11 | 12 | describe '#to_influx' do 13 | before do 14 | allow(Parser).to receive(:current_time_in_influx_format).and_return(1434055562000000000) 15 | end 16 | 17 | it 'returns a string in influxdb format' do 18 | expected_string = "cpi_duration,method=create_stemcell value=15.95 1434055562000000000\n" + 19 | 'cpi_duration,method=something-else value=5.0 1434055562000000000' 20 | expect(subject.to_influx).to eq(expected_string) 21 | end 22 | 23 | context 'with a parameter' do 24 | it 'adds it as a tag' do 25 | expected_string = "cpi_duration,method=create_stemcell,landscape=my-landscape value=15.95 1434055562000000000\n" + 26 | 'cpi_duration,method=something-else,landscape=my-landscape value=5.0 1434055562000000000' 27 | expect(subject.to_influx(landscape: 'my-landscape')).to eq(expected_string) 28 | end 29 | 30 | it 'raises if the parameter is not a hash' do 31 | expect { 32 | subject.to_influx('hello') 33 | }.to raise_error(ArgumentError) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /ci/ruby_scripts/influxdb-post/upload-stats.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'json' 4 | require 'net/http' 5 | require_relative 'lib/parser' 6 | 7 | unless ENV['INFLUXDB_IP'] && ENV['INFLUXDB_PORT'] && ENV['PIPELINE_NAME'] && ENV['INFLUXDB_USER'] && ENV['INFLUXDB_PASSWORD'] 8 | puts 'Set up environment first. INFLUXDB_IP, INFLUXDB_PORT, INFLUXDB_USER, INFLUXDB_PASSWORD and PIPELINE_NAME need to be set.' 9 | exit 1 10 | end 11 | 12 | filename = ARGV[0] 13 | puts "Filename: #{filename}" 14 | unless File.readable?(filename) 15 | puts "usage: #{$0} stats.log" 16 | exit 1 17 | end 18 | 19 | data = Parser.new(filename).to_influx(landscape: ENV['PIPELINE_NAME']) 20 | puts data 21 | 22 | http = Net::HTTP.new(ENV['INFLUXDB_IP'], ENV['INFLUXDB_PORT']) 23 | http.use_ssl = true 24 | request = Net::HTTP::Post.new('/write?db=validator') 25 | request.basic_auth(ENV['INFLUXDB_USER'], ENV['INFLUXDB_PASSWORD']) 26 | request.body = data 27 | response = http.request(request) 28 | 29 | unless response.code == '204' 30 | puts 'Error sending data to InfluxDB' 31 | exit 1 32 | end 33 | -------------------------------------------------------------------------------- /ci/tasks/bump-openstack-cpi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | export TERM=xterm-256color 4 | 5 | URL=$(cat ./bosh-openstack-cpi-release/url) 6 | SHA=$(cat ./bosh-openstack-cpi-release/sha1) 7 | 8 | cp -r validator-src-in/. validator-src-cpi-bumped 9 | pushd validator-src-cpi-bumped 10 | 11 | sed -i'' "/bosh-openstack-cpi/,+3s|url: .*$|url: $URL|" validator.template.yml 12 | sed -i'' "/bosh-openstack-cpi/,+3s|sha1: .*$|sha1: $SHA|" validator.template.yml 13 | 14 | git diff --exit-code validator.template.yml || exit_code=$? 15 | if [ -v exit_code ]; then 16 | git config --global user.email cf-bosh-eng@pivotal.io 17 | git config --global user.name CI 18 | git add validator.template.yml 19 | git commit -m "auto-bump openstack CPI" 20 | else 21 | echo "No new bosh-openstack-cpi-release version found" 22 | fi 23 | -------------------------------------------------------------------------------- /ci/tasks/bump-openstack-cpi.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | 4 | image_resource: 5 | type: docker-image 6 | source: 7 | repository: boshcpi/openstack-cpi-release 8 | 9 | inputs: 10 | - name: validator-src-in 11 | - name: bosh-openstack-cpi-release 12 | 13 | outputs: 14 | - name: validator-src-cpi-bumped 15 | 16 | run: 17 | path: validator-src-in/ci/tasks/bump-openstack-cpi.sh 18 | -------------------------------------------------------------------------------- /ci/tasks/cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | source validator-src-in/ci/tasks/utils.sh 6 | 7 | init_openstack_cli_env 8 | 9 | if [[ $(openstack role list 2>&1) != *"HTTP 403"* ]]; then 10 | echo "Exiting the script, since it might be executed with admin rights!" 11 | exit 1 12 | fi 13 | 14 | OPENSTACK_PROJECT_ID=$(openstack project list --format json | jq --raw-output --arg project $BOSH_OPENSTACK_PROJECT '.[] | select(.Name == $project) | .ID') 15 | if [ -z "$OPENSTACK_PROJECT_ID" ]; then 16 | echo "Error: Failed to get OpenStack project" 17 | exit 1 18 | fi 19 | 20 | exit_code=0 21 | 22 | openstack_delete_entities() { 23 | local entity=${1:-} 24 | local list_args=${2:-} 25 | local delete_args=${3:-} 26 | id_list=$(openstack $entity list $list_args --format json | jq --raw-output '.[].ID') 27 | echo "Received list of all ${entity}s: ${id_list}" 28 | for id in $id_list 29 | do 30 | echo "Deleting $entity $id ..." 31 | openstack $entity delete $delete_args $id || exit_code=$? 32 | done 33 | } 34 | 35 | openstack_delete_ports() { 36 | for port in $(openstack port list --project=$OPENSTACK_PROJECT_ID -c ID -f value) 37 | do 38 | port_json=$(openstack port show --format json "$port") 39 | port_id_to_be_deleted=$(jq --raw-output '. | select( (.device_owner == "" or .device_owner == null or .device_owner =="compute:nova") and (.status == "DOWN") and (.device_id == "" or .device_id == null) ) | .id' <<< "$port_json") 40 | if [[ -n ${port_id_to_be_deleted} ]]; then 41 | echo "Deleting port ${port_id_to_be_deleted}" 42 | openstack port delete "${port_id_to_be_deleted}" || exit_code=$? 43 | fi 44 | done 45 | } 46 | # Destroy all images and snapshots and volumes 47 | 48 | echo "Starting cleanup for project: $BOSH_OPENSTACK_PROJECT" 49 | echo "openstack cli version:" 50 | openstack --version 51 | 52 | echo "Deleting servers #########################" 53 | openstack_delete_entities "server" 54 | echo "Deleting images #########################" 55 | openstack_delete_entities "image" "--private --limit 1000 --property owner=$OPENSTACK_PROJECT_ID" 56 | echo "Deleting snapshots #########################" 57 | openstack_delete_entities "volume snapshot" 58 | echo "Deleting volumes #########################" 59 | openstack_delete_entities "volume" 60 | echo "Deleting ports #########################" 61 | openstack_delete_ports 62 | 63 | if [ -d "${tmpdir:-}" ]; then 64 | echo "Deleting temp dir with cacert.pem" 65 | rm -rf "${tmpdir}" 66 | fi 67 | 68 | exit ${exit_code} 69 | -------------------------------------------------------------------------------- /ci/tasks/cleanup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | image_resource: 4 | type: docker-image 5 | source: 6 | repository: boshcpi/openstack-cpi-release 7 | inputs: 8 | - name: validator-src-in 9 | run: 10 | path: validator-src-in/ci/tasks/cleanup.sh 11 | params: 12 | BOSH_OPENSTACK_DOMAIN_NAME: replace-me 13 | BOSH_OPENSTACK_AUTH_URL: replace-me 14 | BOSH_OPENSTACK_USERNAME: replace-me 15 | BOSH_OPENSTACK_API_KEY: replace-me 16 | BOSH_OPENSTACK_PROJECT: replace-me 17 | BOSH_OPENSTACK_CA_CERT: replace-me 18 | BOSH_OPENSTACK_INTERFACE: replace-me 19 | -------------------------------------------------------------------------------- /ci/tasks/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | pushd validator-src-in 5 | bundle install 6 | bundle exec rspec ci/assets/config_renderer/ 7 | bundle exec rspec spec/ 8 | -------------------------------------------------------------------------------- /ci/tasks/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | 4 | image_resource: 5 | type: docker-image 6 | source: 7 | repository: boshcpi/openstack-cpi-release 8 | 9 | inputs: 10 | - name: validator-src-in 11 | 12 | run: 13 | path: validator-src-in/ci/tasks/test.sh 14 | -------------------------------------------------------------------------------- /ci/tasks/utils.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | optional_value() { 5 | local name=$1 6 | local value=$(eval echo '$'$name) 7 | if [ "$value" == 'replace-me' ]; then 8 | echo "unsetting optional environment variable $name" 9 | unset $name 10 | fi 11 | } 12 | 13 | init_openstack_cli_env(){ 14 | : ${BOSH_OPENSTACK_AUTH_URL:?} 15 | : ${BOSH_OPENSTACK_USERNAME:?} 16 | : ${BOSH_OPENSTACK_API_KEY:?} 17 | : ${BOSH_OPENSTACK_PROJECT:?} 18 | : ${BOSH_OPENSTACK_DOMAIN_NAME:?} 19 | : ${BOSH_OPENSTACK_INTERFACE:?} 20 | optional_value BOSH_OPENSTACK_CA_CERT 21 | 22 | export OS_DEFAULT_DOMAIN=$BOSH_OPENSTACK_DOMAIN_NAME 23 | export OS_AUTH_URL=$BOSH_OPENSTACK_AUTH_URL 24 | export OS_USERNAME=$BOSH_OPENSTACK_USERNAME 25 | export OS_PASSWORD=$BOSH_OPENSTACK_API_KEY 26 | export OS_PROJECT_NAME=$BOSH_OPENSTACK_PROJECT 27 | export OS_DOMAIN_NAME=$BOSH_OPENSTACK_DOMAIN_NAME 28 | export OS_IDENTITY_API_VERSION=3 29 | export OS_INTERFACE=$BOSH_OPENSTACK_INTERFACE 30 | 31 | if [ -n "$BOSH_OPENSTACK_CA_CERT" ]; then 32 | tmpdir=$(mktemp -dt "$(basename $0).XXXXXXXXXX") 33 | cacert="$tmpdir/cacert.pem" 34 | echo "Writing cacert.pem to $cacert" 35 | echo "$BOSH_OPENSTACK_CA_CERT" > $cacert 36 | export OS_CACERT=$cacert 37 | fi 38 | 39 | } 40 | -------------------------------------------------------------------------------- /ci/tasks/validate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | : ${AUTH_URL:?} 5 | : ${USERNAME:?} 6 | : ${API_KEY:?} 7 | : ${DOMAIN:?} 8 | : ${PROJECT:?} 9 | : ${PROJECT_ID:?} 10 | : ${DEFAULT_KEY_NAME:?} 11 | : ${STATIC_IP:?} 12 | : ${PRIVATE_KEY:?} 13 | : ${INSTANCE_TYPE:?} 14 | : ${NTP_SERVER:?} 15 | : ${CA_CERT:-""} 16 | : ${AVAILABILITY_ZONE:-""} 17 | : ${OBJECT_STORAGE:?} 18 | : ${EXPECTED_FLAVORS:?} 19 | : ${EXPECTED_QUOTAS:?} 20 | : ${EXPECTED_ENDPOINTS:?} 21 | : ${AUTO_ANTI_AFFINITY:-""} 22 | 23 | # terraform output variables 24 | metadata=terraform-validator/metadata 25 | export NETWORK_ID=$(cat ${metadata} | jq --raw-output ".validator_net_id") 26 | export FLOATING_IP=$(cat ${metadata} | jq --raw-output ".validator_floating_ip") 27 | 28 | STEMCELL="$(pwd)/$(ls stemcell/*.tgz)" 29 | 30 | report_performance_stats(){ 31 | echo 'Stats:' 32 | cat ~/.cf-openstack-validator/logs/stats.log 33 | if [ ! -z ${INFLUXDB_IP:-} ] && [ ! -z ${INFLUXDB_PORT:-} ] && [ ! -z ${INFLUXDB_USER:-} ] && [ ! -z ${INFLUXDB_PASSWORD:-} ]; then 34 | echo 'Sending stats to performance database' 35 | ruby ci/ruby_scripts/influxdb-post/upload-stats.rb ~/.cf-openstack-validator/logs/stats.log 36 | fi 37 | } 38 | 39 | # Copy to user's home, because we don't have write permissions on the source directory 40 | cp -r validator-src-cpi-bumped ~ 41 | 42 | pushd ~/validator-src-cpi-bumped 43 | 44 | echo "${PRIVATE_KEY}" > cf-validator.rsa_id 45 | chmod 400 cf-validator.rsa_id 46 | 47 | ci/assets/config_renderer/render validator.template.yml > validator.yml 48 | cat validator.yml 49 | 50 | bundle install --path .bundle 51 | 52 | ./validate -s "$STEMCELL" -c validator.yml 53 | 54 | #report_performance_stats 55 | 56 | CONFIG_DRIVE='disk' ci/assets/config_renderer/render validator.template.yml > validator.yml 57 | cat validator.yml 58 | 59 | ./validate -s "$STEMCELL" -c validator.yml 60 | 61 | #report_performance_stats 62 | -------------------------------------------------------------------------------- /ci/tasks/validate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | 4 | image_resource: 5 | type: docker-image 6 | source: 7 | repository: boshcpi/cf-openstack-validator-ci 8 | tag: 'latest' 9 | 10 | inputs: 11 | - name: stemcell 12 | - name: validator-src-cpi-bumped 13 | - name: terraform-validator 14 | 15 | params: 16 | AUTH_URL: "" 17 | USERNAME: "" 18 | API_KEY: "" 19 | DOMAIN: "" 20 | PROJECT: "" 21 | PROJECT_ID: "" 22 | DEFAULT_KEY_NAME: "" 23 | NETWORK_ID: "" 24 | FLOATING_IP: "" 25 | STATIC_IP: "" 26 | PRIVATE_KEY: "" 27 | INSTANCE_TYPE: "" 28 | NTP_SERVER: "" 29 | CA_CERT: "" 30 | AVAILABILITY_ZONE: "" 31 | OBJECT_STORAGE: "" 32 | EXPECTED_FLAVORS: "" 33 | EXPECTED_QUOTAS: "" 34 | EXPECTED_ENDPOINTS: "" 35 | INFLUXDB_IP: "" 36 | INFLUXDB_PORT: "" 37 | INFLUXDB_USER: "" 38 | INFLUXDB_PASSWORD: "" 39 | PIPELINE_NAME: "" 40 | AUTO_ANTI_AFFINITY: "" 41 | 42 | run: 43 | path: validator-src-cpi-bumped/ci/tasks/validate.sh 44 | -------------------------------------------------------------------------------- /ci/terraform/terraform.tfvars.template: -------------------------------------------------------------------------------- 1 | # copy to terraform.tfvars and replace params 2 | 3 | auth_url = "" 4 | user_name = "" 5 | password = "" 6 | domain_name = "" 7 | tenant_name = "" 8 | 9 | 10 | ext_net_name = "" 11 | ext_net_id = "" 12 | region_name = "" 13 | availability_zone = "" 14 | openstack_default_key_public_key = "" 15 | dns_nameservers = [] 16 | -------------------------------------------------------------------------------- /ci/terraform/validator.tf: -------------------------------------------------------------------------------- 1 | # provider configuration 2 | 3 | provider "openstack" { 4 | auth_url = var.auth_url 5 | user_name = var.user_name 6 | password = var.password 7 | tenant_name = var.tenant_name 8 | domain_name = var.domain_name 9 | insecure = var.insecure 10 | cacert_file = var.cacert_file 11 | } 12 | 13 | # key pairs 14 | 15 | resource "openstack_compute_keypair_v2" "openstack_default_key_name" { 16 | region = var.region_name 17 | name = "${var.name_prefix}${var.tenant_name}-validator" 18 | public_key = var.openstack_default_key_public_key 19 | } 20 | 21 | # networks 22 | 23 | resource "openstack_networking_network_v2" "validator_net" { 24 | region = var.region_name 25 | name = "${var.name_prefix}validator" 26 | admin_state_up = "true" 27 | } 28 | 29 | resource "openstack_networking_subnet_v2" "validator_sub" { 30 | region = var.region_name 31 | network_id = openstack_networking_network_v2.validator_net.id 32 | cidr = var.net_cidr 33 | ip_version = 4 34 | name = "${var.name_prefix}validator_sub" 35 | allocation_pool { 36 | start = var.allocation_pool_start 37 | end = var.allocation_pool_end 38 | } 39 | gateway_ip = var.gateway_ip 40 | enable_dhcp = "true" 41 | dns_nameservers = var.dns_nameservers 42 | } 43 | 44 | # router 45 | 46 | resource "openstack_networking_router_v2" "default_router" { 47 | region = var.region_name 48 | name = "${var.name_prefix}validator-router" 49 | admin_state_up = "true" 50 | external_network_id = var.ext_net_id 51 | } 52 | 53 | resource "openstack_networking_router_interface_v2" "validator_port" { 54 | region = var.region_name 55 | router_id = openstack_networking_router_v2.default_router.id 56 | subnet_id = openstack_networking_subnet_v2.validator_sub.id 57 | } 58 | 59 | # floating ips 60 | 61 | resource "openstack_compute_floatingip_v2" "validator_floating_ip" { 62 | region = var.region_name 63 | pool = var.ext_net_name 64 | } 65 | 66 | resource "openstack_networking_secgroup_v2" "validator_secgroup" { 67 | region = var.region_name 68 | name = "${var.name_prefix}validator" 69 | description = "validator security group" 70 | } 71 | 72 | resource "openstack_networking_secgroup_rule_v2" "secgroup_rule_1" { 73 | direction = "ingress" 74 | ethertype = "IPv4" 75 | protocol = "tcp" 76 | remote_group_id = openstack_networking_secgroup_v2.validator_secgroup.id 77 | security_group_id = openstack_networking_secgroup_v2.validator_secgroup.id 78 | } 79 | 80 | resource "openstack_networking_secgroup_rule_v2" "secgroup_rule_2" { 81 | direction = "ingress" 82 | ethertype = "IPv4" 83 | protocol = "icmp" 84 | remote_group_id = openstack_networking_secgroup_v2.validator_secgroup.id 85 | security_group_id = openstack_networking_secgroup_v2.validator_secgroup.id 86 | } 87 | 88 | resource "openstack_networking_secgroup_rule_v2" "secgroup_rule_3" { 89 | direction = "ingress" 90 | ethertype = "IPv4" 91 | protocol = "tcp" 92 | port_range_min = 22 93 | port_range_max = 22 94 | remote_ip_prefix = "0.0.0.0/0" 95 | security_group_id = openstack_networking_secgroup_v2.validator_secgroup.id 96 | } 97 | 98 | -------------------------------------------------------------------------------- /ci/terraform/vars.tf: -------------------------------------------------------------------------------- 1 | # input variables 2 | 3 | # access coordinates/credentials 4 | variable "auth_url" { 5 | description = "Authentication endpoint URL for OpenStack provider (only scheme+host+port, but without path!)" 6 | } 7 | 8 | variable "domain_name" { 9 | description = "OpenStack domain name" 10 | } 11 | 12 | variable "user_name" { 13 | description = "OpenStack pipeline technical user name" 14 | } 15 | 16 | variable "password" { 17 | description = "OpenStack user password" 18 | } 19 | 20 | variable "tenant_name" { 21 | description = "OpenStack project/tenant name" 22 | } 23 | 24 | variable "insecure" { 25 | default = "false" 26 | description = "SSL certificate validation" 27 | } 28 | 29 | variable "name_prefix" { 30 | default = "" 31 | description = "Prefix for names of infrastucture components" 32 | } 33 | 34 | variable "net_cidr" { 35 | default = "10.0.1.0/24" 36 | description = "CIDR of validator subnet" 37 | } 38 | 39 | variable "allocation_pool_start" { 40 | default = "10.0.1.200" 41 | description = "Allocation pool start" 42 | } 43 | 44 | variable "allocation_pool_end" { 45 | default = "10.0.1.254" 46 | description = "Allocation pool end" 47 | } 48 | 49 | variable "gateway_ip" { 50 | default = "10.0.1.1" 51 | description = "Default gateway" 52 | } 53 | 54 | variable "cacert_file" { 55 | default = "" 56 | description = "CA File" 57 | } 58 | 59 | variable "dns_nameservers" { 60 | description = "list of DNS server IPs" 61 | type = list(string) 62 | } 63 | 64 | # external network coordinates 65 | variable "ext_net_name" { 66 | description = "OpenStack external network name to register floating IP" 67 | } 68 | 69 | variable "ext_net_id" { 70 | description = "OpenStack external network id to create router interface port" 71 | } 72 | 73 | # region/zone coordinates 74 | variable "region_name" { 75 | description = "OpenStack region name" 76 | } 77 | 78 | variable "availability_zone" { 79 | description = "OpenStack availability zone name" 80 | } 81 | 82 | variable "openstack_default_key_public_key" { 83 | } 84 | 85 | output "validator_net_id" { 86 | value = openstack_networking_network_v2.validator_net.id 87 | } 88 | 89 | output "validator_floating_ip" { 90 | value = openstack_compute_floatingip_v2.validator_floating_ip.address 91 | } 92 | 93 | output "openstack_default_key_name" { 94 | value = openstack_compute_keypair_v2.openstack_default_key_name.name 95 | } 96 | 97 | output "security_group" { 98 | value = openstack_networking_secgroup_v2.validator_secgroup.name 99 | } 100 | -------------------------------------------------------------------------------- /docs/list_of_executed_tests.md: -------------------------------------------------------------------------------- 1 | # List of executed tests 2 | 3 | ### Testing the CPI API 4 | * Upload stemcell 5 | * Create VM 6 | * Find VM 7 | * Set VM metadata tags 8 | * Create disk 9 | * Find disk 10 | * Attach disk to VM 11 | * Detach disk from VM 12 | * Create disk snapshot 13 | * Delete disk snapshot 14 | * Delete disk 15 | * Delete VM 16 | * Delete stemcell 17 | 18 | ### Other OpenStack tests 19 | * Check API rate limit 20 | * Check required versions of OpenStack projects 21 | * CPI requires API version 1 for glance and cinder 22 | * Security group settings 23 | * Check if security group rules allow necessary incoming/outgoing ports 24 | * Outbound internet access from a VM 25 | * Store and retrieve user-data 26 | * from the HTTP metadata service 27 | * from config-drive 28 | * Attach a floating IP 29 | * Access a VM over ssh from the outside 30 | * Timeservers can be reached 31 | * Static networking is possible 32 | * Access one VM from another VM 33 | * Create a large volume 34 | 35 | ### Further reading: 36 | * [Specification of the CPI API v1](http://bosh.io/docs/cpi-api-v1.html) 37 | * [Detailed list of OpenStack API calls of the OpenStack CPI](https://github.com/cloudfoundry/bosh-openstack-cpi-release/blob/master/docs/openstack-api-calls.md) 38 | -------------------------------------------------------------------------------- /docs/openstack_configurations.md: -------------------------------------------------------------------------------- 1 | # Additional OpenStack related configuration options 2 | 3 | Depending on you OpenStack configuration, you might need additional configuration options for the validator to run. Note that all of these settings need to be done similarly for BOSH once you deploy a Director or Cloud Foundry. 4 | 5 | ## Using self-signed certificates 6 | 7 | You can add your certificate chain in the property `openstack.connection_options.ca_cert`. Read more on the topic at [bosh.io](http://bosh.io/docs/openstack-self-signed-endpoints.html). 8 | 9 | ## Using boot disks from block storage instead of hypervisor-local storage 10 | 11 | By default, hypervisor-local storage is used for a VMs boot disk. If your OpenStack setup requires you to use disks from block storage instead, you can set `openstack.boot_from_volume: true`. 12 | 13 | ## Using custom ephemeral disk size 14 | 15 | By default, the root disk size and ephemeral disk size of the OpenStack flavor determine the ephemeral disk size available on a BOSH stemcell. If you want to specify your disk size independent of the flavor's disk sizes, you need to enable `openstack.boot_from_volume: true` as described above and configure a different root disk size in `cloud_config.vm_types.['default'].cloud_properties.root_disk.size`. We recommend a minimum size of 10GB. 16 | You can calculate the available ephemeral disk size as `root_disk.size - 3GB - flavor_RAM`. 17 | 18 | ## Using flavors with 0 root disk size 19 | 20 | See above. 21 | 22 | ## Using internal ntp servers 23 | 24 | By default, the validator uses an external ntp server from pool.ntp.org. If your OpenStack installation cannot access external ntp servers, e.g. firewall restrictions, you need to specify an internal ntp server in the property `validator.ntp`. Working time synchronization is necessary for many security concepts, such as token expiration time. 25 | 26 | ## Using config-drive instead of metadata service 27 | 28 | By default, the VMs created try to receive data from OpenStack's HTTP metadata service. If your OpenStack installation doesn't provide medata and userdata over HTTP, but requires you to a config-drive instead, you need to specify this in the property `openstack.config_drive: cdrom` 29 | 30 | ## Using nova-networking 31 | 32 | By default, the OpenStack uses neutron for networking since version 28. If you require nova-networking, switch on `openstack.use_nova_networking: true` to turn on compatibility mode in the CPI. Be aware that future OpenStack versions will remove this API at some point. See [documentation on bosh.io](http://bosh.io/docs/openstack-nova-networking.html) for additional information. 33 | 34 | ## Using a non-default region 35 | 36 | By default, OpenStack uses one default region. If you are using a different one, you can add it in the property `openstack.region`. 37 | 38 | ## Ignoring server availability zones 39 | 40 | BOSH will attempt to create volumes in a Cinder availability zone with the same name as the availability zone of the server that the volume is being created for. If your Cinder and Nova availability zones do not have matching names, this will lead to volume creation failures. 41 | 42 | The OpenStack Validator will fail the `Your OpenStack using the CPI can create a disk in same AZ as VM` spec with an error message similar to `OpenStack API Bad Request (Invalid input received: Availability zone 'nova-zone' is invalid)`. 43 | 44 | BOSH can be told to ignore server availability zones by setting the `ignore_server_availability_zone` CPI property to `true`; the OpenStack Validator will pass this setting through if configured in `validator.yml` as `openstack.ignore_server_availability_zone: true`. -------------------------------------------------------------------------------- /extensions/auto_anti_affinity/README.md: -------------------------------------------------------------------------------- 1 | # Auto-Anti-Affinity Extension 2 | 3 | This extension verifies that your OpenStack supports soft-anti-affinity policy for server groups. 4 | 5 | ## Configuration 6 | 7 | You need to specify the ID of the project you run the validator against. 8 | 9 | Add the extension to your `validator.yml`: 10 | 11 | ```yaml 12 | extensions: 13 | paths: [./extensions/auto_anti_affinity] 14 | config: 15 | auto_anti_affinity: 16 | project_id: 17 | ``` 18 | -------------------------------------------------------------------------------- /extensions/auto_anti_affinity/auto_anti_affinity_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Auto-anti-affinity' do 2 | 3 | before(:all) do 4 | @resource_tracker = Validator::Api::ResourceTracker.create 5 | end 6 | 7 | let(:project_id) { Validator::Api.configuration.extensions['auto_anti_affinity']['project_id'] } 8 | 9 | let(:compute_quota) do 10 | Validator::Api::FogOpenStack.compute.get_quota(project_id).body['quota_set'] 11 | end 12 | 13 | it 'quota is unlimited for server groups' do 14 | quota_server_groups = compute_quota['server_groups'] 15 | expect(quota_server_groups).to eq(-1), "Quota for server_groups should be '-1' but is '#{quota_server_groups}'" 16 | end 17 | 18 | it 'quota is unlimited for server group members' do 19 | quota_members = compute_quota['server_group_members'] 20 | expect(quota_members).to eq(-1), "Quota for server_groups should be '-1' but is '#{quota_members}'" 21 | end 22 | 23 | it "can create a server group with 'soft-anti-affinity'", cpi_api: true do 24 | begin 25 | server_group = @resource_tracker.produce(:server_groups) { 26 | Validator::Api::FogOpenStack.compute.server_groups.create('validator-test', 'soft-anti-affinity').id 27 | } 28 | rescue Excon::Errors::BadRequest => error 29 | if error.message.match(/Invalid input.*'soft-anti-affinity' is not one of/) 30 | message = "The server group policy 'soft-anti-affinity' is not supported by your OpenStack. The feature is available with Nova Microversion 2.15 (OpenStack Mitaka and higher)." 31 | fail(message) 32 | else 33 | raise error 34 | end 35 | end 36 | 37 | expect(server_group).to be 38 | end 39 | end -------------------------------------------------------------------------------- /extensions/external_endpoints/README.md: -------------------------------------------------------------------------------- 1 | # External Endpoints Extension 2 | 3 | This extension verifies that the given endpoints are reachable. 4 | 5 | ## Configuration 6 | 7 | Create an `endpoints.yml` and include each endpoint you want to check for. 8 | 9 | ```yaml 10 | - host: github.com 11 | port: 80 12 | 13 | - host: bosh.io 14 | port: 80 15 | ``` 16 | 17 | Add the extension to your `validator.yml`: 18 | 19 | ```yaml 20 | extensions: 21 | paths: [./extensions/external_endpoints] 22 | config: 23 | external_endpoints: 24 | expected_endpoints: 25 | ``` 26 | -------------------------------------------------------------------------------- /extensions/external_endpoints/external_endpoints_spec.rb: -------------------------------------------------------------------------------- 1 | include Validator::Api::CpiHelpers 2 | 3 | describe 'test access to external endpoints' do 4 | 5 | before(:all) do 6 | @compute = Validator::Api::FogOpenStack.compute 7 | @config = Validator::Api.configuration 8 | @resource_tracker = Validator::Api::ResourceTracker.create 9 | 10 | @stemcell_path = stemcell_path 11 | @cpi = cpi(cpi_path, log_path) 12 | end 13 | 14 | config = Validator::Api.configuration.extensions 15 | endpoints = YAML.load_file( config['external_endpoints']['expected_endpoints']) || [] 16 | 17 | it 'prepare image' do 18 | stemcell_manifest = YAML.load_file(File.join(@stemcell_path, 'stemcell.MF')) 19 | stemcell_cid = with_cpi('Stemcell could not be uploaded') { 20 | @resource_tracker.produce(:images, provide_as: :stemcell_cid) { 21 | @cpi.create_stemcell(File.join(@stemcell_path, 'image'), stemcell_manifest['cloud_properties']) 22 | } 23 | } 24 | expect(stemcell_cid).to be 25 | end 26 | 27 | it 'prepare VM with image and floating IP' do 28 | stemcell_cid = @resource_tracker.consumes(:stemcell_cid, 'No stemcell to create VM from') 29 | 30 | vm_cid = with_cpi('Floating IP could not be attached.') { 31 | @resource_tracker.produce(:servers, provide_as: :vm_cid) { 32 | @cpi.create_vm( 33 | 'agent-id', 34 | stemcell_cid, 35 | @config.default_vm_type_cloud_properties, 36 | network_spec_with_floating_ip, 37 | [], 38 | {} 39 | ) 40 | } 41 | } 42 | 43 | vm = @compute.servers.get(vm_cid) 44 | vm.wait_for { ready? } 45 | 46 | expect(vm).to be 47 | end 48 | 49 | context 'connecting to endpoints' do 50 | 51 | endpoints.each do |endpoint| 52 | it "can access #{endpoint['host']}:#{endpoint['port']}" do 53 | @resource_tracker.consumes(:vm_cid, 'No VM to check') 54 | 55 | command = "nc -vz #{endpoint['host']} #{endpoint['port']}" 56 | 57 | floating_ip = @config.validator['floating_ip'] 58 | output, err, status = execute_ssh_command_on_vm_with_retry( 59 | @config.private_key_path, 60 | floating_ip, 61 | command 62 | ) 63 | 64 | expect(status.exitstatus).to eq(0), 65 | error_message("Failed to reach endpoint from VM with IP #{floating_ip}.", command, err, output) 66 | end 67 | end 68 | 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /extensions/flavors/README.md: -------------------------------------------------------------------------------- 1 | # Flavors Extensions 2 | 3 | This extension verifies that the given flavors exist in `openstack.project` configured in `validator.yml`. 4 | 5 | ## Configuration 6 | 7 | Create a `flavors.yml` which describes each flavor you want to check in OpenStack. You can describe a flavor using with the following key/value pairs: 8 | 9 | | key | value | mandatory | 10 | | ----- |------|-----------| 11 | | name | string | yes | 12 | | vcpus | integer | yes | 13 | | ram | integer [MiB] | yes | 14 | | ephemeral | integer [GiB] | yes | 15 | | metadata | key/value pairs | no | 16 | 17 | Each value is evaluated one to one against the flavor in OpenStack, except for `ephemeral` which is evaluated in one of the following ways: 18 | 19 | - If the flavor only has Root Disk: 20 | 21 | > Root Disk >= 3 [GiB] + `ephemeral` [GiB] + `ram` [GiB] 22 | 23 | - If the flavor has Root Disk and Ephemeral Disk: 24 | 25 | > Root Disk >= 3 and Ephemeral Disk >= `ephemeral` [GiB] + `ram` [GiB] 26 | 27 | The [OpenStack admin guide](https://docs.openstack.org/admin-guide/compute-flavors.html#extra-specs) provides an overview of possible metadata. 28 | 29 | Once all flavors are defined, configure the extension in the `validator.yml`: 30 | 31 | ```yaml 32 | extensions: 33 | paths: [./extensions/flavors] 34 | config: 35 | flavors: 36 | expected_flavors: 37 | ``` 38 | 39 | ## Examples 40 | 41 | ```yaml 42 | - name: m1.small 43 | vcpus: 1 44 | ram: 2048 45 | ephemeral: 0 46 | - name: m1.medium 47 | vcpus: 2 48 | ram: 4096 49 | ephemeral: 0 50 | metadata: 51 | hw_rng:allowed: 'True' 52 | ``` 53 | -------------------------------------------------------------------------------- /extensions/flavors/flavors_spec.rb: -------------------------------------------------------------------------------- 1 | fdescribe 'Flavors' do 2 | let(:compute) do 3 | Validator::Api::FogOpenStack.compute 4 | end 5 | 6 | config = Validator::Api.configuration.extensions 7 | 8 | flavors = YAML.load_file(config['flavors']['expected_flavors']) 9 | 10 | let(:expected_flavor_properties) do 11 | flavors.map { |flavor| flavor.fetch('metadata', {}).keys }.flatten.uniq 12 | end 13 | 14 | it 'can get list of flavors' do 15 | flavor_list = compute.flavors 16 | expect(flavor_list).not_to be_nil, 'could not get list of flavors' 17 | end 18 | 19 | flavors.each do |flavor| 20 | describe "'#{flavor['name']}'" do 21 | let(:os_flavor) { compute.flavors.find { |f| f.name == flavor['name'] } } 22 | 23 | it 'exists' do 24 | fail_message = "Missing flavor '#{flavor['name']}'" 25 | expect(os_flavor).not_to be_nil, fail_message 26 | end 27 | 28 | it 'is configured' do 29 | Validator::Api.skip_test('flavor not present') unless os_flavor 30 | 31 | expected_attributes = ['ephemeral', 'name', 'ram', 'vcpus'] 32 | given_attributes = flavor.keys.sort 33 | missing_attributes = expected_attributes - given_attributes 34 | expect(missing_attributes.empty?).to eq(true), "Following flavor attributes are missing: #{missing_attributes.join(',')}" 35 | 36 | ram_size_gb = flavor['ram'] / 1024 37 | 38 | if os_flavor.ephemeral.nil? || os_flavor.ephemeral == 0 39 | disks_fail_message = " disk >= #{3 + flavor['ephemeral'] + ram_size_gb} GiB (root (3 GiB) + ephemeral disk (#{flavor['ephemeral']} GiB) + ram (#{ram_size_gb} GiB))" 40 | disks_expectation_value = os_flavor.disk >= flavor['ephemeral'] + ram_size_gb + 3 41 | else 42 | disks_fail_message = " disk >= 3 GiB \n" \ 43 | " ephemeral disk >= #{flavor['ephemeral'] + ram_size_gb} GiB (ephemeral disk (#{flavor['ephemeral']} GiB) + ram (#{ram_size_gb} GiB))" 44 | disks_expectation_value = (os_flavor.ephemeral >= flavor['ephemeral'] + ram_size_gb) && (os_flavor.disk >= 3) 45 | end 46 | 47 | fail_message = "Expected: \n" + 48 | flavor_vcpus_to_s(flavor, method(:get_value_from_hash)) + 49 | flavor_ram_to_s(flavor, method(:get_value_from_hash)) + 50 | disks_fail_message + 51 | flavor_properties_to_s(flavor, method(:get_value_from_hash)) + 52 | "\nGot (OpenStack): \n" + 53 | flavor_to_s(os_flavor, method(:get_value_from_object)) 54 | 55 | expect( 56 | os_flavor.vcpus == flavor['vcpus'] && 57 | os_flavor.ram == flavor['ram'] && 58 | disks_expectation_value && 59 | check_flavor_properties(flavor.fetch('metadata', {}), os_flavor.metadata) 60 | ).to eq(true), fail_message 61 | end 62 | 63 | def get_value_from_hash(flavor, key) 64 | flavor.fetch(key, {}) 65 | end 66 | 67 | def get_value_from_object(flavor, key) 68 | flavor.send(key) 69 | end 70 | 71 | def flavor_to_s(flavor, get_value) 72 | flavor_vcpus_to_s(flavor, get_value) + 73 | flavor_ram_to_s(flavor, get_value) + 74 | " disk: #{get_value.call(flavor, 'disk')} GiB\n" \ 75 | " ephemeral: #{get_value.call(flavor, 'ephemeral')} GiB" + 76 | flavor_properties_to_s(flavor, get_value) 77 | end 78 | 79 | def flavor_vcpus_to_s(flavor, get_value) 80 | " vcpus: #{get_value.call(flavor, 'vcpus')}\n" 81 | end 82 | 83 | def flavor_ram_to_s(flavor, get_value) 84 | " ram: #{get_value.call(flavor, 'ram')} MiB\n" 85 | end 86 | 87 | def flavor_properties_to_s(flavor, get_value) 88 | result = '' 89 | if expected_flavor_properties != [] 90 | result += "\n Properties:" 91 | expected_flavor_properties.each do |property| 92 | result += "\n #{property}: #{get_value.call(flavor, 'metadata')[property] || 'not set'}" 93 | end 94 | end 95 | result 96 | end 97 | 98 | def check_flavor_properties(properties, os_properties) 99 | result = true 100 | expected_flavor_properties.each do |property| 101 | result &&= properties[property] == os_properties[property] 102 | end 103 | result 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /extensions/object_storage/README.md: -------------------------------------------------------------------------------- 1 | # Object Storage Extension 2 | 3 | This extension verifies that the Object Storage of your OpenStack can be used by the CloudFoundry Cloud Controller. 4 | 5 | ## Configuration 6 | 7 | Configure a `Temp-url-key` in OpenStack as described [here](https://docs.openstack.org/swift/latest/api/temporary_url_middleware.html#secret-keys). 8 | The 'tempurl' feature also needs to be configured in Swift by an OpenStack administrator. 9 | 10 | Add the extension to your `validator.yml`: 11 | 12 | ```yaml 13 | openstack: 14 | wait_for_swift: 5 15 | extensions: 16 | paths: [./extensions/object_storage] 17 | config: 18 | object_storage: 19 | openstack: 20 | openstack_temp_url_key: 21 | ``` 22 | -------------------------------------------------------------------------------- /extensions/object_storage/cloud_controller_blobstore_spec.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'securerandom' 3 | 4 | describe 'Cloud Controller using Swift as blobstore', cpi_api: true do 5 | let(:storage) { 6 | storage_config = {:openstack_temp_url_key => Validator::Api.configuration.extensions['object_storage']['openstack']['openstack_temp_url_key']} 7 | Validator::Api::FogOpenStack.storage(storage_config) 8 | } 9 | 10 | before(:all) do 11 | @resource_tracker = Validator::Api::ResourceTracker.create 12 | @validator_dirname = "validator-key-#{SecureRandom.uuid}" 13 | end 14 | 15 | it 'can create a directory' do 16 | directory_id = Validator::Api::FogOpenStack.with_openstack('Directory could not be created') do 17 | @resource_tracker.produce(:directories, provide_as: :root) { 18 | root = storage.directories.create({ 19 | key: @validator_dirname, 20 | public: false 21 | }) 22 | wait_for_swift 23 | root.key 24 | } 25 | end 26 | 27 | expect(directory_id).to_not be_nil 28 | end 29 | 30 | it 'can get a directory' do 31 | expect(test_directory.key).to eq(@validator_dirname) 32 | end 33 | 34 | it 'can upload a blob' do 35 | directory = test_directory 36 | expect{ 37 | Validator::Api::FogOpenStack.with_openstack('Blob could not be uploaded') do 38 | @resource_tracker.produce(:files, provide_as: :simple_blob) do 39 | file = directory.files.create({ 40 | key: 'validator-test-blob', 41 | body: 'Hello World', 42 | content_type: 'text/plain', 43 | public: false 44 | }) 45 | wait_for_swift 46 | [directory.key, file.key] 47 | end 48 | end 49 | }.not_to raise_error 50 | end 51 | 52 | it 'can create a temporary url' do 53 | _, file_key = @resource_tracker.consumes(:simple_blob) 54 | root_dir = test_directory 55 | 56 | file = Validator::Api::FogOpenStack.with_openstack('Blob could not be downloaded') do 57 | root_dir.files.get(file_key) 58 | end 59 | 60 | url = Validator::Api::FogOpenStack.with_openstack('Temporary URL could not be created') do 61 | file.url(Time.now.utc + 360000) 62 | end 63 | 64 | expect(url).to_not be_nil 65 | 66 | response = Validator::Api::FogOpenStack.with_openstack('Temporary URL could not be accessed') do 67 | Excon.get(url, configure_ssl_options) 68 | end 69 | error_message = < 67 | project_id: 68 | ``` 69 | -------------------------------------------------------------------------------- /extensions/quotas/quotas_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Quotas' do 2 | 3 | config = Validator::Api.configuration.extensions 4 | loaded_quotas = YAML.load_file(config['quotas']['expected_quotas']) 5 | quotas = loaded_quotas ? loaded_quotas : {} 6 | 7 | let(:project_id) { config['quotas']['project_id'] } 8 | 9 | unless loaded_quotas 10 | it 'executes quota tests' do 11 | skip("No quota expectation defined in #{config['quotas']['expected_quotas']}") 12 | end 13 | end 14 | 15 | context 'compute' do 16 | let(:compute_quota) do 17 | Validator::Api::FogOpenStack.compute.get_quota(project_id).body['quota_set'] 18 | end 19 | compute_quotas = quotas['compute'] ? quotas['compute'] : [] 20 | 21 | compute_quotas.each do |key, value| 22 | it key do 23 | os_quota = compute_quota[key] 24 | expect(os_quota == -1 || os_quota >= value).to eq(true), "Quota for '#{key}' should be greater than '#{value}', but is '#{os_quota}'" 25 | end 26 | end 27 | end 28 | 29 | context 'volume' do 30 | let(:volume_quota) do 31 | Validator::Api::FogOpenStack.volume.get_quota(project_id).body['quota_set'] 32 | end 33 | volume_quotas = quotas['volume'] ? quotas['volume'] : [] 34 | 35 | volume_quotas.each do |key, value| 36 | it key do 37 | os_quota = volume_quota[key] 38 | expect(os_quota == -1 || os_quota >= value).to eq(true), "Quota for '#{key}' should be greater than '#{value}', but is '#{os_quota}'" 39 | end 40 | end 41 | end 42 | 43 | context 'network' do 44 | let(:network_quota) do 45 | Validator::Api::FogOpenStack.network.get_quota(project_id).body['quota'] 46 | end 47 | network_quotas = quotas['network'] ? quotas['network'] : [] 48 | 49 | network_quotas.each do |key, value| 50 | it key do 51 | os_quota = network_quota[key] 52 | expect(os_quota == -1 || os_quota >= value).to eq(true), "Quota for '#{key}' should be greater than '#{value}', but is '#{os_quota}'" 53 | end 54 | end 55 | end 56 | end -------------------------------------------------------------------------------- /git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | RED='\033[0;35m' 6 | NC='\033[0m' # No Color 7 | 8 | if hash cred-alert-cli 2>/dev/null; then 9 | CMD="git diff --cached | cred-alert-cli scan --diff" 10 | echo "Running '${CMD}'" 11 | eval "$CMD" 12 | else 13 | >&2 echo -e "${RED}Not checking whether credentials get committed. cred-alert-cli is not installed.${NC}" 14 | fi 15 | 16 | unset GIT_DIR 17 | scripts/rubocop-staged 18 | -------------------------------------------------------------------------------- /lib/validator.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'json' 3 | require 'rspec/core' 4 | require 'yaml' 5 | require 'membrane' 6 | require 'fog/openstack' 7 | require 'securerandom' 8 | require 'open3' 9 | require 'tmpdir' 10 | require 'pathname' 11 | require 'socket' 12 | require 'logger' 13 | require 'benchmark' 14 | 15 | require_relative 'validator/converter' 16 | require_relative 'validator/formatter' 17 | require_relative 'validator/instrumentor' 18 | require_relative 'validator/redactor' 19 | require_relative 'validator/network_helper' 20 | require_relative 'validator/stats_log' 21 | require_relative 'validator/api' 22 | require_relative 'validator/config_validator' 23 | require_relative 'validator/extensions' 24 | require_relative 'validator/resources' 25 | require_relative 'validator/external_cpi' 26 | -------------------------------------------------------------------------------- /lib/validator/api.rb: -------------------------------------------------------------------------------- 1 | require_relative 'api/fog_openstack' 2 | require_relative 'api/cpi_helpers' 3 | require_relative 'api/resource_tracker' 4 | require_relative 'api/helpers' 5 | require_relative 'api/configuration' 6 | require_relative 'api/validator_error' 7 | 8 | module Validator 9 | module Api 10 | def self.skip_test(message) 11 | RSpec.current_example.example_group_instance.skip(message) 12 | end 13 | 14 | # Return a configuration object representing the validator.yml configuration 15 | # 16 | # The custom configuration setting `validator_config` is defined when starting the test suite in spec_helper.rb 17 | def self.configuration 18 | RSpec::configuration.validator_config 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /lib/validator/api/configuration.rb: -------------------------------------------------------------------------------- 1 | module Validator 2 | module Api 3 | class Configuration 4 | 5 | attr_reader :path 6 | 7 | def initialize(path) 8 | @path = path 9 | end 10 | 11 | def all 12 | @configuration ||= begin 13 | YAML.load_file(@path) 14 | end 15 | end 16 | 17 | def validator 18 | all.fetch('validator') 19 | end 20 | 21 | def openstack 22 | Converter.convert_and_apply_defaults(all.fetch('openstack')) 23 | end 24 | 25 | def cloud_config 26 | all.fetch('cloud_config') 27 | end 28 | 29 | def default_vm_type_cloud_properties 30 | cloud_config['vm_types'][0]['cloud_properties'] 31 | end 32 | 33 | def extensions 34 | all.fetch('extensions', {}).fetch('config', {}) 35 | end 36 | 37 | def custom_extension_paths 38 | all 39 | .fetch('extensions', {}) 40 | .fetch('paths', []) 41 | .map { |path| File.expand_path(path, File.dirname(@path)) } 42 | end 43 | 44 | def validate_extension_paths 45 | custom_extension_paths.each do |path| 46 | raise Validator::Api::ValidatorError, "Extension path '#{path}' is not a directory." unless File.directory?(path) 47 | end 48 | end 49 | 50 | def private_key_path 51 | File.expand_path(validator['private_key_path'], File.dirname(@path)) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/validator/api/cpi_helpers.rb: -------------------------------------------------------------------------------- 1 | module Validator 2 | module Api 3 | module CpiHelpers 4 | 5 | def log_path 6 | RSpec::configuration.options.log_path 7 | end 8 | 9 | def stemcell_path 10 | RSpec.configuration.options.stemcell_path 11 | end 12 | 13 | def cpi_path 14 | RSpec.configuration.options.cpi_bin_path 15 | end 16 | 17 | def with_cpi(error_message) 18 | yield if block_given? 19 | rescue => e 20 | fail("#{error_message} OpenStack error: #{e.message}") 21 | end 22 | 23 | def execute_ssh_command_on_vm_with_retry(private_key_path, ip, command, time_in_seconds = 60, frequency = 3) 24 | output, err, status = retry_command(time_in_seconds, frequency){ execute_ssh(private_key_path, ip, command) } 25 | 26 | validate_ssh_connection(err, status) 27 | 28 | [output, err, status] 29 | end 30 | 31 | def retry_command(time_in_seconds = 60, frequency = 3) 32 | start_time = Time.new 33 | if block_given? 34 | loop do 35 | output, err, status = yield 36 | 37 | if status.exitstatus == 0 || Time.now - start_time > time_in_seconds 38 | break [output, err, status] 39 | end 40 | 41 | sleep(frequency) 42 | end 43 | end 44 | end 45 | 46 | def execute_ssh(private_key_path, ip, command) 47 | stdout, stderr, status = Open3.capture3 "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i #{private_key_path} vcap@#{ip} -C '#{command}'" 48 | stderr_without_ssh_warning = stderr.gsub(/Warning: Permanently added (.|\s)+? logging and monitoring./, '') 49 | [stdout, stderr_without_ssh_warning, status] 50 | end 51 | 52 | def validate_ssh_connection(err, status) 53 | if status.exitstatus == 255 54 | if err.include? 'Permission denied (publickey)' 55 | fail "Failed to ssh to VM with floating IP: Permission denied.\n" + 56 | "Possible causes:\n" + 57 | "- SSH key mismatch\n" + 58 | "- the key has not been provisioned, because the OpenStack metadata service was not reachable\n\n" + 59 | "Error is: #{err}" 60 | end 61 | 62 | fail "Failed to ssh to VM with floating IP.\nError is: #{err}" 63 | end 64 | end 65 | 66 | def error_message(message, command, err, output) 67 | stderr = 'stderr: ' 68 | stdout = 'stdout: ' 69 | "#{message}\n" \ 70 | "Executed remote command: $ #{command}\n" \ 71 | "#{stderr}#{indent(err, stderr)}\n" \ 72 | "#{stdout}#{indent(output, stdout)}" 73 | end 74 | 75 | def indent(msg, space_text) 76 | msg.gsub("\n", "\n#{indentation(space_text)}") 77 | end 78 | 79 | def indentation(text) 80 | ' ' * text.size 81 | end 82 | 83 | def network_spec 84 | { 85 | 'default' => { 86 | 'type' => 'dynamic', 87 | 'cloud_properties' => { 88 | 'net_id' => Validator::Api.configuration.validator['network_id'] 89 | } 90 | } 91 | } 92 | end 93 | 94 | def network_spec_with_static_ip 95 | { 96 | 'default' => { 97 | 'type' => 'manual', 98 | 'ip' => Validator::Api.configuration.validator['static_ip'], 99 | 'cloud_properties' => { 100 | 'net_id' => Validator::Api.configuration.validator['network_id'] 101 | } 102 | } 103 | } 104 | end 105 | 106 | def network_spec_with_floating_ip 107 | { 108 | 'default' => { 109 | 'type' => 'dynamic', 110 | 'cloud_properties' => { 111 | 'net_id' => Validator::Api.configuration.validator['network_id'] 112 | } 113 | }, 114 | 'vip' => { 115 | 'type' => 'vip', 116 | 'ip' => Validator::Api.configuration.validator['floating_ip'], 117 | } 118 | } 119 | end 120 | 121 | def cpi(cpi_path_arg = RSpec.configuration.options.cpi_bin_path, log_path_arg = RSpec.configuration.options.log_path) 122 | logger = Logger.new("#{log_path_arg}/testsuite.log") 123 | Validator::ExternalCpi.new(cpi_path_arg, logger, "#{log_path_arg}/cpi.log", "#{log_path_arg}/stats.log") 124 | end 125 | 126 | def wait_for_swift 127 | seconds = Validator::Api.configuration.openstack['wait_for_swift'].to_i || 0 128 | sleep seconds 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/validator/api/fog_openstack.rb: -------------------------------------------------------------------------------- 1 | module Validator 2 | module Api 3 | class FogOpenStack 4 | class << self 5 | def compute 6 | handle_socket_error do 7 | Fog::OpenStack::Compute.new(convert_to_fog_params(openstack_params)) 8 | end 9 | end 10 | 11 | def network 12 | handle_socket_error do 13 | Fog::OpenStack::Network.new(convert_to_fog_params(openstack_params)) 14 | end 15 | end 16 | 17 | def image 18 | handle_socket_error do 19 | begin 20 | Fog::OpenStack::Image::V2.new(convert_to_fog_params(openstack_params)) 21 | rescue Fog::OpenStack::Errors::ServiceUnavailable 22 | Fog::OpenStack::Image::V1.new(convert_to_fog_params(openstack_params)) 23 | end 24 | end 25 | end 26 | 27 | def volume 28 | handle_socket_error do 29 | begin 30 | Fog::OpenStack::Volume::V2.new(convert_to_fog_params(openstack_params)) 31 | rescue Fog::OpenStack::Errors::ServiceUnavailable, Fog::Errors::NotFound 32 | Fog::OpenStack::Volume::V1.new(convert_to_fog_params(openstack_params)) 33 | end 34 | end 35 | end 36 | 37 | def storage(storage_params = {}) 38 | fog_params = convert_to_fog_params(openstack_params) 39 | fog_params.merge!(storage_params) 40 | 41 | handle_socket_error do 42 | Fog::OpenStack::Storage.new(fog_params) 43 | end 44 | end 45 | 46 | def with_openstack(error_message) 47 | yield if block_given? 48 | rescue => e 49 | log_path = RSpec.configuration.options.log_path 50 | logger = Logger.new(File.join(log_path, 'testsuite.log')) 51 | logger.error(e.message) 52 | message = "More details can be found in '#{log_path}'" 53 | if e.class == Excon::Errors::Forbidden 54 | message = "The user '#{Validator::Api.configuration.openstack['username']}' does not have required permissions." 55 | end 56 | fail("#{error_message}: #{message}") 57 | end 58 | 59 | private 60 | 61 | def handle_socket_error(&block) 62 | yield 63 | rescue Excon::Errors::SocketError => e 64 | raise ValidatorError, "Could not connect to '#{openstack_params['auth_url']}' \nException message: #{e.message} \nBacktrace: #{e.backtrace}" 65 | end 66 | 67 | def openstack_params 68 | Api.configuration.openstack 69 | end 70 | 71 | def convert_to_fog_params(options) 72 | add_exconn_instrumentor(options) 73 | { 74 | :openstack_auth_url => options['auth_url'], 75 | :openstack_username => options['username'], 76 | :openstack_api_key => options['api_key'], 77 | :openstack_tenant => options['tenant'], 78 | :openstack_project_name => options['project'], 79 | :openstack_domain_name => options['domain'], 80 | :openstack_region => options['region'], 81 | :openstack_endpoint_type => options['endpoint_type'], 82 | :connection_options => options['connection_options'] 83 | } 84 | end 85 | 86 | def add_exconn_instrumentor(options) 87 | if options['connection_options'] 88 | options['connection_options'].merge!({ 'instrumentor' => Validator::Instrumentor }) 89 | end 90 | end 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/validator/api/helpers.rb: -------------------------------------------------------------------------------- 1 | module Validator 2 | module Api 3 | module Helpers 4 | 5 | def registry_port 6 | endpoint = YAML.load_file(RSpec::configuration.options.cpi_config)['cloud']['properties']['registry']['endpoint'] 7 | endpoint.scan(/\d+/).join.to_i 8 | end 9 | 10 | def create_server(port) 11 | require 'socket' 12 | server = TCPServer.new('localhost', port) 13 | 14 | accept_thread = Thread.new { 15 | loop do 16 | Thread.start(server.accept) do |socket| 17 | request = socket.gets 18 | response = "{\"settings\":\"{}\"}\n" 19 | headers = create_headers [ 20 | 'HTTP/1.1 200 Ok', 21 | 'Content-Type: application/json', 22 | "Content-Length: #{response.bytesize}", 23 | 'Connection: close'] 24 | socket.print headers 25 | socket.print "\r\n" 26 | socket.print response 27 | socket.close 28 | end 29 | end 30 | } 31 | 32 | [server, accept_thread] 33 | end 34 | 35 | def create_headers(headers) 36 | headers.map { |line| "#{line}\r\n" }.join('') 37 | end 38 | 39 | def kill_server(server_thread) 40 | Thread.kill(server_thread) 41 | end 42 | 43 | def openstack_suite 44 | return @openstack_suite if @openstack_suite 45 | @openstack_suite = RSpec.describe 'Your OpenStack', order: :openstack do 46 | 47 | before(:all) do 48 | _, @server_thread = create_server(registry_port) 49 | end 50 | 51 | after(:all) do 52 | kill_server(@server_thread) 53 | end 54 | 55 | end 56 | end 57 | 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/validator/api/resource_tracker.rb: -------------------------------------------------------------------------------- 1 | module Validator 2 | module Api 3 | class ResourceTracker 4 | 5 | RESOURCE_SERVICES = { 6 | compute: [:flavors, :key_pairs, :servers, :server_groups], 7 | network: [:networks, :ports, :subnets, :floating_ips, :routers, :security_groups, :security_group_rules], 8 | image: [:images], 9 | volume: [:volumes, :snapshots], 10 | storage: [:files, :directories] 11 | } 12 | 13 | class Base 14 | extend Validator::Api::CpiHelpers 15 | 16 | def initialize(wait_for: Proc.new { true }) 17 | @wait_for = wait_for 18 | end 19 | 20 | def name(resource) 21 | resource.name 22 | end 23 | 24 | def get_ready(type, id) 25 | resource = FogOpenStack.send(service(type)).send(type).get(id) 26 | resource.wait_for(&@wait_for) if resource 27 | resource 28 | rescue Fog::Errors::NotFound 29 | nil 30 | end 31 | 32 | def destroy(type, id) 33 | resource = get_ready(type, id) 34 | if resource 35 | resource.destroy 36 | end 37 | end 38 | 39 | def service(resource_type) 40 | RESOURCE_SERVICES.each do |service, types| 41 | return service if types.include?(resource_type) 42 | end 43 | 44 | nil 45 | end 46 | end 47 | 48 | class Volumes < Base 49 | def name(resource) 50 | if resource.respond_to?(:name) 51 | resource.name 52 | else 53 | resource.display_name 54 | end 55 | end 56 | end 57 | 58 | class Images < Base 59 | 60 | def get_ready(type, id) 61 | if id =~ / light$/ 62 | OpenStruct.new(:name => "light_stemcell_#{@id}") 63 | else 64 | super(type, id) 65 | end 66 | end 67 | 68 | def destroy(_, stemcell_cid) 69 | Base.cpi.delete_stemcell(stemcell_cid) 70 | true 71 | rescue Validator::ExternalCpi::CpiError => e 72 | false 73 | end 74 | 75 | end 76 | 77 | class Servers < Base 78 | def destroy(_, vm_cid) 79 | Base.cpi.delete_vm(vm_cid) 80 | true 81 | rescue Validator::ExternalCpi::CpiError => e 82 | false 83 | end 84 | end 85 | 86 | class Directories < Base 87 | def name(resource) 88 | resource.key 89 | end 90 | 91 | def destroy(type, id) 92 | directory = FogOpenStack.storage.directories.get(id) 93 | return unless directory 94 | 95 | directory.files.each do |file| 96 | begin 97 | file.destroy 98 | rescue Fog::OpenStack::Storage::NotFound 99 | # surpress exception, resource will eventually be consistent 100 | nil 101 | end 102 | end 103 | Base.wait_for_swift 104 | 105 | begin 106 | directory.destroy 107 | rescue Fog::OpenStack::Storage::NotFound 108 | true 109 | end 110 | end 111 | end 112 | 113 | class Files < Base 114 | def name(resource) 115 | resource.key 116 | end 117 | 118 | def get_ready(type, id) 119 | directory_id = id[0] 120 | file_id = id[1] 121 | directory = super(:directories, directory_id) 122 | directory.files.get(file_id) if directory 123 | end 124 | 125 | def destroy(type, id) 126 | begin 127 | super(type, id) 128 | rescue Fog::OpenStack::Storage::NotFound 129 | true 130 | end 131 | end 132 | end 133 | 134 | class ServerGroups < Base 135 | def destroy(type, id) 136 | FogOpenStack.send(:compute).delete_server_group(id) 137 | end 138 | end 139 | 140 | RESOURCE_HANDLER = { 141 | images: Images.new(wait_for: Proc.new { status == 'active' } ), 142 | servers: Servers.new(wait_for: Proc.new { ready? }), 143 | volumes: Volumes.new(wait_for: Proc.new { ready? }), 144 | snapshots: Volumes.new(wait_for: Proc.new { status == 'available' }), 145 | networks: Base.new(wait_for: Proc.new { status == 'ACTIVE' }), 146 | ports: Base.new(wait_for: Proc.new { status == 'ACTIVE' }), 147 | routers: Base.new(wait_for: Proc.new { status == 'ACTIVE' }), 148 | directories: Directories.new, 149 | files: Files.new, 150 | server_groups: ServerGroups.new 151 | } 152 | 153 | ## 154 | # Creates a new resource tracker instance. Each instance manages its own set 155 | # of resources. 156 | # 157 | def self.create 158 | RSpec::configuration.validator_resources.new_tracker 159 | end 160 | 161 | def initialize 162 | @resources = [] 163 | end 164 | 165 | def count 166 | resources.length 167 | end 168 | 169 | ## 170 | # Create and track a resource. 171 | # 172 | # = Params 173 | # +type+: One of those listed in +RESOURCE_SERVICES+, e.g.: +:servers+ 174 | # +provide_as+: (optional) The name to be used to access the value via the +consume+ method. 175 | # If it is not given, it cannot be consumed. 176 | # = Block 177 | # The block has to yield an OpenStack resource id. This resource id is used to cleanup the 178 | # resource. 179 | # 180 | # = Examples 181 | # resource_id = resources.provide(resource_type, provide_as: :my_resource_name) { resource_id } 182 | # resource_id_not_consumable = resources.provide(resource_type) { resource_id } 183 | # 184 | def produce(type, provide_as: nil) 185 | fog_service = service(type) 186 | 187 | unless fog_service 188 | raise ArgumentError, "Invalid resource type '#{type}', use #{ResourceTracker.resource_types.join(', ')}" 189 | end 190 | 191 | 192 | if block_given? 193 | resource_id = yield 194 | resource_handler = RESOURCE_HANDLER.fetch(type, Base.new) 195 | 196 | resource = resource_handler.get_ready(type, resource_id) 197 | 198 | @resources << { 199 | type: type, 200 | id: resource_id, 201 | provide_as: provide_as, 202 | name: resource_handler.name(resource), 203 | test_description: RSpec.current_example&.full_description 204 | } 205 | resource_id 206 | end 207 | end 208 | 209 | ## 210 | # Get the resource id of a tracked resource for the given name. If a resource with the given 211 | # name cannot be found the test calling +consume+ will be marked as pending. 212 | # 213 | # = Params 214 | # +name+: The name which has been given to +produce+ as +:provide_as+ 215 | # +message+: (optional) Message to be presented to the user, if the resource cannot be found 216 | # 217 | # = Examples 218 | # resource_id = resources.provide(resource_type, provide_as: :my_resource_name) { resource_id } 219 | # resource_id = resources.consume(:my_resource_name) 220 | # 221 | def consumes(name, message = "Required resource '#{name}' does not exist.") 222 | value = @resources.find { |resource| resource.fetch(:provide_as) == name } 223 | 224 | if value == nil 225 | Api.skip_test(message) 226 | end 227 | value[:id] 228 | end 229 | 230 | def cleanup 231 | resources.map do |resource| 232 | RESOURCE_HANDLER.fetch(resource[:type], Base.new).destroy(resource[:type], resource[:id]) 233 | end.all? 234 | end 235 | 236 | def resources 237 | @resources.reject do |resource| 238 | nil == RESOURCE_HANDLER.fetch(resource[:type], Base.new).get_ready(resource[:type], resource[:id]) 239 | end 240 | end 241 | 242 | def self.resource_types 243 | RESOURCE_SERVICES.values.flatten 244 | end 245 | 246 | private 247 | 248 | def service(resource_type) 249 | RESOURCE_SERVICES.each do |service, types| 250 | return service if types.include?(resource_type) 251 | end 252 | 253 | nil 254 | end 255 | end 256 | end 257 | end 258 | -------------------------------------------------------------------------------- /lib/validator/api/validator_error.rb: -------------------------------------------------------------------------------- 1 | module Validator::Api 2 | class ValidatorError < StandardError; end 3 | end 4 | -------------------------------------------------------------------------------- /lib/validator/cli.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | require 'tmpdir' 3 | require 'json' 4 | require 'yaml' 5 | require 'membrane' 6 | require 'socket' 7 | require 'open-uri' 8 | require 'digest' 9 | require 'pathname' 10 | 11 | require_relative 'config_validator' 12 | require_relative 'converter' 13 | require_relative 'network_helper' 14 | require_relative 'redactor' 15 | require_relative 'api/configuration' 16 | require_relative 'api/validator_error' 17 | require_relative 'cli/untar' 18 | require_relative 'cli/context' 19 | require_relative 'cli/cf_openstack_validator' 20 | require_relative 'cli/error_with_log_details' 21 | -------------------------------------------------------------------------------- /lib/validator/cli/context.rb: -------------------------------------------------------------------------------- 1 | module Validator::Cli 2 | Options = Struct.new(:packages_dir, :log_path, :stemcell_path, :cpi_bin_path, :config_path, :cpi_config, :skip_cleanup?, :verbose?) 3 | 4 | class Context 5 | 6 | attr_reader :openstack_cpi_bin_from_env, :working_dir, :config, :cpi_json_path, :jobs_config_path, :cacert_path 7 | 8 | attr_accessor :cpi_bin_path, :cpi_release_path 9 | 10 | def initialize(cli_options) 11 | @cli_options = cli_options 12 | @cpi_release_path = @cli_options[:cpi_release] 13 | @working_dir = @cli_options[:working_dir] || "#{ENV['HOME']}/.cf-openstack-validator" 14 | ensure_working_directory(@working_dir) 15 | @working_dir = File.expand_path(@working_dir) 16 | @path_from_env = ENV['PATH'] 17 | @openstack_cpi_bin_from_env = ENV['OPENSTACK_CPI_BIN'] 18 | @cpi_bin_path = File.join(@working_dir, 'cpi') 19 | @config = Validator::Api::Configuration.new(config_path) 20 | @jobs_config_path = File.join(@working_dir, 'jobs', 'openstack_cpi', 'config') 21 | @cpi_json_path = File.join(@jobs_config_path, 'cpi.json') 22 | @cacert_path = File.join(@jobs_config_path, 'cacert.pem') 23 | end 24 | 25 | def tag 26 | @cli_options[:tag] 27 | end 28 | 29 | def skip_cleanup? 30 | @cli_options[:skip_cleanup] 31 | end 32 | 33 | def verbose? 34 | @cli_options[:verbose] 35 | end 36 | 37 | def fail_fast? 38 | @cli_options[:fail_fast] 39 | end 40 | 41 | def stemcell 42 | @cli_options[:stemcell] 43 | end 44 | 45 | def config_path 46 | @cli_options[:config_path] 47 | end 48 | 49 | def validator_root_dir 50 | File.expand_path('../../../../', __FILE__) 51 | end 52 | 53 | def extracted_cpi_release_dir 54 | File.join(working_dir, 'cpi-release') 55 | end 56 | 57 | def path_environment 58 | cpi_executable_path = File.join(working_dir, 'packages', 'ruby_openstack_cpi', 'bin') 59 | "#{cpi_executable_path}:#{@path_from_env}" 60 | end 61 | 62 | def gems_folder 63 | File.join(working_dir, 'packages', 'ruby_openstack_cpi', 'lib', 'ruby', 'gems', '*') 64 | end 65 | 66 | def packages_path 67 | File.join(working_dir, 'packages') 68 | end 69 | 70 | def create_validator_options 71 | Options.new( 72 | packages_path, 73 | File.join(working_dir, 'logs'), 74 | File.join(working_dir, 'stemcell'), 75 | cpi_bin_path, 76 | config_path, 77 | cpi_json_path, 78 | skip_cleanup?, 79 | verbose? 80 | ).freeze 81 | end 82 | 83 | private 84 | 85 | def ensure_working_directory(directory) 86 | unless File.directory?(directory) 87 | FileUtils.mkdir(directory) 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/validator/cli/error_with_log_details.rb: -------------------------------------------------------------------------------- 1 | module Validator::Cli 2 | class ErrorWithLogDetails < Validator::Api::ValidatorError 3 | attr_reader :log_path 4 | 5 | def initialize(error_message, log_path) 6 | @log_path = log_path 7 | @error_message = error_message 8 | end 9 | 10 | def message 11 | "Error: #{@error_message}\n\nMore details can be found in #{@log_path}\n" 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /lib/validator/cli/untar.rb: -------------------------------------------------------------------------------- 1 | module Validator::Cli 2 | class Untar 3 | def self.extract_archive(archive, destination) 4 | FileUtils.mkdir_p(destination) 5 | 6 | _, stderr, status = Open3.capture3("tar -xzf #{archive} -C #{destination}") 7 | if status.exitstatus != 0 8 | raise StandardError.new("Error extracting '#{archive}' to '#{destination}': #{stderr}") 9 | end 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /lib/validator/config_validator.rb: -------------------------------------------------------------------------------- 1 | module Validator 2 | class ConfigValidator 3 | 4 | class ReplacedString < Membrane::Schemas::Base 5 | REPLACE_ME = // 6 | 7 | def validate(object) 8 | Membrane::Schemas::Class.new(String).validate(object) 9 | fail!(object) if REPLACE_ME =~ object 10 | end 11 | 12 | def fail!(object) 13 | emsg = "Found placeholder '#{object}'" 14 | raise Membrane::SchemaValidationError.new(emsg) 15 | end 16 | end 17 | 18 | CONFIG_SCHEMA = Membrane::SchemaParser.parse do 19 | { 20 | 'openstack' => { 21 | 'auth_url' => ReplacedString.new, 22 | 'username' => ReplacedString.new, 23 | 'password' => ReplacedString.new, 24 | optional('domain') => ReplacedString.new, 25 | optional('project') => ReplacedString.new, 26 | optional('tenant') => String, 27 | optional('region') => String, 28 | optional('endpoint_type') => String, 29 | optional('state_timeout') => Numeric, 30 | optional('stemcell_public_visibility') => bool, 31 | optional('connection_options') => Hash, 32 | optional('boot_from_volume') => bool, 33 | optional('default_key_name') => String, 34 | optional('default_security_groups') => [String], 35 | optional('wait_resource_poll_interval') => Integer, 36 | optional('config_drive') => enum('disk', 'cdrom', nil), 37 | optional('human_readable_vm_names') => bool 38 | }, 39 | 'validator' => { 40 | 'network_id' => ReplacedString.new, 41 | 'floating_ip' => ReplacedString.new, 42 | 'static_ip' => ReplacedString.new, 43 | 'private_key_path' => String, 44 | 'releases' => [{ 45 | 'name' => 'bosh-openstack-cpi', 46 | 'url' => String, 47 | 'sha1' => String 48 | }] 49 | }, 50 | 'cloud_config' => { 51 | 'vm_types' => [{ 52 | 'name' => String, 53 | 'cloud_properties' => { 54 | 'instance_type' => ReplacedString.new, 55 | optional('availability_zone') => String, 56 | optional('root_disk') => { 57 | 'size' => Numeric 58 | } 59 | } 60 | }] 61 | }, 62 | optional('extensions') => { 63 | optional('paths') => [ReplacedString.new], 64 | optional('config') => Hash 65 | } 66 | } 67 | end 68 | 69 | def self.validate(config) 70 | begin 71 | CONFIG_SCHEMA.validate(config) 72 | rescue Membrane::SchemaValidationError => e 73 | raise Validator::Api::ValidatorError, "`validator.yml` is not valid:\n#{e.message}" 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/validator/converter.rb: -------------------------------------------------------------------------------- 1 | module Validator 2 | class Converter 3 | 4 | def self.cacert_path=(value) 5 | @@cacert_path = value 6 | end 7 | 8 | def self.openstack_defaults 9 | { 10 | "default_key_name" => "cf-validator", 11 | "default_security_groups" => ["default"], 12 | "wait_resource_poll_interval" => 5, 13 | "ignore_server_availability_zone" => false, 14 | "endpoint_type" => "publicURL", 15 | "state_timeout" => 300, 16 | "stemcell_public_visibility" => false, 17 | "boot_from_volume" => false, 18 | "use_dhcp" => true, 19 | "human_readable_vm_names" => true 20 | } 21 | end 22 | 23 | def self.cpi_config(openstack_params, registry_port) 24 | { 25 | "cloud" => { 26 | "plugin" => "openstack", 27 | "properties" => { 28 | "openstack" => openstack_params, 29 | "registry" => { 30 | "endpoint" => "http://localhost:#{registry_port}", 31 | "user" => "fake", 32 | "password" => "fake" 33 | } 34 | } 35 | } 36 | } 37 | end 38 | 39 | def self.to_cpi_json(openstack_config) 40 | registry_port = NetworkHelper.next_free_ephemeral_port 41 | 42 | cpi_config(openstack_config, registry_port) 43 | end 44 | 45 | def self.base_converters 46 | { 47 | 'password' => ->(_, value) { ['api_key', value] }, 48 | 'connection_options' => { 49 | 'ca_cert' => ->(_, value) { 50 | return nil if value.to_s == '' 51 | ssl_ca_file_path = @@cacert_path 52 | File.write(ssl_ca_file_path, value) 53 | ['ssl_ca_file', ssl_ca_file_path] 54 | } 55 | } 56 | } 57 | end 58 | 59 | def self.keystone_v2_converters 60 | { 61 | 'auth_url' => ->(key, value) { 62 | if value.match(/\/v2.0(?=\/|$)/) 63 | url_without_version = value.slice(0..(value.index(/\/v2.0(?=\/|$)/))) 64 | [key, remove_url_trailing_slash(url_without_version)] 65 | else 66 | [key, remove_url_trailing_slash(value)] 67 | end 68 | }, 69 | 'domain' => ->(key, value) { 70 | nil 71 | }, 72 | 'project' => ->(key, value) { 73 | nil 74 | } 75 | }.merge(base_converters) 76 | end 77 | 78 | def self.keystone_v3_converters 79 | { 80 | 'auth_url' => ->(key, value) { 81 | if value.match(/\/v3(?=\/|$)/) 82 | url_without_version = value.slice(0..(value.index(/\/v3(?=\/|$)/))) 83 | [key, remove_url_trailing_slash(url_without_version)] 84 | else 85 | [key, remove_url_trailing_slash(value)] 86 | end 87 | }, 88 | 'tenant' => ->(key, value) { 89 | nil 90 | } 91 | }.merge(base_converters) 92 | end 93 | 94 | def self.convert_and_apply_defaults(openstack_params) 95 | converters = is_v2(openstack_params.fetch('auth_url')) ? keystone_v2_converters : keystone_v3_converters 96 | apply_converters(openstack_defaults.merge(openstack_params), converters) 97 | end 98 | 99 | def self.apply_converters(hash, converters) 100 | no_op = -> (*args) { args } 101 | 102 | hash.map do |key, value| 103 | converter = converters.fetch(key, no_op) 104 | if converter.is_a?(Hash) && value.is_a?(Hash) 105 | [key, apply_converters(value, converter)] 106 | else 107 | converter.call(key, value) 108 | end 109 | end.compact.to_h 110 | end 111 | 112 | def self.is_v2(auth_url) 113 | auth_url.match(/\/v2.0(?=\/|$)/) 114 | end 115 | 116 | def self.remove_url_trailing_slash(url) 117 | url.end_with?('/') ? url[0..-2] : url 118 | end 119 | 120 | private_class_method :apply_converters 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/validator/extensions.rb: -------------------------------------------------------------------------------- 1 | module Validator 2 | 3 | class Extensions 4 | class << self 5 | def all 6 | extensions_paths.map do |path| 7 | Dir.glob(File.join(path, '*_spec.rb')) 8 | end.flatten 9 | end 10 | 11 | def eval(specs, binding) 12 | specs.each do |file| 13 | puts "Evaluating extension: #{file}" 14 | begin 15 | binding.eval(File.read(file), file) 16 | rescue Exception => e 17 | puts e 18 | puts e.backtrace if RSpec::configuration.options.verbose? 19 | raise e 20 | end 21 | end 22 | nil 23 | end 24 | 25 | private 26 | 27 | def extensions_paths 28 | RSpec.configuration.validator_config.custom_extension_paths 29 | end 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /lib/validator/external_cpi.rb: -------------------------------------------------------------------------------- 1 | require 'membrane' 2 | require 'open3' 3 | 4 | module Validator 5 | class ExternalCpi 6 | class CpiError < StandardError; end 7 | class NonExecutable < CpiError; end 8 | class InvalidResponse < CpiError; end 9 | 10 | 11 | RESPONSE_SCHEMA = Membrane::SchemaParser.parse do 12 | { 13 | 'result' => any, 14 | 'error' => enum(nil, 15 | { 'type' => String, 16 | 'message' => String, 17 | 'ok_to_retry' => bool 18 | } 19 | ), 20 | 'log' => String 21 | } 22 | end 23 | 24 | def initialize(cpi_path, logger, cpi_task_log_path, stats_log_path) 25 | @cpi_path = cpi_path 26 | @logger = logger 27 | @cpi_task_log_path = cpi_task_log_path 28 | @stats_log_path = stats_log_path 29 | end 30 | 31 | def current_vm_id(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 32 | def create_stemcell(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 33 | def delete_stemcell(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 34 | def create_vm(*arguments) invoke_cpi_method(__method__.to_s, *arguments); end 35 | def info(*arguments) invoke_cpi_method(__method__.to_s, *arguments); end 36 | def delete_vm(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 37 | def has_vm(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 38 | def reboot_vm(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 39 | def set_vm_metadata(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 40 | def create_disk(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 41 | def set_disk_metadata(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 42 | def has_disk(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 43 | def delete_disk(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 44 | def attach_disk(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 45 | def detach_disk(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 46 | def snapshot_disk(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 47 | def delete_snapshot(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 48 | def get_disks(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 49 | def ping(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end 50 | 51 | private 52 | 53 | def invoke_cpi_method(method_name, *arguments) 54 | context = { 55 | 'director_uuid' => 'validator', 56 | 'request_id' => "#{generate_request_id}" 57 | } 58 | 59 | request_json = JSON.dump(request(method_name, arguments, context)) 60 | redacted_request = request(method_name, redact_arguments(method_name, arguments), redact_context(context)) 61 | 62 | cpi_exec_path = checked_cpi_exec_path 63 | env = { 64 | 'PATH' => '/usr/sbin:/usr/bin:/sbin:/bin', 'TMPDIR' => ENV['TMPDIR'], 65 | 'BOSH_PACKAGES_DIR' => File.join(File.dirname(cpi_exec_path), 'packages'), 66 | 'BOSH_JOBS_DIR' => File.join(File.dirname(cpi_exec_path), 'jobs') 67 | } 68 | 69 | @logger.debug("External CPI sending request: #{JSON.dump(redacted_request)} with command: #{cpi_exec_path}") 70 | cpi_response, stderr, exit_status = nil, nil, nil 71 | measure = Benchmark.measure { 72 | cpi_response, stderr, exit_status = Open3.capture3(env, cpi_exec_path, stdin_data: request_json, unsetenv_others: true) 73 | } 74 | @logger.debug("External CPI got response: #{cpi_response}, err: #{stderr}, exit_status: #{exit_status}") 75 | 76 | parsed_response = parsed_response(cpi_response) 77 | validate_response(parsed_response) 78 | 79 | save_cpi_log(parsed_response['log']) 80 | save_cpi_log(stderr) 81 | 82 | save_stats_log(redacted_request, measure) 83 | 84 | if parsed_response['error'] 85 | handle_error(parsed_response['error'], method_name) 86 | end 87 | 88 | parsed_response['result'] 89 | end 90 | 91 | def checked_cpi_exec_path 92 | unless File.executable?(@cpi_path) 93 | raise NonExecutable, "Failed to run cpi: '#{@cpi_path}' is not executable" 94 | end 95 | @cpi_path 96 | end 97 | 98 | def redact_context(context) 99 | return context if @properties_from_cpi_config.nil? 100 | Hash[context.map{|k,v|[k,@properties_from_cpi_config.keys.include?(k) ? '' : v]}] 101 | end 102 | 103 | def redact_arguments(method_name, arguments) 104 | if method_name == 'create_vm' 105 | redact_from_env_in_create_vm_arguments(arguments) 106 | else 107 | arguments 108 | end 109 | end 110 | 111 | def redact_from_env_in_create_vm_arguments(arguments) 112 | redacted_arguments = arguments.clone 113 | env = redacted_arguments[5] #{} 114 | env = redact_all_but(['bosh'], env) 115 | env['bosh'] = redact_all_but(['group', 'groups'], env.fetch('bosh',{})) 116 | redacted_arguments[5] = env 117 | redacted_arguments 118 | end 119 | 120 | def redact_all_but(keys, hash) 121 | Hash[hash.map { |k,v| [k, keys.include?(k) ? v.dup : ''] }] 122 | end 123 | 124 | def request(method_name, arguments, context) 125 | { 126 | 'method' => method_name, 127 | 'arguments' => arguments, 128 | 'context' => context 129 | } 130 | end 131 | 132 | def handle_error(error_response, method_name) 133 | error_type = error_response['type'] 134 | error_message = error_response['message'] 135 | 136 | raise Validator::ExternalCpi::CpiError, "CPI error '#{error_type}' with message '#{error_message}' in '#{method_name}' CPI method" 137 | end 138 | 139 | def save_cpi_log(output) 140 | File.open(@cpi_task_log_path, 'a') do |f| 141 | f.write(output) 142 | end 143 | end 144 | 145 | def save_stats_log(request, measure) 146 | StatsLog.new(@stats_log_path).append(request, measure) 147 | end 148 | 149 | def parsed_response(input) 150 | begin 151 | JSON.load(input) 152 | rescue JSON::ParserError => e 153 | raise InvalidResponse, "Invalid CPI response - ParserError - #{e.message}" 154 | end 155 | end 156 | 157 | def validate_response(response) 158 | RESPONSE_SCHEMA.validate(response) 159 | rescue Membrane::SchemaValidationError => e 160 | raise InvalidResponse, "Invalid CPI response - SchemaValidationError: #{e.message}" 161 | end 162 | 163 | def generate_request_id 164 | Random.rand(100000..999999) 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /lib/validator/formatter.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/core' 2 | require 'pathname' 3 | 4 | module Validator 5 | class TestsuiteFormatter < RSpec::Core::Formatters::DocumentationFormatter 6 | RSpec::Core::Formatters.register self, :dump_failures, :dump_pending, :dump_summary, 7 | :example_started, :example_pending, :example_failed 8 | 9 | def initialize(output) 10 | super 11 | @options = RSpec::configuration.options 12 | end 13 | 14 | def example_started(notification) 15 | output.print "#{current_indentation}#{notification.example.description}... " 16 | end 17 | 18 | def example_passed(notification) 19 | output.puts RSpec::Core::Formatters::ConsoleCodes.wrap("passed", :success) 20 | end 21 | 22 | def example_failed(notification) 23 | output.puts RSpec::Core::Formatters::ConsoleCodes.wrap("failed", :failure) 24 | end 25 | 26 | def example_pending(pending) 27 | pending_msg = pending.example.execution_result.pending_message 28 | output.puts RSpec::Core::Formatters::ConsoleCodes.wrap("skipped: #{pending_msg}", :pending) 29 | end 30 | 31 | def dump_failures(notification) 32 | return if notification.failure_notifications.empty? 33 | formatted = "\nFailures:\n" 34 | notification.failure_notifications.each_with_index do |failure, index| 35 | formatted << formatted_failure(failure, index+1) 36 | end 37 | output.puts formatted 38 | end 39 | 40 | def dump_pending(notification) 41 | end 42 | 43 | def dump_summary(summary) 44 | output.puts "\nFinished in #{summary.formatted_duration} " \ 45 | "(files took #{summary.formatted_load_time} to load)\n" \ 46 | "#{summary.colorized_totals_line}\n" 47 | output.puts "Resources: #{RSpec.configuration.validator_resources.summary}" 48 | 49 | if summary.failure_count > 0 50 | output.puts "\nYou can find more information in the logs at #{File.join(Pathname.new(@options.log_path).cleanpath, 'testsuite.log')}" 51 | end 52 | end 53 | 54 | private 55 | 56 | def formatted_failure(failure, failure_number, colorizer = ::RSpec::Core::Formatters::ConsoleCodes) 57 | if @options.verbose? 58 | failure.fully_formatted(failure_number, colorizer) 59 | else 60 | formatted = "\n #{failure_number}) #{failure.description}\n" 61 | formatted << colorizer.wrap("#{indent(failure.exception.message, failure_number)}\n", RSpec.configuration.failure_color) 62 | end 63 | end 64 | 65 | def indent(message, failure_number) 66 | message.lines.map {|l| "#{indentation(failure_number)}#{l}"}.join('') 67 | end 68 | 69 | def indentation(failure_number) 70 | ' ' * (failure_number.to_s.size + 4) 71 | end 72 | end 73 | end -------------------------------------------------------------------------------- /lib/validator/instrumentor.rb: -------------------------------------------------------------------------------- 1 | module Validator 2 | class Instrumentor 3 | 4 | REDACTED = '' 5 | @logger = nil 6 | 7 | def self.logger 8 | @logger = @logger || Logger.new("#{File.join(RSpec::configuration.options.log_path, 'excon.log')}") 9 | end 10 | 11 | def self.instrument(name, params = {}) 12 | redacted_params = redact(params) 13 | logger.debug("#{name} #{redacted_params}") 14 | 15 | evaluated_block = nil 16 | if block_given? 17 | measure = Benchmark.measure { evaluated_block = yield } 18 | stats_log_path = File.join(RSpec::configuration.options.log_path, 'fog_stats.log') 19 | StatsLog.new(stats_log_path).append({ method: name, arguments: redacted_params }, measure) 20 | evaluated_block 21 | end 22 | end 23 | 24 | def self.redact(params) 25 | redacted_params = params.dup 26 | redact_body(redacted_params, 'auth.passwordCredentials.password') 27 | redact_body(redacted_params, 'server.user_data') 28 | redact_body(redacted_params, 'auth.identity.password.user.password') 29 | redact_headers(redacted_params, 'X-Auth-Token') 30 | redacted_params 31 | end 32 | 33 | private 34 | 35 | def self.redact_body(params, json_path) 36 | return unless params.has_key?(:body) && params[:body].is_a?(String) 37 | return unless params.has_key?(:headers) && params[:headers]['Content-Type'] == 'application/json' 38 | 39 | begin 40 | json_content = JSON.parse(params[:body]) 41 | rescue JSON::ParserError 42 | return 43 | end 44 | json_content = Redactor.redact(json_content, json_path) 45 | params[:body] = JSON.dump(json_content) 46 | end 47 | 48 | def self.redact_headers(params, property) 49 | return unless params.has_key?(:headers) 50 | 51 | headers = params[:headers] = params[:headers].dup 52 | 53 | headers.store(property, REDACTED) 54 | end 55 | 56 | def self.fetch_property 57 | -> (hash, property) { hash.fetch(property, {})} 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/validator/network_helper.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | 3 | module Validator 4 | class NetworkHelper 5 | def self.next_free_ephemeral_port 6 | socket = Socket.new(:INET, :STREAM, 0) 7 | socket.bind(Addrinfo.tcp('127.0.0.1', 0)) 8 | port = socket.local_address.ip_port 9 | socket.close 10 | port 11 | end 12 | 13 | def self.vm_ip_to_ssh(vm_id, config, compute) 14 | if config.validator['use_external_ip'] 15 | config.validator['floating_ip'] 16 | else 17 | server = compute.servers.get(vm_id) 18 | server.addresses.values.first.dig(0,'addr') 19 | end 20 | end 21 | 22 | def self.ssh_port_open?(configured_security_groups, network) 23 | check_remote_group_id_empty = Validator::Api.configuration.validator['use_external_ip'] 24 | port_open_in_any_security_group?('ingress', 22, 'tcp', configured_security_groups, check_remote_group_id_empty, network) 25 | end 26 | 27 | def self.port_open_in_any_security_group?(direction, port, protocol, security_groups, check_remote_group_id_empty = false, network) 28 | port_open = false 29 | security_groups.each { |security_group| port_open ||= port_open?(direction, port, protocol, security_group, check_remote_group_id_empty, network) } 30 | port_open 31 | end 32 | 33 | def self.port_open?(direction, port, protocol, security_group, check_remote_group_id_empty = false, network) 34 | security_group = network.security_groups.find { |sg| sg.name == security_group } 35 | rule = security_group.security_group_rules.find { |rule| 36 | result = rule.direction == direction && rule.ethertype == 'IPv4' && protocol_included?(rule, protocol) && port_in_range?(port, rule) 37 | if check_remote_group_id_empty 38 | result = result && rule.remote_group_id == nil 39 | end 40 | result 41 | } 42 | rule != nil 43 | end 44 | 45 | def self.port_in_range?(port, rule) 46 | any_range?(rule) || (rule.port_range_min <= port && port <= rule.port_range_max) 47 | end 48 | 49 | def self.any_range?(rule) 50 | rule.port_range_min == nil && rule.port_range_max == nil 51 | end 52 | 53 | def self.protocol_included?(rule, protocol) 54 | rule.protocol == nil || rule.protocol == protocol 55 | end 56 | end 57 | end -------------------------------------------------------------------------------- /lib/validator/redactor.rb: -------------------------------------------------------------------------------- 1 | module Validator 2 | class Redactor 3 | 4 | REDACTED = '' 5 | 6 | def self.clone_and_redact(hash, path) 7 | hash = clone(hash) 8 | if hash.nil? 9 | hash 10 | else 11 | redact(hash, path) 12 | end 13 | end 14 | 15 | def self.redact(hash, json_path) 16 | properties = json_path.split('.') 17 | property_to_redact = properties.pop 18 | 19 | target_hash = properties.reduce(hash, &fetch_property) 20 | target_hash.store(property_to_redact, REDACTED) if target_hash.has_key? property_to_redact 21 | 22 | hash 23 | end 24 | 25 | private 26 | 27 | def self.clone(hash) 28 | JSON.parse(hash.to_json) 29 | rescue 30 | nil 31 | end 32 | 33 | def self.fetch_property 34 | -> (hash, property) { hash.fetch(property, {})} 35 | end 36 | 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/validator/resources.rb: -------------------------------------------------------------------------------- 1 | module Validator 2 | class Resources 3 | 4 | def initialize 5 | @trackers = [] 6 | end 7 | 8 | def new_tracker 9 | (@trackers << Api::ResourceTracker.new).last 10 | end 11 | 12 | def cleanup() 13 | @trackers.delete_if { |tracker| tracker.cleanup } 14 | 15 | @trackers.empty? 16 | end 17 | 18 | def count 19 | @trackers.inject(0) { |memo, tracker| 20 | memo + tracker.count 21 | } 22 | end 23 | 24 | def summary 25 | return 'All resources have been cleaned up' if count == 0 26 | 27 | resources_by_type = @trackers.map { |tracker| 28 | tracker.resources 29 | }.flatten.group_by { |resource| resource[:type] } 30 | 31 | resource_type_summary = Api::ResourceTracker.resource_types.map do |resource_type| 32 | resources = resources_by_type[resource_type] 33 | " #{resource_type_heading(resource_type)}:\n#{format_resources(resources)}" unless resources.nil? 34 | end.join 35 | 36 | "The following resources might not have been cleaned up:\n" + resource_type_summary 37 | end 38 | 39 | private 40 | 41 | def resource_type_heading(resource_type) 42 | if resource_type == :servers 43 | "VMs" 44 | else 45 | resource_type.to_s.capitalize.gsub('_', ' ') 46 | end 47 | end 48 | 49 | def format_resources(resources) 50 | resources.map { |resource| " - Name: #{resource[:name]}\n UUID: #{resource[:id]}\n Created by test: #{resource[:test_description]}\n" }.join 51 | end 52 | 53 | end 54 | end -------------------------------------------------------------------------------- /lib/validator/stats_log.rb: -------------------------------------------------------------------------------- 1 | module Validator 2 | class StatsLog 3 | def initialize(path) 4 | @path = path 5 | end 6 | 7 | def append(request, measure) 8 | File.open(@path, 'a') do |f| 9 | f.puts(JSON.dump({ 'request' => request, 'duration' => measure.real })) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /sample_extensions/dummy_extension_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'My extension' do 2 | 3 | it 'is true' do 4 | expect(true).to be(true) 5 | end 6 | 7 | context 'when requiring custom configuration' do 8 | let(:config) { Validator::Api::configuration.extensions } 9 | 10 | it 'is available' do 11 | expect(config['custom-config-key']).to eq('custom-config-value') 12 | end 13 | end 14 | 15 | context 'when accessing OpenStack API' do 16 | 17 | context 'compute' do 18 | let(:compute) { Validator::Api::FogOpenStack.compute } 19 | 20 | it 'is provided by the validator' do 21 | expect(compute.servers).to be_a(Fog::OpenStack::Compute::Servers) 22 | end 23 | end 24 | 25 | context 'network' do 26 | let(:network) { Validator::Api::FogOpenStack.network } 27 | 28 | it 'is provided by the validator' do 29 | expect(network.networks).to be_a(Fog::OpenStack::Network::Networks) 30 | end 31 | end 32 | end 33 | 34 | context 'when using resource management' do 35 | let(:compute) { Validator::Api::FogOpenStack.compute } 36 | 37 | before(:all) do 38 | @resource_tracker = Validator::Api::ResourceTracker.create 39 | end 40 | 41 | it 'produces a resource' do 42 | resource_id = @resource_tracker.produce(:volumes, provide_as: :test_volume) { 43 | compute.volumes.create({ 44 | :name => 'validator-test-volume', 45 | :description => '', 46 | :size => 1 47 | }).id 48 | } 49 | 50 | expect(resource_id).to_not be_nil 51 | end 52 | 53 | it 'consumes an existing resource' do 54 | resource_id = @resource_tracker.consumes(:test_volume) 55 | 56 | expect(resource_id).to_not be_nil 57 | end 58 | 59 | it 'consumes a non-existing resource' do 60 | @resource_tracker.consumes(:non_existing_resource) 61 | 62 | fail('Test should have been marked pending') 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /scripts/rubocop-staged: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'English' 5 | 6 | Dir.chdir(File.expand_path(__dir__ + '/..')) do 7 | command = 'bundle exec rubocop-git --cached' 8 | puts("Running '#{command}'") 9 | system(command) 10 | end 11 | 12 | exit $CHILD_STATUS.exitstatus 13 | -------------------------------------------------------------------------------- /spec/assets/broken-cpi-release/packages/broken_package/packaging: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Log to STDOUT" 4 | (>&2 echo "Log to STDERR") 5 | 6 | exit 1 7 | 8 | -------------------------------------------------------------------------------- /spec/assets/cpi-release.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/spec/assets/cpi-release.tgz -------------------------------------------------------------------------------- /spec/assets/cpi-release/packages/dummy_package/packaging: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Log to STDOUT" 4 | (>&2 echo "Log to STDERR") 5 | 6 | echo $BOSH_INSTALL_TARGET >$BOSH_INSTALL_TARGET/compiled_file 7 | echo $BOSH_PACKAGES_DIR >>$BOSH_INSTALL_TARGET/compiled_file 8 | 9 | -------------------------------------------------------------------------------- /spec/assets/dummy.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/spec/assets/dummy.tgz -------------------------------------------------------------------------------- /spec/assets/expected_cpi.json: -------------------------------------------------------------------------------- 1 | { 2 | "cloud": { 3 | "plugin": "openstack", 4 | "properties": { 5 | "openstack": { 6 | "auth_url": "https://auth.url", 7 | "username": "username", 8 | "api_key": "password", 9 | "domain": "domain", 10 | "project": "project", 11 | "default_key_name": "cf-validator", 12 | "default_security_groups": ["default"], 13 | "wait_resource_poll_interval": 5, 14 | "ignore_server_availability_zone": false, 15 | "endpoint_type": "publicURL", 16 | "state_timeout": 300, 17 | "stemcell_public_visibility": false, 18 | "connection_options": { 19 | "ssl_ca_file": "./cacert.pem" 20 | }, 21 | "boot_from_volume": false, 22 | "use_dhcp": true, 23 | "human_readable_vm_names": true 24 | }, 25 | "registry": { 26 | "endpoint": "http://localhost:11111", 27 | "user": "fake", 28 | "password": "fake" 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /spec/assets/expected_cpi_keystone_v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "cloud": { 3 | "plugin": "openstack", 4 | "properties": { 5 | "openstack": { 6 | "auth_url": "https://auth.url/identity", 7 | "username": "username", 8 | "api_key": "password", 9 | "tenant": "tenant", 10 | "default_key_name": "cf-validator", 11 | "default_security_groups": ["default"], 12 | "wait_resource_poll_interval": 5, 13 | "ignore_server_availability_zone": false, 14 | "endpoint_type": "publicURL", 15 | "state_timeout": 300, 16 | "stemcell_public_visibility": false, 17 | "connection_options": { 18 | "ssl_ca_file": "./cacert.pem" 19 | }, 20 | "boot_from_volume": false, 21 | "use_dhcp": true, 22 | "human_readable_vm_names": true 23 | }, 24 | "registry": { 25 | "endpoint": "http://localhost:11111", 26 | "user": "fake", 27 | "password": "fake" 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /spec/assets/validator.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | validator: 4 | network_id: some_network_id 5 | floating_ip: some_floating_ip 6 | static_ip: some_static_ip 7 | private_key_path: cf-validator.rsa_id 8 | releases: 9 | - name: bosh-openstack-cpi 10 | url: cpi-download-url 11 | sha1: cpi-sha1 12 | 13 | cloud_config: 14 | vm_types: 15 | - name: default 16 | cloud_properties: 17 | instance_type: some_instance_type 18 | 19 | openstack: 20 | auth_url: "https://auth.url/v3" 21 | username: "username" 22 | password: "password" 23 | domain: "domain" 24 | project: "project" 25 | connection_options: 26 | ssl_ca_file: "./cacert.pem" 27 | boot_from_volume: false 28 | -------------------------------------------------------------------------------- /spec/assets/validator_keystone_v2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | validator: 4 | network_id: some_network_id 5 | floating_ip: some_floating_ip 6 | static_ip: some_static_ip 7 | private_key_path: cf-validator.rsa_id 8 | releases: 9 | - name: bosh-openstack-cpi 10 | url: cpi-download-url 11 | sha1: cpi-sha1 12 | 13 | cloud_config: 14 | vm_types: 15 | - name: default 16 | cloud_properties: 17 | instance_type: some_instance_type 18 | 19 | openstack: 20 | auth_url: "https://auth.url/identity/v2.0" 21 | username: "username" 22 | password: "password" 23 | tenant: "tenant" 24 | project: "project" 25 | connection_options: 26 | ssl_ca_file: "./cacert.pem" 27 | boot_from_volume: false 28 | -------------------------------------------------------------------------------- /spec/integration/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'open3' 3 | 4 | def run_validator(args) 5 | bin_path = File.expand_path('../../../bin', __FILE__) 6 | cmd = "#{bin_path}/cf-openstack-validator #{args}" 7 | Open3.capture3(cmd) 8 | end 9 | 10 | def help_text 11 | < 'another_value' }) 56 | end 57 | end 58 | 59 | describe '#cloud_config' do 60 | let(:validator_config_content) do 61 | < 'another_value' }) 70 | end 71 | end 72 | 73 | describe '#default_vm_type_cloud_properties' do 74 | let(:validator_config_content) do 75 | < 'another_value' }) 86 | end 87 | end 88 | 89 | describe '#extensions' do 90 | 91 | let(:validator_config_content) { nil } 92 | 93 | context 'when missing in validator.yml' do 94 | it 'returns an empty hash' do 95 | expect(subject.extensions).to eq({}) 96 | end 97 | end 98 | 99 | context 'when extension configuration is defined in the validator.yml' do 100 | let(:validator_config_content) do 101 | <<-EOF 102 | extensions: 103 | config: 104 | the: hash 105 | second: value 106 | EOF 107 | end 108 | 109 | it 'returns the hash' do 110 | expect(subject.extensions).to eq({'the' => 'hash', 'second' => 'value'}) 111 | end 112 | end 113 | end 114 | 115 | describe '#openstack' do 116 | it 'uses Converter to convert values from validator.yml' do 117 | allow(YAML).to receive(:load_file).and_return({'openstack' => {}}) 118 | allow(Validator::Converter).to receive(:convert_and_apply_defaults) 119 | 120 | subject.openstack 121 | 122 | expect(Validator::Converter).to have_received(:convert_and_apply_defaults) 123 | end 124 | end 125 | 126 | describe '#custom_extension_paths' do 127 | context 'with absolute paths' do 128 | let(:validator_config_content) do 129 | <<-EOF 130 | extensions: 131 | paths: 132 | - /tmp 133 | EOF 134 | end 135 | 136 | it 'returns same paths' do 137 | expect(subject.custom_extension_paths).to eq(['/tmp']) 138 | end 139 | end 140 | 141 | context 'with relative paths' do 142 | let(:validator_config_content) do 143 | <<-EOF 144 | extensions: 145 | paths: 146 | - some-directory 147 | EOF 148 | end 149 | 150 | before do 151 | FileUtils.mkdir_p(File.join(tmpdir, 'some-directory')) 152 | end 153 | 154 | it 'returns expanded paths' do 155 | expect(subject.custom_extension_paths).to eq([File.join(tmpdir, 'some-directory')]) 156 | end 157 | end 158 | 159 | context 'with an empty configuration file' do 160 | it 'should return an empty array' do 161 | expect(subject.custom_extension_paths).to eq([]) 162 | end 163 | end 164 | end 165 | 166 | describe '#validate_extension_paths' do 167 | context 'with a valid path' do 168 | let(:validator_config_content) do 169 | <<-EOF 170 | extensions: 171 | paths: 172 | - existing-directory 173 | EOF 174 | end 175 | 176 | before(:each) do 177 | FileUtils.mkdir(File.join(tmpdir, 'existing-directory')) 178 | end 179 | 180 | it 'does not raise an error' do 181 | expect { 182 | subject.validate_extension_paths 183 | }.to_not raise_error 184 | end 185 | end 186 | context 'with invalid paths' do 187 | let(:validator_config_content) do 188 | <<-EOF 189 | extensions: 190 | paths: 191 | - /non-existent-directory 192 | EOF 193 | end 194 | 195 | it 'raises error' do 196 | expect { 197 | subject.validate_extension_paths 198 | }.to raise_error Validator::Api::ValidatorError, /'\/non-existent-directory' is not a directory./ 199 | end 200 | end 201 | end 202 | 203 | describe '#private_key_path' do 204 | context 'given a relative path to the config file' do 205 | 206 | let(:validator_config_content) do 207 | <<-EOF 208 | --- 209 | validator: 210 | private_key_path: ./private/key/path 211 | EOF 212 | end 213 | 214 | it 'specifies the private key path relative to the validator.yml' do 215 | expect(subject.private_key_path).to eq(File.join(tmpdir, '/private/key/path')) 216 | end 217 | end 218 | 219 | context 'given an absolute path' do 220 | let(:validator_config_content) do 221 | <<-EOF 222 | --- 223 | validator: 224 | private_key_path: /absolute/private/key/path 225 | EOF 226 | end 227 | 228 | it 'specifies the private key path relative to the validator.yml' do 229 | expect(subject.private_key_path).to eq('/absolute/private/key/path') 230 | end 231 | end 232 | end 233 | end 234 | -------------------------------------------------------------------------------- /spec/unit/validator/api/fog_openstack_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | module Validator::Api 4 | describe FogOpenStack do 5 | 6 | let(:openstack_params) { {} } 7 | 8 | before(:each) do 9 | configuration = instance_double(Validator::Api::Configuration) 10 | allow(configuration).to receive(:openstack).and_return(openstack_params) 11 | allow(Validator::Api).to receive(:configuration).and_return(configuration) 12 | end 13 | 14 | describe '.image' do 15 | 16 | context 'when V2 is available' do 17 | before(:each) do 18 | allow(Fog::OpenStack::Image::V2).to receive(:new).and_return(instance_double(Fog::OpenStack::Image::V2)) 19 | end 20 | 21 | it 'uses V2 by default' do 22 | FogOpenStack.image 23 | 24 | expect(Fog::OpenStack::Image::V2).to have_received(:new) 25 | end 26 | end 27 | 28 | context 'when only V1 is supported' do 29 | before(:each) do 30 | allow(Fog::OpenStack::Image::V2).to receive(:new).and_raise(Fog::OpenStack::Errors::ServiceUnavailable) 31 | allow(Fog::OpenStack::Image::V1).to receive(:new).and_return(instance_double(Fog::OpenStack::Image::V1)) 32 | end 33 | 34 | it 'falls back to V1' do 35 | FogOpenStack.image 36 | 37 | expect(Fog::OpenStack::Image::V1).to have_received(:new) 38 | end 39 | end 40 | 41 | context 'when V2 raises other than ServiceUnavailable' do 42 | before(:each) do 43 | allow(Fog::OpenStack::Image::V1).to receive(:new) 44 | allow(Fog::OpenStack::Image::V2).to receive(:new).and_raise('some_error') 45 | end 46 | 47 | it 'raises' do 48 | expect { 49 | FogOpenStack.image 50 | }.to raise_error('some_error') 51 | end 52 | end 53 | 54 | context 'when a socket error occurs' do 55 | let(:openstack_params){ { 'auth_url' => 'http://some.url' } } 56 | 57 | before(:each) do 58 | allow(Fog::OpenStack::Image::V2).to receive(:new).and_raise(Excon::Errors::SocketError) 59 | end 60 | 61 | it 'includes the url on the error message' do 62 | expect { 63 | FogOpenStack.image 64 | }.to raise_error(Validator::Api::ValidatorError, /Could not connect to 'http:\/\/some.url' \nException message:.*\nBacktrace:/) 65 | end 66 | end 67 | end 68 | 69 | describe '.volume' do 70 | 71 | context 'when V2 is available' do 72 | before(:each) do 73 | allow(Fog::OpenStack::Volume::V2).to receive(:new).and_return(instance_double(Fog::OpenStack::Volume::V2)) 74 | end 75 | 76 | it 'uses V2 by default' do 77 | FogOpenStack.volume 78 | 79 | expect(Fog::OpenStack::Volume::V2).to have_received(:new) 80 | end 81 | end 82 | 83 | context 'when only V1 is supported' do 84 | before(:each) do 85 | allow(Fog::OpenStack::Volume::V2).to receive(:new).and_raise(Fog::OpenStack::Errors::ServiceUnavailable) 86 | allow(Fog::OpenStack::Volume::V1).to receive(:new).and_return(instance_double(Fog::OpenStack::Volume::V1)) 87 | end 88 | 89 | it 'falls back to V1' do 90 | FogOpenStack.volume 91 | 92 | expect(Fog::OpenStack::Volume::V1).to have_received(:new) 93 | end 94 | end 95 | 96 | context 'when V2 raises other than ServiceUnavailable' do 97 | before(:each) do 98 | allow(Fog::OpenStack::Volume::V1).to receive(:new) 99 | allow(Fog::OpenStack::Volume::V2).to receive(:new).and_raise('some_error') 100 | end 101 | 102 | it 'raises' do 103 | expect { 104 | FogOpenStack.volume 105 | }.to raise_error('some_error') 106 | end 107 | end 108 | 109 | context 'when a socket error occurs' do 110 | let(:openstack_params){ { 'auth_url' => 'http://some.url' } } 111 | 112 | before(:each) do 113 | allow(Fog::OpenStack::Volume::V2).to receive(:new).and_raise(Excon::Errors::SocketError) 114 | end 115 | 116 | it 'wraps the error' do 117 | expect { 118 | FogOpenStack.volume 119 | }.to raise_error(Validator::Api::ValidatorError, /Could not connect to 'http:\/\/some.url' \nException message:.*\nBacktrace:/) 120 | end 121 | end 122 | end 123 | 124 | describe '.compute' do 125 | context 'when a socket error occurs' do 126 | let(:openstack_params){ { 'auth_url' => 'http://some.url' } } 127 | 128 | before(:each) do 129 | allow(Fog::OpenStack::Compute).to receive(:new).and_raise(Excon::Errors::SocketError) 130 | end 131 | 132 | it 'wraps the error' do 133 | expect { 134 | FogOpenStack.compute 135 | }.to raise_error(Validator::Api::ValidatorError, /Could not connect to 'http:\/\/some.url' \nException message:.*\nBacktrace:/) 136 | end 137 | end 138 | 139 | context 'when correct openstack params are passed' do 140 | let(:openstack_params){ { 'auth_url' => 'http://some.url' } } 141 | it 'uses and converts those into FOG params' do 142 | expect(Fog::OpenStack::Compute).to receive(:new).with(hash_including(:openstack_auth_url => 'http://some.url')) 143 | FogOpenStack.compute 144 | end 145 | end 146 | end 147 | 148 | describe '.network' do 149 | context 'when a socket error occurs' do 150 | let(:openstack_params){ { 'auth_url' => 'http://some.url' } } 151 | 152 | before(:each) do 153 | allow(Fog::OpenStack::Network).to receive(:new).and_raise(Excon::Errors::SocketError) 154 | end 155 | 156 | it 'wraps the error' do 157 | expect { 158 | FogOpenStack.network 159 | }.to raise_error(Validator::Api::ValidatorError, /Could not connect to 'http:\/\/some.url' \nException message:.*\nBacktrace:/) 160 | end 161 | end 162 | end 163 | 164 | describe '.storage' do 165 | context 'when a socket error occurs' do 166 | let(:openstack_params){ { 'auth_url' => 'http://some.url' } } 167 | 168 | before(:each) do 169 | allow(Fog::OpenStack::Storage).to receive(:new).and_raise(Excon::Errors::SocketError) 170 | end 171 | 172 | it 'wraps the error' do 173 | expect { 174 | FogOpenStack.storage 175 | }.to raise_error(Validator::Api::ValidatorError, /Could not connect to 'http:\/\/some.url' \nException message:.*\nBacktrace:/) 176 | end 177 | end 178 | 179 | context 'when correct openstack params are passed' do 180 | let(:openstack_params){ { 'auth_url' => 'http://some.url' } } 181 | it 'uses and converts those into FOG params' do 182 | expect(Fog::OpenStack::Storage).to receive(:new).with(hash_including(:openstack_auth_url => 'http://some.url')) 183 | FogOpenStack.storage 184 | end 185 | end 186 | end 187 | 188 | describe '.with_openstack' do 189 | it 'calls the given block' do 190 | expect(FogOpenStack.with_openstack('some message') { 'Yeah!' }).to eq('Yeah!') 191 | end 192 | 193 | context 'when block raises an error' do 194 | let(:logger) { instance_double(Logger) } 195 | 196 | before do 197 | allow(Logger).to receive(:new).and_return(logger) 198 | allow(logger).to receive(:error) 199 | allow(RSpec::configuration).to receive(:options).and_return(double('options', cpi_bin_path: nil, log_path: 'some_file_path')) 200 | end 201 | 202 | it 're-raises error with the given error message and hint to log file' do 203 | expect{ 204 | FogOpenStack.with_openstack('some user-defined message') { raise 'original error message' } 205 | }.to raise_error("some user-defined message: More details can be found in 'some_file_path'") 206 | end 207 | 208 | it 'logs the original error message' do 209 | expect{ 210 | FogOpenStack.with_openstack('some user-defined message') { raise 'original error message' } 211 | }.to raise_error(/some user-defined message/) 212 | 213 | expect(logger).to have_received(:error).with('original error message') 214 | end 215 | 216 | context "when error type is 'Excon::Errors::Forbidden'" do 217 | let(:configuration) { instance_double(Validator::Api::Configuration, openstack: { 'username' => 'some-user' }) } 218 | 219 | before(:each) do 220 | allow(Validator::Api).to receive(:configuration).and_return(configuration) 221 | end 222 | it 're-raises error with helping message' do 223 | expect{ 224 | FogOpenStack.with_openstack('some user-defined message') { raise Excon::Errors::Forbidden.new 'original error message' } 225 | }.to raise_error("some user-defined message: The user 'some-user' does not have required permissions.") 226 | end 227 | end 228 | end 229 | end 230 | end 231 | end 232 | -------------------------------------------------------------------------------- /spec/unit/validator/api_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe 'API' do 4 | describe '.skip_test' do 5 | it 'should make test pending' do |test| 6 | expect(test.example_group_instance).to receive(:skip).with('some message') 7 | 8 | Validator::Api.skip_test('some message') 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /spec/unit/validator/cli/context_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | require 'securerandom' 3 | 4 | module Validator::Cli 5 | describe Context do 6 | let(:working_directory) { Dir.mktmpdir } 7 | let(:default_options) { 8 | {working_dir: working_directory} 9 | } 10 | let(:cli_options) { {} } 11 | let(:subject) { 12 | Context.new(cli_options.merge(default_options)) 13 | } 14 | 15 | after(:each) do 16 | if File.exists?(working_directory) 17 | FileUtils.rm_r(working_directory) 18 | end 19 | end 20 | 21 | describe '#cpi_bin_path' do 22 | it 'sets the default' do 23 | expect(subject.cpi_bin_path).to eq(File.join(subject.working_dir, 'cpi')) 24 | end 25 | end 26 | 27 | describe :openstack_cpi_bin_from_env do 28 | context 'when ENV var is set' do 29 | before do 30 | ENV['OPENSTACK_CPI_BIN'] = 'some-path' 31 | end 32 | 33 | after do 34 | ENV.delete('OPENSTACK_CPI_BIN') 35 | end 36 | 37 | it 'returns the value of OPENSTACK_CPI_BIN environment variable' do 38 | expect(subject.openstack_cpi_bin_from_env).to eq('some-path') 39 | end 40 | end 41 | 42 | context 'when ENV var is not set' do 43 | it 'returns nil' do 44 | expect(subject.openstack_cpi_bin_from_env).to be(nil) 45 | end 46 | end 47 | 48 | end 49 | 50 | describe :working_dir do 51 | context 'when path does not exist' do 52 | it 'creates a directory' do 53 | path = subject.working_dir 54 | 55 | expect(File.directory?(path)).to be(true) 56 | end 57 | end 58 | 59 | context 'when path points to an existing file' do 60 | it 'raises an error' do 61 | path = File.join(working_directory, '.cf-openstack-validator') 62 | FileUtils.touch(path) 63 | 64 | expect { 65 | Context.new(cli_options.merge({working_dir: path})) 66 | }.to raise_error Errno::EEXIST 67 | end 68 | end 69 | 70 | context 'when path points to an existing directory' do 71 | it 'does not raise' do 72 | path = File.join(working_directory, '.cf-openstack-validator') 73 | FileUtils.mkdir_p(path) 74 | expect { 75 | subject.working_dir 76 | }.to_not raise_error 77 | 78 | expect(File.directory?(path)).to be(true) 79 | end 80 | context 'when path is a relative path' do 81 | let(:working_directory) { 82 | "relative-working-directory-#{SecureRandom.uuid}" 83 | } 84 | 85 | before(:each) { 86 | FileUtils.mkdir_p(working_directory) 87 | } 88 | it 'is updated to an absolute path' do 89 | expect(subject.working_dir).to start_with('/') 90 | expect(subject.working_dir).to include(working_directory) 91 | end 92 | end 93 | end 94 | end 95 | 96 | describe '#path_environment' do 97 | it 'should return path environment' do 98 | expect(subject.path_environment).to eq("#{File.join(subject.working_dir, 'packages', 'ruby_openstack_cpi', 'bin')}:#{ENV['PATH']}") 99 | end 100 | end 101 | 102 | describe '#gems_folder' do 103 | it 'should return gems folder path' do 104 | expect(subject.gems_folder).to eq(File.join(subject.working_dir, 'packages', 'ruby_openstack_cpi', 'lib', 'ruby', 'gems', '*')) 105 | end 106 | end 107 | 108 | describe '#packages_path' do 109 | it 'should return gems folder path' do 110 | expect(subject.packages_path).to eq(File.join(subject.working_dir, 'packages')) 111 | end 112 | end 113 | 114 | describe '#create_validator_options' do 115 | let(:cli_options) do 116 | { 117 | config_path: 'validator_config_path', 118 | skip_cleanup: true, 119 | verbose: true 120 | } 121 | end 122 | 123 | it 'sets all options' do 124 | expected_options = Validator::Cli::Options.new( 125 | File.join(working_directory, 'packages'), 126 | File.join(working_directory, 'logs'), 127 | File.join(working_directory, 'stemcell'), 128 | File.join(working_directory, 'cpi'), 129 | 'validator_config_path', 130 | File.join(working_directory, 'jobs/openstack_cpi/config/cpi.json'), 131 | true, 132 | true 133 | ) 134 | 135 | expect(subject.create_validator_options).to eq(expected_options) 136 | end 137 | 138 | it 'returns a frozen Option instance' do 139 | expect(subject.create_validator_options.frozen?).to eq(true) 140 | end 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /spec/unit/validator/cli/error_with_log_details_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../spec_helper' 2 | 3 | module Validator::Cli 4 | describe ErrorWithLogDetails do 5 | 6 | subject { ErrorWithLogDetails.new(error_message, log_path) } 7 | describe '.new' do 8 | it 'raises without a log path' do 9 | expect{ ErrorWithLogDetails.new }.to raise_error(ArgumentError) 10 | end 11 | end 12 | 13 | describe '#log_path' do 14 | let(:log_path) { File.join('/tmp') } 15 | let(:error_message) { "Some error message" } 16 | it 'returns the log path' do 17 | expect(subject.log_path).to eq('/tmp') 18 | end 19 | end 20 | 21 | describe '#message' do 22 | let(:log_path) { 'a-log-path'} 23 | let(:error_message) { 'an error message' } 24 | 25 | it 'prints error message and log path' do 26 | expected_output = < { 10 | 'auth_url'=> '', 11 | 'username'=> '', 12 | 'password'=> '', 13 | 'domain'=> '', 14 | 'project'=> '' 15 | }, 16 | 'validator'=> { 17 | 'network_id' => '', 18 | 'floating_ip' => '', 19 | 'static_ip' => '', 20 | 'private_key_path' => '', 21 | 'releases' => [{ 22 | 'name' => 'bosh-openstack-cpi', 23 | 'url' => 'String', 24 | 'sha1' => 'String' 25 | }] 26 | }, 27 | 'cloud_config'=> { 28 | 'vm_types' => [{ 29 | 'name' => 'String', 30 | 'cloud_properties' => { 31 | 'instance_type' => '' 32 | } 33 | }] 34 | } 35 | } 36 | end 37 | 38 | it 'validates a given object' do 39 | expect { 40 | Validator::ConfigValidator.validate(valid_config) 41 | }.to_not raise_error 42 | end 43 | 44 | context 'when a required property is missing' do 45 | it 'returns an error' do 46 | invalid_config = valid_config 47 | invalid_config['openstack'].delete('auth_url') 48 | 49 | expect { 50 | Validator::ConfigValidator.validate(invalid_config) 51 | }.to raise_error(Validator::Api::ValidatorError, /auth_url => Missing/) 52 | end 53 | end 54 | 55 | context 'when a property has a wrong type' do 56 | it 'returns an error' do 57 | invalid_config = valid_config 58 | invalid_config['openstack']['auth_url'] = 5 59 | 60 | expect { 61 | Validator::ConfigValidator.validate(invalid_config) 62 | }.to raise_error(Validator::Api::ValidatorError, /auth_url => Expected instance of String/) 63 | end 64 | end 65 | 66 | context 'when an optional property has a wrong type' do 67 | it 'returns an error' do 68 | invalid_config = valid_config 69 | invalid_config['openstack']['stemcell_public_visibility'] = 'hello' 70 | 71 | expect { 72 | Validator::ConfigValidator.validate(invalid_config) 73 | }.to raise_error(Validator::Api::ValidatorError, /stemcell_public_visibility => Expected instance of true or false/) 74 | end 75 | end 76 | 77 | context 'when cpi release name has a wrong value' do 78 | it 'returns an error' do 79 | invalid_config = valid_config 80 | invalid_config['validator']['releases'][0]['name'] = 'wrong-name' 81 | 82 | expect { 83 | Validator::ConfigValidator.validate(invalid_config) 84 | }.to raise_error(Validator::Api::ValidatorError, "#{error_msg_prefix}{ validator => { releases => At index 0: { name => Expected bosh-openstack-cpi, given wrong-name } } }") 85 | end 86 | end 87 | 88 | { 89 | 'openstack' => ['auth_url', 'username', 'password', 'domain', 'project'], 90 | 'validator' => ['network_id', 'floating_ip', 'static_ip'] 91 | }.each do |outer_key, inner_keys| 92 | inner_keys.each do |inner_key| 93 | context "when value '#{outer_key}.#{inner_key}' is ''" do 94 | it 'returns an error' do 95 | invalid_config = valid_config 96 | invalid_config[outer_key][inner_key] = '' 97 | 98 | expect { 99 | Validator::ConfigValidator.validate(invalid_config) 100 | }.to raise_error(Validator::Api::ValidatorError, "#{error_msg_prefix}{ #{outer_key} => { #{inner_key} => Found placeholder '' } }") 101 | end 102 | end 103 | end 104 | end 105 | 106 | context "when value 'cloud_config.vm_types[0].cloud_properties.instance_type' is ''" do 107 | it 'returns an error' do 108 | invalid_config = valid_config 109 | invalid_config['cloud_config']['vm_types'][0]['cloud_properties']['instance_type'] = '' 110 | 111 | expect { 112 | Validator::ConfigValidator.validate(invalid_config) 113 | }.to raise_error(Validator::Api::ValidatorError, "#{error_msg_prefix}{ cloud_config => { vm_types => At index 0: { cloud_properties => { instance_type => Found placeholder '' } } } }") 114 | end 115 | end 116 | 117 | context "when value 'extensions.paths[0]' is ''" do 118 | it 'returns an error' do 119 | invalid_config = valid_config 120 | invalid_config['extensions'] = {'paths' => ['']} 121 | 122 | expect { 123 | Validator::ConfigValidator.validate(invalid_config) 124 | }.to raise_error(Validator::Api::ValidatorError, "#{error_msg_prefix}{ extensions => { paths => At index 0: Found placeholder '' } }") 125 | end 126 | end 127 | 128 | context "when value 'cloud_config.vm_types[0].cloud_properties.root_disk.size' is a number" do 129 | it 'does not return an error' do 130 | valid_config['cloud_config']['vm_types'][0]['cloud_properties']['root_disk'] = { 'size' => 42 } 131 | 132 | expect { 133 | Validator::ConfigValidator.validate(valid_config) 134 | }.to_not raise_error 135 | end 136 | end 137 | 138 | context "when value 'cloud_config.vm_types[0].cloud_properties.root_disk.size' is a string" do 139 | it 'returns an error' do 140 | invalid_config = valid_config 141 | invalid_config['cloud_config']['vm_types'][0]['cloud_properties']['root_disk'] = { 'size' => 'some-string' } 142 | 143 | expect { 144 | Validator::ConfigValidator.validate(invalid_config) 145 | }.to raise_error(Validator::Api::ValidatorError, /size => Expected instance of Numeric, given an instance of String/) 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /spec/unit/validator/extensions_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe Validator::Extensions do 4 | 5 | before(:each) do 6 | @tmpdir = Dir.mktmpdir 7 | @cf_openstack_validator = File.join(@tmpdir, 'cf-openstack-validator') 8 | FileUtils.mkdir(@cf_openstack_validator) 9 | @validator_config = File.join(@cf_openstack_validator, 'validator.yml') 10 | 11 | allow(RSpec.configuration).to receive(:validator_config).and_return(Validator::Api::Configuration.new(@validator_config)) 12 | end 13 | 14 | after(:each) do 15 | FileUtils.rm_rf(@tmpdir) 16 | end 17 | 18 | describe '.all' do 19 | 20 | let(:absolute_path_to_extensions) { Dir.mktmpdir } 21 | let(:validator_config_content) do 22 | <<-EOF 23 | extensions: 24 | paths: [#{absolute_path_to_extensions}] 25 | EOF 26 | end 27 | 28 | before(:each) do 29 | File.write(@validator_config, validator_config_content) 30 | end 31 | 32 | after(:each) do 33 | FileUtils.rmtree(absolute_path_to_extensions) 34 | end 35 | 36 | context 'when there is no `extensions` section in the `validator.yml`' do 37 | 38 | let(:validator_config_content) { "---\n{}" } 39 | 40 | it 'returns no specs' do 41 | expect(Validator::Extensions.all.size).to eq(0) 42 | end 43 | end 44 | 45 | context 'when there is an `extensions` section in the `validator.yml`' do 46 | 47 | context 'when no path is given' do 48 | let(:validator_config_content) do 49 | <<-EOF 50 | extensions: 51 | paths: [] 52 | EOF 53 | end 54 | 55 | it 'returns no specs' do 56 | expect(Validator::Extensions.all.size).to eq(0) 57 | end 58 | end 59 | 60 | context 'when a path is given' do 61 | 62 | context 'and is absolute' do 63 | 64 | let(:absolute_path_to_extensions) { Dir.mktmpdir } 65 | let(:validator_config_content) do 66 | <<-EOF 67 | extensions: 68 | paths: [#{absolute_path_to_extensions}] 69 | EOF 70 | end 71 | 72 | 73 | context 'and contains no _spec.rb files' do 74 | it 'returns no specs' do 75 | expect(Validator::Extensions.all.size).to eq(0) 76 | end 77 | end 78 | 79 | context 'and contains multiple _spec.rb files' do 80 | before do 81 | FileUtils.touch(File.join(absolute_path_to_extensions, 'test1_spec.rb')) 82 | FileUtils.touch(File.join(absolute_path_to_extensions, 'test2_spec.rb')) 83 | end 84 | 85 | it 'returns all specs' do 86 | specs = Validator::Extensions.all 87 | expect(specs.size).to eq(2) 88 | expect(specs).to include("#{absolute_path_to_extensions}/test1_spec.rb", "#{absolute_path_to_extensions}/test2_spec.rb") 89 | end 90 | 91 | context 'and also contains non-spec files' do 92 | before do 93 | FileUtils.touch(File.join(absolute_path_to_extensions, 'some-file')) 94 | end 95 | 96 | it 'returns only the spec files' do 97 | specs = Validator::Extensions.all 98 | expect(specs.size).to equal(2) 99 | expect(specs).to include("#{absolute_path_to_extensions}/test1_spec.rb", "#{absolute_path_to_extensions}/test2_spec.rb") 100 | end 101 | end 102 | end 103 | 104 | context 'and folder does not exist' do 105 | it 'returns no specs' do 106 | specs = Validator::Extensions.all 107 | expect(specs.size).to eq(0) 108 | end 109 | end 110 | end 111 | 112 | context 'and is relative' do 113 | let(:validator_config_content) do 114 | <<-EOF 115 | extensions: 116 | paths: [../my-extensions] 117 | EOF 118 | end 119 | 120 | before(:each) do 121 | my_extensions = File.join(@tmpdir, 'my-extensions') 122 | FileUtils.mkdir(my_extensions) 123 | @non_default_spec = File.join(my_extensions, 'my_spec.rb') 124 | FileUtils.touch(@non_default_spec) 125 | end 126 | 127 | it 'returns all specs' do 128 | specs = Validator::Extensions.all 129 | expect(specs.size).to eq(1) 130 | expect(specs).to eq([@non_default_spec]) 131 | end 132 | 133 | after(:each) do 134 | FileUtils.rmtree(File.join(@tmpdir, 'my-extensions')) 135 | end 136 | end 137 | end 138 | end 139 | 140 | end 141 | 142 | describe '.eval' do 143 | let(:verbose) {false} 144 | 145 | before do 146 | @extensionsdir = File.join(@tmpdir, 'extensions') 147 | FileUtils.mkdir(@extensionsdir) 148 | @specs = [ 149 | File.join(@extensionsdir, 'test1_spec.rb'), 150 | File.join(@extensionsdir, 'test2_spec.rb') 151 | ] 152 | 153 | @specs.each { |spec| FileUtils.touch(spec) } 154 | 155 | File.write(@validator_config, '---') 156 | 157 | allow(RSpec::configuration).to receive(:options).and_return(double('options', verbose?: verbose)) 158 | end 159 | 160 | it 'tells which extension it is running' do 161 | expect { 162 | Validator::Extensions.eval(@specs, binding) 163 | }.to output("Evaluating extension: #{@extensionsdir}/test1_spec.rb\nEvaluating extension: #{@extensionsdir}/test2_spec.rb\n").to_stdout 164 | end 165 | 166 | context 'when extension evaluation raises an exception' do 167 | before do 168 | File.write(File.join(@extensionsdir, 'a_syntax_error_spec.rb'), '%fa#23') 169 | @specs << File.join(@extensionsdir, 'a_syntax_error_spec.rb') 170 | end 171 | 172 | it 'returns an error object' do 173 | allow($stdout).to receive(:puts) 174 | expect{ 175 | Validator::Extensions.eval(@specs, binding) 176 | }.to raise_error(SyntaxError) 177 | end 178 | 179 | it 'prints to the error to stdout' do 180 | expect{ 181 | begin 182 | Validator::Extensions.eval(@specs, binding) 183 | rescue SyntaxError 184 | # not relevant for test 185 | end 186 | }.to output(/unknown type of %string\n%fa#23\n /).to_stdout 187 | end 188 | 189 | context 'when verbose option is true' do 190 | let(:verbose) {true} 191 | 192 | it 'prints the errors backtrace to stdout' do 193 | expect { 194 | begin 195 | Validator::Extensions.eval(@specs, binding) 196 | rescue SyntaxError 197 | # not relevant for test 198 | end 199 | }.to output(/:in `eval'/).to_stdout 200 | end 201 | end 202 | end 203 | end 204 | end -------------------------------------------------------------------------------- /spec/unit/validator/external_cpi_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe Validator::ExternalCpi do 4 | 5 | let(:tmpdir) { Dir.mktmpdir } 6 | let(:log_file) { File.join(tmpdir, 'testsuite.log') } 7 | let(:cpi_task_log_path) { File.join(tmpdir, 'task.log') } 8 | let(:logger) { Logger.new(log_file) } 9 | let(:cpi_path) { '/path/to/cpi' } 10 | let(:stats_log_path) { File.join(tmpdir, 'stats.log') } 11 | let(:response) { 12 | { 13 | 'result' => '', 14 | 'error' => { 15 | 'type' => 'Bosh::Clouds::NotSupported', 16 | 'message' => 'The given method is not supported', 17 | 'ok_to_retry' => false 18 | }, 19 | 'log' => '' 20 | }.to_json 21 | } 22 | 23 | subject { 24 | Validator::ExternalCpi.new(cpi_path, logger, cpi_task_log_path, stats_log_path) 25 | } 26 | 27 | before(:each) { 28 | FileUtils.touch(log_file) 29 | FileUtils.touch(cpi_task_log_path) 30 | allow(Open3).to receive(:capture3).and_return([response, nil, nil]) 31 | } 32 | 33 | after(:each) { 34 | FileUtils.rm_rf(tmpdir) 35 | } 36 | 37 | context 'when the cpi is not executable' do 38 | it 'raises' do 39 | expect{ 40 | subject.current_vm_id 41 | }.to raise_error(Validator::ExternalCpi::NonExecutable) 42 | end 43 | end 44 | 45 | context 'when the cpi responce is not a hash' do 46 | let(:response) { {} } 47 | before do 48 | allow(File).to receive(:executable?).with(cpi_path).and_return(cpi_path) 49 | end 50 | 51 | it 'raises' do 52 | expect{ 53 | subject.current_vm_id 54 | }.to raise_error(Validator::ExternalCpi::InvalidResponse) 55 | end 56 | end 57 | 58 | context 'when the cpi returns an error' do 59 | before do 60 | allow(File).to receive(:executable?).with(cpi_path).and_return(cpi_path) 61 | end 62 | 63 | it 'raises' do 64 | expect{ 65 | subject.current_vm_id 66 | }.to raise_error(Validator::ExternalCpi::CpiError, "CPI error 'Bosh::Clouds::NotSupported' with message 'The given method is not supported' in 'current_vm_id' CPI method") 67 | end 68 | end 69 | 70 | context 'when the cpi does not return an error' do 71 | let(:response) { 72 | { 73 | 'result' => '', 74 | 'error' => nil, 75 | 'log' => '' 76 | }.to_json 77 | } 78 | 79 | before do 80 | allow(File).to receive(:executable?).with(cpi_path).and_return(cpi_path) 81 | end 82 | 83 | it 'sets a director_uuid in the context' do 84 | subject.current_vm_id 85 | 86 | expect(Open3).to have_received(:capture3).with(anything, anything, contains_director_uuid('validator')) 87 | end 88 | 89 | context 'logging stats' do 90 | let(:start_time) { Time.new(2016,12,12,1,0,0).utc } 91 | let(:end_time) { Time.new(2016,12,12,1,1,30).utc } 92 | let(:duration) { (end_time - start_time) } 93 | let(:response) { 94 | { 95 | 'result' => '', 96 | 'error' => nil, 97 | 'log' => '' 98 | }.to_json 99 | } 100 | 101 | before(:each) do 102 | allow(subject).to receive(:generate_request_id).and_return('777777') 103 | allow(Benchmark).to receive(:measure) do |&block| 104 | block.call 105 | instance_double(Benchmark::Tms, real: duration) 106 | end 107 | end 108 | 109 | it 'logs the data to the given path' do 110 | subject.current_vm_id('1', '2', '3', '4') 111 | 112 | expect(JSON.load(File.read(stats_log_path))).to eq({ 113 | 'request' => { 114 | 'method' => 'current_vm_id', 115 | 'arguments' => ['1', '2', '3', '4'], 116 | 'context' => { 117 | 'director_uuid' => 'validator', 118 | 'request_id' => '777777' 119 | } 120 | }, 121 | 'duration' => 90 122 | }) 123 | end 124 | 125 | it 'appends additional calls to the file' do 126 | subject.current_vm_id('1', '2', '3', '4') 127 | subject.current_vm_id('6', '7', '8', '9') 128 | 129 | calls = File.read(stats_log_path).split("\n") 130 | 131 | expect(JSON.load(calls[0])).to eq({ 132 | 'request' => { 133 | 'method' => 'current_vm_id', 134 | 'arguments' => ['1', '2', '3', '4'], 135 | 'context' => { 136 | 'director_uuid' => 'validator', 137 | 'request_id' => '777777' 138 | } 139 | }, 140 | 'duration' => 90 141 | }) 142 | 143 | expect(JSON.load(calls[1])).to eq({ 144 | 'request' => { 145 | 'method' => 'current_vm_id', 146 | 'arguments' => ['6', '7', '8', '9'], 147 | 'context' => { 148 | 'director_uuid' => 'validator', 149 | 'request_id' => '777777' 150 | } 151 | }, 152 | 'duration' => 90 153 | }) 154 | end 155 | end 156 | end 157 | end 158 | 159 | RSpec::Matchers.define :contains_director_uuid do |value| 160 | match do |actual| 161 | request = JSON.load(actual[:stdin_data]) 162 | request['context']['director_uuid'] == value 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /spec/unit/validator/formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe Validator::TestsuiteFormatter do 4 | 5 | before(:each) { 6 | allow(RSpec::configuration).to receive(:options).and_return(double('options')) 7 | } 8 | 9 | subject { 10 | Validator::TestsuiteFormatter.new output 11 | } 12 | 13 | let(:output) { 14 | output = StringIO.new 15 | } 16 | 17 | describe '#example_started' do 18 | let(:notification) { 19 | instance_double(RSpec::Core::Notifications::ExampleNotification) 20 | } 21 | 22 | let(:example) { double('example', description: 'example_name') } 23 | 24 | it 'prints the identation and example name' do 25 | allow(notification).to receive(:example).and_return(example) 26 | allow(subject).to receive(:current_indentation).and_return('IDENTATION') 27 | 28 | subject.example_started(notification) 29 | 30 | expect(output.string).to eq('IDENTATIONexample_name... ') 31 | end 32 | end 33 | 34 | describe '#example_passed' do 35 | it 'it prints `passed` in success color' do 36 | allow(RSpec::Core::Formatters::ConsoleCodes).to receive(:wrap).and_call_original 37 | 38 | subject.example_passed(nil) 39 | 40 | expect(RSpec::Core::Formatters::ConsoleCodes).to have_received(:wrap).with(anything, :success) 41 | expect(output.string).to eq("passed\n") 42 | end 43 | end 44 | 45 | describe '#example_failed' do 46 | it 'it prints `failed` in failure color' do 47 | allow(RSpec::Core::Formatters::ConsoleCodes).to receive(:wrap).and_call_original 48 | 49 | subject.example_failed(nil) 50 | 51 | expect(RSpec::Core::Formatters::ConsoleCodes).to have_received(:wrap).with(anything, :failure) 52 | expect(output.string).to eq("failed\n") 53 | end 54 | end 55 | 56 | describe '#example_pending' do 57 | 58 | let(:notification) { 59 | execution_result = double('execution_result', pending_message: 'pending_message') 60 | example = double('example', execution_result: execution_result) 61 | instance_double(RSpec::Core::Notifications::FailedExampleNotification, example:example) 62 | } 63 | 64 | it 'it prints the skipping reason in pending color' do 65 | allow(RSpec::Core::Formatters::ConsoleCodes).to receive(:wrap).and_call_original 66 | 67 | subject.example_pending(notification) 68 | 69 | expect(RSpec::Core::Formatters::ConsoleCodes).to have_received(:wrap).with(anything, :pending) 70 | expect(output.string).to eq("skipped: pending_message\n") 71 | end 72 | end 73 | 74 | describe '#dump_failures' do 75 | 76 | let(:notification) { 77 | instance_double(RSpec::Core::Notifications::ExamplesNotification) 78 | } 79 | 80 | let(:verbose) { false } 81 | 82 | before do 83 | allow(RSpec::configuration).to receive(:options).and_return(double('options', verbose?: verbose)) 84 | end 85 | 86 | context 'when no failure occurred' do 87 | it 'should not print anything' do 88 | allow(notification).to receive(:failure_notifications).and_return([]) 89 | 90 | subject.dump_failures(notification) 91 | 92 | expect(output.string).to be_empty 93 | end 94 | end 95 | 96 | context 'when there is a failure' do 97 | let(:failure_notification) { mock_failure_notification('Failure description', "Failure exception\nover multiple lines") } 98 | 99 | it 'should report only the error number, error description and the error message' do 100 | allow(notification).to receive(:failure_notifications).and_return([failure_notification]) 101 | 102 | subject.dump_failures(notification) 103 | 104 | expect(output.string).to eq( 105 | "\nFailures:\n" \ 106 | "\n" \ 107 | " 1) Failure description\n" \ 108 | " Failure exception\n" \ 109 | " over multiple lines\n" 110 | ) 111 | end 112 | 113 | context 'and VERBOSE_FORMATTER is used' do 114 | let(:verbose) { true } 115 | 116 | let(:failure_notification) { instance_double(RSpec::Core::Notifications::FailedExampleNotification) } 117 | 118 | it 'should report full stacktrace' do 119 | expect(failure_notification).to receive(:fully_formatted).and_return('some backtrace') 120 | allow(notification).to receive(:failure_notifications).and_return([failure_notification]) 121 | 122 | subject.dump_failures(notification) 123 | 124 | expect(output.string).to eq("\nFailures:\n"\ 125 | "some backtrace\n" 126 | ) 127 | end 128 | end 129 | end 130 | 131 | context 'when there are multiple failures' do 132 | let(:failure_notification1) { mock_failure_notification('Failure description1', 'Failure exception1') } 133 | let(:failure_notification2) { mock_failure_notification('Failure description2', 'Failure exception2') } 134 | 135 | let(:verbose) { false } 136 | 137 | it 'should report only the error number, error description and the error message' do 138 | allow(notification).to receive(:failure_notifications).and_return([failure_notification1, failure_notification2]) 139 | 140 | subject.dump_failures(notification) 141 | 142 | expect(output.string).to eq("\nFailures:\n" \ 143 | "\n" \ 144 | " 1) Failure description1\n" \ 145 | " Failure exception1\n" \ 146 | "\n" \ 147 | " 2) Failure description2\n" \ 148 | " Failure exception2\n" 149 | ) 150 | end 151 | 152 | context 'and VERBOSE_FORMATTER is used' do 153 | let(:verbose) { true } 154 | 155 | let(:failure_notification1) { instance_double(RSpec::Core::Notifications::FailedExampleNotification) } 156 | let(:failure_notification2) { instance_double(RSpec::Core::Notifications::FailedExampleNotification) } 157 | 158 | it 'should report full stacktrace' do 159 | expect(failure_notification1).to receive(:fully_formatted).and_return("some backtrace\n") 160 | expect(failure_notification2).to receive(:fully_formatted).and_return("some other backtrace\n") 161 | allow(notification).to receive(:failure_notifications).and_return([failure_notification1, failure_notification2]) 162 | 163 | subject.dump_failures(notification) 164 | 165 | expect(output.string).to eq("\nFailures:\n"\ 166 | "some backtrace\n"\ 167 | "some other backtrace\n" 168 | ) 169 | end 170 | end 171 | end 172 | 173 | def mock_failure_notification(description, exception_message) 174 | failure_notification = instance_double(RSpec::Core::Notifications::FailedExampleNotification) 175 | allow(failure_notification).to receive(:description).and_return(description) 176 | allow(failure_notification).to receive(:exception).and_return(Exception.new(exception_message)) 177 | failure_notification 178 | end 179 | end 180 | 181 | describe '#dump_pending' do 182 | it 'should not produce output' do 183 | # Allow exactly ***no*** interaction with the notification object 184 | notification = instance_double(RSpec::Core::Notifications::ExamplesNotification) 185 | 186 | subject.dump_pending(notification) 187 | 188 | expect(output.string).to be_empty 189 | end 190 | end 191 | 192 | describe '#dump_summary' do 193 | 194 | let(:failure_count) { 0 } 195 | let(:summary) { instance_double(RSpec::Core::Notifications::SummaryNotification) } 196 | let(:resources) { instance_double(Validator::Resources) } 197 | 198 | before(:each) do 199 | allow_any_instance_of(RSpec::Core::Configuration).to receive(:validator_resources).and_return(resources) 200 | 201 | allow(resources).to receive(:summary).and_return('resources-summary') 202 | allow(summary).to receive(:formatted_duration).and_return('47.11') 203 | allow(summary).to receive(:formatted_load_time).and_return('11.47') 204 | allow(summary).to receive(:failure_count).and_return(failure_count) 205 | allow(summary).to receive(:colorized_totals_line).and_return('3 examples, 1 failures, 1 pending') 206 | end 207 | 208 | it 'should report successful, pending and failing messages' do 209 | subject.dump_summary(summary) 210 | 211 | expect(output.string).to include("\nFinished in 47.11 (files took 11.47 to load)\n3 examples, 1 failures, 1 pending\n") 212 | end 213 | 214 | it 'gets the summary from the resource tracker' do 215 | subject.dump_summary(summary) 216 | 217 | expect(output.string).to include('resources-summary') 218 | end 219 | 220 | context 'with test failures' do 221 | 222 | let(:failure_count) { 1 } 223 | 224 | before(:each) do 225 | allow(RSpec::configuration).to receive(:options).and_return(double('options', log_path: 'test/path')) 226 | end 227 | 228 | it 'points the user to the log file' do 229 | subject.dump_summary(summary) 230 | 231 | expect(output.string).to match(/You can find more information in the logs at test\/path\/testsuite.log/) 232 | end 233 | end 234 | end 235 | end -------------------------------------------------------------------------------- /spec/unit/validator/instrumentor_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | describe Validator::Instrumentor do 3 | describe '.instrument' do 4 | let(:name) { 'foo' } 5 | let(:params) { { foo: 'bar'} } 6 | let(:logger) { instance_double(Logger) } 7 | let(:log_path) { Dir.mktmpdir } 8 | let(:options) { instance_double('options', log_path: log_path) } 9 | 10 | subject { Validator::Instrumentor } 11 | 12 | before(:each) do 13 | allow_any_instance_of(RSpec::Core::Configuration).to receive(:options).and_return(options) 14 | allow(Validator::Instrumentor).to receive(:logger).and_return(logger) 15 | allow(logger).to receive(:debug) 16 | end 17 | 18 | after(:each) do 19 | FileUtils.rm_rf(log_path) 20 | end 21 | 22 | it 'logs requests' do 23 | subject.instrument(name, params) 24 | 25 | expect(logger).to have_received(:debug).with("#{name} #{params}") 26 | end 27 | 28 | context 'with a block' do 29 | it 'logs performance when given a block' do 30 | subject.instrument(name, params) { 'this is a block' } 31 | 32 | expect(File.readable?(File.join(log_path, 'fog_stats.log'))).to be_truthy 33 | end 34 | end 35 | 36 | context 'without a block' do 37 | it 'logs performance when given a block' do 38 | subject.instrument(name, params) 39 | 40 | expect(File.readable?(File.join(log_path, 'fog_stats.log'))).to be_falsy 41 | end 42 | end 43 | 44 | it 'does not manipulate the original hash' do 45 | params_with_body = { body: '{}' } 46 | old_body = params_with_body.fetch(:body) 47 | 48 | subject.instrument(name, params_with_body) 49 | 50 | expect(params_with_body.fetch(:body)).to be(old_body) 51 | end 52 | 53 | context 'with non-json text in body' do 54 | let(:params) { { 55 | body: 'non-json', 56 | headers: { 57 | 'Content-Type' => 'text/plain' 58 | } 59 | } } 60 | 61 | it 'returns original body' do 62 | redacted_params = subject.redact(params) 63 | 64 | expect(redacted_params[:body]).to eq(params[:body]) 65 | end 66 | end 67 | 68 | context 'when content is not valid JSON' do 69 | let(:params) { { 70 | body: 'non-json', 71 | headers: { 72 | 'Content-Type' => 'application/json' 73 | } 74 | } } 75 | 76 | it 'returns the original body' do 77 | redacted_params = subject.redact(params) 78 | 79 | expect(redacted_params[:body]).to eq(params[:body]) 80 | end 81 | end 82 | 83 | context 'with v2 password in body' do 84 | let(:body) { { 85 | auth: { 86 | passwordCredentials: { 87 | password: 'my-password' 88 | } 89 | } 90 | } } 91 | let(:params) { { 92 | body: JSON.dump(body), 93 | headers: { 94 | 'Content-Type' => 'application/json' 95 | } 96 | } } 97 | 98 | it 'redacts v2 password' do 99 | redacted_params = subject.redact(params) 100 | 101 | parsed_body = JSON.parse(redacted_params[:body]) 102 | expect(parsed_body['auth']['passwordCredentials']['password']).to eq('') 103 | end 104 | end 105 | 106 | context 'with non-string body (could be File)' do 107 | it 'does nothing' do 108 | params_with_file_body = {body: 5} 109 | 110 | subject.instrument(name, params_with_file_body) 111 | 112 | expect(logger).to have_received(:debug).with("#{name} #{params_with_file_body}") 113 | end 114 | end 115 | 116 | context 'with v3 password in body' do 117 | let(:body) { { 118 | auth: { 119 | identity: { 120 | password: { 121 | user: { 122 | password: 'my-password' 123 | } 124 | } 125 | } 126 | } 127 | } } 128 | let(:params) { { 129 | body: JSON.dump(body), 130 | headers: { 131 | 'Content-Type' => 'application/json' 132 | } 133 | } } 134 | 135 | it 'redacts v3 password' do 136 | redacted_params = subject.redact(params) 137 | 138 | parsed_body = JSON.parse(redacted_params[:body]) 139 | expect(parsed_body['auth']['identity']['password']['user']['password']).to eq('') 140 | end 141 | 142 | end 143 | 144 | context 'with server.user_data in body' do 145 | let(:body) { { 146 | server: { 147 | user_data: 'user data' 148 | } 149 | } } 150 | let(:params) { { 151 | body: JSON.dump(body), 152 | headers: { 153 | 'Content-Type' => 'application/json' 154 | } 155 | } } 156 | 157 | it 'redacts server.user_data' do 158 | redacted_params = subject.redact(params) 159 | 160 | parsed_body = JSON.parse(redacted_params[:body]) 161 | expect(parsed_body['server']['user_data']).to eq('') 162 | end 163 | end 164 | 165 | context 'with x-auth-token in header' do 166 | let(:headers) { { 167 | 'X-Auth-Token' => 'token' 168 | } } 169 | let(:params) { { headers: headers } } 170 | 171 | it 'redacts params' do 172 | redacted_params = subject.redact(params) 173 | 174 | expect(redacted_params[:headers]['X-Auth-Token']).to eq('') 175 | end 176 | 177 | end 178 | 179 | it 'yields' do 180 | expect { |b| Validator::Instrumentor.instrument(name, params, &b) }.to yield_control 181 | end 182 | 183 | end 184 | end 185 | -------------------------------------------------------------------------------- /spec/unit/validator/network_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | describe Validator::NetworkHelper do 4 | describe '.next_free_ephemeral_port' do 5 | 6 | it 'uses the right Addrinfo for socket binding' do 7 | addr_info = Addrinfo.tcp('127.0.0.1', 0) 8 | expect(Addrinfo).to receive(:tcp).with('127.0.0.1', 0).and_return(addr_info) 9 | expect_any_instance_of(Socket).to receive(:bind).with(addr_info) 10 | 11 | Validator::NetworkHelper.next_free_ephemeral_port 12 | end 13 | 14 | it 'returns next ephemeral port number' do 15 | expect_any_instance_of(Socket).to receive(:local_address).and_return(Addrinfo.tcp('127.0.0.1', 4444)) 16 | 17 | expect(Validator::NetworkHelper.next_free_ephemeral_port).to eq(4444) 18 | end 19 | 20 | end 21 | 22 | describe '.vm_ip_to_ssh' do 23 | context 'use_external_ip is true' do 24 | it 'returns the configured floating ip' do 25 | config = double('config', :validator => {'use_external_ip' => true, 'floating_ip' => 'some-floating-ip'}) 26 | 27 | vm_ip_to_ssh = Validator::NetworkHelper.vm_ip_to_ssh('some-vm-id', config, nil) 28 | 29 | expect(vm_ip_to_ssh).to eq('some-floating-ip') 30 | end 31 | end 32 | 33 | context 'use_external_ip is false' do 34 | it 'returns the vm private ip' do 35 | config = double('config', :validator => {'use_external_ip' => false}) 36 | server = double('server', :addresses => {'vm-address' => [{'addr' => 'vm-private-ip'}]}) 37 | compute = double('compute', :servers => double('servers', :get => server)) 38 | 39 | vm_ip_to_ssh = Validator::NetworkHelper.vm_ip_to_ssh('some-vm-id', config, compute) 40 | 41 | expect(vm_ip_to_ssh).to eq('vm-private-ip') 42 | end 43 | end 44 | end 45 | 46 | describe 'security groups' do 47 | let(:port_range_max) { 54 } 48 | let(:port_range_min) { 50 } 49 | let(:remote_group_id) { 'some-remote-group-id' } 50 | let(:security_group_rule) { double('rule', :direction => 'ingress', :ethertype => 'IPv4', :protocol => 'tcp', :port_range_min => port_range_min, :port_range_max => port_range_max, :remote_group_id => remote_group_id) } 51 | let(:security_group) { double('security_group', :name => 'some-sg', :security_group_rules => [security_group_rule]) } 52 | let(:network) { double('network', :security_groups => [security_group]) } 53 | 54 | describe '.port_open_in_any_security_group?' do 55 | 56 | it 'returns true when port is open' do 57 | port_open = Validator::NetworkHelper.port_open_in_any_security_group?('ingress', 53, 'tcp', ['some-sg'], network) 58 | 59 | expect(port_open).to eq(true) 60 | end 61 | 62 | it 'returns false when port is closed' do 63 | port_open = Validator::NetworkHelper.port_open_in_any_security_group?('ingress', 49, 'tcp', ['some-sg'], network) 64 | 65 | expect(port_open).to eq(false) 66 | end 67 | end 68 | 69 | describe '.ssh_port_open?' do 70 | let(:port_range_max) { 22 } 71 | let(:port_range_min) { 22 } 72 | let(:use_external_ip) { true } 73 | 74 | before(:each) do 75 | allow(Validator::Api).to receive(:configuration).and_return(double('configuration', :validator => {'use_external_ip' => use_external_ip})) 76 | end 77 | 78 | context 'use_external_ip is true' do 79 | context 'ssh allowed from everywhere' do 80 | let(:remote_group_id) { nil } 81 | 82 | it 'returns true' do 83 | port_open = Validator::NetworkHelper.ssh_port_open?(['some-sg'], network) 84 | 85 | expect(port_open).to eq(true) 86 | end 87 | end 88 | 89 | context 'ssh is not allowed from everywhere' do 90 | it 'returns false' do 91 | port_open = Validator::NetworkHelper.ssh_port_open?(['some-sg'], network) 92 | 93 | expect(port_open).to eq(false) 94 | end 95 | end 96 | end 97 | 98 | context 'use_external_ip is false' do 99 | let(:use_external_ip) { false } 100 | 101 | context 'ssh allowed from everywhere' do 102 | let(:remote_group_id) { nil } 103 | 104 | it 'returns true' do 105 | port_open = Validator::NetworkHelper.ssh_port_open?(['some-sg'], network) 106 | 107 | expect(port_open).to eq(true) 108 | end 109 | end 110 | 111 | context 'ssh is not allowed from everywhere' do 112 | it 'returns true' do 113 | port_open = Validator::NetworkHelper.ssh_port_open?(['some-sg'], network) 114 | 115 | expect(port_open).to eq(true) 116 | end 117 | end 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /spec/unit/validator/redactor_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | describe Validator::Redactor do 3 | 4 | subject { Validator::Redactor } 5 | let(:hash) { 6 | { 7 | 'a' => { 8 | 'b' => { 9 | 'property' => 'secret' 10 | } 11 | } 12 | } 13 | } 14 | 15 | let(:hash_with_symbols) { 16 | { 17 | :a => { 18 | :b => { 19 | :property => 'secret' 20 | } 21 | } 22 | } 23 | } 24 | 25 | describe '.redact' do 26 | it 'redacts a given paths from the given hash' do 27 | redacted_hash = subject.redact(hash, 'a.b.property') 28 | 29 | expect(redacted_hash).to be(hash) 30 | expect(redacted_hash['a']['b']['property']).to eq('') 31 | end 32 | 33 | context 'when given property does not exist' do 34 | let(:hash) { {} } 35 | it 'does not add the redacted string' do 36 | 37 | redacted_hash = subject.redact(hash, 'property') 38 | 39 | expect(redacted_hash['property']).to be_nil 40 | end 41 | end 42 | 43 | context 'given hash with symbols' do 44 | 45 | it 'does not redact a given path from the given hash' do 46 | redacted_hash = subject.redact(hash_with_symbols, 'a.b.property') 47 | 48 | expect(redacted_hash).to be(hash_with_symbols) 49 | expect(redacted_hash[:a][:b][:property]).to eq('secret') 50 | end 51 | end 52 | end 53 | 54 | describe '.clone_and_redact' do 55 | it 'clones and redacts a given paths from the given hash' do 56 | redacted_hash = subject.clone_and_redact(hash, 'a.b.property') 57 | 58 | expect(redacted_hash).to_not be(hash) 59 | expect(redacted_hash['a']['b']['property']).to eq('') 60 | expect(hash['a']['b']['property']).to eq('secret') 61 | end 62 | 63 | context 'given hash with symbols' do 64 | it 'clones and redacts a given paths from the given hash' do 65 | redacted_hash = subject.clone_and_redact(hash_with_symbols, 'a.b.property') 66 | 67 | expect(redacted_hash).to_not be(hash_with_symbols) 68 | expect(redacted_hash['a']['b']['property']).to eq('') 69 | expect(hash_with_symbols[:a][:b][:property]).to eq('secret') 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/unit/validator/resources_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../spec_helper' 2 | 3 | module Validator 4 | 5 | describe Resources do 6 | let(:compute) { double('compute', servers: servers, key_pairs: key_pairs, delete_server_group: double('delete_server_group_request')) } 7 | let(:network) { double('network', networks: networks) } 8 | let(:image) { double('image', images: images) } 9 | let(:volume) { double('volume', volumes: volumes) } 10 | let(:storage) { double('storage') } 11 | 12 | let(:server_entries){ [] } 13 | let(:servers) { 14 | OpenStackResourceCollection.new(server_entries) 15 | } 16 | let(:volume_entries){ [] } 17 | let(:volumes) { 18 | OpenStackResourceCollection.new(volume_entries) 19 | } 20 | let(:image_entries) { [] } 21 | let(:images) { 22 | OpenStackResourceCollection.new(image_entries) 23 | } 24 | let(:network_entries){ [] } 25 | let(:networks) { 26 | OpenStackResourceCollection.new(network_entries) 27 | } 28 | let(:key_pair_entries){ [] } 29 | let(:key_pairs) { 30 | OpenStackResourceCollection.new(key_pair_entries) 31 | } 32 | let(:validator_config) do 33 | { 'openstack' => { 'wait_for_swift' => 0 } } 34 | end 35 | 36 | before (:each) do 37 | allow(Api::FogOpenStack).to receive(:compute).and_return(compute) 38 | allow(Api::FogOpenStack).to receive(:network).and_return(network) 39 | allow(Api::FogOpenStack).to receive(:image).and_return(image) 40 | allow(Api::FogOpenStack).to receive(:volume).and_return(volume) 41 | allow(Api::FogOpenStack).to receive(:storage).and_return(storage) 42 | allow(RSpec::configuration).to receive(:options).and_return(double('options', cpi_bin_path: nil, log_path: nil)) 43 | allow(RSpec::configuration).to receive(:validator_config).and_return(double('config', validator_config)) 44 | end 45 | 46 | describe '.create' do 47 | 48 | it 'should create a Resources instance' do 49 | expect(subject.new_tracker).to be_a(Api::ResourceTracker) 50 | end 51 | end 52 | 53 | describe '.cleanup' do 54 | let(:resources) { 55 | OpenStackResourceCollection.new([ 56 | {id: '1234-1234-1234-1234', destroyable: true}, 57 | {id: '1111-1111-1234-1234', destroyable: true} 58 | ]) 59 | } 60 | 61 | context 'when all resources can be cleaned up' do 62 | let(:log_path) {Dir.mktmpdir} 63 | 64 | before(:each) do 65 | allow(Logger).to receive(:new).and_return(nil) 66 | allow(Api::ResourceTracker).to receive(:log_path).and_return(log_path) 67 | end 68 | 69 | after do 70 | FileUtils.rm_rf( log_path ) if File.exists?( log_path ) 71 | end 72 | 73 | Api::ResourceTracker.resource_types.each do |type| 74 | it "cleans produced resources for #{type}" do 75 | cpi = instance_double(Validator::ExternalCpi, delete_vm: nil, delete_stemcell: nil) 76 | allow(Validator::ExternalCpi).to receive(:new).and_return(cpi) 77 | allow(compute).to receive(type).and_return(resources) 78 | allow(network).to receive(type).and_return(resources) 79 | allow(image).to receive(type).and_return(resources) 80 | allow(volume).to receive(type).and_return(resources) 81 | allow(storage).to receive(type).and_return(resources) 82 | 83 | if type == :files 84 | allow(storage).to receive(:directories).and_return(resources) 85 | subject.new_tracker.produce(type, provide_as: :resource_id1) { 86 | ['1234-1234-1234-1234', '1111-1111-1234-1234'] 87 | } 88 | else 89 | subject.new_tracker.produce(type, provide_as: :resource_id1) { 90 | '1234-1234-1234-1234' 91 | } 92 | subject.new_tracker.produce(type, provide_as: :resource_id2) { 93 | '1111-1111-1234-1234' 94 | } 95 | end 96 | 97 | subject.cleanup 98 | 99 | expect(subject.count).to eq(0) 100 | end 101 | end 102 | end 103 | 104 | context 'when some resource could not be deleted' do 105 | let(:volume_entries) { 106 | [ 107 | {id: '1234', destroyable: false}, 108 | {id: '5678', destroyable: true} 109 | ] 110 | } 111 | 112 | it 'returns false' do 113 | subject.new_tracker.produce(:volumes) { '1234' } 114 | subject.new_tracker.produce(:volumes) { '5678' } 115 | expect(subject.count).to eq(2) 116 | 117 | expect(subject.cleanup).to eq(false) 118 | 119 | expect(subject.count).to eq(1) 120 | end 121 | end 122 | 123 | context 'when some resource does not exist in openstack' do 124 | let(:image_entries) { [{id: '1234', destroyable: true}] } 125 | it 'cleans up those resources anyway' do 126 | subject.new_tracker.produce(:images) { '1234' } 127 | 128 | images.clear 129 | 130 | expect(subject.cleanup).to eq(true) 131 | end 132 | end 133 | 134 | end 135 | 136 | describe '.summary' do 137 | context 'when multiple tests produce resources' do 138 | let(:server_entries) { [{id: '1234-1234-1234-1234', name: 'server-1'}] } 139 | let(:volume_entries) { [ 140 | {id: '1111-1111-1111-1111', name: 'volume-1'}, 141 | {id: '0000-0000-0000-0000', name: 'volume-2'} 142 | ] } 143 | let(:key_pair_entries) { [{id: '1-2-3-4', name: 'keypair-1'}]} 144 | before(:all) { 145 | @subject = Resources.new 146 | } 147 | it 'a server' do 148 | @subject.new_tracker.produce(:servers) { '1234-1234-1234-1234' } 149 | end 150 | 151 | it 'a volume' do 152 | @subject.new_tracker.produce(:volumes) { '1111-1111-1111-1111' } 153 | end 154 | 155 | it 'another volume' do 156 | @subject.new_tracker.produce(:volumes) { '0000-0000-0000-0000' } 157 | end 158 | 159 | it 'a key pair' do 160 | @subject.new_tracker.produce(:key_pairs) { '1-2-3-4'} 161 | end 162 | 163 | it 'returns all tracked resources as printable string' do 164 | expect(@subject.summary).to eq(< 'test-disk-tagging'}) 141 | } 142 | end 143 | 144 | it 'can attach the disk to the VM' do 145 | vm_cid = @resource_tracker.consumes(:vm_cid, 'No VM to attach disk to') 146 | disk_cid = @resource_tracker.consumes(:disk_cid, 'No disk to attach') 147 | 148 | with_cpi("Disk '#{disk_cid}' could not be attached to VM '#{vm_cid}'.") { 149 | @cpi.attach_disk(vm_cid, disk_cid) 150 | } 151 | end 152 | 153 | it 'can detach the disk from the VM' do 154 | vm_cid = @resource_tracker.consumes(:vm_cid, 'No VM to detach disk from') 155 | disk_cid = @resource_tracker.consumes(:disk_cid, 'No disk to detach') 156 | 157 | with_cpi("Disk '#{disk_cid}' could not be detached from VM '#{vm_cid}'.") { 158 | @cpi.detach_disk(vm_cid, disk_cid) 159 | } 160 | end 161 | 162 | it 'can take a snapshot' do 163 | disk_cid = @resource_tracker.consumes(:disk_cid, 'No disk to create snapshot from') 164 | metadata = {'director_name' => 'validator-test', 'job' => 'validator-test', 'instance_id' => 'validator-test'} 165 | 166 | snapshot_cid = with_cpi("Snapshot for disk '#{disk_cid}' could not be taken.") { 167 | @resource_tracker.produce(:snapshots, provide_as: :snapshot_cid) { 168 | @cpi.snapshot_disk(disk_cid, metadata) 169 | } 170 | } 171 | 172 | expect(snapshot_cid).to be 173 | end 174 | 175 | it 'can delete a snapshot' do 176 | snapshot_cid = @resource_tracker.consumes(:snapshot_cid, 'No snapshot to delete') 177 | disk_cid = @resource_tracker.consumes(:disk_cid, 'No disk to delete snapshot from') 178 | 179 | with_cpi("Snapshot '#{snapshot_cid}' for disk '#{disk_cid}' could not be deleted.") { 180 | @cpi.delete_snapshot(snapshot_cid) 181 | } 182 | end 183 | 184 | it 'can delete the disk' do 185 | disk_cid = @resource_tracker.consumes(:disk_cid, 'No disk to delete') 186 | 187 | with_cpi("Disk '#{disk_cid}' could not be deleted.") { 188 | @cpi.delete_disk(disk_cid) 189 | } 190 | end 191 | 192 | it 'can delete the VM' do 193 | vm_cid = @resource_tracker.consumes(:vm_cid, 'No vm to delete') 194 | 195 | with_cpi("VM '#{vm_cid}' could not be deleted.") { 196 | @cpi.delete_vm(vm_cid) 197 | } 198 | end 199 | 200 | it 'can delete a stemcell' do 201 | stemcell_cid = @resource_tracker.consumes(:stemcell_cid, 'No stemcell to delete') 202 | 203 | with_cpi('Stemcell could not be deleted') { 204 | @cpi.delete_stemcell(stemcell_cid) 205 | } 206 | end 207 | end 208 | end 209 | -------------------------------------------------------------------------------- /src/specs/extension_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'spec_helper' 2 | 3 | specs = Validator::Extensions.all 4 | if specs.size > 0 5 | openstack_suite.describe 'Extensions', position: 3, order: :global do 6 | Validator::Extensions.eval(specs, binding) 7 | end 8 | end -------------------------------------------------------------------------------- /src/specs/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../lib/validator' 2 | require_relative 'support/resource_tracker' 3 | 4 | include Validator::Api::Helpers 5 | include Validator::Api::CpiHelpers 6 | 7 | RSpec.configure do |config| 8 | config.register_ordering(:openstack) do |items| 9 | items.sort_by { |item| item.metadata[:position] } 10 | end 11 | 12 | config.add_setting :validator_config 13 | config.validator_config = Validator::Api::Configuration.new(RSpec::configuration.options.config_path) 14 | 15 | config.add_setting :validator_resources 16 | config.validator_resources = Validator::Resources.new 17 | 18 | config.after(:all) do 19 | RSpec::configuration.validator_resources.cleanup unless RSpec::configuration.options.skip_cleanup? 20 | end 21 | end -------------------------------------------------------------------------------- /src/specs/support/resource_tracker.rb: -------------------------------------------------------------------------------- 1 | RSpec.shared_context "resource tracker" do 2 | before(:all) do 3 | @resource_tracker = Validator::Api::ResourceTracker.create 4 | end 5 | 6 | after(:all) do 7 | @resource_tracker.cleanup 8 | end 9 | end -------------------------------------------------------------------------------- /validate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bundle exec bin/cf-openstack-validator $@ -------------------------------------------------------------------------------- /validator.template.yml: -------------------------------------------------------------------------------- 1 | --- 2 | openstack: 3 | auth_url: # Keystone V3 URL 4 | username: # String 5 | password: # String 6 | domain: # String 7 | project: # String 8 | default_key_name: cf-validator # String 9 | default_security_groups: [default] # List of String 10 | boot_from_volume: false # Boolean 11 | config_drive: ~ # One of cdrom, disk, or nil 12 | connection_options: # all connection options that are supported by Excon 13 | ssl_verify_peer: true # Boolean 14 | # ca_cert: # Multiline yaml String containing certificate chain 15 | 16 | validator: 17 | use_external_ip: false # Defines from where the Validator is executed. False means from inside your OpenStack. 18 | network_id: # String UUID 19 | floating_ip: # String IP 20 | static_ip: # String IP. This IP is used to create a VM with a static IP. 21 | private_key_path: cf-validator.rsa_id # relative to validator.yml, or absolute path 22 | ntp: [0.pool.ntp.org, 1.pool.ntp.org] # List of String 23 | releases: 24 | - name: bosh-openstack-cpi 25 | url: https://bosh.io/d/github.com/cloudfoundry/bosh-openstack-cpi-release?v=44 26 | sha1: 7c9be83eb11214db85ef5320f26ec9db8fab353f 27 | 28 | cloud_config: 29 | vm_types: # See https://bosh.io/docs/cloud-config.html#vm-types 30 | - name: default # Don't change this name 31 | cloud_properties: 32 | instance_type: # String 33 | # availability_zone: # String 34 | # root_disk: {size: 20} # Integer, size in GiB. Used together with openstack.boot_from_volume. 35 | 36 | extensions: 37 | paths: [] # paths pointing to a directory. Absolute or relative to this config file. 38 | config: {} # everything below 'config' is available in tests as `Validator::Api.configuration.extensions` inside your test code 39 | -------------------------------------------------------------------------------- /vendor/package/builder-3.2.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/builder-3.2.3.gem -------------------------------------------------------------------------------- /vendor/package/diff-lcs-1.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/diff-lcs-1.3.gem -------------------------------------------------------------------------------- /vendor/package/excon-0.60.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/excon-0.60.0.gem -------------------------------------------------------------------------------- /vendor/package/fog-core-1.45.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/fog-core-1.45.0.gem -------------------------------------------------------------------------------- /vendor/package/fog-json-1.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/fog-json-1.0.2.gem -------------------------------------------------------------------------------- /vendor/package/fog-openstack-0.1.24.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/fog-openstack-0.1.24.gem -------------------------------------------------------------------------------- /vendor/package/formatador-0.2.5.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/formatador-0.2.5.gem -------------------------------------------------------------------------------- /vendor/package/ipaddress-0.8.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/ipaddress-0.8.3.gem -------------------------------------------------------------------------------- /vendor/package/membrane-1.1.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/membrane-1.1.0.gem -------------------------------------------------------------------------------- /vendor/package/mime-types-3.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/mime-types-3.1.gem -------------------------------------------------------------------------------- /vendor/package/mime-types-data-3.2016.0521.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/mime-types-data-3.2016.0521.gem -------------------------------------------------------------------------------- /vendor/package/multi_json-1.13.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/multi_json-1.13.1.gem -------------------------------------------------------------------------------- /vendor/package/rspec-3.5.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/rspec-3.5.0.gem -------------------------------------------------------------------------------- /vendor/package/rspec-core-3.5.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/rspec-core-3.5.4.gem -------------------------------------------------------------------------------- /vendor/package/rspec-expectations-3.5.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/rspec-expectations-3.5.0.gem -------------------------------------------------------------------------------- /vendor/package/rspec-mocks-3.5.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/rspec-mocks-3.5.0.gem -------------------------------------------------------------------------------- /vendor/package/rspec-support-3.5.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-attic/cf-openstack-validator/8a8c97171df179b1314409f3d35731ea99c29882/vendor/package/rspec-support-3.5.0.gem -------------------------------------------------------------------------------- /vendor_gems.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | export BUNDLE_APP_CONFIG=$(mktemp -d $TMPDIR/bundler_config_XXXXXX) 6 | export BUNDLE_CACHE_PATH="vendor/package" 7 | export BUNDLE_WITHOUT="development:test" 8 | 9 | bundle package 10 | 11 | rm -rf $BUNDLE_APP_CONFIG 12 | --------------------------------------------------------------------------------