diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 8fe3f7c922..463339ee2f 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -198,7 +198,6 @@ jobs: timeout: 2h privileged: true params: - COVERAGE: false DB: mysql RAKE_TASK: spec:unit:director @@ -221,7 +220,6 @@ jobs: timeout: 2h privileged: true params: - COVERAGE: false DB: postgresql RAKE_TASK: spec:unit:director @@ -244,7 +242,6 @@ jobs: timeout: 2h privileged: true params: - COVERAGE: false DB: postgresql RAKE_TASK: spec:unit:director @@ -309,7 +306,6 @@ jobs: image: integration-postgres-15-image privileged: true params: - COVERAGE: false DB: postgresql RAKE_TASK: spec:integration PARALLEL_TEST_MULTIPLY_PROCESSES: "0.5" @@ -368,7 +364,6 @@ jobs: image: integration-mysql-8-0-image privileged: true params: - COVERAGE: false DB: mysql RAKE_TASK: spec:integration @@ -1389,7 +1384,6 @@ jobs: input_mapping: bosh: bosh-out params: - COVERAGE: false DB: sqlite RAKE_TASK: spec:unit:parallel - put: bosh diff --git a/ci/tasks/test-rake-task.yml b/ci/tasks/test-rake-task.yml index a0a55cc31f..405b8f8b6c 100644 --- a/ci/tasks/test-rake-task.yml +++ b/ci/tasks/test-rake-task.yml @@ -15,7 +15,7 @@ run: path: bosh-ci/ci/tasks/test-rake-task.sh params: - COVERAGE: true + COVERAGE: false DB: RAKE_TASK: SPEC_PATH: ~ diff --git a/src/bosh-director/lib/bosh/director/cloud_factory.rb b/src/bosh-director/lib/bosh/director/cloud_factory.rb index 1ec1900e52..61f49c1efc 100644 --- a/src/bosh-director/lib/bosh/director/cloud_factory.rb +++ b/src/bosh-director/lib/bosh/director/cloud_factory.rb @@ -58,9 +58,9 @@ def get(cpi_name, stemcell_api_version = nil) begin info_response = cloud.info || {} - cpi_api_version = info_response.fetch('api_version', 1) - rescue - cpi_api_version = 1 + cpi_api_version = info_response.fetch('api_version') + rescue StandardError + raise Bosh::Clouds::NotSupported, 'CPI must report api_version via info' end supported_cpi_version = [cpi_api_version, Bosh::Director::Config.preferred_cpi_api_version].min @logger.debug("Using cpi_version #{supported_cpi_version} for CPI #{cpi_name}") diff --git a/src/bosh-director/lib/clouds/external_cpi_response_wrapper.rb b/src/bosh-director/lib/clouds/external_cpi_response_wrapper.rb index bbe708b820..5be83ffc3f 100644 --- a/src/bosh-director/lib/clouds/external_cpi_response_wrapper.rb +++ b/src/bosh-director/lib/clouds/external_cpi_response_wrapper.rb @@ -20,7 +20,6 @@ def set_disk_metadata(*arguments); invoke_cpi_method(__method__.to_s, *arguments def create_disk(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end def has_disk(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end def delete_disk(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end - def attach_disk(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end def detach_disk(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end def snapshot_disk(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end def delete_snapshot(*arguments); invoke_cpi_method(__method__.to_s, *arguments); end @@ -36,44 +35,29 @@ def invoke_cpi_method(method, *arguments) end def create_vm(*args) - cpi_response = @cpi.create_vm(*args) - - response = [] - if @cpi_api_version >= 2 - response = cpi_response - else - response << cpi_response - end - - response + @cpi.create_vm(*args) end - - def create_stemcell(*args) - final_args = args.take(2) - if @cpi_api_version >= 3 - final_args = args - end - return @cpi.create_stemcell(*final_args) + def create_stemcell(*args) + final_args = @cpi_api_version >= 3 ? args : args.take(2) + @cpi.create_stemcell(*final_args) end - def attach_disk(*args) cpi_response = @cpi.attach_disk(*args) - - if @cpi_api_version >= 2 - raise Bosh::Clouds::AttachDiskResponseError, 'No disk_hint' if cpi_response.nil? || cpi_response.empty? - cpi_response - else - nil - end + raise Bosh::Clouds::AttachDiskResponseError, 'No disk_hint' if cpi_response.nil? || cpi_response.empty? + cpi_response end private def check_cpi_api_support - unsupported = @cpi_api_version > Bosh::Director::Config.preferred_cpi_api_version - raise Bosh::Clouds::NotSupported, "CPI API version #{@cpi_api_version} is not supported." if unsupported + if @cpi_api_version < 2 + raise Bosh::Clouds::NotSupported, "CPI API version #{@cpi_api_version} is not supported. Minimum required version is 2." + end + if @cpi_api_version > Bosh::Director::Config.preferred_cpi_api_version + raise Bosh::Clouds::NotSupported, "CPI API version #{@cpi_api_version} is not supported." + end end end end diff --git a/src/bosh-director/spec/unit/bosh/director/az_cloud_factory_spec.rb b/src/bosh-director/spec/unit/bosh/director/az_cloud_factory_spec.rb index 792e2b6da7..94ff38f6d0 100644 --- a/src/bosh-director/spec/unit/bosh/director/az_cloud_factory_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/az_cloud_factory_spec.rb @@ -13,13 +13,13 @@ module Bosh::Director before do allow(Bosh::Director::Config).to receive(:uuid).and_return('snoopy-uuid') - allow(Bosh::Director::Config).to receive(:preferred_cpi_api_version).and_return(1) + allow(Bosh::Director::Config).to receive(:preferred_cpi_api_version).and_return(2) allow(Bosh::Director::Config).to receive(:cloud_options).and_return('provider' => { 'path' => '/path/to/default/cpi' }) allow(Bosh::Clouds::ExternalCpi).to receive(:new).with('/path/to/default/cpi', 'snoopy-uuid', instance_of(Logging::Logger), stemcell_api_version: nil).and_return(default_cloud) - allow(default_cloud).to receive(:info) + allow(default_cloud).to receive(:info).and_return('api_version' => 2) allow(default_cloud).to receive(:request_cpi_api_version=) end @@ -126,7 +126,7 @@ module Bosh::Director before do expect(az_cloud_factory.uses_cpi_config?).to be_truthy clouds.each do |cloud| - allow(cloud).to receive(:info) + allow(cloud).to receive(:info).and_return('api_version' => 2) allow(cloud).to receive(:request_cpi_api_version=) end end diff --git a/src/bosh-director/spec/unit/bosh/director/cloud_factory_spec.rb b/src/bosh-director/spec/unit/bosh/director/cloud_factory_spec.rb index 9ed897fb3f..3ac7ce6184 100644 --- a/src/bosh-director/spec/unit/bosh/director/cloud_factory_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/cloud_factory_spec.rb @@ -7,7 +7,7 @@ module Bosh::Director let(:cloud) { instance_double(Bosh::Clouds::ExternalCpi) } let(:parsed_cpi_config) { CpiConfig::ParsedCpiConfig.new(cpis) } let(:cpis) { [] } - let(:cpi_api_version) { 1 } + let(:cpi_api_version) { 2 } let(:cpi_info) do { 'stemcell_formats' => 'some-stemcell-support-format', @@ -50,14 +50,13 @@ module Bosh::Director end end - context 'old CPIs do not return the version from info' do + context 'CPIs that do not report a version from info' do let(:cpi_info) do {} end - it 'creates cloud with CPI API version of 1' do - expect(cloud).to receive(:request_cpi_api_version=).with(1) - cloud_factory.get(nil, stemcell_api_version) + it 'raises NotSupported' do + expect { cloud_factory.get(nil, stemcell_api_version) }.to raise_error(Bosh::Clouds::NotSupported) end end end @@ -95,7 +94,7 @@ module Bosh::Director shared_examples_for 'lookup for clouds' do before do - allow(cloud).to receive(:info).and_return({}) + allow(cloud).to receive(:info).and_return('api_version' => 2) allow(cloud).to receive(:request_cpi_api_version=) end @@ -205,7 +204,7 @@ module Bosh::Director stemcell_api_version: nil).and_return(clouds[2]) clouds.each do |cloud| - allow(cloud).to receive(:info) + allow(cloud).to receive(:info).and_return('api_version' => 2) allow(cloud).to receive(:request_cpi_api_version=) end end @@ -215,7 +214,7 @@ module Bosh::Director instance_of(Logging::Logger), properties_from_cpi_config: cpis[1].properties, stemcell_api_version: nil).and_return(clouds[1]) - allow(clouds[1]).to receive(:info).and_return({}) + allow(clouds[1]).to receive(:info).and_return('api_version' => 2) expect(Bosh::Clouds::ExternalCpiResponseWrapper).to receive(:new).with(clouds[1], kind_of(Integer)).and_return(cloud_wrapper) cloud = cloud_factory.get('name2') diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/compilation_instance_pool_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/compilation_instance_pool_spec.rb index 4acd740bd4..c28265049f 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/compilation_instance_pool_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/compilation_instance_pool_spec.rb @@ -17,7 +17,7 @@ module Bosh::Director let(:another_agent_client) { instance_double('Bosh::Director::AgentClient') } let(:availability_zone) { nil } let(:cloud) { instance_double(Bosh::Clouds::ExternalCpi) } - let(:cloud_wrapper) { Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud, 1) } + let(:cloud_wrapper) { Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud, 2) } let(:package) { instance_double(Models::Package, name: 'fake-package') } let(:package2) { instance_double(Models::Package, name: 'fake-package2') } @@ -141,12 +141,12 @@ module Bosh::Director before do allow(Config).to receive(:cloud_options).and_return('provider' => { 'path' => '/path/to/default/cpi' }) - allow(cloud).to receive(:create_vm) + allow(cloud).to receive(:create_vm).and_return([nil, {}]) allow(cloud).to receive(:set_vm_metadata) - allow(cloud).to receive(:info).and_return({}) - allow(cloud).to receive(:request_cpi_api_version=).with(1) - allow(cloud).to receive(:request_cpi_api_version).and_return(1) - allow(Config).to receive(:preferred_cpi_api_version).and_return(1) + allow(cloud).to receive(:info).and_return('api_version' => 2) + allow(cloud).to receive(:request_cpi_api_version=).with(2) + allow(cloud).to receive(:request_cpi_api_version).and_return(2) + allow(Config).to receive(:preferred_cpi_api_version).and_return(2) allow(Bosh::Clouds::ExternalCpi).to receive(:new).and_return(cloud) allow(Bosh::Clouds::ExternalCpiResponseWrapper).to receive(:new).with(cloud, anything).and_return(cloud_wrapper) allow(network).to receive(:network_settings) @@ -196,7 +196,7 @@ module Bosh::Director expected_network_settings, [], compilation_env, - ) + ).and_return([nil, {}]) action end @@ -273,6 +273,7 @@ module Bosh::Director allow(cloud).to receive(:create_vm) do |_, _, cloud_properties| expect(cloud_properties['vm_resources']).to eq('foo') + [nil, {}] end action @@ -462,7 +463,7 @@ module Bosh::Director anything, anything, anything, - ) + ).and_return([nil, {}]) compilation_instance_pool.with_reused_vm(stemcell, package) {} end diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/stages/package_compile_stage_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/stages/package_compile_stage_spec.rb index 70f8a1e174..46fb62e03f 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/stages/package_compile_stage_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/stages/package_compile_stage_spec.rb @@ -142,7 +142,7 @@ module Bosh::Director allow(plan).to receive(:network).with('default').and_return(network) - allow(Config).to receive(:preferred_cpi_api_version).and_return(1) + allow(Config).to receive(:preferred_cpi_api_version).and_return(2) allow(Config).to receive(:current_job).and_return(update_job) allow(Config).to receive(:name).and_return('fake-director-name') @@ -152,7 +152,7 @@ module Bosh::Director allow(Config).to receive(:nats_client_ca_private_key_path).and_return(director_config['nats']['client_ca_private_key_path']) allow(Config).to receive(:nats_client_ca_certificate_path).and_return(director_config['nats']['client_ca_certificate_path']) allow(Bosh::Director::Config).to receive(:event_log).and_return(event_log) - allow(cloud).to receive(:info) + allow(cloud).to receive(:info).and_return({ 'api_version' => 2 }) allow(cloud).to receive(:request_cpi_api_version=) allow(cloud).to receive(:request_cpi_api_version) allow(cloud).to receive(:set_vm_metadata) @@ -679,7 +679,7 @@ def compile_package_with_url_encrypt_stub(args) expect(cloud).to receive(:create_vm).once.ordered .with(instance_of(String), stemcell.models.first.cid, {}, net, [], { 'bosh' => { 'group' => 'fake-director-name-mycloud-compilation-deadbeef', 'groups' => expected_groups } }) - .and_return(vm_cid) + .and_return([vm_cid, {}]) allow(AgentClient).to receive(:with_agent_id).and_return(agent) @@ -776,7 +776,7 @@ def compile_package_with_url_encrypt_stub(args) expect(cloud).to receive(:create_vm) .with(instance_of(String), @stemcell_a.models.first.cid, {}, net, [], { 'bosh' => { 'group' => 'fake-director-name-mycloud-compilation-deadbeef', 'groups' => expected_groups } }) - .and_return(vm_cid) + .and_return([vm_cid, {}]) allow(AgentClient).to receive(:with_agent_id).and_return(agent) @@ -874,7 +874,7 @@ def compile_package_with_url_encrypt_stub(args) before do Bosh::Director::Config.trusted_certs = director_test_certs - allow(cloud).to receive(:create_vm).and_return('new-vm-cid') + allow(cloud).to receive(:create_vm).and_return(['new-vm-cid', {}]) allow(AgentClient).to receive_messages(with_agent_id: client) allow(cloud).to receive(:delete_vm) allow(client).to receive(:update_settings) diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/stages/update_stage_functional_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/stages/update_stage_functional_spec.rb index d0c81e2031..4ed15a3303 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/stages/update_stage_functional_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/stages/update_stage_functional_spec.rb @@ -71,11 +71,14 @@ module Bosh::Director::DeploymentPlan::Stages allow(Bosh::Director::Config).to receive(:event_log).and_return(event_log) allow(Bosh::Director::Config).to receive(:uuid).and_return('meow-uuid') allow(Bosh::Director::Config).to receive(:cloud_options).and_return('provider' => { 'path' => '/path/to/default/cpi' }) - allow(Bosh::Director::Config).to receive(:preferred_cpi_api_version).and_return(1) + allow(Bosh::Director::Config).to receive(:preferred_cpi_api_version).and_return(2) allow(Bosh::Director::Config).to receive(:enable_short_lived_nats_bootstrap_credentials).and_return(true) director_config = SpecHelper.director_config_hash allow(Bosh::Director::Config).to receive(:nats_client_ca_private_key_path).and_return(director_config['nats']['client_ca_private_key_path']) allow(Bosh::Director::Config).to receive(:nats_client_ca_certificate_path).and_return(director_config['nats']['client_ca_certificate_path']) + external_cpi = instance_double(Bosh::Clouds::ExternalCpi) + allow(Bosh::Clouds::ExternalCpi).to receive(:new).and_return(external_cpi) + allow(external_cpi).to receive(:info).and_return('api_version' => 2) allow(Bosh::Clouds::ExternalCpiResponseWrapper).to receive(:new).with(anything, anything).and_return(cloud) allow(variables_interpolator).to receive(:interpolate_template_spec_properties).and_return({}) allow(variables_interpolator).to receive(:interpolated_versioned_variables_changed?).and_return(false) @@ -116,7 +119,7 @@ module Bosh::Director::DeploymentPlan::Stages anything, anything, ) - .and_return(['vm-cid-2']) + .and_return(['vm-cid-2', {}]) .ordered update_step.perform @@ -148,7 +151,7 @@ module Bosh::Director::DeploymentPlan::Stages allow(agent_client).to receive(:prepare) allow(agent_client).to receive(:run_script) allow(agent_client).to receive(:start) - allow(cloud).to receive(:create_vm).and_return(['vm-cid-2']) + allow(cloud).to receive(:create_vm).and_return(['vm-cid-2', {}]) end it "creates an instance with 'lifecycle' in the spec" do diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/steps/create_vm_step_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/steps/create_vm_step_spec.rb index 6e084f2afa..86ace29a0c 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/steps/create_vm_step_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/steps/create_vm_step_spec.rb @@ -12,7 +12,7 @@ module Steps let(:disks) { [instance.model.managed_persistent_disk_cid].compact } let(:cloud_factory) { instance_double(AZCloudFactory) } let(:cloud) { instance_double('Bosh::Clouds::ExternalCpi', :request_cpi_api_version= => nil) } - let(:cpi_api_version) { 1 } + let(:cpi_api_version) { 2 } let(:cloud_wrapper) { Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud, cpi_api_version) } let(:deployment) { FactoryBot.create(:models_deployment, name: 'deployment_name') } let(:vm_type) { DeploymentPlan::VmType.new('name' => 'fake-vm-type', 'cloud_properties' => cloud_properties) } @@ -22,7 +22,7 @@ module Steps let(:event_log) { Bosh::Director::EventLog::Log.new(task_writer) } let(:env) { DeploymentPlan::Env.new({}) } let(:dns_encoder) { instance_double(DnsEncoder) } - let(:create_vm_response) { ['new-vm-cid', {}, {}] } + let(:create_vm_response) { ['new-vm-cid', {}] } let(:metadata_err) { 'metadata_err' } let(:report) { Stages::Report.new } let(:delete_vm_step) { instance_double(DeleteVmStep) } @@ -171,7 +171,7 @@ module Steps before do allow(deployment).to receive(:last_successful_variable_set).and_return(variable_set) - allow(Config).to receive(:preferred_cpi_api_version).and_return(1) + allow(Config).to receive(:preferred_cpi_api_version).and_return(2) allow(Config).to receive(:current_job).and_return(update_job) Config.name = 'fake-director-name' Config.max_vm_create_tries = 2 @@ -192,7 +192,7 @@ module Steps end it 'sets vm on given report' do - allow(cloud_wrapper).to receive(:create_vm).and_return(['', {}, {}]) + allow(cloud_wrapper).to receive(:create_vm).and_return(['fake-vm-cid', {}]) subject.perform(report) expect(report.vm).to eq(vm_model) @@ -517,7 +517,7 @@ module Steps end context 'when there is a vm creation error' do - let(:create_vm_response) { ['fake-vm-cid', {}, {}] } + let(:create_vm_response) { ['fake-vm-cid', {}] } it 'should retry creating a VM if it is told it is a retryable error' do expect(cloud_wrapper).to receive(:create_vm).once.and_raise(Bosh::Clouds::VMCreationFailed.new(true)) expect(cloud_wrapper).to receive(:create_vm).once.and_return(create_vm_response) @@ -565,14 +565,14 @@ module Steps # create_vm now expected to return an array, so object response need to transformed into array expect(cloud_wrapper).to receive(:create_vm) do |*args| env_id = args[5].object_id - [env_id, {}, {}] + ['fake-vm-cid', {}] end subject.perform(report) expect(cloud_wrapper).to receive(:create_vm) do |*args| expect(args[5].object_id).not_to eq(env_id) - [env_id, {}, {}] + ['fake-vm-cid', {}] end subject.perform(report) diff --git a/src/bosh-director/spec/unit/bosh/director/jobs/update_stemcell_spec.rb b/src/bosh-director/spec/unit/bosh/director/jobs/update_stemcell_spec.rb index dcf203f2a4..9dd9a34c25 100644 --- a/src/bosh-director/spec/unit/bosh/director/jobs/update_stemcell_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/jobs/update_stemcell_spec.rb @@ -76,7 +76,7 @@ stemcell_api_version: nil).and_return(cloud) allow(cloud).to receive(:request_cpi_api_version=) - allow(cloud).to receive(:info).and_return('stemcell_formats' => ['dummy']) + allow(cloud).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) allow(event_log).to receive(:begin_stage).and_return(event_log_stage) allow(event_log_stage).to receive(:advance_and_track).and_yield [nil] @@ -97,7 +97,7 @@ context 'when the stemcell tarball is valid' do context 'uploading a local stemcell' do it 'should upload a local stemcell' do - expect(cloud).to receive(:info).and_return('stemcell_formats' => ['dummy']) + expect(cloud).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) expect(cloud).to receive(:create_stemcell).with(anything, { 'ram' => '2gb' }) do |image, _| contents = File.open(image, &:read) expect(contents).to eq(stemcell_image_content) @@ -134,7 +134,7 @@ let(:stemcell_options) { { 'sha1' => 'eeaec4f77e2014966f7f01e949c636b9f9992757' } } it 'should upload a local stemcell' do - expect(cloud).to receive(:info).and_return('stemcell_formats' => ['dummy']) + expect(cloud).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) expect(cloud).to receive(:create_stemcell).with(anything, { 'ram' => '2gb' }) do |image, _| contents = File.open(image, &:read) expect(contents).to eq(stemcell_image_content) @@ -162,7 +162,7 @@ let(:stemcell_options) { { 'remote' => true } } it 'should upload a remote stemcell' do - expect(cloud).to receive(:info).and_return('stemcell_formats' => ['dummy']) + expect(cloud).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) expect(cloud).to receive(:create_stemcell).with(anything, { 'ram' => '2gb' }) do |image, _| contents = File.open(image, &:read) expect(contents).to eql(stemcell_image_content) @@ -218,7 +218,7 @@ end it 'should upload a remote stemcell' do - expect(cloud).to receive(:info).and_return('stemcell_formats' => ['dummy']) + expect(cloud).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) expect(cloud).to receive(:create_stemcell).with(anything, { 'ram' => '2gb' }) do |image, _| contents = File.open(image, &:read) expect(contents).to eql(stemcell_image_content) @@ -246,7 +246,7 @@ end it 'should cleanup the stemcell file' do - expect(cloud).to receive(:info).and_return('stemcell_formats' => ['dummy']) + expect(cloud).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) expect(cloud).to receive(:create_stemcell).with(anything, { 'ram' => '2gb' }) do |image, _| contents = File.open(image, &:read) expect(contents).to eql(stemcell_image_content) @@ -264,7 +264,7 @@ end it 'should quietly ignore duplicate upload and not create a stemcell in the cloud' do - expect(cloud).to receive(:info).and_return('stemcell_formats' => ['dummy']) + expect(cloud).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) expected_steps = 5 expect(event_log).to receive(:begin_stage).with('Update stemcell', expected_steps) expect(event_log_stage).to receive(:advance_and_track).exactly(expected_steps).times @@ -277,7 +277,7 @@ it 'should quietly ignore duplicate remote uploads and not create a stemcell in the cloud' do expected_steps = 6 - expect(cloud).to receive(:info).and_return('stemcell_formats' => ['dummy']) + expect(cloud).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) expect(event_log).to receive(:begin_stage).with('Update stemcell', expected_steps) expect(event_log_stage).to receive(:advance_and_track).exactly(expected_steps).times @@ -293,7 +293,7 @@ let(:stemcell_options) { { 'fix' => true } } it 'should upload stemcell and update db with --fix option set' do - expect(cloud).to receive(:info).and_return('stemcell_formats' => ['dummy']) + expect(cloud).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) expect(cloud).to receive(:create_stemcell).and_return 'new-stemcell-cid' expected_steps = 5 expect(event_log).to receive(:begin_stage).with('Update stemcell', expected_steps) @@ -340,14 +340,14 @@ expect(cloud1).to receive(:create_stemcell).with(anything, { 'ram' => '2gb' }).and_return('stemcell-cid1') expect(cloud3).to receive(:create_stemcell).with(anything, { 'ram' => '2gb' }).and_return('stemcell-cid3') - expect(cloud1).to receive(:info).and_return('stemcell_formats' => ['dummy']) - expect(cloud2).to receive(:info).and_return('stemcell_formats' => ['dummy1']) - expect(cloud3).to receive(:info).and_return('stemcell_formats' => ['dummy']) + expect(cloud1).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) + expect(cloud2).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy1']) + expect(cloud3).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) expect(cloud_factory).to receive(:all_names).exactly(3).times.and_return(%w[cloud1 cloud2 cloud3]) - expect(cloud_factory).to receive(:get).with('cloud1').and_return(Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud1, 1)) - expect(cloud_factory).to receive(:get).with('cloud2').and_return(Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud2, 1)) - expect(cloud_factory).to receive(:get).with('cloud3').and_return(Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud3, 1)) + expect(cloud_factory).to receive(:get).with('cloud1').and_return(Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud1, 2)) + expect(cloud_factory).to receive(:get).with('cloud2').and_return(Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud2, 2)) + expect(cloud_factory).to receive(:get).with('cloud3').and_return(Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud3, 2)) expected_steps = 11 expect(event_log).to receive(:begin_stage).with('Update stemcell', expected_steps) @@ -394,8 +394,8 @@ it 'skips creating a stemcell match when a CPI fails' do expect(cloud1).to receive(:create_stemcell).with(anything, { 'ram' => '2gb' }).and_raise('I am flaky') - expect(cloud1).to receive(:info).and_return('stemcell_formats' => ['dummy']) - expect(cloud_factory).to receive(:get).with('cloud1').and_return(Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud1, 1)) + expect(cloud1).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) + expect(cloud_factory).to receive(:get).with('cloud1').and_return(Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud1, 2)) expect(cloud_factory).to receive(:all_names).exactly(3).times.and_return(['cloud1']) expect { subject.perform }.to raise_error 'I am flaky' @@ -411,13 +411,13 @@ it 'creates one stemcell and one stemcell match per cpi' do expect(cloud_factory).to receive(:all_names).exactly(3).times.and_return(%w[cloud1 cloud2 cloud3]) - expect(cloud_factory).to receive(:get).with('cloud1').and_return(Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud1, 1)) - expect(cloud_factory).to receive(:get).with('cloud2').and_return(Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud2, 1)) - expect(cloud_factory).to receive(:get).with('cloud3').and_return(Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud3, 1)) + expect(cloud_factory).to receive(:get).with('cloud1').and_return(Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud1, 2)) + expect(cloud_factory).to receive(:get).with('cloud2').and_return(Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud2, 2)) + expect(cloud_factory).to receive(:get).with('cloud3').and_return(Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud3, 2)) - expect(cloud1).to receive(:info).and_return('stemcell_formats' => ['dummy']) - expect(cloud2).to receive(:info).and_return('stemcell_formats' => ['dummy1']) - expect(cloud3).to receive(:info).and_return('stemcell_formats' => ['dummy']) + expect(cloud1).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) + expect(cloud2).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy1']) + expect(cloud3).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) expect(cloud3).to receive(:create_stemcell).with(anything, { 'ram' => '2gb' }).and_return('stemcell-cid3') @@ -444,12 +444,12 @@ end it 'still works with the default cpi' do - expect(cloud).to receive(:info).and_return('stemcell_formats' => ['dummy']) + expect(cloud).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) expect(cloud).to receive(:create_stemcell).with(anything, { 'ram' => '2gb' }).and_return('stemcell-cid') expect(cloud_factory).to receive(:get_cpi_aliases).with('').and_return(['']) expect(cloud_factory).to receive(:all_names).exactly(3).times.and_return(['']) - expect(cloud_factory).to receive(:get).with('').and_return(Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud, 1)) + expect(cloud_factory).to receive(:get).with('').and_return(Bosh::Clouds::ExternalCpiResponseWrapper.new(cloud, 2)) expected_steps = 5 expect(event_log).to receive(:begin_stage).with('Update stemcell', expected_steps) @@ -485,7 +485,7 @@ context 'when stemcell does not have stemcell formats' do it 'should not fail' do - expect(cloud).to receive(:info).and_return('stemcell_formats' => ['dummy']) + expect(cloud).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) expect(cloud).to receive(:create_stemcell).with(anything, { 'ram' => '2gb' }) do |image, _| contents = File.open(image, &:read) expect(contents).to eql(stemcell_image_content) @@ -507,7 +507,7 @@ end it 'should not fail' do - expect(cloud).to receive(:info).and_return({}) + expect(cloud).to receive(:info).and_return('api_version' => 2) expect(cloud).to receive(:create_stemcell).with(anything, { 'ram' => '2gb' }) do |image, _| contents = File.open(image, &:read) expect(contents).to eql(stemcell_image_content) @@ -518,7 +518,7 @@ end end - context 'when cpi does not support stemcell formats' do + context 'when cpi does not implement info' do let(:manifest) do { 'name' => 'jeos', @@ -528,15 +528,10 @@ } end - it 'should not fail' do + it 'raises NotSupported' do expect(cloud).to receive(:info).and_raise(Bosh::Clouds::NotImplemented) - expect(cloud).to receive(:create_stemcell).with(anything, { 'ram' => '2gb' }) do |image, _| - contents = File.open(image, &:read) - expect(contents).to eql(stemcell_image_content) - 'stemcell-cid' - end - expect { subject.perform }.not_to raise_error + expect { subject.perform }.to raise_error(Bosh::Clouds::NotSupported) end end end @@ -552,7 +547,7 @@ end it 'should not fail' do - expect(cloud).to receive(:info).and_return('stemcell_formats' => ['dummy']) + expect(cloud).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) expect(cloud).to receive(:create_stemcell).with(anything, { 'ram' => '2gb' }) do |image, _| contents = File.open(image, &:read) expect(contents).to eql(stemcell_image_content) @@ -578,7 +573,7 @@ end it 'should update api_version' do - expect(cloud).to receive(:info).and_return('stemcell_formats' => ['dummy']) + expect(cloud).to receive(:info).and_return('api_version' => 2, 'stemcell_formats' => ['dummy']) expect(cloud).to receive(:create_stemcell).with(anything, { 'ram' => '2gb' }) do |image, _| contents = File.open(image, &:read) expect(contents).to eq(stemcell_image_content) diff --git a/src/bosh-director/spec/unit/bosh/director/problem_handlers/missing_vm_spec.rb b/src/bosh-director/spec/unit/bosh/director/problem_handlers/missing_vm_spec.rb index 5a4bd5e801..d86dc32f54 100644 --- a/src/bosh-director/spec/unit/bosh/director/problem_handlers/missing_vm_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/problem_handlers/missing_vm_spec.rb @@ -110,7 +110,7 @@ module Bosh::Director before do allow(Config).to receive(:uuid).and_return('woof-uuid') allow(Config).to receive(:cloud_options).and_return('provider' => { 'path' => '/path/to/default/cpi' }) - allow(fake_cloud).to receive(:info) + allow(fake_cloud).to receive(:info).and_return({ 'api_version' => 2 }) allow(fake_cloud).to receive(:set_vm_metadata) allow(fake_cloud).to receive(:request_cpi_api_version=) allow(fake_cloud).to receive(:request_cpi_api_version) @@ -165,7 +165,7 @@ def expect_vm_to_be_created 'bosh' => { 'group' => String, 'groups' => anything }, }, ) - .and_return('new-vm-cid') + .and_return(['new-vm-cid', {}]) fake_job_context diff --git a/src/bosh-director/spec/unit/bosh/director/problem_handlers/unresponsive_agent_spec.rb b/src/bosh-director/spec/unit/bosh/director/problem_handlers/unresponsive_agent_spec.rb index 12685a964d..76da589dfb 100644 --- a/src/bosh-director/spec/unit/bosh/director/problem_handlers/unresponsive_agent_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/problem_handlers/unresponsive_agent_spec.rb @@ -54,8 +54,8 @@ def make_handler(instance, cloud, _, data = {}) allow(Config).to receive(:nats_client_ca_private_key_path).and_return(director_config['nats']['client_ca_private_key_path']) allow(Config).to receive(:nats_client_ca_certificate_path).and_return(director_config['nats']['client_ca_certificate_path']) - allow(cloud).to receive(:info) - allow(cloud).to receive(:request_cpi_api_version).and_return(1) + allow(cloud).to receive(:info).and_return('api_version' => 2) + allow(cloud).to receive(:request_cpi_api_version).and_return(2) allow(cloud).to receive(:request_cpi_api_version=) allow(Bosh::Clouds::ExternalCpi).to receive(:new).with('/path/to/default/cpi', 'woof-uuid', @@ -220,7 +220,7 @@ def expect_vm_to_be_created networks, [], { 'key1' => 'value1', 'bosh' => { 'group' => String, 'groups' => anything } } - ).and_return('new-vm-cid') + ).and_return(['new-vm-cid', {}]) expect(fake_new_agent).to receive(:wait_until_ready).ordered expect(fake_new_agent).to receive(:update_settings).ordered diff --git a/src/bosh-director/spec/unit/clouds/external_cpi_response_wrapper_spec.rb b/src/bosh-director/spec/unit/clouds/external_cpi_response_wrapper_spec.rb index 3932d3c5cc..2b1a79f8aa 100644 --- a/src/bosh-director/spec/unit/clouds/external_cpi_response_wrapper_spec.rb +++ b/src/bosh-director/spec/unit/clouds/external_cpi_response_wrapper_spec.rb @@ -561,151 +561,4 @@ def self.it_raises_an_error_with_ok_to_retry(error_class) end end - describe 'when cpi_version is 1' do - let(:cpi_api_version) { 1 } - - describe '#create_vm' do - let(:cpi_response) { JSON.dump(result: 'fake-result', error: nil, log: 'fake-log') } - let(:redacted_network_settings) { nil } - let(:expected_response) { ['fake-result'] } - - it_calls_cpi_method( - :create_vm, - 'fake-agent-id', - 'fake-stemcell-cid', - { 'cloud' => 'props' }, - nil, - ['fake-disk-cid'], - 'bosh' => { - 'group' => 'my-group', - 'groups' => ['my-first-group'], - 'password' => 'my-secret-password', - }, - 'other' => 'value', - ) - - context 'when network settings hash cloud properties is absent' do - let(:expected_response) { ['fake-result'] } - let(:redacted_network_settings) do - { - 'net' => { - 'type' => 'manual', - 'ip' => '10.10.0.2', - 'netmask' => '255.255.255.0', - 'default' => %w[dns gateway], - 'dns' => ['10.10.0.2'], - 'gateway' => '10.10.0.1', - }, - } - end - - it_calls_cpi_method( - :create_vm, - 'fake-agent-id', - 'fake-stemcell-cid', - { 'cloud' => 'props' }, - { - 'net' => { - 'type' => 'manual', - 'ip' => '10.10.0.2', - 'netmask' => '255.255.255.0', - 'default' => %w[dns gateway], - 'dns' => ['10.10.0.2'], - 'gateway' => '10.10.0.1', - }, - }, - ['fake-disk-cid'], - 'bosh' => { - 'group' => 'my-group', - 'groups' => ['my-first-group'], - 'password' => 'my-secret-password', - }, - 'other' => 'value', - ) - end - end - - describe('#attach_disk') do - let(:cpi_response) { JSON.dump(result: 'fake-result', error: nil, log: 'fake-log') } - let(:expected_response) { nil } - it_calls_cpi_method(:attach_disk, 'fake-vm-cid', 'fake-disk-cid') - end - - describe 'forwards all other methods without change' do - let(:cpi_response) { JSON.dump(result: 'fake-result', error: nil, log: 'fake-log') } - let(:expected_response) { 'fake-result' } - - context('#current_vm_id') do - it_calls_cpi_method(:current_vm_id) - end - - context('#create_stemcell') do - it_calls_cpi_method(:create_stemcell, 'fake-stemcell-image-path', 'cloud' => 'props') - end - - context('#delete_stemcell') do - it_calls_cpi_method(:delete_stemcell, 'fake-stemcell-cid') - end - - context('#delete_vm') do - it_calls_cpi_method(:delete_vm, 'fake-vm-cid') - end - - context('#has_vm') do - it_calls_cpi_method(:has_vm, 'fake-vm-cid') - end - - context('#reboot_vm') do - it_calls_cpi_method(:reboot_vm, 'fake-vm-cid') - end - - context('#set_vm_metadata') do - it_calls_cpi_method(:set_vm_metadata, 'fake-vm-cid', 'metadata' => 'hash') - end - - context('#set_disk_metadata') do - it_calls_cpi_method(:set_disk_metadata, 'fake-disk-cid', 'metadata' => 'hash') - end - - context('#create_disk') do - it_calls_cpi_method(:create_disk, 100_000, { 'type' => 'gp2' }, 'fake-vm-cid') - end - - context('#has_disk') do - it_calls_cpi_method(:has_disk, 'fake-disk-cid') - end - - context('#delete_disk') do - it_calls_cpi_method(:delete_disk, 'fake-disk-cid') - end - - context('#detach_disk') do - it_calls_cpi_method(:detach_disk, 'fake-vm-cid', 'fake-disk-cid') - end - - context('#snapshot_disk') do - it_calls_cpi_method(:snapshot_disk, 'fake-disk-cid') - end - - context('#delete_snapshot') do - it_calls_cpi_method(:delete_snapshot, 'fake-snapshot-cid') - end - - context('#resize_disk') do - it_calls_cpi_method(:resize_disk, 'fake-disk-cid', 1024) - end - - context('#get_disks') do - it_calls_cpi_method(:get_disks, 'fake-vm-cid') - end - - context('#ping') do - it_calls_cpi_method(:ping) - end - - context('#info') do - it_calls_cpi_method(:info) - end - end - end end diff --git a/src/bosh-director/spec/unit/clouds/external_cpi_spec.rb b/src/bosh-director/spec/unit/clouds/external_cpi_spec.rb index 8b2cf978d0..dd030ef6ac 100644 --- a/src/bosh-director/spec/unit/clouds/external_cpi_spec.rb +++ b/src/bosh-director/spec/unit/clouds/external_cpi_spec.rb @@ -649,12 +649,13 @@ def self.it_raises_an_error_with_ok_to_retry(error_class) let(:logger) { Logging::Logger.new('ExternalCpi') } let(:cpi_log_path) { '/var/vcap/task/5/cpi' } let(:config) { double('Bosh::Director::Config', logger: logger, cpi_task_log: cpi_log_path) } - let(:external_cpi) { described_class.new(asset_path("bin/dummy_cpi"), 'fake-director-uuid', logger) } + let(:external_cpi_executable) { File.expand_path('../../assets/bin/dummy_cpi', __dir__) } + let(:external_cpi) { described_class.new(external_cpi_executable, 'fake-director-uuid', logger) } before do stub_const('Bosh::Director::Config', config) FileUtils.mkdir_p('/var/vcap/task/5') - allow(File).to receive(:executable?).with(asset_path('bin/dummy_cpi')).and_return(true) + allow(File).to receive(:executable?).with(external_cpi_executable).and_return(true) allow(Open3).to receive(:popen3).and_wrap_original do |original_method, *args, &block| # We need to make sure Open3.popen3 gets called with a env path where ruby exists. # We happen to know it is /usr/local/bin/ in this case. diff --git a/src/spec/integration/cpi_and_agent_spec.rb b/src/spec/integration/cpi_and_agent_spec.rb index c423d6d3e3..ad6d31e94c 100644 --- a/src/spec/integration/cpi_and_agent_spec.rb +++ b/src/spec/integration/cpi_and_agent_spec.rb @@ -10,7 +10,7 @@ def create_vm_sequence(vm_cid, agent_id) target: 'cpi', method: 'create_vm', agent_id: agent_id, - response_matcher: be(vm_cid), + response_matcher: eq([vm_cid, {}]), }, { target: 'cpi', method: 'set_vm_metadata', vm_cid: vm_cid }, { target: 'agent', method: 'ping', agent_id: agent_id, can_repeat: true }, @@ -63,6 +63,8 @@ def create_vm_with_persistent_disk_calls(vm_cid, agent_id, disk_cid) def attach_disk_sequence(vm_cid, agent_id, disk_cid) [ { target: 'cpi', method: 'attach_disk', vm_cid: vm_cid, argument_matcher: include('disk_id' => disk_cid) }, + { target: 'agent', method: 'ping', agent_id: agent_id, can_repeat: true }, + { target: 'agent', method: 'add_persistent_disk', agent_id: agent_id }, { target: 'cpi', method: 'set_disk_metadata', argument_matcher: include('disk_cid' => disk_cid) }, { target: 'agent', method: 'ping', agent_id: agent_id, can_repeat: true }, { target: 'agent', method: 'mount_disk', agent_id: agent_id, argument_matcher: match([disk_cid]) }, @@ -196,7 +198,8 @@ def update_sequence(old_vm_id, old_vm_agent_id, updated_vm_id, updated_vm_agent_ let(:instances) { 1 } let(:old_vm_id) do - old_vm_create_invocation.response + response = old_vm_create_invocation.response + response.is_a?(Array) ? response.first : response end let(:old_vm_agent_id) do diff --git a/src/spec/integration/cpi_versions/director_cpi_v1_spec.rb b/src/spec/integration/cpi_versions/director_cpi_v1_spec.rb deleted file mode 100644 index a0c4315960..0000000000 --- a/src/spec/integration/cpi_versions/director_cpi_v1_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'spec_helper' - -describe 'director behaviour', type: :integration do - with_reset_sandbox_before_each(dummy_cpi_api_version: 1) - - let(:cpi_version) { 1 } - let(:cpi_version_string) { "\"api_version\":#{cpi_version}" } - let(:response_string) { nil } - let(:search_filter_string) { nil } - - shared_examples_for 'using CPI specific cpi_api_version' do - before do - manifest_hash = SharedSupport::DeploymentManifestHelper.deployment_manifest(instances: 1) - manifest_hash['instance_groups'][0]['persistent_disk'] = 1000 - output = deploy_from_scratch(manifest_hash: manifest_hash) - task_id = IntegrationSupport::OutputParser.new(output).task_id - @task_output = bosh_runner.run("task #{task_id} --debug") - end - - it 'responds with specific cpi_api_version results' do - api_version_filter = @task_output.split(/\n+/).select { |i| i[/\[external-cpi\] \[cpi-\d{6}\].*"method":"create_vm"/] } - expect(api_version_filter).to_not be_empty - api_version_filter.each do |result| - expect(result).to include(cpi_version_string) - end - - cpi_method_call_filter = @task_output.split(/\n+/).select { |i| i[/\[external-cpi\] \[cpi-\d{6}\]/] } - expect(cpi_method_call_filter).to_not be_empty - cpi_method_call_filter.each_with_index do |result, index| - expect(result).to match(response_string) if result.include?(search_filter_string) - end - end - end - - context 'when cpi_version < 2' do - context 'create_vm' do - let(:response_string) { /"result":"\d+"/ } - let(:search_filter_string) { 'DEBUG - Dummy: create_vm' } - - it_behaves_like 'using CPI specific cpi_api_version' - end - - context 'attach_disk' do - let(:response_string) { /"result":\d+/ } - let(:search_filter_string) { 'DEBUG - Saving input for attach_disk' } - - it_behaves_like 'using CPI specific cpi_api_version' - end - end -end diff --git a/src/spec/integration/unmanaged_persistent_disks_spec.rb b/src/spec/integration/unmanaged_persistent_disks_spec.rb index df12ae608c..63cd229cf5 100644 --- a/src/spec/integration/unmanaged_persistent_disks_spec.rb +++ b/src/spec/integration/unmanaged_persistent_disks_spec.rb @@ -257,27 +257,4 @@ def upload_multidisk_release expect(attached_disks_cids).to match_array(disk_hints_cids) end - context 'when CPI is v1' do - with_reset_sandbox_before_each(dummy_cpi_api_version: 1) - - before do - upload_multidisk_release - upload_stemcell - - upload_cloud_config(cloud_config_hash: cloud_config_hash) - @deploy_output = deploy_simple_manifest(manifest_hash: manifest_hash) - end - - it 'director should not send add_persistent_disk action to agent' do - agent_dir = current_sandbox.cpi.agent_dir_for_vm_cid(director.instances.first.vm_cid) - - disk_names = JSON.parse(File.read("#{agent_dir}/bosh/disk_associations.json")) - expect(disk_names).to include( - 'high-iops-persistent-disk-name', 'low-iops-persistent-disk-name' - ) - - v2_only_disk_settings_file = "#{agent_dir}/bosh/persistent_disk_hints.json" - expect(File.exist?(v2_only_disk_settings_file)).to be_falsey - end - end end diff --git a/src/spec/integration/vm_delete_spec.rb b/src/spec/integration/vm_delete_spec.rb index 5f927276c5..d3554efada 100644 --- a/src/spec/integration/vm_delete_spec.rb +++ b/src/spec/integration/vm_delete_spec.rb @@ -104,7 +104,7 @@ it 'deletes the vm by its vm_cid' do network = { 'a' => { 'ip' => '192.168.1.5', 'type' => 'dynamic' } } - id = current_sandbox.cpi.create_vm(SecureRandom.uuid, current_sandbox.cpi.latest_stemcell['id'], {}, network, [], env) + id, _ = current_sandbox.cpi.create_vm(SecureRandom.uuid, current_sandbox.cpi.latest_stemcell['id'], {}, network, [], env) expect(current_sandbox.cpi.has_vm(id)).to be_truthy bosh_runner.run("delete-vm #{id}", deployment_name: 'simple') diff --git a/src/spec/integration_support/bin/dummy_cpi b/src/spec/integration_support/bin/dummy_cpi index dfe78fc01f..19d5d36861 100755 --- a/src/spec/integration_support/bin/dummy_cpi +++ b/src/spec/integration_support/bin/dummy_cpi @@ -8,8 +8,7 @@ require 'stringio' require 'yaml' require 'cloud' -require_relative '../clouds/dummy' -require_relative '../clouds/dummy_v2' +require_relative '../dummy_cpi' module Logging def self.appenders @@ -65,12 +64,11 @@ begin context = request['context'] requested_api_version = request['api_version'] - dummy = case requested_api_version - when 2 - Bosh::Clouds::DummyV2.new(cpi_config, context) - else - Bosh::Clouds::Dummy.new(cpi_config, context, cpi_config['api_version']) - end + unless requested_api_version.nil? || requested_api_version == 2 + raise "Unsupported CPI API version: #{requested_api_version}. Only version 2 is supported." + end + + dummy = DummyCpi.new(cpi_config, context) result = dummy.send(command, *arguments) rescue StandardError, ScriptError => e diff --git a/src/spec/integration_support/clouds/dummy.rb b/src/spec/integration_support/clouds/dummy.rb deleted file mode 100644 index a53853f857..0000000000 --- a/src/spec/integration_support/clouds/dummy.rb +++ /dev/null @@ -1,943 +0,0 @@ -require 'digest/sha1' -require 'fileutils' -require 'ipaddr' -require 'json' -require 'membrane' -require 'securerandom' -require 'socket' -require 'yaml' -require 'clouds/errors' - -module Bosh - module Clouds - class Dummy - class NotImplemented < StandardError; end - - BOSH_REPO_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', '..')) - - attr_reader :commands - attr_accessor :options - - def initialize(options, context, api_version) - @options = options - @context = context - @api_version = api_version - @stemcell_api_version = options.fetch('api_version', nil) - - @supported_formats = context['formats'] || ['dummy'] - @base_dir = options['dir'] - if @base_dir.nil? - raise ArgumentError, 'Must specify dir' - end - - @running_vms_dir = File.join(@base_dir, 'running_vms') - @vm_repo = VMRepo.new(@running_vms_dir) - @tmp_dir = File.join(@base_dir, 'tmp') - FileUtils.mkdir_p(@tmp_dir) - - @logger = Logging::Logger.new("DummyCPI_#{object_id}") - @logger.add_appenders(Logging.appenders.io( - "DummyCPIIO_#{object_id}", - options['log_buffer'] || STDOUT - )) - - @commands = CommandTransport.new(@base_dir, @logger) - @inputs_recorder = InputsRecorder.new(@base_dir, @logger, @context) - - prepare - rescue Errno::EACCES - raise ArgumentError, "cannot create dummy cloud base directory #{@base_dir}" - end - - CREATE_STEMCELL_SCHEMA = Membrane::SchemaParser.parse { {image_path: String, cloud_properties: Hash} } - def create_stemcell(image_path, cloud_properties) - validate_and_record_inputs(CREATE_STEMCELL_SCHEMA, __method__, image_path, cloud_properties) - - content = File.read(image_path) - data = YAML.load(content, aliases: true) - data.merge!('image' => image_path) - stemcell_id = SecureRandom.uuid - - File.write(stemcell_file(stemcell_id), YAML.dump(data)) - stemcell_id - end - - DELETE_STEMCELL_SCHEMA = Membrane::SchemaParser.parse { {stemcell_id: String} } - def delete_stemcell(stemcell_id) - validate_and_record_inputs(DELETE_STEMCELL_SCHEMA, __method__, stemcell_id) - FileUtils.rm(stemcell_file(stemcell_id)) - end - - CREATE_VM_SCHEMA = Membrane::SchemaParser.parse do - { - agent_id: String, - stemcell_id: String, - cloud_properties: Hash, - networks: Hash, - disk_cids: [String], - env: Hash, - } - end - - def create_vm(agent_id, stemcell_id, cloud_properties, networks, disk_cids, env) - @logger.debug('Dummy: create_vm') - validate_and_record_inputs(CREATE_VM_SCHEMA, __method__, agent_id, stemcell_id, cloud_properties, networks, disk_cids, env) - - ips = [] - cmd = commands.next_create_vm_cmd - - if cmd.failed? - raise Bosh::Clouds::CloudError.new('Creating vm failed') - end - - networks.each do |network_name, network| - if network['type'] != 'dynamic' - ips << { 'network' => network_name, 'ip' => network.fetch('ip') } - else - if cmd.ip_address - ip_address = cmd.ip_address - elsif cloud_properties['az_name'] - ip_address = cmd.ip_address_for_az(cloud_properties['az_name']) - else - ip_address = IPAddr.new(rand(0..IPAddr::IN4MASK), Socket::AF_INET).to_s - end - - if ip_address - ips << { 'network' => network_name, 'ip' => ip_address } - write_agent_default_network(agent_id, ip_address) - end - end - end - - allocate_ips(ips) - - write_agent_settings(agent_id, { - agent_id: agent_id, - blobstore: @options['agent']['blobstore'], - ntp: [], - disks: { persistent: {} }, - networks: networks, - vm: { name: "vm-#{agent_id}" }, - cert: '', - env: env, - mbus: @options['nats'], - }) - - agent_process_agent_id = agent_id - if commands.create_vm_unresponsive_agent || - agent_id == commands.unresponsive_agent_agent_id - agent_process_agent_id = 'unresponsive-agent-fake-id-' + SecureRandom.uuid - end - - agent_pid = spawn_agent_process(agent_process_agent_id, cloud_properties['legacy_agent_path']) - vm = VM.new(agent_pid.to_s, agent_id, cloud_properties, ips) - - @vm_repo.save(vm) - - vm.id - end - - DELETE_VM_SCHEMA = Membrane::SchemaParser.parse { {vm_cid: String} } - def delete_vm(vm_cid) - validate_and_record_inputs(DELETE_VM_SCHEMA, __method__, vm_cid) - commands.wait_for_unpause_delete_vms - detach_disks_attached_to_vm(vm_cid) - agent_pid = vm_cid.to_i - Process.kill('KILL', agent_pid) - - # rubocop:disable HandleExceptions - rescue Errno::ESRCH - raise Bosh::Clouds::VMNotFound if commands.raise_vmnotfound - # rubocop:enable HandleExceptions - ensure - free_ips(vm_cid) - FileUtils.rm_rf(File.join(@base_dir, 'running_vms', vm_cid)) - end - - REBOOT_VM_SCHEMA = Membrane::SchemaParser.parse { {vm_cid: String} } - def reboot_vm(vm_cid) - validate_and_record_inputs(REBOOT_VM_SCHEMA, __method__, vm_cid) - raise Bosh::Clouds::NotImplemented, 'Dummy CPI does not implement reboot_vm' - end - - HAS_VM_SCHEMA = Membrane::SchemaParser.parse { {vm_cid: String} } - def has_vm(vm_cid) - validate_and_record_inputs(HAS_VM_SCHEMA, __method__, vm_cid) - @vm_repo.exists?(vm_cid) - end - - def info - record_inputs(__method__, nil) - { - stemcell_formats: @supported_formats, - }.tap do |response| - response['api_version'] = @api_version unless @api_version.nil? - end - end - - HAS_DISK_SCHEMA = Membrane::SchemaParser.parse { {disk_id: String} } - def has_disk(disk_id) - validate_and_record_inputs(HAS_DISK_SCHEMA, __method__, disk_id) - File.exist?(disk_file(disk_id)) - end - - ATTACH_DISK_SCHEMA = Membrane::SchemaParser.parse { {vm_cid: String, disk_id: String} } - def attach_disk(vm_cid, disk_id) - validate_and_record_inputs(ATTACH_DISK_SCHEMA, __method__, vm_cid, disk_id) - - raise Bosh::Clouds::NotImplemented, 'Bosh::Clouds::NotImplemented' if commands.raise_attach_disk_not_implemented - - if disk_attached?(disk_id) - raise "#{disk_id} is already attached to an instance" - end - file = attachment_file(vm_cid, disk_id) - FileUtils.mkdir_p(File.dirname(file)) - FileUtils.touch(file) - - @logger.debug("Attached disk: '#{disk_id}' to vm: '#{vm_cid}' at attachment file: #{file}") - - agent_id = agent_id_for_vm_id(vm_cid) - settings = read_agent_settings(agent_id) - settings['disks']['persistent'][disk_id] = 'attached' - write_agent_settings(agent_id, settings) - end - - DETACH_DISK_SCHEMA = Membrane::SchemaParser.parse { {vm_cid: String, disk_id: String} } - def detach_disk(vm_cid, disk_id) - validate_and_record_inputs(DETACH_DISK_SCHEMA, __method__, vm_cid, disk_id) - - raise Bosh::Clouds::NotImplemented, 'Bosh::Clouds::NotImplemented' if commands.raise_detach_disk_not_implemented - - unless disk_attached_to_vm?(vm_cid, disk_id) - raise Bosh::Clouds::DiskNotAttached, "#{disk_id} is not attached to instance #{vm_cid}" - end - FileUtils.rm_rf(attachment_path(disk_id)) - - agent_id = agent_id_for_vm_id(vm_cid) - settings = read_agent_settings(agent_id) - settings['disks']['persistent'].delete(disk_id) - write_agent_settings(agent_id, settings) - end - - CREATE_DISK_SCHEMA = Membrane::SchemaParser.parse { {size: Integer, cloud_properties: Hash, vm_locality: String} } - def create_disk(size, cloud_properties, vm_locality) - validate_and_record_inputs(CREATE_DISK_SCHEMA, __method__, size, cloud_properties, vm_locality) - disk_id = SecureRandom.hex - file = disk_file(disk_id) - FileUtils.mkdir_p(File.dirname(file)) - disk_info = JSON.generate({size: size, cloud_properties: cloud_properties, vm_locality: vm_locality}) - File.write(file, disk_info) - disk_id - end - - DELETE_DISK_SCHEMA = Membrane::SchemaParser.parse { {disk_id: String} } - def delete_disk(disk_id) - validate_and_record_inputs(DELETE_DISK_SCHEMA, __method__, disk_id) - FileUtils.rm(disk_file(disk_id)) - end - - CREATE_NETWORK_SCHEMA = Membrane::SchemaParser.parse { { subnet_definition: Hash } } - def create_network(subnet_definition) - validate_and_record_inputs(CREATE_NETWORK_SCHEMA, __method__, subnet_definition) - - raise subnet_definition['cloud_properties']['error'] if subnet_definition['cloud_properties'].key?('error') - - network_id = SecureRandom.hex - file = network_file(network_id) - FileUtils.mkdir_p(File.dirname(file)) - network_info = JSON.generate(subnet_definition) - File.write(file, network_info) - addr_properties = {} - if subnet_definition.key?('netmask_bits') - addr_properties['range'] = '192.168.10.0/24' - addr_properties['gateway'] = '192.168.10.1' - addr_properties['reserved'] = ['192.168.10.2'] - end - - [network_id, addr_properties, { name: network_id }] - end - - DELETE_NETWORK_SCHEMA = Membrane::SchemaParser.parse { { network_id: String } } - def delete_network(network_id) - validate_and_record_inputs(DELETE_NETWORK_SCHEMA, __method__, network_id) - FileUtils.rm(network_file(network_id)) - end - - SNAPSHOT_DISK_SCHEMA = Membrane::SchemaParser.parse { { disk_id: String, metadata: Hash } } - def snapshot_disk(disk_id, metadata) - validate_and_record_inputs(SNAPSHOT_DISK_SCHEMA, __method__, disk_id, metadata) - snapshot_id = SecureRandom.hex - file = snapshot_file(snapshot_id) - FileUtils.mkdir_p(File.dirname(file)) - File.write(file, metadata.to_json) - snapshot_id - end - - DELETE_SNAPSHOT_SCHEMA = Membrane::SchemaParser.parse { {snapshot_id: String} } - def delete_snapshot(snapshot_id) - validate_and_record_inputs(DELETE_SNAPSHOT_SCHEMA, __method__, snapshot_id) - FileUtils.rm(snapshot_file(snapshot_id)) - end - - RESIZE_DISK_SCHEMA = Membrane::SchemaParser.parse { {disk_id: String, new_size: Integer } } - def resize_disk(disk_id, new_size) - validate_and_record_inputs(RESIZE_DISK_SCHEMA, __method__, disk_id, new_size) - - raise Bosh::Clouds::NotImplemented, 'Bosh::Clouds::NotImplemented' if commands.raise_resize_disk_not_implemented - - disk_info_file = disk_file(disk_id) - disk_info = JSON.parse(File.read(disk_info_file)) - disk_info['size'] = new_size - File.write(disk_info_file, JSON.generate(disk_info)) - end - - UPDATE_DISK_SCHEMA = Membrane::SchemaParser.parse { {disk_id: String, new_size: Integer, cloud_properties: Hash} } - def update_disk(disk_id, new_size, cloud_properties) - validate_and_record_inputs(UPDATE_DISK_SCHEMA, __method__, disk_id, new_size, cloud_properties) - - raise Bosh::Clouds::NotImplemented, 'Bosh::Clouds::NotImplemented' if commands.raise_update_disk_not_implemented - - disk_info_file = disk_file(disk_id) - disk_info = JSON.parse(File.read(disk_info_file)) - disk_info['size'] = new_size - disk_info['cloud_properties'] = cloud_properties - File.write(disk_info_file, JSON.generate(disk_info)) - nil - end - - SET_VM_METADATA_SCHEMA = Membrane::SchemaParser.parse { {vm_cid: String, metadata: Hash} } - def set_vm_metadata(vm_cid, metadata) - raise 'Set VM metadata failed!!!' if commands.set_vm_metadata_should_fail? - validate_and_record_inputs(SET_VM_METADATA_SCHEMA, __method__, vm_cid, metadata) - end - - SET_DISK_METADATA_SCHEMA = Membrane::SchemaParser.parse { {disk_cid: String, metadata: Hash} } - def set_disk_metadata(disk_cid, metadata) - validate_and_record_inputs(SET_DISK_METADATA_SCHEMA, __method__, disk_cid, metadata) - end - - CALCULATE_VM_CLOUD_PROPERTIES_SCHEMA = Membrane::SchemaParser.parse { { vm_resources: {'ram' => Integer, 'cpu' => Integer, 'ephemeral_disk_size' => Integer} } } - def calculate_vm_cloud_properties(vm_resources) - validate_and_record_inputs( - CALCULATE_VM_CLOUD_PROPERTIES_SCHEMA, - __method__, - vm_resources - ) - instance_type = @context['cvcpkey'].nil? ? 'dummy' : @context['cvcpkey'] - { - instance_type: instance_type, - cpu: vm_resources['cpu'], - ram: vm_resources['ram'], - ephemeral_disk: { size: vm_resources['ephemeral_disk_size'] } - } - end - - # Additional Dummy test helpers - - def prepare - FileUtils.mkdir_p(@base_dir) - end - - def reset - FileUtils.rm_rf(@base_dir) - prepare - end - - def vm_cids - # Shuffle so that no one relies on the order of VMs - Dir.glob(File.join(@running_vms_dir, '*')).map { |vm| File.basename(vm) }.shuffle - end - - def disk_cids - # Shuffle so that no one relies on the order of disks - Dir.glob(disk_file('*')).map { |disk| File.basename(disk) }.shuffle - end - - def network_cids - # Shuffle so that no one relies on the order of networks - Dir.glob(network_file('*')).map { |network| File.basename(network) }.shuffle - end - - def kill_agents - vm_cids.each do |agent_pid| - kill_process(agent_pid) - end - end - - def kill_process(agent_pid) - Process.kill('KILL', agent_pid.to_i) - # rubocop:disable HandleExceptions - rescue Errno::ESRCH - # rubocop:enable HandleExceptions - end - - def agent_log_path(agent_id) - "#{@base_dir}/agent.#{agent_id}.log" - end - - def read_cloud_properties(vm_cid) - @vm_repo.load(vm_cid).cloud_properties - end - - def invocations - @inputs_recorder.read_all - end - - def invocations_for_method(method) - @inputs_recorder.read(method) - end - - def all_stemcells - files = Dir.entries(@base_dir).select { |file| file.match(/stemcell_./) } - - Dir.chdir(@base_dir) do - [].tap do |results| - files.each do |file| - # data --> [{ 'name' => 'ubuntu-stemcell', 'version': '1', 'image' => }] - data = YAML.load(File.read(file), aliases: true) - results << { 'id' => file.sub(/^stemcell_/, '') }.merge(data) - end - end.sort { |a, b| a[:version].to_i <=> b[:version].to_i } - end - end - - def latest_stemcell - all_stemcells.last - end - - def all_snapshots - if File.exist?(snapshot_file('')) - Dir.glob(snapshot_file('*')) - else - [] - end - end - - def all_ips - Dir.glob(File.join(@base_dir, 'dummy_cpi_networks', '*')) - .reject { |path| File.directory?(path) } - .map { |path| File.basename(path) } - end - - def agent_dir_for_vm_cid(vm_cid) - agent_id = agent_id_for_vm_id(vm_cid) - agent_base_dir(agent_id) - end - - def disk_attached_to_vm?(vm_cid, disk_id) - File.exist?(attachment_file(vm_cid, disk_id)) - end - - def current_apply_spec_for_vm(vm_cid) - agent_base_dir = agent_dir_for_vm_cid(vm_cid) - spec_file = File.join(agent_base_dir, 'bosh', 'spec.json') - JSON.parse(File.read(spec_file)) - end - - def attached_disk_infos(vm_cid) - agent_id = agent_id_for_vm_id(vm_cid) - settings = read_agent_settings(agent_id) - return [] unless settings.has_key?('disks') && settings['disks'].has_key?('persistent') - - settings['disks']['persistent'].inject([]) do |memo, disk_attachment| - disk_cid = disk_attachment[0] - device_path = disk_attachment[1] - - disk_info_hash = JSON.parse(File.read(disk_file(disk_cid))) - disk_info_hash['disk_cid'] = disk_cid - disk_info_hash['device_path'] = device_path - - memo << disk_info_hash - end - end - - def spawn_agent_process(agent_id, legacy_agent_path = nil) - root_dir = File.join(agent_base_dir(agent_id), 'root_dir') - FileUtils.mkdir_p(File.join(root_dir, 'etc', 'logrotate.d')) - - agent_cmd = agent_cmd(agent_id, legacy_agent_path) - agent_log = agent_log_path(agent_id) - - agent_pid = Process.spawn( - { 'TMPDIR' => @tmp_dir }, - *agent_cmd, - { - chdir: agent_base_dir(agent_id), - out: agent_log, - err: agent_log, - } - ) - - begin - Process.getpgid(agent_pid) - rescue => e - raise RuntimeError, "Expected agent to be running: #{e}" - end - - Process.detach(agent_pid) - - agent_pid - end - - private - - def allocate_ips(ips) - ips.each do |ip| - begin - network_dir = File.join(@base_dir, 'dummy_cpi_networks') - FileUtils.makedirs(network_dir) - File.open(File.join(network_dir, ip['ip']), File::WRONLY|File::CREAT|File::EXCL).close - rescue Errno::EEXIST - # at this point we should actually free all the IPs we successfully allocated before the collision, - # but in practice the tests only feed in one IP per VM so that cleanup code would never be exercised - raise "IP Address #{ip['ip']} in network '#{ip['network']}' is already in use" - end - end - end - - def free_ips(vm_cid) - return unless @vm_repo.exists?(vm_cid) - vm = @vm_repo.load(vm_cid) - vm.ips.each do |ip| - FileUtils.rm_rf(File.join(@base_dir, 'dummy_cpi_networks', ip['ip'])) - end - end - - def agent_id_for_vm_id(vm_cid) - @vm_repo.load(vm_cid).agent_id - end - - def agent_settings_file(agent_id) - # Even though dummy CPI has complete access to agent execution file system - # it should never write directly to settings.json because - # the agent is responsible for retrieving the settings from the CPI. - File.join(agent_base_dir(agent_id), 'bosh', 'dummy-cpi-agent-env.json') - end - - def agent_base_dir(agent_id) - "#{@base_dir}/agent-base-dir-#{agent_id}" - end - - def write_agent_settings(agent_id, settings) - FileUtils.mkdir_p(File.dirname(agent_settings_file(agent_id))) - File.write(agent_settings_file(agent_id), JSON.generate(settings)) - end - - def write_agent_default_network(agent_id, ip_address) - # Agent looks for following file to resolve default network on dummy infrastructure - path = File.join(agent_base_dir(agent_id), 'bosh', 'dummy-default-network-settings.json') - FileUtils.mkdir_p(File.dirname(path)) - File.write(path, JSON.generate('ip' => ip_address)) - end - - def agent_cmd(agent_id, legacy_agent_path) - if legacy_agent_path.nil? - go_agent_exe = File.join(BOSH_REPO_ROOT, 'src', 'tmp', 'bin', 'bosh-agent') - else - go_agent_exe = legacy_agent_path - end - - agent_config_file = File.join(agent_base_dir(agent_id), 'agent.json') - - agent_config = { - 'Infrastructure' => { - 'Settings' => { - 'Sources' => [{ - 'Type' => 'File', - 'SettingsPath' => agent_settings_file(agent_id) - }] - } - } - } - - File.write(agent_config_file, JSON.generate(agent_config)) - - %W[#{go_agent_exe} -b #{agent_base_dir(agent_id)} -P dummy -M dummy-nats -C #{agent_config_file}] - end - - def read_agent_settings(agent_id) - JSON.parse(File.read(agent_settings_file(agent_id))) - end - - def stemcell_file(stemcell_id) - File.join(@base_dir, "stemcell_#{stemcell_id}") - end - - def disk_file(disk_id) - File.join(@base_dir, 'disks', disk_id) - end - - def network_file(network_id) - File.join(@base_dir, 'networks', network_id) - end - - def disk_attached?(disk_id) - File.exist?(attachment_path(disk_id)) - end - - def detach_disks_attached_to_vm(vm_cid) - @logger.debug("Detaching disks for vm #{vm_cid}") - Dir.glob(attachment_file(vm_cid, '*')) do |file_path| - @logger.debug("Detaching found attachment #{file_path}") - FileUtils.rm_rf(File.dirname(file_path)) - end - end - - def attachment_file(vm_cid, disk_id) - File.join(attachment_path(disk_id), vm_cid) - end - - def attachment_path(disk_id) - File.join(@base_dir, 'attachments', disk_id) - end - - def snapshot_file(snapshot_id) - File.join(@base_dir, 'snapshots', snapshot_id) - end - - def validate_and_record_inputs(schema, the_method, *args) - parameter_names_to_values = parameter_names_to_values(the_method, *args) - begin - schema.validate(parameter_names_to_values) - rescue Membrane::SchemaValidationError => err - raise ArgumentError, "Invalid arguments sent to #{the_method}: #{err.message}" - end - record_inputs(the_method, parameter_names_to_values) - end - - def record_inputs(method, args) - @inputs_recorder.record(method, args) - end - - def parameter_names_to_values(the_method, *the_method_args) - hash = {} - method(the_method).parameters.each_with_index do |param, index| - hash[param[1]] = the_method_args[index] - end - hash - end - - # Example file system layout for arranging commands information. - # Currently uses file system as transport but could be switch to use NATS. - # base_dir/cpi/create_vm/next -> {"something": true} - class CommandTransport - def initialize(base_dir, logger) - @cpi_commands = File.join(base_dir, 'cpi_commands') - @logger = logger - end - - def pause_delete_vms - @logger.debug('Pausing delete_vms') - path = File.join(@cpi_commands, 'pause_delete_vms') - FileUtils.mkdir_p(File.dirname(path)) - File.write(path, 'marker') - end - - def unpause_delete_vms - @logger.debug('Unpausing delete_vms') - FileUtils.rm_rf File.join(@cpi_commands, 'pause_delete_vms') - FileUtils.rm_rf File.join(@cpi_commands, 'wait_for_unpause_delete_vms') - end - - def wait_for_delete_vms - @logger.debug('Wait for delete_vms') - path = File.join(@cpi_commands, 'wait_for_unpause_delete_vms') - sleep(0.1) until File.exist?(path) - end - - def wait_for_unpause_delete_vms - path = File.join(@cpi_commands, 'wait_for_unpause_delete_vms') - FileUtils.mkdir_p(File.dirname(path)) - File.write(path, 'marker') - - path = File.join(@cpi_commands, 'pause_delete_vms') - if File.exist?(path) - @logger.debug('Wait for unpausing delete_vms') - end - sleep(0.1) while File.exist?(path) - end - - def make_create_vm_always_use_dynamic_ip(ip_address) - @logger.debug("Making create_vm method to set ip address #{ip_address}") - FileUtils.mkdir_p(File.dirname(always_path)) - File.write(always_path, ip_address) - end - - def set_dynamic_ips_for_azs(az_to_ips) - @logger.debug("Making create_vm method to set #{az_to_ips.inspect}") - FileUtils.mkdir_p(File.dirname(azs_path)) - File.write(azs_path, JSON.generate(az_to_ips)) - end - - def make_create_vm_always_fail - @logger.debug('Making create_vm method always fail') - FileUtils.mkdir_p(File.dirname(failed_path)) - File.write(failed_path, '') - end - - def allow_create_vm_to_succeed - @logger.debug('Allowing create_vm method to succeed (removing any mandatory failures)') - FileUtils.rm(failed_path) - end - - def next_create_vm_cmd - @logger.debug('Reading create_vm configuration') - ip_address = File.read(always_path) if File.exist?(always_path) - azs_content = File.exist?(azs_path) ? File.read(azs_path) : nil - azs_to_ip = azs_content.to_s.strip.empty? ? {} : JSON.parse(azs_content) - failed = File.exist?(failed_path) - CreateVmCommand.new(ip_address, azs_to_ip, failed) - end - - def make_set_vm_metadata_always_fail - @logger.debug('Making set_vm_metadata method always fail') - FileUtils.mkdir_p(File.dirname(set_vm_metadata_path_fail_path)) - File.write(set_vm_metadata_path_fail_path, '') - end - - def allow_set_vm_metadata_to_succeed - @logger.debug('Allowing set_vm_metadata method to succeed (removing any mandatory failures)') - FileUtils.rm(set_vm_metadata_path_fail_path) - end - - def set_vm_metadata_should_fail? - @logger.info('Reading set_vm_metadata configuration') - File.exist?(set_vm_metadata_path_fail_path) - end - - def make_delete_vm_to_raise_vmnotfound - @logger.info('Making delete_vm method to raise VMNotFound exception') - FileUtils.mkdir_p(File.dirname(raise_vmnotfound_path)) - File.write(raise_vmnotfound_path, '') - end - - def raise_vmnotfound - @logger.info('Reading delete_vm configuration') - File.exist?(raise_vmnotfound_path) - end - - def allow_detach_disk_to_succeed - @logger.debug('Allowing detach_disk method to succeed (removing any mandatory failures)') - FileUtils.rm(raise_detach_disk_not_implemented_path) - end - - def make_detach_disk_to_raise_not_implemented - @logger.info('Making detach_disk method to raise NotImplemented exception') - FileUtils.mkdir_p(File.dirname(raise_detach_disk_not_implemented_path)) - FileUtils.touch(raise_detach_disk_not_implemented_path) - end - - def make_create_vm_have_unresponsive_agent_for_agent_id(agent_id) - @logger.info("Making create_vm method create with an unresponsive agent for agent_id #{agent_id}") - FileUtils.mkdir_p(File.dirname(create_vm_unresponsive_agent_agent_id_path)) - File.write(create_vm_unresponsive_agent_agent_id_path, agent_id) - end - - def make_create_vm_have_unresponsive_agent - @logger.info('Making create_vm method create with an unresponsive agent') - FileUtils.mkdir_p(File.dirname(create_vm_unresponsive_agent_path)) - FileUtils.touch(create_vm_unresponsive_agent_path) - end - - def allow_create_vm_to_have_responsive_agent - @logger.info('Making create_vm method create with a responsive agent') - FileUtils.rm_f(create_vm_unresponsive_agent_agent_id_path) - FileUtils.rm_f(create_vm_unresponsive_agent_path) - end - - def allow_attach_disk_to_succeed - @logger.debug('Allowing attach_disk method to succeed (removing any mandatory failures)') - FileUtils.rm(raise_attach_disk_not_implemented_path) - end - - def make_attach_disk_to_raise_not_implemented - @logger.info('Making attach_disk method to raise NotImplemented exception') - FileUtils.mkdir_p(File.dirname(raise_attach_disk_not_implemented_path)) - FileUtils.touch(raise_attach_disk_not_implemented_path) - end - - def raise_attach_disk_not_implemented - @logger.info('Reading attach_disk_not_implemented') - File.exist?(raise_attach_disk_not_implemented_path) - end - - def create_vm_unresponsive_agent - @logger.info('Reading create_vm_unresponsive_agent') - File.exist?(create_vm_unresponsive_agent_path) - end - - def unresponsive_agent_agent_id - @logger.info('Reading create_vm_unresponsive_agent_agent_id') - File.read(create_vm_unresponsive_agent_agent_id_path) - rescue StandardError - false - end - - def make_resize_disk_to_raise_not_implemented - @logger.info('Making resize_disk method to raise NotImplemented exception') - FileUtils.mkdir_p(File.dirname(raise_resize_disk_not_implemented_path)) - FileUtils.touch(raise_resize_disk_not_implemented_path) - end - - def raise_detach_disk_not_implemented - @logger.info('Reading detach_disk_not_implemented') - File.exist?(raise_detach_disk_not_implemented_path) - end - - def raise_resize_disk_not_implemented - @logger.info('Reading resize_disk_not_implemented') - File.exist?(raise_resize_disk_not_implemented_path) - end - - def make_update_disk_to_raise_not_implemented - @logger.info('Making update_disk method to raise NotImplemented exception') - FileUtils.mkdir_p(File.dirname(raise_update_disk_not_implemented_path)) - FileUtils.touch(raise_update_disk_not_implemented_path) - end - - def raise_update_disk_not_implemented - @logger.info('Reading update_disk_not_implemented') - File.exist?(raise_update_disk_not_implemented_path) - end - - private - - def azs_path - File.join(@cpi_commands, 'create_vm', 'az_ips') - end - - def always_path - File.join(@cpi_commands, 'create_vm', 'always') - end - - def failed_path - File.join(@cpi_commands, 'create_vm', 'fail') - end - - def set_vm_metadata_path_fail_path - File.join(@cpi_commands, 'update_vm_metadata', 'fail') - end - - def raise_vmnotfound_path - File.join(@cpi_commands, 'delete_vm', 'fail') - end - - def raise_detach_disk_not_implemented_path - File.join(@cpi_commands, 'detach_disk', 'not_implemented') - end - - def raise_attach_disk_not_implemented_path - File.join(@cpi_commands, 'attach_disk', 'not_implemented') - end - - def create_vm_unresponsive_agent_path - File.join(@cpi_commands, 'create_vm', 'unresponsive_agent') - end - - def create_vm_unresponsive_agent_agent_id_path - File.join(@cpi_commands, 'create_vm', 'unresponsive_agent_agent_id') - end - - def raise_resize_disk_not_implemented_path - File.join(@cpi_commands, 'resize_disk', 'not_implemented') - end - - def raise_update_disk_not_implemented_path - File.join(@cpi_commands, 'update_disk', 'not_implemented') - end - end - - class ConfigureNetworksCommand < Struct.new(:not_supported); end - class CreateVmCommand - attr_reader :ip_address, :failed - - def initialize(ip_address, azs_to_ip, failed) - @ip_address = ip_address - @azs_to_ip = azs_to_ip - @failed = failed - end - - def ip_address_for_az(az_name) - @azs_to_ip[az_name] - end - - def failed? - failed - end - end - - class InputsRecorder - def initialize(base_dir, logger, context) - @cpi_inputs_dir = File.join(base_dir, 'cpi_inputs') - @logger = logger - @context = context - end - - def record(method, args) - FileUtils.mkdir_p(@cpi_inputs_dir) - data = {method_name: method, inputs: args, context: @context} - @logger.debug("Saving input for #{method} #{ordered_file_path}") - File.open(ordered_file_path, 'a') { |f| f.puts(JSON.dump(data)) } - end - - def read(method_name) - @logger.debug("Reading input for #{method_name}") - read_all.select do |invocation| - invocation.method_name == method_name - end - end - - def read_all - @logger.debug("Reading all inputs: #{File.read(ordered_file_path)}") - result = [] - File.read(ordered_file_path).split("\n").each do |request| - data = JSON.parse(request) - result << CpiInvocation.new(data['method_name'], data['inputs'], data['context']) - end - result - end - - def ordered_file_path - File.join(@cpi_inputs_dir, 'all_requests') - end - end - - class VM < Struct.new(:id, :agent_id, :cloud_properties, :ips) - end - - class VMRepo - def initialize(running_vms_dir) - @running_vms_dir = running_vms_dir - FileUtils.mkdir_p(@running_vms_dir) - end - - def load(id) - attrs = JSON.parse(File.read(vm_file(id))) - VM.new(id, attrs.fetch('agent_id'), attrs.fetch('cloud_properties'), attrs.fetch('ips')) - end - - def exists?(id) - File.exist?(vm_file(id)) - end - - def save(vm) - serialized_vm = JSON.dump({ - 'agent_id' => vm.agent_id, - 'cloud_properties' => vm.cloud_properties, - 'ips' => vm.ips, - }) - - File.write(vm_file(vm.id), serialized_vm) - end - - private - - def vm_file(vm_cid) - File.join(@running_vms_dir, vm_cid) - end - end - - class CpiInvocation < Struct.new(:method_name, :inputs, :context); end - end - end -end diff --git a/src/spec/integration_support/clouds/dummy_v2.rb b/src/spec/integration_support/clouds/dummy_v2.rb deleted file mode 100644 index 3ce7eb88b1..0000000000 --- a/src/spec/integration_support/clouds/dummy_v2.rb +++ /dev/null @@ -1,47 +0,0 @@ -require_relative 'dummy' - -module Bosh - module Clouds - class DummyV2 < Bosh::Clouds::Dummy - - def initialize(options, context) - super(options, context, 2) - end - - # rubocop:disable ParameterLists - def create_vm(agent_id, stemcell_id, cloud_properties, networks, disk_cids, env) - vm_cid = Dummy.instance_method(:create_vm).bind(self).call(agent_id, stemcell_id, cloud_properties, networks, disk_cids, env) - - [ - vm_cid, - {}, - ] - end - - ATTACH_DISK_SCHEMA = Membrane::SchemaParser.parse { { vm_cid: String, disk_id: String } } - def attach_disk(vm_cid, disk_id) - validate_and_record_inputs(ATTACH_DISK_SCHEMA, __method__, vm_cid, disk_id) - raise "#{disk_id} is already attached to an instance" if disk_attached?(disk_id) - file = attachment_file(vm_cid, disk_id) - FileUtils.mkdir_p(File.dirname(file)) - FileUtils.touch(file) - - @logger.debug("Attached disk: '#{disk_id}' to vm: '#{vm_cid}' at attachment file: #{file}") - - agent_id = agent_id_for_vm_id(vm_cid) - settings = read_agent_settings(agent_id) - settings['disks']['persistent'][disk_id] = 'attached' - write_agent_settings(agent_id, settings) - file - end - - def info - record_inputs(__method__, nil) - { - api_version: 2, - stemcell_formats: @supported_formats, - } - end - end - end -end diff --git a/src/spec/integration_support/dummy_cpi.rb b/src/spec/integration_support/dummy_cpi.rb new file mode 100644 index 0000000000..df5b83d784 --- /dev/null +++ b/src/spec/integration_support/dummy_cpi.rb @@ -0,0 +1,936 @@ +require 'digest/sha1' +require 'fileutils' +require 'ipaddr' +require 'json' +require 'membrane' +require 'securerandom' +require 'socket' +require 'yaml' +require 'clouds/errors' + +class DummyCpi + BOSH_REPO_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..')) + + attr_reader :commands + attr_accessor :options + + def initialize(options, context) + @options = options + @context = context + + @supported_formats = context['formats'] || ['dummy'] + @base_dir = options['dir'] + raise ArgumentError, 'Must specify dir' if @base_dir.nil? + + @running_vms_dir = File.join(@base_dir, 'running_vms') + @vm_repo = VMRepo.new(@running_vms_dir) + @tmp_dir = File.join(@base_dir, 'tmp') + FileUtils.mkdir_p(@tmp_dir) + + @logger = Logging::Logger.new("DummyCPI_#{object_id}") + @logger.add_appenders(Logging.appenders.io( + "DummyCPIIO_#{object_id}", + options['log_buffer'] || STDOUT + )) + + @commands = CommandTransport.new(@base_dir, @logger) + @inputs_recorder = InputsRecorder.new(@base_dir, @logger, @context) + + prepare + rescue Errno::EACCES + raise ArgumentError, "cannot create dummy cloud base directory #{@base_dir}" + end + + def info + record_inputs(__method__, nil) + { + api_version: 2, + stemcell_formats: @supported_formats, + } + end + + CREATE_STEMCELL_SCHEMA = Membrane::SchemaParser.parse { {image_path: String, cloud_properties: Hash} } + def create_stemcell(image_path, cloud_properties) + validate_and_record_inputs(CREATE_STEMCELL_SCHEMA, __method__, image_path, cloud_properties) + + content = File.read(image_path) + data = YAML.load(content, aliases: true) + data.merge!('image' => image_path) + stemcell_id = SecureRandom.uuid + + File.write(stemcell_file(stemcell_id), YAML.dump(data)) + stemcell_id + end + + DELETE_STEMCELL_SCHEMA = Membrane::SchemaParser.parse { {stemcell_id: String} } + def delete_stemcell(stemcell_id) + validate_and_record_inputs(DELETE_STEMCELL_SCHEMA, __method__, stemcell_id) + FileUtils.rm(stemcell_file(stemcell_id)) + end + + CREATE_VM_SCHEMA = Membrane::SchemaParser.parse do + { + agent_id: String, + stemcell_id: String, + cloud_properties: Hash, + networks: Hash, + disk_cids: [String], + env: Hash, + } + end + + # rubocop:disable ParameterLists + def create_vm(agent_id, stemcell_id, cloud_properties, networks, disk_cids, env) + @logger.debug('Dummy: create_vm') + validate_and_record_inputs(CREATE_VM_SCHEMA, __method__, agent_id, stemcell_id, cloud_properties, networks, disk_cids, env) + + ips = [] + cmd = commands.next_create_vm_cmd + + raise Bosh::Clouds::CloudError.new('Creating vm failed') if cmd.failed? + + networks.each do |network_name, network| + if network['type'] != 'dynamic' + ips << { 'network' => network_name, 'ip' => network.fetch('ip') } + else + if cmd.ip_address + ip_address = cmd.ip_address + elsif cloud_properties['az_name'] + ip_address = cmd.ip_address_for_az(cloud_properties['az_name']) + else + ip_address = IPAddr.new(rand(0..IPAddr::IN4MASK), Socket::AF_INET).to_s + end + + if ip_address + ips << { 'network' => network_name, 'ip' => ip_address } + write_agent_default_network(agent_id, ip_address) + end + end + end + + allocate_ips(ips) + + write_agent_settings(agent_id, { + agent_id: agent_id, + blobstore: @options['agent']['blobstore'], + ntp: [], + disks: { persistent: {} }, + networks: networks, + vm: { name: "vm-#{agent_id}" }, + cert: '', + env: env, + mbus: @options['nats'], + }) + + agent_process_agent_id = agent_id + if commands.create_vm_unresponsive_agent || + agent_id == commands.unresponsive_agent_agent_id + agent_process_agent_id = 'unresponsive-agent-fake-id-' + SecureRandom.uuid + end + + agent_pid = spawn_agent_process(agent_process_agent_id, cloud_properties['legacy_agent_path']) + vm = VM.new(agent_pid.to_s, agent_id, cloud_properties, ips) + + @vm_repo.save(vm) + + [vm.id, {}] + end + # rubocop:enable ParameterLists + + DELETE_VM_SCHEMA = Membrane::SchemaParser.parse { {vm_cid: String} } + def delete_vm(vm_cid) + validate_and_record_inputs(DELETE_VM_SCHEMA, __method__, vm_cid) + commands.wait_for_unpause_delete_vms + + unless @vm_repo.exists?(vm_cid) + raise Bosh::Clouds::VMNotFound if commands.raise_vmnotfound + return + end + + detach_disks_attached_to_vm(vm_cid) + agent_pid = Integer(vm_cid, 10) + raise Errno::ESRCH, "Invalid PID #{agent_pid}" if agent_pid <= 0 + Process.kill('KILL', agent_pid) + + # rubocop:disable HandleExceptions + rescue Errno::ESRCH + raise Bosh::Clouds::VMNotFound if commands.raise_vmnotfound + # rubocop:enable HandleExceptions + ensure + free_ips(vm_cid) + FileUtils.rm_rf(File.join(@base_dir, 'running_vms', vm_cid)) + end + + REBOOT_VM_SCHEMA = Membrane::SchemaParser.parse { {vm_cid: String} } + def reboot_vm(vm_cid) + validate_and_record_inputs(REBOOT_VM_SCHEMA, __method__, vm_cid) + raise Bosh::Clouds::NotImplemented, 'Dummy CPI does not implement reboot_vm' + end + + HAS_VM_SCHEMA = Membrane::SchemaParser.parse { {vm_cid: String} } + def has_vm(vm_cid) + validate_and_record_inputs(HAS_VM_SCHEMA, __method__, vm_cid) + @vm_repo.exists?(vm_cid) + end + + HAS_DISK_SCHEMA = Membrane::SchemaParser.parse { {disk_id: String} } + def has_disk(disk_id) + validate_and_record_inputs(HAS_DISK_SCHEMA, __method__, disk_id) + File.exist?(disk_file(disk_id)) + end + + ATTACH_DISK_SCHEMA = Membrane::SchemaParser.parse { {vm_cid: String, disk_id: String} } + def attach_disk(vm_cid, disk_id) + validate_and_record_inputs(ATTACH_DISK_SCHEMA, __method__, vm_cid, disk_id) + + raise Bosh::Clouds::NotImplemented, 'Bosh::Clouds::NotImplemented' if commands.raise_attach_disk_not_implemented + raise "#{disk_id} is already attached to an instance" if disk_attached?(disk_id) + + file = attachment_file(vm_cid, disk_id) + FileUtils.mkdir_p(File.dirname(file)) + FileUtils.touch(file) + + @logger.debug("Attached disk: '#{disk_id}' to vm: '#{vm_cid}' at attachment file: #{file}") + + agent_id = agent_id_for_vm_id(vm_cid) + settings = read_agent_settings(agent_id) + settings['disks']['persistent'][disk_id] = 'attached' + write_agent_settings(agent_id, settings) + file + end + + DETACH_DISK_SCHEMA = Membrane::SchemaParser.parse { {vm_cid: String, disk_id: String} } + def detach_disk(vm_cid, disk_id) + validate_and_record_inputs(DETACH_DISK_SCHEMA, __method__, vm_cid, disk_id) + + raise Bosh::Clouds::NotImplemented, 'Bosh::Clouds::NotImplemented' if commands.raise_detach_disk_not_implemented + + unless disk_attached_to_vm?(vm_cid, disk_id) + raise Bosh::Clouds::DiskNotAttached, "#{disk_id} is not attached to instance #{vm_cid}" + end + FileUtils.rm_rf(attachment_path(disk_id)) + + agent_id = agent_id_for_vm_id(vm_cid) + settings = read_agent_settings(agent_id) + settings['disks']['persistent'].delete(disk_id) + write_agent_settings(agent_id, settings) + end + + CREATE_DISK_SCHEMA = Membrane::SchemaParser.parse { {size: Integer, cloud_properties: Hash, vm_locality: String} } + def create_disk(size, cloud_properties, vm_locality) + validate_and_record_inputs(CREATE_DISK_SCHEMA, __method__, size, cloud_properties, vm_locality) + disk_id = SecureRandom.hex + file = disk_file(disk_id) + FileUtils.mkdir_p(File.dirname(file)) + disk_info = JSON.generate({size: size, cloud_properties: cloud_properties, vm_locality: vm_locality}) + File.write(file, disk_info) + disk_id + end + + DELETE_DISK_SCHEMA = Membrane::SchemaParser.parse { {disk_id: String} } + def delete_disk(disk_id) + validate_and_record_inputs(DELETE_DISK_SCHEMA, __method__, disk_id) + FileUtils.rm(disk_file(disk_id)) + end + + CREATE_NETWORK_SCHEMA = Membrane::SchemaParser.parse { { subnet_definition: Hash } } + def create_network(subnet_definition) + validate_and_record_inputs(CREATE_NETWORK_SCHEMA, __method__, subnet_definition) + + raise subnet_definition['cloud_properties']['error'] if subnet_definition['cloud_properties'].key?('error') + + network_id = SecureRandom.hex + file = network_file(network_id) + FileUtils.mkdir_p(File.dirname(file)) + network_info = JSON.generate(subnet_definition) + File.write(file, network_info) + addr_properties = {} + if subnet_definition.key?('netmask_bits') + addr_properties['range'] = '192.168.10.0/24' + addr_properties['gateway'] = '192.168.10.1' + addr_properties['reserved'] = ['192.168.10.2'] + end + + [network_id, addr_properties, { name: network_id }] + end + + DELETE_NETWORK_SCHEMA = Membrane::SchemaParser.parse { { network_id: String } } + def delete_network(network_id) + validate_and_record_inputs(DELETE_NETWORK_SCHEMA, __method__, network_id) + FileUtils.rm(network_file(network_id)) + end + + SNAPSHOT_DISK_SCHEMA = Membrane::SchemaParser.parse { { disk_id: String, metadata: Hash } } + def snapshot_disk(disk_id, metadata) + validate_and_record_inputs(SNAPSHOT_DISK_SCHEMA, __method__, disk_id, metadata) + snapshot_id = SecureRandom.hex + file = snapshot_file(snapshot_id) + FileUtils.mkdir_p(File.dirname(file)) + File.write(file, metadata.to_json) + snapshot_id + end + + DELETE_SNAPSHOT_SCHEMA = Membrane::SchemaParser.parse { {snapshot_id: String} } + def delete_snapshot(snapshot_id) + validate_and_record_inputs(DELETE_SNAPSHOT_SCHEMA, __method__, snapshot_id) + FileUtils.rm(snapshot_file(snapshot_id)) + end + + RESIZE_DISK_SCHEMA = Membrane::SchemaParser.parse { {disk_id: String, new_size: Integer } } + def resize_disk(disk_id, new_size) + validate_and_record_inputs(RESIZE_DISK_SCHEMA, __method__, disk_id, new_size) + + raise Bosh::Clouds::NotImplemented, 'Bosh::Clouds::NotImplemented' if commands.raise_resize_disk_not_implemented + + disk_info_file = disk_file(disk_id) + disk_info = JSON.parse(File.read(disk_info_file)) + disk_info['size'] = new_size + File.write(disk_info_file, JSON.generate(disk_info)) + end + + UPDATE_DISK_SCHEMA = Membrane::SchemaParser.parse { {disk_id: String, new_size: Integer, cloud_properties: Hash} } + def update_disk(disk_id, new_size, cloud_properties) + validate_and_record_inputs(UPDATE_DISK_SCHEMA, __method__, disk_id, new_size, cloud_properties) + + raise Bosh::Clouds::NotImplemented, 'Bosh::Clouds::NotImplemented' if commands.raise_update_disk_not_implemented + + disk_info_file = disk_file(disk_id) + disk_info = JSON.parse(File.read(disk_info_file)) + disk_info['size'] = new_size + disk_info['cloud_properties'] = cloud_properties + File.write(disk_info_file, JSON.generate(disk_info)) + nil + end + + SET_VM_METADATA_SCHEMA = Membrane::SchemaParser.parse { {vm_cid: String, metadata: Hash} } + def set_vm_metadata(vm_cid, metadata) + raise 'Set VM metadata failed!!!' if commands.set_vm_metadata_should_fail? + validate_and_record_inputs(SET_VM_METADATA_SCHEMA, __method__, vm_cid, metadata) + end + + SET_DISK_METADATA_SCHEMA = Membrane::SchemaParser.parse { {disk_cid: String, metadata: Hash} } + def set_disk_metadata(disk_cid, metadata) + validate_and_record_inputs(SET_DISK_METADATA_SCHEMA, __method__, disk_cid, metadata) + end + + CALCULATE_VM_CLOUD_PROPERTIES_SCHEMA = Membrane::SchemaParser.parse { { vm_resources: {'ram' => Integer, 'cpu' => Integer, 'ephemeral_disk_size' => Integer} } } + def calculate_vm_cloud_properties(vm_resources) + validate_and_record_inputs( + CALCULATE_VM_CLOUD_PROPERTIES_SCHEMA, + __method__, + vm_resources + ) + instance_type = @context['cvcpkey'].nil? ? 'dummy' : @context['cvcpkey'] + { + instance_type: instance_type, + cpu: vm_resources['cpu'], + ram: vm_resources['ram'], + ephemeral_disk: { size: vm_resources['ephemeral_disk_size'] } + } + end + + # Additional test helpers + + def prepare + FileUtils.mkdir_p(@base_dir) + end + + def reset + FileUtils.rm_rf(@base_dir) + prepare + end + + def vm_cids + Dir.glob(File.join(@running_vms_dir, '*')).map { |vm| File.basename(vm) }.shuffle + end + + def disk_cids + Dir.glob(disk_file('*')).map { |disk| File.basename(disk) }.shuffle + end + + def network_cids + Dir.glob(network_file('*')).map { |network| File.basename(network) }.shuffle + end + + def kill_agents + vm_cids.each { |agent_pid| kill_process(agent_pid) } + end + + def kill_process(agent_pid) + pid = Integer(agent_pid, 10) + return if pid <= 0 + + Process.kill('KILL', pid) + # rubocop:disable HandleExceptions + rescue Errno::ESRCH, ArgumentError + # rubocop:enable HandleExceptions + end + + def agent_log_path(agent_id) + "#{@base_dir}/agent.#{agent_id}.log" + end + + def read_cloud_properties(vm_cid) + @vm_repo.load(vm_cid).cloud_properties + end + + def invocations + @inputs_recorder.read_all + end + + def invocations_for_method(method) + @inputs_recorder.read(method) + end + + def all_stemcells + files = Dir.entries(@base_dir).select { |file| file.match(/stemcell_./) } + + Dir.chdir(@base_dir) do + [].tap do |results| + files.each do |file| + data = YAML.load(File.read(file), aliases: true) + results << { 'id' => file.sub(/^stemcell_/, '') }.merge(data) + end + end.sort_by { |stemcell| stemcell['version'].to_s.split('.').map(&:to_i) } + end + end + + def latest_stemcell + all_stemcells.last + end + + def all_snapshots + if File.exist?(snapshot_file('')) + Dir.glob(snapshot_file('*')) + else + [] + end + end + + def all_ips + Dir.glob(File.join(@base_dir, 'dummy_cpi_networks', '*')) + .reject { |path| File.directory?(path) } + .map { |path| File.basename(path) } + end + + def agent_dir_for_vm_cid(vm_cid) + agent_id = agent_id_for_vm_id(vm_cid) + agent_base_dir(agent_id) + end + + def disk_attached_to_vm?(vm_cid, disk_id) + File.exist?(attachment_file(vm_cid, disk_id)) + end + + def current_apply_spec_for_vm(vm_cid) + agent_base_dir = agent_dir_for_vm_cid(vm_cid) + spec_file = File.join(agent_base_dir, 'bosh', 'spec.json') + JSON.parse(File.read(spec_file)) + end + + def attached_disk_infos(vm_cid) + agent_id = agent_id_for_vm_id(vm_cid) + settings = read_agent_settings(agent_id) + return [] unless settings.has_key?('disks') && settings['disks'].has_key?('persistent') + + settings['disks']['persistent'].inject([]) do |memo, disk_attachment| + disk_cid = disk_attachment[0] + device_path = disk_attachment[1] + + disk_info_hash = JSON.parse(File.read(disk_file(disk_cid))) + disk_info_hash['disk_cid'] = disk_cid + disk_info_hash['device_path'] = device_path + + memo << disk_info_hash + end + end + + def spawn_agent_process(agent_id, legacy_agent_path = nil) + root_dir = File.join(agent_base_dir(agent_id), 'root_dir') + FileUtils.mkdir_p(File.join(root_dir, 'etc', 'logrotate.d')) + + agent_cmd = agent_cmd(agent_id, legacy_agent_path) + agent_log = agent_log_path(agent_id) + + agent_pid = Process.spawn( + { 'TMPDIR' => @tmp_dir }, + *agent_cmd, + { + chdir: agent_base_dir(agent_id), + out: agent_log, + err: agent_log, + } + ) + + begin + Process.getpgid(agent_pid) + rescue => e + raise RuntimeError, "Expected agent to be running: #{e}" + end + + Process.detach(agent_pid) + + agent_pid + end + + private + + def allocate_ips(ips) + ips.each do |ip| + begin + network_dir = File.join(@base_dir, 'dummy_cpi_networks') + FileUtils.makedirs(network_dir) + File.open(File.join(network_dir, ip['ip']), File::WRONLY|File::CREAT|File::EXCL).close + rescue Errno::EEXIST + raise "IP Address #{ip['ip']} in network '#{ip['network']}' is already in use" + end + end + end + + def free_ips(vm_cid) + return unless @vm_repo.exists?(vm_cid) + vm = @vm_repo.load(vm_cid) + vm.ips.each do |ip| + FileUtils.rm_rf(File.join(@base_dir, 'dummy_cpi_networks', ip['ip'])) + end + end + + def agent_id_for_vm_id(vm_cid) + @vm_repo.load(vm_cid).agent_id + end + + def agent_settings_file(agent_id) + File.join(agent_base_dir(agent_id), 'bosh', 'dummy-cpi-agent-env.json') + end + + def agent_base_dir(agent_id) + "#{@base_dir}/agent-base-dir-#{agent_id}" + end + + def write_agent_settings(agent_id, settings) + FileUtils.mkdir_p(File.dirname(agent_settings_file(agent_id))) + File.write(agent_settings_file(agent_id), JSON.generate(settings)) + end + + def write_agent_default_network(agent_id, ip_address) + path = File.join(agent_base_dir(agent_id), 'bosh', 'dummy-default-network-settings.json') + FileUtils.mkdir_p(File.dirname(path)) + File.write(path, JSON.generate('ip' => ip_address)) + end + + def agent_cmd(agent_id, legacy_agent_path) + go_agent_exe = if legacy_agent_path.nil? + File.join(BOSH_REPO_ROOT, 'src', 'tmp', 'bin', 'bosh-agent') + else + legacy_agent_path + end + + agent_config_file = File.join(agent_base_dir(agent_id), 'agent.json') + + agent_config = { + 'Infrastructure' => { + 'Settings' => { + 'Sources' => [{ + 'Type' => 'File', + 'SettingsPath' => agent_settings_file(agent_id) + }] + } + } + } + + File.write(agent_config_file, JSON.generate(agent_config)) + + %W[#{go_agent_exe} -b #{agent_base_dir(agent_id)} -P dummy -M dummy-nats -C #{agent_config_file}] + end + + def read_agent_settings(agent_id) + JSON.parse(File.read(agent_settings_file(agent_id))) + end + + def stemcell_file(stemcell_id) + File.join(@base_dir, "stemcell_#{stemcell_id}") + end + + def disk_file(disk_id) + File.join(@base_dir, 'disks', disk_id) + end + + def network_file(network_id) + File.join(@base_dir, 'networks', network_id) + end + + def disk_attached?(disk_id) + File.exist?(attachment_path(disk_id)) + end + + def detach_disks_attached_to_vm(vm_cid) + @logger.debug("Detaching disks for vm #{vm_cid}") + Dir.glob(attachment_file(vm_cid, '*')) do |file_path| + @logger.debug("Detaching found attachment #{file_path}") + FileUtils.rm_rf(File.dirname(file_path)) + end + end + + def attachment_file(vm_cid, disk_id) + File.join(attachment_path(disk_id), vm_cid) + end + + def attachment_path(disk_id) + File.join(@base_dir, 'attachments', disk_id) + end + + def snapshot_file(snapshot_id) + File.join(@base_dir, 'snapshots', snapshot_id) + end + + def validate_and_record_inputs(schema, the_method, *args) + parameter_names_to_values = parameter_names_to_values(the_method, *args) + begin + schema.validate(parameter_names_to_values) + rescue Membrane::SchemaValidationError => err + raise ArgumentError, "Invalid arguments sent to #{the_method}: #{err.message}" + end + record_inputs(the_method, parameter_names_to_values) + end + + def record_inputs(method, args) + @inputs_recorder.record(method, args) + end + + def parameter_names_to_values(the_method, *the_method_args) + hash = {} + method(the_method).parameters.each_with_index do |param, index| + hash[param[1]] = the_method_args[index] + end + hash + end + + class CommandTransport + def initialize(base_dir, logger) + @cpi_commands = File.join(base_dir, 'cpi_commands') + @logger = logger + end + + def pause_delete_vms + @logger.debug('Pausing delete_vms') + path = File.join(@cpi_commands, 'pause_delete_vms') + FileUtils.mkdir_p(File.dirname(path)) + File.write(path, 'marker') + end + + def unpause_delete_vms + @logger.debug('Unpausing delete_vms') + FileUtils.rm_rf File.join(@cpi_commands, 'pause_delete_vms') + FileUtils.rm_rf File.join(@cpi_commands, 'wait_for_unpause_delete_vms') + end + + WAIT_TIMEOUT_SECONDS = 300 + + def wait_for_delete_vms + @logger.debug('Wait for delete_vms') + path = File.join(@cpi_commands, 'wait_for_unpause_delete_vms') + deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + WAIT_TIMEOUT_SECONDS + loop do + break if File.exist?(path) + raise "Timed out after #{WAIT_TIMEOUT_SECONDS}s waiting for delete_vms signal" if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline + sleep(0.1) + end + end + + def wait_for_unpause_delete_vms + path = File.join(@cpi_commands, 'wait_for_unpause_delete_vms') + FileUtils.mkdir_p(File.dirname(path)) + File.write(path, 'marker') + + path = File.join(@cpi_commands, 'pause_delete_vms') + @logger.debug('Wait for unpausing delete_vms') if File.exist?(path) + deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + WAIT_TIMEOUT_SECONDS + loop do + break unless File.exist?(path) + raise "Timed out after #{WAIT_TIMEOUT_SECONDS}s waiting for unpause_delete_vms" if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline + sleep(0.1) + end + end + + def make_create_vm_always_use_dynamic_ip(ip_address) + @logger.debug("Making create_vm method to set ip address #{ip_address}") + FileUtils.mkdir_p(File.dirname(always_path)) + File.write(always_path, ip_address) + end + + def set_dynamic_ips_for_azs(az_to_ips) + @logger.debug("Making create_vm method to set #{az_to_ips.inspect}") + FileUtils.mkdir_p(File.dirname(azs_path)) + File.write(azs_path, JSON.generate(az_to_ips)) + end + + def make_create_vm_always_fail + @logger.debug('Making create_vm method always fail') + FileUtils.mkdir_p(File.dirname(failed_path)) + File.write(failed_path, '') + end + + def allow_create_vm_to_succeed + @logger.debug('Allowing create_vm method to succeed (removing any mandatory failures)') + FileUtils.rm(failed_path) + end + + def next_create_vm_cmd + @logger.debug('Reading create_vm configuration') + ip_address = File.read(always_path) if File.exist?(always_path) + azs_content = File.exist?(azs_path) ? File.read(azs_path) : nil + azs_to_ip = azs_content.to_s.strip.empty? ? {} : JSON.parse(azs_content) + failed = File.exist?(failed_path) + CreateVmCommand.new(ip_address, azs_to_ip, failed) + end + + def make_set_vm_metadata_always_fail + @logger.debug('Making set_vm_metadata method always fail') + FileUtils.mkdir_p(File.dirname(set_vm_metadata_path_fail_path)) + File.write(set_vm_metadata_path_fail_path, '') + end + + def allow_set_vm_metadata_to_succeed + @logger.debug('Allowing set_vm_metadata method to succeed (removing any mandatory failures)') + FileUtils.rm(set_vm_metadata_path_fail_path) + end + + def set_vm_metadata_should_fail? + @logger.info('Reading set_vm_metadata configuration') + File.exist?(set_vm_metadata_path_fail_path) + end + + def make_delete_vm_to_raise_vmnotfound + @logger.info('Making delete_vm method to raise VMNotFound exception') + FileUtils.mkdir_p(File.dirname(raise_vmnotfound_path)) + File.write(raise_vmnotfound_path, '') + end + + def raise_vmnotfound + @logger.info('Reading delete_vm configuration') + File.exist?(raise_vmnotfound_path) + end + + def allow_detach_disk_to_succeed + @logger.debug('Allowing detach_disk method to succeed (removing any mandatory failures)') + FileUtils.rm(raise_detach_disk_not_implemented_path) + end + + def make_detach_disk_to_raise_not_implemented + @logger.info('Making detach_disk method to raise NotImplemented exception') + FileUtils.mkdir_p(File.dirname(raise_detach_disk_not_implemented_path)) + FileUtils.touch(raise_detach_disk_not_implemented_path) + end + + def make_create_vm_have_unresponsive_agent_for_agent_id(agent_id) + @logger.info("Making create_vm method create with an unresponsive agent for agent_id #{agent_id}") + FileUtils.mkdir_p(File.dirname(create_vm_unresponsive_agent_agent_id_path)) + File.write(create_vm_unresponsive_agent_agent_id_path, agent_id) + end + + def make_create_vm_have_unresponsive_agent + @logger.info('Making create_vm method create with an unresponsive agent') + FileUtils.mkdir_p(File.dirname(create_vm_unresponsive_agent_path)) + FileUtils.touch(create_vm_unresponsive_agent_path) + end + + def allow_create_vm_to_have_responsive_agent + @logger.info('Making create_vm method create with a responsive agent') + FileUtils.rm_f(create_vm_unresponsive_agent_agent_id_path) + FileUtils.rm_f(create_vm_unresponsive_agent_path) + end + + def allow_attach_disk_to_succeed + @logger.debug('Allowing attach_disk method to succeed (removing any mandatory failures)') + FileUtils.rm(raise_attach_disk_not_implemented_path) + end + + def make_attach_disk_to_raise_not_implemented + @logger.info('Making attach_disk method to raise NotImplemented exception') + FileUtils.mkdir_p(File.dirname(raise_attach_disk_not_implemented_path)) + FileUtils.touch(raise_attach_disk_not_implemented_path) + end + + def raise_attach_disk_not_implemented + @logger.info('Reading attach_disk_not_implemented') + File.exist?(raise_attach_disk_not_implemented_path) + end + + def create_vm_unresponsive_agent + @logger.info('Reading create_vm_unresponsive_agent') + File.exist?(create_vm_unresponsive_agent_path) + end + + def unresponsive_agent_agent_id + @logger.info('Reading create_vm_unresponsive_agent_agent_id') + File.read(create_vm_unresponsive_agent_agent_id_path) + rescue StandardError + false + end + + def make_resize_disk_to_raise_not_implemented + @logger.info('Making resize_disk method to raise NotImplemented exception') + FileUtils.mkdir_p(File.dirname(raise_resize_disk_not_implemented_path)) + FileUtils.touch(raise_resize_disk_not_implemented_path) + end + + def raise_detach_disk_not_implemented + @logger.info('Reading detach_disk_not_implemented') + File.exist?(raise_detach_disk_not_implemented_path) + end + + def raise_resize_disk_not_implemented + @logger.info('Reading resize_disk_not_implemented') + File.exist?(raise_resize_disk_not_implemented_path) + end + + def make_update_disk_to_raise_not_implemented + @logger.info('Making update_disk method to raise NotImplemented exception') + FileUtils.mkdir_p(File.dirname(raise_update_disk_not_implemented_path)) + FileUtils.touch(raise_update_disk_not_implemented_path) + end + + def raise_update_disk_not_implemented + @logger.info('Reading update_disk_not_implemented') + File.exist?(raise_update_disk_not_implemented_path) + end + + private + + def azs_path + File.join(@cpi_commands, 'create_vm', 'az_ips') + end + + def always_path + File.join(@cpi_commands, 'create_vm', 'always') + end + + def failed_path + File.join(@cpi_commands, 'create_vm', 'fail') + end + + def set_vm_metadata_path_fail_path + File.join(@cpi_commands, 'update_vm_metadata', 'fail') + end + + def raise_vmnotfound_path + File.join(@cpi_commands, 'delete_vm', 'fail') + end + + def raise_detach_disk_not_implemented_path + File.join(@cpi_commands, 'detach_disk', 'not_implemented') + end + + def raise_attach_disk_not_implemented_path + File.join(@cpi_commands, 'attach_disk', 'not_implemented') + end + + def create_vm_unresponsive_agent_path + File.join(@cpi_commands, 'create_vm', 'unresponsive_agent') + end + + def create_vm_unresponsive_agent_agent_id_path + File.join(@cpi_commands, 'create_vm', 'unresponsive_agent_agent_id') + end + + def raise_resize_disk_not_implemented_path + File.join(@cpi_commands, 'resize_disk', 'not_implemented') + end + + def raise_update_disk_not_implemented_path + File.join(@cpi_commands, 'update_disk', 'not_implemented') + end + end + + class ConfigureNetworksCommand < Struct.new(:not_supported); end + class CreateVmCommand + attr_reader :ip_address, :failed + + def initialize(ip_address, azs_to_ip, failed) + @ip_address = ip_address + @azs_to_ip = azs_to_ip + @failed = failed + end + + def ip_address_for_az(az_name) + @azs_to_ip[az_name] + end + + def failed? + failed + end + end + + class InputsRecorder + def initialize(base_dir, logger, context) + @cpi_inputs_dir = File.join(base_dir, 'cpi_inputs') + @logger = logger + @context = context + end + + def record(method, args) + FileUtils.mkdir_p(@cpi_inputs_dir) + data = {method_name: method, inputs: args, context: @context} + @logger.debug("Saving input for #{method} #{ordered_file_path}") + File.open(ordered_file_path, 'a') { |f| f.puts(JSON.dump(data)) } + end + + def read(method_name) + @logger.debug("Reading input for #{method_name}") + read_all.select do |invocation| + invocation.method_name == method_name + end + end + + def read_all + @logger.debug("Reading all inputs: #{File.read(ordered_file_path)}") + result = [] + File.read(ordered_file_path).split("\n").each do |request| + data = JSON.parse(request) + result << CpiInvocation.new(data['method_name'], data['inputs'], data['context']) + end + result + end + + def ordered_file_path + File.join(@cpi_inputs_dir, 'all_requests') + end + end + + class VM < Struct.new(:id, :agent_id, :cloud_properties, :ips) + end + + class VMRepo + def initialize(running_vms_dir) + @running_vms_dir = running_vms_dir + FileUtils.mkdir_p(@running_vms_dir) + end + + def load(id) + attrs = JSON.parse(File.read(vm_file(id))) + VM.new(id, attrs.fetch('agent_id'), attrs.fetch('cloud_properties'), attrs.fetch('ips')) + end + + def exists?(id) + File.exist?(vm_file(id)) + end + + def save(vm) + serialized_vm = JSON.dump({ + 'agent_id' => vm.agent_id, + 'cloud_properties' => vm.cloud_properties, + 'ips' => vm.ips, + }) + + File.write(vm_file(vm.id), serialized_vm) + end + + private + + def vm_file(vm_cid) + File.join(@running_vms_dir, vm_cid) + end + end + + class CpiInvocation < Struct.new(:method_name, :inputs, :context); end +end diff --git a/src/spec/integration_support/invocations_helper.rb b/src/spec/integration_support/invocations_helper.rb index 75353b4074..fa10ce793a 100644 --- a/src/spec/integration_support/invocations_helper.rb +++ b/src/spec/integration_support/invocations_helper.rb @@ -4,7 +4,9 @@ module InvocationsHelper AGENT_TARGET = 'agent'.freeze def cid_and_agent(vm_creation_call) - [vm_creation_call.response, vm_creation_call.arguments['agent_id']] + response = vm_creation_call.response + vm_cid = response.is_a?(Array) ? response.first : response + [vm_cid, vm_creation_call.arguments['agent_id']] end def get_invocations(task_output) diff --git a/src/spec/integration_support/sandbox.rb b/src/spec/integration_support/sandbox.rb index e8df6e0732..4d70ff60f4 100644 --- a/src/spec/integration_support/sandbox.rb +++ b/src/spec/integration_support/sandbox.rb @@ -6,7 +6,7 @@ require 'tmpdir' require 'tempfile' -require 'integration_support/clouds/dummy' +require 'integration_support/dummy_cpi' require 'integration_support/constants' require 'integration_support/service' @@ -269,7 +269,7 @@ def initialize(bosh_cli:, # Note that this is not the same object # as dummy cpi used as the external CPI subprocess - @cpi = Bosh::Clouds::Dummy.new( + @cpi = DummyCpi.new( { 'dir' => cloud_storage_dir, 'agent' => { @@ -283,8 +283,7 @@ def initialize(bosh_cli:, 'nats' => @nats_url, 'log_buffer' => @logger, }, - {}, - 1 + {} ) reconfigure @@ -472,7 +471,7 @@ def reconfigure(options={}) @agent_wait_timeout = options.fetch(:agent_wait_timeout, 600) @keep_unreachable_vms = options.fetch(:keep_unreachable_vms, false) @with_incorrect_nats_server_ca = options.fetch(:with_incorrect_nats_server_ca, false) - @dummy_cpi_api_version = options.fetch(:dummy_cpi_api_version, 1) + @dummy_cpi_api_version = options.fetch(:dummy_cpi_api_version, 2) @nats_url = "nats://localhost:#{nats_port}" @cpi.options['nats'] = @nats_url diff --git a/src/spec/integration_support/spec/clouds/dummy_spec.rb b/src/spec/integration_support/spec/dummy_cpi_spec.rb similarity index 67% rename from src/spec/integration_support/spec/clouds/dummy_spec.rb rename to src/spec/integration_support/spec/dummy_cpi_spec.rb index 3bc84f57d5..a2b49c9283 100644 --- a/src/spec/integration_support/spec/clouds/dummy_spec.rb +++ b/src/spec/integration_support/spec/dummy_cpi_spec.rb @@ -1,11 +1,10 @@ require 'spec_helper' require 'stringio' require 'tempfile' -require 'logging' # loaded by bosh/director in spec_helper; explicit here for clarity -require 'integration_support/clouds/dummy' -require 'integration_support/clouds/dummy_v2' +require 'logging' +require 'integration_support/dummy_cpi' -describe Bosh::Clouds::Dummy do +describe DummyCpi do let(:tmpdir) { Dir.mktmpdir('dummy_cpi_spec') } let(:base_options) do @@ -22,12 +21,12 @@ } end - subject(:dummy) { described_class.new(base_options, {}, 1) } + subject(:cpi) { described_class.new(base_options, {}) } after { FileUtils.rm_rf(tmpdir) } before do - allow(dummy).to receive(:spawn_agent_process).and_return(99999) + allow(cpi).to receive(:spawn_agent_process).and_return(99999) allow(Process).to receive(:kill) end @@ -36,18 +35,18 @@ end def create_test_vm(agent_id: 'agent-1', networks: static_networks) - dummy.create_vm(agent_id, 'stemcell-1', {}, networks, [], {}) + cpi.create_vm(agent_id, 'stemcell-1', {}, networks, [], {}) end def create_test_disk(size: 1024) - dummy.create_disk(size, {}, 'vm-locality') + cpi.create_disk(size, {}, 'vm-locality') end def create_test_stemcell image_file = Tempfile.new(['stemcell', '.tgz'], tmpdir) image_file.write(YAML.dump('name' => 'test-stemcell', 'version' => '1')) image_file.close - dummy.create_stemcell(image_file.path, {}) + cpi.create_stemcell(image_file.path, {}) end # ----------------------------------------------------------------------- @@ -57,28 +56,23 @@ def create_test_stemcell describe '#initialize' do it 'creates the base directory' do new_dir = File.join(tmpdir, 'new_base') - described_class.new(base_options.merge('dir' => new_dir), {}, 1) + described_class.new(base_options.merge('dir' => new_dir), {}) expect(File.directory?(new_dir)).to be(true) end it 'raises ArgumentError when dir is not specified' do expect { - described_class.new(base_options.reject { |k, _| k == 'dir' }, {}, 1) + described_class.new(base_options.reject { |k, _| k == 'dir' }, {}) }.to raise_error(ArgumentError, /Must specify dir/) end - it 'stores the api_version' do - cpi = described_class.new(base_options, {}, 2) - expect(cpi.info['api_version']).to eq(2) - end - it 'uses the context formats when provided' do - cpi = described_class.new(base_options, { 'formats' => ['ubuntu-stemcell'] }, 1) - expect(cpi.info[:stemcell_formats]).to eq(['ubuntu-stemcell']) + c = described_class.new(base_options, { 'formats' => ['ubuntu-stemcell'] }) + expect(c.info[:stemcell_formats]).to eq(['ubuntu-stemcell']) end it 'defaults to dummy stemcell format when context formats not set' do - expect(dummy.info[:stemcell_formats]).to eq(['dummy']) + expect(cpi.info[:stemcell_formats]).to eq(['dummy']) end end @@ -87,18 +81,12 @@ def create_test_stemcell # ----------------------------------------------------------------------- describe '#info' do - it 'returns stemcell_formats' do - expect(dummy.info).to include(stemcell_formats: ['dummy']) - end - - it 'includes api_version when set' do - cpi = described_class.new(base_options, {}, 2) - expect(cpi.info['api_version']).to eq(2) + it 'returns api_version 2' do + expect(cpi.info[:api_version]).to eq(2) end - it 'omits api_version when nil' do - cpi = described_class.new(base_options, {}, nil) - expect(cpi.info.keys).not_to include('api_version') + it 'returns stemcell_formats' do + expect(cpi.info).to include(stemcell_formats: ['dummy']) end end @@ -129,7 +117,7 @@ def create_test_stemcell describe '#delete_stemcell' do it 'removes the stemcell file' do stemcell_id = create_test_stemcell - dummy.delete_stemcell(stemcell_id) + cpi.delete_stemcell(stemcell_id) stemcell_file = File.join(tmpdir, "stemcell_#{stemcell_id}") expect(File.exist?(stemcell_file)).to be(false) end @@ -139,11 +127,11 @@ def create_test_stemcell it 'returns all created stemcells' do create_test_stemcell create_test_stemcell - expect(dummy.all_stemcells.size).to eq(2) + expect(cpi.all_stemcells.size).to eq(2) end it 'returns empty when no stemcells exist' do - expect(dummy.all_stemcells).to be_empty + expect(cpi.all_stemcells).to be_empty end end @@ -152,24 +140,27 @@ def create_test_stemcell # ----------------------------------------------------------------------- describe '#create_vm' do - it 'returns a VM cid string' do - vm_cid = create_test_vm + it 'returns a [vm_cid, network_settings] tuple' do + result = create_test_vm + expect(result).to be_an(Array) + expect(result.size).to eq(2) + vm_cid, network_settings = result expect(vm_cid).to be_a(String) - expect(vm_cid).not_to be_empty + expect(network_settings).to eq({}) end it 'spawns an agent process' do - expect(dummy).to receive(:spawn_agent_process).and_return(99999) + expect(cpi).to receive(:spawn_agent_process).and_return(99999) create_test_vm end it 'allocates the static IP address' do create_test_vm(networks: { 'a' => { 'type' => 'manual', 'ip' => '10.0.0.1' } }) - expect(dummy.all_ips).to include('10.0.0.1') + expect(cpi.all_ips).to include('10.0.0.1') end it 'raises CloudError when create_vm is configured to fail' do - dummy.commands.make_create_vm_always_fail + cpi.commands.make_create_vm_always_fail expect { create_test_vm }.to raise_error(Bosh::Clouds::CloudError, /Creating vm failed/) @@ -193,56 +184,55 @@ def create_test_stemcell describe '#has_vm' do it 'returns true for an existing VM' do - vm_cid = create_test_vm - expect(dummy.has_vm(vm_cid)).to be(true) + vm_cid, _ = create_test_vm + expect(cpi.has_vm(vm_cid)).to be(true) end it 'returns false for a non-existent VM' do - expect(dummy.has_vm('nonexistent-cid')).to be(false) + expect(cpi.has_vm('nonexistent-cid')).to be(false) end end describe '#delete_vm' do it 'removes the VM' do - vm_cid = create_test_vm - dummy.delete_vm(vm_cid) - expect(dummy.has_vm(vm_cid)).to be(false) + vm_cid, _ = create_test_vm + cpi.delete_vm(vm_cid) + expect(cpi.has_vm(vm_cid)).to be(false) end it 'frees the allocated IPs' do - vm_cid = create_test_vm(networks: { 'a' => { 'type' => 'manual', 'ip' => '10.0.0.2' } }) - dummy.delete_vm(vm_cid) - expect(dummy.all_ips).not_to include('10.0.0.2') + vm_cid, _ = create_test_vm(networks: { 'a' => { 'type' => 'manual', 'ip' => '10.0.0.2' } }) + cpi.delete_vm(vm_cid) + expect(cpi.all_ips).not_to include('10.0.0.2') end it 'raises VMNotFound when configured to do so' do - dummy.commands.make_delete_vm_to_raise_vmnotfound - vm_cid = create_test_vm + cpi.commands.make_delete_vm_to_raise_vmnotfound + vm_cid, _ = create_test_vm allow(Process).to receive(:kill).and_raise(Errno::ESRCH) expect { - dummy.delete_vm(vm_cid) + cpi.delete_vm(vm_cid) }.to raise_error(Bosh::Clouds::VMNotFound) end end describe '#vm_cids' do it 'returns all current VM cids' do - # Return distinct PIDs so each VM gets its own unique cid (vm_cid == agent_pid.to_s) - allow(dummy).to receive(:spawn_agent_process).and_return(10001, 10002) - vm1 = create_test_vm(agent_id: 'agent-a', networks: { 'net' => { 'type' => 'manual', 'ip' => '10.0.0.1' } }) - vm2 = create_test_vm(agent_id: 'agent-b', networks: { 'net' => { 'type' => 'manual', 'ip' => '10.0.0.2' } }) - expect(dummy.vm_cids).to contain_exactly(vm1, vm2) + allow(cpi).to receive(:spawn_agent_process).and_return(10001, 10002) + vm1, _ = create_test_vm(agent_id: 'agent-a', networks: { 'net' => { 'type' => 'manual', 'ip' => '10.0.0.1' } }) + vm2, _ = create_test_vm(agent_id: 'agent-b', networks: { 'net' => { 'type' => 'manual', 'ip' => '10.0.0.2' } }) + expect(cpi.vm_cids).to contain_exactly(vm1, vm2) end it 'returns empty when no VMs exist' do - expect(dummy.vm_cids).to be_empty + expect(cpi.vm_cids).to be_empty end end describe '#reboot_vm' do it 'raises NotImplemented' do expect { - dummy.reboot_vm('some-vm-cid') + cpi.reboot_vm('some-vm-cid') }.to raise_error(Bosh::Clouds::NotImplemented, /does not implement reboot_vm/) end end @@ -273,19 +263,19 @@ def create_test_stemcell describe '#has_disk' do it 'returns true for an existing disk' do disk_id = create_test_disk - expect(dummy.has_disk(disk_id)).to be(true) + expect(cpi.has_disk(disk_id)).to be(true) end it 'returns false for a non-existent disk' do - expect(dummy.has_disk('nonexistent-disk')).to be(false) + expect(cpi.has_disk('nonexistent-disk')).to be(false) end end describe '#delete_disk' do it 'removes the disk' do disk_id = create_test_disk - dummy.delete_disk(disk_id) - expect(dummy.has_disk(disk_id)).to be(false) + cpi.delete_disk(disk_id) + expect(cpi.has_disk(disk_id)).to be(false) end end @@ -293,7 +283,7 @@ def create_test_stemcell it 'returns all created disk ids' do id1 = create_test_disk id2 = create_test_disk - expect(dummy.disk_cids).to contain_exactly(id1, id2) + expect(cpi.disk_cids).to contain_exactly(id1, id2) end end @@ -302,63 +292,69 @@ def create_test_stemcell # ----------------------------------------------------------------------- describe '#attach_disk' do - let(:vm_cid) { create_test_vm } + let(:vm_cid) { create_test_vm.first } let(:disk_id) { create_test_disk } it 'attaches a disk to a VM' do - dummy.attach_disk(vm_cid, disk_id) - expect(dummy.disk_attached_to_vm?(vm_cid, disk_id)).to be(true) + cpi.attach_disk(vm_cid, disk_id) + expect(cpi.disk_attached_to_vm?(vm_cid, disk_id)).to be(true) + end + + it 'returns the attachment file path' do + result = cpi.attach_disk(vm_cid, disk_id) + expect(result).to be_a(String) + expect(File.exist?(result)).to be(true) end it 'records the disk in agent settings' do - dummy.attach_disk(vm_cid, disk_id) - infos = dummy.attached_disk_infos(vm_cid) + cpi.attach_disk(vm_cid, disk_id) + infos = cpi.attached_disk_infos(vm_cid) expect(infos.map { |i| i['disk_cid'] }).to include(disk_id) end it 'raises when disk is already attached' do - dummy.attach_disk(vm_cid, disk_id) + cpi.attach_disk(vm_cid, disk_id) expect { - dummy.attach_disk(vm_cid, disk_id) + cpi.attach_disk(vm_cid, disk_id) }.to raise_error(RuntimeError, /already attached/) end it 'raises NotImplemented when configured to do so' do - dummy.commands.make_attach_disk_to_raise_not_implemented + cpi.commands.make_attach_disk_to_raise_not_implemented expect { - dummy.attach_disk(vm_cid, disk_id) + cpi.attach_disk(vm_cid, disk_id) }.to raise_error(Bosh::Clouds::NotImplemented) end end describe '#detach_disk' do - let(:vm_cid) { create_test_vm } + let(:vm_cid) { create_test_vm.first } let(:disk_id) { create_test_disk } - before { dummy.attach_disk(vm_cid, disk_id) } + before { cpi.attach_disk(vm_cid, disk_id) } it 'detaches a disk from a VM' do - dummy.detach_disk(vm_cid, disk_id) - expect(dummy.disk_attached_to_vm?(vm_cid, disk_id)).to be(false) + cpi.detach_disk(vm_cid, disk_id) + expect(cpi.disk_attached_to_vm?(vm_cid, disk_id)).to be(false) end it 'removes the disk from agent settings' do - dummy.detach_disk(vm_cid, disk_id) - infos = dummy.attached_disk_infos(vm_cid) + cpi.detach_disk(vm_cid, disk_id) + infos = cpi.attached_disk_infos(vm_cid) expect(infos.map { |i| i['disk_cid'] }).not_to include(disk_id) end it 'raises DiskNotAttached when disk is not attached' do - dummy.detach_disk(vm_cid, disk_id) + cpi.detach_disk(vm_cid, disk_id) expect { - dummy.detach_disk(vm_cid, disk_id) + cpi.detach_disk(vm_cid, disk_id) }.to raise_error(Bosh::Clouds::DiskNotAttached) end it 'raises NotImplemented when configured to do so' do - dummy.commands.make_detach_disk_to_raise_not_implemented + cpi.commands.make_detach_disk_to_raise_not_implemented expect { - dummy.detach_disk(vm_cid, disk_id) + cpi.detach_disk(vm_cid, disk_id) }.to raise_error(Bosh::Clouds::NotImplemented) end end @@ -371,15 +367,15 @@ def create_test_stemcell let(:disk_id) { create_test_disk(size: 1024) } it 'updates the disk size' do - dummy.resize_disk(disk_id, 2048) + cpi.resize_disk(disk_id, 2048) disk_info = JSON.parse(File.read(File.join(tmpdir, 'disks', disk_id))) expect(disk_info['size']).to eq(2048) end it 'raises NotImplemented when configured to do so' do - dummy.commands.make_resize_disk_to_raise_not_implemented + cpi.commands.make_resize_disk_to_raise_not_implemented expect { - dummy.resize_disk(disk_id, 2048) + cpi.resize_disk(disk_id, 2048) }.to raise_error(Bosh::Clouds::NotImplemented) end end @@ -388,16 +384,16 @@ def create_test_stemcell let(:disk_id) { create_test_disk(size: 1024) } it 'updates the disk size and cloud properties' do - dummy.update_disk(disk_id, 4096, { 'type' => 'ssd' }) + cpi.update_disk(disk_id, 4096, { 'type' => 'ssd' }) disk_info = JSON.parse(File.read(File.join(tmpdir, 'disks', disk_id))) expect(disk_info['size']).to eq(4096) expect(disk_info['cloud_properties']).to eq('type' => 'ssd') end it 'raises NotImplemented when configured to do so' do - dummy.commands.make_update_disk_to_raise_not_implemented + cpi.commands.make_update_disk_to_raise_not_implemented expect { - dummy.update_disk(disk_id, 4096, {}) + cpi.update_disk(disk_id, 4096, {}) }.to raise_error(Bosh::Clouds::NotImplemented) end end @@ -410,15 +406,15 @@ def create_test_stemcell let(:disk_id) { create_test_disk } it 'returns a snapshot id' do - snapshot_id = dummy.snapshot_disk(disk_id, { 'env' => 'test' }) + snapshot_id = cpi.snapshot_disk(disk_id, { 'env' => 'test' }) expect(snapshot_id).to be_a(String) expect(snapshot_id).not_to be_empty end it 'persists snapshot metadata' do - dummy.snapshot_disk(disk_id, { 'label' => 'backup' }) - expect(dummy.all_snapshots.size).to eq(1) - snapshot_file = dummy.all_snapshots.first + cpi.snapshot_disk(disk_id, { 'label' => 'backup' }) + expect(cpi.all_snapshots.size).to eq(1) + snapshot_file = cpi.all_snapshots.first metadata = JSON.parse(File.read(snapshot_file)) expect(metadata['label']).to eq('backup') end @@ -428,9 +424,9 @@ def create_test_stemcell let(:disk_id) { create_test_disk } it 'removes the snapshot' do - snapshot_id = dummy.snapshot_disk(disk_id, {}) - dummy.delete_snapshot(snapshot_id) - expect(dummy.all_snapshots).to be_empty + snapshot_id = cpi.snapshot_disk(disk_id, {}) + cpi.delete_snapshot(snapshot_id) + expect(cpi.all_snapshots).to be_empty end end @@ -440,30 +436,30 @@ def create_test_stemcell describe '#create_network' do it 'returns [network_id, addr_properties, tags]' do - result = dummy.create_network({ 'cloud_properties' => {} }) + result = cpi.create_network({ 'cloud_properties' => {} }) expect(result).to be_an(Array) expect(result.size).to eq(3) expect(result[0]).to be_a(String) end it 'includes range/gateway when netmask_bits is specified' do - _, addr_props, _ = dummy.create_network({ 'cloud_properties' => {}, 'netmask_bits' => 24 }) + _, addr_props, _ = cpi.create_network({ 'cloud_properties' => {}, 'netmask_bits' => 24 }) expect(addr_props['range']).not_to be_nil expect(addr_props['gateway']).not_to be_nil end it 'raises when cloud_properties includes an error key' do expect { - dummy.create_network({ 'cloud_properties' => { 'error' => 'something went wrong' } }) + cpi.create_network({ 'cloud_properties' => { 'error' => 'something went wrong' } }) }.to raise_error('something went wrong') end end describe '#delete_network' do it 'removes the network' do - network_id, _, _ = dummy.create_network({ 'cloud_properties' => {} }) - dummy.delete_network(network_id) - expect(dummy.network_cids).not_to include(network_id) + network_id, _, _ = cpi.create_network({ 'cloud_properties' => {} }) + cpi.delete_network(network_id) + expect(cpi.network_cids).not_to include(network_id) end end @@ -472,16 +468,16 @@ def create_test_stemcell # ----------------------------------------------------------------------- describe '#set_vm_metadata' do - let(:vm_cid) { create_test_vm } + let(:vm_cid) { create_test_vm.first } it 'succeeds normally' do - expect { dummy.set_vm_metadata(vm_cid, { 'tag' => 'value' }) }.not_to raise_error + expect { cpi.set_vm_metadata(vm_cid, { 'tag' => 'value' }) }.not_to raise_error end it 'raises when configured to fail' do - dummy.commands.make_set_vm_metadata_always_fail + cpi.commands.make_set_vm_metadata_always_fail expect { - dummy.set_vm_metadata(vm_cid, {}) + cpi.set_vm_metadata(vm_cid, {}) }.to raise_error(RuntimeError, /Set VM metadata failed/) end end @@ -490,7 +486,7 @@ def create_test_stemcell let(:disk_id) { create_test_disk } it 'succeeds without errors' do - expect { dummy.set_disk_metadata(disk_id, { 'key' => 'val' }) }.not_to raise_error + expect { cpi.set_disk_metadata(disk_id, { 'key' => 'val' }) }.not_to raise_error end end @@ -500,7 +496,7 @@ def create_test_stemcell describe '#calculate_vm_cloud_properties' do it 'returns a cloud properties hash with instance_type' do - result = dummy.calculate_vm_cloud_properties({ 'ram' => 1024, 'cpu' => 2, 'ephemeral_disk_size' => 10 }) + result = cpi.calculate_vm_cloud_properties({ 'ram' => 1024, 'cpu' => 2, 'ephemeral_disk_size' => 10 }) expect(result[:instance_type]).to eq('dummy') expect(result[:cpu]).to eq(2) expect(result[:ram]).to eq(1024) @@ -508,8 +504,8 @@ def create_test_stemcell end it 'uses cvcpkey from context when present' do - cpi = described_class.new(base_options, { 'cvcpkey' => 'xlarge' }, 1) - result = cpi.calculate_vm_cloud_properties({ 'ram' => 2048, 'cpu' => 4, 'ephemeral_disk_size' => 20 }) + c = described_class.new(base_options, { 'cvcpkey' => 'xlarge' }) + result = c.calculate_vm_cloud_properties({ 'ram' => 2048, 'cpu' => 4, 'ephemeral_disk_size' => 20 }) expect(result[:instance_type]).to eq('xlarge') end end @@ -521,15 +517,14 @@ def create_test_stemcell describe '#invocations' do it 'records CPI method invocations' do create_test_disk - invocations = dummy.invocations - # method_name is stored via JSON, so it comes back as a String + invocations = cpi.invocations expect(invocations.map(&:method_name)).to include('create_disk') end it 'records multiple different method invocations' do create_test_disk create_test_vm - methods_called = dummy.invocations.map(&:method_name) + methods_called = cpi.invocations.map(&:method_name) expect(methods_called).to include('create_disk', 'create_vm') end end @@ -538,8 +533,7 @@ def create_test_stemcell it 'returns only invocations for the specified method' do create_test_disk create_test_vm - # method_name is stored as a String after JSON round-trip - create_disk_invocations = dummy.invocations_for_method('create_disk') + create_disk_invocations = cpi.invocations_for_method('create_disk') expect(create_disk_invocations.size).to eq(1) expect(create_disk_invocations.first.method_name).to eq('create_disk') end @@ -552,15 +546,15 @@ def create_test_stemcell describe '#kill_agents' do it 'kills all running VMs without error' do create_test_vm(agent_id: 'agent-a', networks: { 'net' => { 'type' => 'manual', 'ip' => '10.0.0.1' } }) - expect { dummy.kill_agents }.not_to raise_error + expect { cpi.kill_agents }.not_to raise_error end end describe '#reset' do it 'removes and recreates the base directory' do create_test_disk - dummy.reset - expect(dummy.disk_cids).to be_empty + cpi.reset + expect(cpi.disk_cids).to be_empty end end @@ -569,7 +563,7 @@ def create_test_stemcell # ----------------------------------------------------------------------- describe 'CommandTransport' do - subject(:commands) { dummy.commands } + subject(:commands) { cpi.commands } describe 'create_vm commands' do it 'defaults to a non-failing create_vm' do @@ -634,62 +628,3 @@ def create_test_stemcell end end end - -describe Bosh::Clouds::DummyV2 do - let(:tmpdir) { Dir.mktmpdir('dummy_v2_cpi_spec') } - - let(:base_options) do - { - 'dir' => tmpdir, - 'agent' => { - 'blobstore' => { - 'provider' => 'local', - 'options' => { 'blobstore_path' => File.join(tmpdir, 'blobstore') }, - }, - }, - 'nats' => 'nats://127.0.0.1:4222', - 'log_buffer' => StringIO.new, - } - end - - subject(:dummy_v2) { described_class.new(base_options, {}) } - - after { FileUtils.rm_rf(tmpdir) } - - before do - allow(dummy_v2).to receive(:spawn_agent_process).and_return(99998) - allow(Process).to receive(:kill) - end - - describe '#info' do - it 'returns api_version 2' do - expect(dummy_v2.info[:api_version]).to eq(2) - end - - it 'returns stemcell_formats' do - expect(dummy_v2.info).to include(stemcell_formats: ['dummy']) - end - end - - describe '#create_vm' do - it 'returns [vm_cid, network_settings] tuple' do - result = dummy_v2.create_vm('agent-1', 'stem-1', {}, { 'a' => { 'type' => 'manual', 'ip' => '10.0.0.1' } }, [], {}) - expect(result).to be_an(Array) - expect(result.size).to eq(2) - vm_cid, network_settings = result - expect(vm_cid).to be_a(String) - expect(network_settings).to eq({}) - end - end - - describe '#attach_disk' do - it 'returns the attachment file path' do - vm_cid, _ = dummy_v2.create_vm('agent-1', 'stem-1', {}, { 'a' => { 'type' => 'manual', 'ip' => '10.0.0.1' } }, [], {}) - disk_id = dummy_v2.create_disk(1024, {}, 'vm-locality') - - result = dummy_v2.attach_disk(vm_cid, disk_id) - expect(result).to be_a(String) - expect(File.exist?(result)).to be(true) - end - end -end diff --git a/src/tasks/spec.rake b/src/tasks/spec.rake index d0eed5ec93..f5a5513ca0 100644 --- a/src/tasks/spec.rake +++ b/src/tasks/spec.rake @@ -52,13 +52,13 @@ namespace :spec do desc 'Run integration_support unit specs' task :integration_support do puts 'Run integration_support unit specs' - sh("cd #{BOSH_REPO_ROOT} && rspec") + sh("cd #{BOSH_REPO_ROOT}/src/ && rspec spec/integration_support/spec/") end namespace :integration_support do task :parallel do puts 'Run parallel integration_support unit specs' - sh("cd #{BOSH_REPO_ROOT} && parallel_rspec spec") + sh("cd #{BOSH_REPO_ROOT}/src/ && parallel_rspec spec/integration_support/spec/") end end @@ -85,13 +85,13 @@ namespace :spec do end desc 'Run all unit tests in parallel' - multitask parallel: %w[spec:unit:release:parallel] + component_dir_names.map{|d| "spec:unit:#{component_symbol(d)}:parallel" } do + multitask parallel: %w[spec:unit:release:parallel spec:unit:integration_support:parallel] + component_dir_names.map{|d| "spec:unit:#{component_symbol(d)}:parallel" } do trap('INT') { exit } end end desc 'Run all unit tests' - task unit: %w[spec:unit:release] + component_dir_names.map{|d| "spec:unit:#{component_symbol(d)}" } + task unit: %w[spec:unit:release spec:unit:integration_support] + component_dir_names.map{|d| "spec:unit:#{component_symbol(d)}" } end desc 'Run unit and integration specs'