require 'puppet' require 'spec_helper' require 'puppet/provider/neutron' require 'tempfile' describe Puppet::Provider::Neutron do def klass described_class end let :credential_hash do { 'project_name' => 'admin_tenant', 'username' => 'admin', 'password' => 'password', 'auth_url' => 'https://192.168.56.210:5000/v2.0/', 'project_domain_name' => 'Default', 'user_domain_name' => 'Default', } end let :credential_error do /Neutron types will not work/ end let :exec_error do /Neutron or Keystone API is not available/ end after :each do klass.reset end describe 'when determining credentials' do it 'should fail if config is empty' do conf = {} klass.expects(:neutron_conf).returns(conf) expect do klass.neutron_credentials end.to raise_error(Puppet::Error, credential_error) end it 'should fail if config does not have keystone_authtoken section.' do conf = {'foo' => 'bar'} klass.expects(:neutron_conf).returns(conf) expect do klass.neutron_credentials end.to raise_error(Puppet::Error, credential_error) end it 'should fail if config does not contain all auth params' do conf = {'keystone_authtoken' => {'invalid_value' => 'foo'}} klass.expects(:neutron_conf).returns(conf) expect do klass.neutron_credentials end.to raise_error(Puppet::Error, credential_error) end end describe 'when invoking the neutron cli' do it 'should set auth credentials in the environment' do authenv = { :OS_AUTH_URL => credential_hash['auth_url'], :OS_USERNAME => credential_hash['username'], :OS_PROJECT_NAME => credential_hash['project_name'], :OS_PASSWORD => credential_hash['password'], :OS_PROJECT_DOMAIN_NAME => credential_hash['project_domain_name'], :OS_USER_DOMAIN_NAME => credential_hash['user_domain_name'], } klass.expects(:get_neutron_credentials).with().returns(credential_hash) klass.expects(:withenv).with(authenv) klass.auth_neutron('test_retries') end it 'should set region in the environment if needed' do authenv = { :OS_AUTH_URL => credential_hash['auth_url'], :OS_USERNAME => credential_hash['username'], :OS_PROJECT_NAME => credential_hash['project_name'], :OS_PASSWORD => credential_hash['password'], :OS_REGION_NAME => 'REGION_NAME', :OS_PROJECT_DOMAIN_NAME => credential_hash['project_domain_name'], :OS_USER_DOMAIN_NAME => credential_hash['user_domain_name'], } cred_hash = credential_hash.merge({'region_name' => 'REGION_NAME'}) klass.expects(:get_neutron_credentials).with().returns(cred_hash) klass.expects(:withenv).with(authenv) klass.auth_neutron('test_retries') end ['[Errno 111] Connection refused', '400-{\'message\': \'\'}', '(HTTP 400)', '503 Service Unavailable', '504 Gateway Time-out', 'Maximum attempts reached', 'Unauthorized: bad credentials', 'Max retries exceeded'].reverse.each do |valid_message| it "should retry when neutron cli returns with error #{valid_message}" do klass.expects(:get_neutron_credentials).with().returns({}) klass.expects(:sleep).with(2).returns(nil) klass.expects(:neutron).twice.with(['test_retries']).raises( Puppet::ExecutionFailure, valid_message).then.returns('') klass.auth_neutron('test_retries') end end end describe 'when listing neutron resources' do it 'should exclude the column header' do output = <<-EOT id net1 net2 EOT klass.expects(:auth_neutron).returns(output) result = klass.list_neutron_resources('foo') expect(result).to eql(['net1', 'net2']) end it 'should return empty list when there are no neutron resources' do output = <<-EOT EOT klass.stubs(:auth_neutron).returns(output) result = klass.list_neutron_resources('foo') expect(result).to eql([]) end it 'should fail if resources list is nil' do klass.stubs(:auth_neutron).returns(nil) expect do klass.list_neutron_resources('foo') end.to raise_error(Puppet::Error, exec_error) end end describe 'when retrieving attributes for neutron resources' do it 'should parse single-valued attributes into a key-value pair' do klass.expects(:auth_neutron).returns('admin_state_up="True"') result = klass.get_neutron_resource_attrs('foo', 'id') expect(result).to eql({"admin_state_up" => 'True'}) end it 'should parse multi-valued attributes into a key-list pair' do output = <<-EOT subnets="subnet1 subnet2 subnet3" EOT klass.expects(:auth_neutron).returns(output) result = klass.get_neutron_resource_attrs('foo', 'id') expect(result).to eql({"subnets" => ['subnet1', 'subnet2', 'subnet3']}) end end describe 'when listing router ports' do let :router do 'router1' end it 'should handle an empty port list' do klass.expects(:auth_neutron).with('router-port-list', '--format=csv', router) result = klass.list_router_ports(router) expect(result).to eql([]) end it 'should handle several ports' do output = <<-EOT "id","name","mac_address","fixed_ips" "1345e576-a21f-4c2e-b24a-b245639852ab","","fa:16:3e:e3:e6:38","{""subnet_id"": ""839a1d2d-2c6e-44fb-9a2b-9b011dce8c2f"", ""ip_address"": ""10.0.0.1""}" "de0dc526-02b2-467c-9832-2c3dc69ac2b4","","fa:16:3e:f6:b5:72","{""subnet_id"": ""e4db0abd-276a-4f69-92ea-8b9e4eacfd43"", ""ip_address"": ""172.24.4.226""}" EOT expected = [{ "fixed_ips"=> "{\"subnet_id\": \"839a1d2d-2c6e-44fb-9a2b-9b011dce8c2f\", \ \"ip_address\": \"10.0.0.1\"}", "name"=>"", "subnet_id"=>"839a1d2d-2c6e-44fb-9a2b-9b011dce8c2f", "id"=>"1345e576-a21f-4c2e-b24a-b245639852ab", "mac_address"=>"fa:16:3e:e3:e6:38"}, {"fixed_ips"=> "{\"subnet_id\": \"e4db0abd-276a-4f69-92ea-8b9e4eacfd43\", \ \"ip_address\": \"172.24.4.226\"}", "name"=>"", "subnet_id"=>"e4db0abd-276a-4f69-92ea-8b9e4eacfd43", "id"=>"de0dc526-02b2-467c-9832-2c3dc69ac2b4", "mac_address"=>"fa:16:3e:f6:b5:72"}] klass.expects(:auth_neutron). with('router-port-list', '--format=csv', router). returns(output) result = klass.list_router_ports(router) expect(result).to eql(expected) end end describe 'when parsing creation output' do it 'should parse valid output into a hash' do data = <<-EOT Created a new network: admin_state_up="True" id="5f9cbed2-d31c-4e9c-be92-87229acb3f69" name="foo" tenant_id="3056a91768d948d399f1d79051a7f221" EOT expected = { 'admin_state_up' => 'True', 'id' => '5f9cbed2-d31c-4e9c-be92-87229acb3f69', 'name' => 'foo', 'tenant_id' => '3056a91768d948d399f1d79051a7f221', } expect(klass.parse_creation_output(data)).to eq(expected) end end describe 'garbage in the csv output' do it '#list_router_ports' do output = <<-EOT /usr/lib/python2.7/dist-packages/urllib3/util/ssl_.py:90: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning. InsecurePlatformWarning "id","name","mac_address","fixed_ips" "1345e576-a21f-4c2e-b24a-b245639852ab","","fa:16:3e:e3:e6:38","{""subnet_id"": ""839a1d2d-2c6e-44fb-9a2b-9b011dce8c2f"", ""ip_address"": ""10.0.0.1""}" EOT expected = [{ "fixed_ips"=> "{\"subnet_id\": \"839a1d2d-2c6e-44fb-9a2b-9b011dce8c2f\", \ \"ip_address\": \"10.0.0.1\"}", "name"=>"", "subnet_id"=>"839a1d2d-2c6e-44fb-9a2b-9b011dce8c2f", "id"=>"1345e576-a21f-4c2e-b24a-b245639852ab", "mac_address"=>"fa:16:3e:e3:e6:38"}] klass.expects(:auth_neutron). with('router-port-list', '--format=csv', 'router1'). returns(output) result = klass.list_router_ports('router1') expect(result).to eql(expected) end it '#list_neutron_resources' do output = <<-EOT /usr/lib/python2.7/dist-packages/urllib3/util/ssl_.py:90: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning. InsecurePlatformWarning id 4a305398-d806-46c5-a6aa-dcd6a4a99330 EOT klass.expects(:auth_neutron). with('subnet-list', '--format=csv', '--column=id', '--quote=none'). returns(output) expected = ['4a305398-d806-46c5-a6aa-dcd6a4a99330'] result = klass.list_neutron_resources('subnet') expect(result).to eql(expected) end end end