# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import argparse
import copy
import os
from typing import Any
from unittest import mock

import fixtures
import keyring
import yaml

from openstack import config
from openstack.config import cloud_region
from openstack.config import defaults
from openstack.config import loader
from openstack import exceptions
from openstack.tests.unit.config import base


def prompt_for_password(prompt=None):
    """Fake prompt function that just returns a constant string"""
    return 'promptpass'


class TestConfig(base.TestCase):
    def test_get_all(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml],
            vendor_files=[self.vendor_yaml],
            secure_files=[self.no_yaml],
        )
        clouds = c.get_all()
        # We add two by hand because the regions cloud is going to exist
        # thrice since it has three regions in it
        user_clouds = [cloud for cloud in base.USER_CONF['clouds'].keys()] + [
            '_test_cloud_regions',
            '_test_cloud_regions',
        ]
        configured_clouds = [cloud.name for cloud in clouds]
        self.assertCountEqual(user_clouds, configured_clouds)

    def test_get_all_clouds(self):
        # Ensure the alias is in place
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml],
            vendor_files=[self.vendor_yaml],
            secure_files=[self.no_yaml],
        )
        clouds = c.get_all_clouds()
        # We add two by hand because the regions cloud is going to exist
        # thrice since it has three regions in it
        user_clouds = [cloud for cloud in base.USER_CONF['clouds'].keys()] + [
            '_test_cloud_regions',
            '_test_cloud_regions',
        ]
        configured_clouds = [cloud.name for cloud in clouds]
        self.assertCountEqual(user_clouds, configured_clouds)

    def test_get_one(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        cloud = c.get_one(validate=False)
        self.assertIsInstance(cloud, cloud_region.CloudRegion)
        self.assertEqual(cloud.name, '')

    def test_get_one_cloud(self):
        # Ensure the alias is in place
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        cloud = c.get_one_cloud(validate=False)
        self.assertIsInstance(cloud, cloud_region.CloudRegion)
        self.assertEqual(cloud.name, '')

    def test_get_one_default_cloud_from_file(self):
        single_conf = base._write_yaml(
            {
                'clouds': {
                    'single': {
                        'auth': {
                            'auth_url': 'http://example.com/v2',
                            'username': 'testuser',
                            'password': 'testpass',
                            'project_name': 'testproject',
                        },
                        'region_name': 'test-region',
                    }
                }
            }
        )
        c = config.OpenStackConfig(
            config_files=[single_conf],
            secure_files=[],
            vendor_files=[self.vendor_yaml],
        )
        cc = c.get_one()
        self.assertEqual(cc.name, 'single')

    def test_remote_profile(self):
        single_conf = base._write_yaml(
            {
                'clouds': {
                    'remote': {
                        'profile': 'https://example.com',
                        'auth': {
                            'username': 'testuser',
                            'password': 'testpass',
                            'project_name': 'testproject',
                        },
                        'region_name': 'test-region',
                    }
                }
            }
        )
        self.register_uris(
            [
                dict(
                    method='GET',
                    uri='https://example.com/.well-known/openstack/api',
                    json={
                        "name": "example",
                        "profile": {
                            "auth": {
                                "auth_url": "https://auth.example.com/v3",
                            }
                        },
                    },
                ),
            ]
        )

        c = config.OpenStackConfig(config_files=[single_conf])
        cc = c.get_one(cloud='remote')
        self.assertEqual(cc.name, 'remote')
        self.assertEqual(cc.auth['auth_url'], 'https://auth.example.com/v3')
        self.assertEqual(cc.auth['username'], 'testuser')

    def test_get_one_auth_defaults(self):
        c = config.OpenStackConfig(config_files=[self.cloud_yaml])
        cc = c.get_one(cloud='_test-cloud_', auth={'username': 'user'})
        self.assertEqual('user', cc.auth['username'])
        assert defaults._defaults is not None
        self.assertEqual(
            defaults._defaults['auth_type'],
            cc.auth_type,
        )

    def test_get_one_with_config_files(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml],
            vendor_files=[self.vendor_yaml],
            secure_files=[self.secure_yaml],
        )
        self.assertIsInstance(c.cloud_config, dict)
        self.assertIn('cache', c.cloud_config)
        self.assertIsInstance(c.cloud_config['cache'], dict)
        self.assertIn('max_age', c.cloud_config['cache'])
        self.assertIn('path', c.cloud_config['cache'])
        cc = c.get_one('_test-cloud_')
        self._assert_cloud_details(cc)
        cc = c.get_one('_test_cloud_no_vendor')
        self._assert_cloud_details(cc)

    def test_get_one_with_int_project_id(self):
        # Real project IDs are strings (which often look like UUIDs), but a
        # numeric value in the config should still be coerced to a string
        # rather than left as an int. This guards the normalization done by
        # _fix_backwards_auth.
        single_conf = base._write_yaml(
            {
                'clouds': {
                    'int-project': {
                        'auth': {
                            'auth_url': 'http://example.com/v2',
                            'username': 'testuser',
                            'password': 'testpass',
                            'project_id': 12345,
                        },
                        'region_name': 'test-region',
                    }
                }
            }
        )
        c = config.OpenStackConfig(
            config_files=[single_conf], secure_files=[], vendor_files=[]
        )
        cc = c.get_one('int-project')
        self.assertEqual('12345', cc.auth['project_id'])

    def test_get_one_with_domain_id(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        cc = c.get_one('_test-cloud-domain-id_')
        self.assertEqual('6789', cc.auth['user_domain_id'])
        self.assertEqual('123456789', cc.auth['project_domain_id'])
        self.assertNotIn('domain_id', cc.auth)
        self.assertNotIn('domain-id', cc.auth)
        self.assertNotIn('domain_id', cc)

    def test_get_one_unscoped_identity(self):
        single_conf = base._write_yaml(
            {
                'clouds': {
                    'unscoped': {
                        'auth': {
                            'auth_url': 'http://example.com/v2',
                            'username': 'testuser',
                            'password': 'testpass',
                        },
                    }
                }
            }
        )
        c = config.OpenStackConfig(
            config_files=[single_conf],
            secure_files=[],
            vendor_files=[self.vendor_yaml],
        )
        cc = c.get_one()
        self.assertEqual('http://example.com/v2', cc.get_endpoint('identity'))

    def test_get_one_domain_scoped(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        cc = c.get_one('_test-cloud-domain-scoped_')
        self.assertEqual('12345', cc.auth['domain_id'])
        self.assertNotIn('user_domain_id', cc.auth)
        self.assertNotIn('project_domain_id', cc.auth)
        self.assertIsNone(cc.get_endpoint('identity'))

    def test_get_one_infer_user_domain(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        cc = c.get_one('_test-cloud-int-project_')
        self.assertEqual('awesome-domain', cc.auth['user_domain_id'])
        self.assertEqual('awesome-domain', cc.auth['project_domain_id'])
        self.assertNotIn('domain_id', cc.auth)
        self.assertNotIn('domain_id', cc)

    def test_get_one_infer_passcode(self):
        single_conf = base._write_yaml(
            {
                'clouds': {
                    'mfa': {
                        'auth_type': 'v3multifactor',
                        'auth_methods': ['v3password', 'v3totp'],
                        'auth': {
                            'auth_url': 'fake_url',
                            'username': 'testuser',
                            'password': 'testpass',
                            'project_name': 'testproject',
                            'project_domain_name': 'projectdomain',
                            'user_domain_name': 'udn',
                        },
                        'region_name': 'test-region',
                    }
                }
            }
        )

        c = config.OpenStackConfig(config_files=[single_conf])
        cc = c.get_one(cloud='mfa', passcode='123')
        self.assertEqual('123', cc.auth['passcode'])

    def test_get_one_with_hyphenated_project_id(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        cc = c.get_one('_test_cloud_hyphenated')
        self.assertEqual('12345', cc.auth['project_id'])

    def test_get_one_with_hyphenated_kwargs(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        cc = c.get_one(
            auth={
                'username': 'testuser',
                'password': 'testpass',
                'project-id': '12345',
                'auth-url': 'http://example.com/v2',
            },
            region_name='test-region',
        )
        self.assertEqual('http://example.com/v2', cc.auth['auth_url'])

    def test_no_environ(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        self.assertRaises(exceptions.ConfigException, c.get_one, 'envvars')

    def test_fallthrough(self):
        c = config.OpenStackConfig(
            config_files=[self.no_yaml],
            vendor_files=[self.no_yaml],
            secure_files=[self.no_yaml],
        )
        for k in os.environ.keys():
            if k.startswith('OS_'):
                self.useFixture(fixtures.EnvironmentVariable(k))
        c.get_one(cloud='defaults', validate=False)

    def test_prefer_ipv6_true(self):
        c = config.OpenStackConfig(
            config_files=[self.no_yaml],
            vendor_files=[self.no_yaml],
            secure_files=[self.no_yaml],
        )
        cc = c.get_one(cloud='defaults', validate=False)
        self.assertTrue(cc.prefer_ipv6)

    def test_prefer_ipv6_false(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        cc = c.get_one(cloud='_test-cloud_')
        self.assertFalse(cc.prefer_ipv6)

    def test_force_ipv4_true(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        cc = c.get_one(cloud='_test-cloud_')
        self.assertTrue(cc.force_ipv4)

    def test_force_ipv4_false(self):
        c = config.OpenStackConfig(
            config_files=[self.no_yaml],
            vendor_files=[self.no_yaml],
            secure_files=[self.no_yaml],
        )
        cc = c.get_one(cloud='defaults', validate=False)
        self.assertFalse(cc.force_ipv4)

    def test_get_one_auth_merge(self):
        c = config.OpenStackConfig(config_files=[self.cloud_yaml])
        cc = c.get_one(cloud='_test-cloud_', auth={'username': 'user'})
        self.assertEqual('user', cc.auth['username'])
        self.assertEqual('testpass', cc.auth['password'])

    def test_get_one_networks(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        cc = c.get_one('_test-cloud-networks_')
        self.assertEqual(
            ['a-public', 'another-public', 'split-default'],
            cc.get_external_networks(),
        )
        self.assertEqual(
            ['a-private', 'another-private', 'split-no-default'],
            cc.get_internal_networks(),
        )
        self.assertEqual('a-public', cc.get_nat_source())
        self.assertEqual('another-private', cc.get_nat_destination())
        self.assertEqual('another-public', cc.get_default_network())
        self.assertEqual(
            ['a-public', 'another-public', 'split-no-default'],
            cc.get_external_ipv4_networks(),
        )
        self.assertEqual(
            ['a-public', 'another-public', 'split-default'],
            cc.get_external_ipv6_networks(),
        )

    def test_get_one_no_networks(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        cc = c.get_one('_test-cloud-domain-scoped_')
        self.assertEqual([], cc.get_external_networks())
        self.assertEqual([], cc.get_internal_networks())
        self.assertIsNone(cc.get_nat_source())
        self.assertIsNone(cc.get_nat_destination())
        self.assertIsNone(cc.get_default_network())

    def test_only_secure_yaml(self):
        c = config.OpenStackConfig(
            config_files=['nonexistent'],
            vendor_files=['nonexistent'],
            secure_files=[self.secure_yaml],
        )
        cc = c.get_one(cloud='_test_cloud_no_vendor', validate=False)
        self.assertEqual('testpass', cc.auth['password'])

    def test_get_cloud_names(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], secure_files=[self.no_yaml]
        )
        self.assertCountEqual(
            [
                '_test-cloud-domain-id_',
                '_test-cloud-domain-scoped_',
                '_test-cloud-int-project_',
                '_test-cloud-networks_',
                '_test-cloud_',
                '_test-cloud_no_region',
                '_test_cloud_hyphenated',
                '_test_cloud_no_vendor',
                '_test_cloud_regions',
                '_test-cloud-override-metrics',
            ],
            c.get_cloud_names(),
        )
        c = config.OpenStackConfig(
            config_files=[self.no_yaml],
            vendor_files=[self.no_yaml],
            secure_files=[self.no_yaml],
        )
        for k in os.environ.keys():
            if k.startswith('OS_'):
                self.useFixture(fixtures.EnvironmentVariable(k))
        c.get_one(cloud='defaults', validate=False)
        self.assertEqual(['defaults'], sorted(c.get_cloud_names()))

    def test_set_one_cloud_creates_file(self):
        config_dir = fixtures.TempDir()
        self.useFixture(config_dir)
        config_path = os.path.join(config_dir.path, 'clouds.yaml')
        config.OpenStackConfig.set_one_cloud(config_path, '_test_cloud_')
        self.assertTrue(os.path.isfile(config_path))
        with open(config_path) as fh:
            self.assertEqual(
                {'clouds': {'_test_cloud_': {}}}, yaml.safe_load(fh)
            )

    def test_set_one_cloud_updates_cloud(self):
        new_config = {'cloud': 'new_cloud', 'auth': {'password': 'newpass'}}

        resulting_cloud_config = {
            'auth': {
                'password': 'newpass',
                'username': 'testuser',
                'auth_url': 'http://example.com/v2',
            },
            'cloud': 'new_cloud',
            'profile': '_test_cloud_in_our_cloud',
            'region_name': 'test-region',
        }
        resulting_config = copy.deepcopy(base.USER_CONF)
        resulting_config['clouds']['_test-cloud_'] = resulting_cloud_config
        config.OpenStackConfig.set_one_cloud(
            self.cloud_yaml, '_test-cloud_', new_config
        )
        with open(self.cloud_yaml) as fh:
            written_config = yaml.safe_load(fh)
            # We write a cache config for testing
            written_config['cache'].pop('path', None)
            self.assertEqual(written_config, resulting_config)

    def test_get_region_no_region_default(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml],
            vendor_files=[self.vendor_yaml],
            secure_files=[self.no_yaml],
        )
        region = c._get_region(cloud='_test-cloud_no_region')
        self.assertEqual(region, {'name': '', 'values': {}})

    def test_get_region_no_region(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml],
            vendor_files=[self.vendor_yaml],
            secure_files=[self.no_yaml],
        )
        region = c._get_region(
            cloud='_test-cloud_no_region', region_name='override-region'
        )
        self.assertEqual(region, {'name': 'override-region', 'values': {}})

    def test_get_region_region_is_none(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml],
            vendor_files=[self.vendor_yaml],
            secure_files=[self.no_yaml],
        )
        region = c._get_region(cloud='_test-cloud_no_region', region_name=None)
        self.assertEqual(region, {'name': '', 'values': {}})

    def test_get_region_region_set(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml],
            vendor_files=[self.vendor_yaml],
            secure_files=[self.no_yaml],
        )
        region = c._get_region(cloud='_test-cloud_', region_name='test-region')
        self.assertEqual(region, {'name': 'test-region', 'values': {}})

    def test_get_region_many_regions_default(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml],
            vendor_files=[self.vendor_yaml],
            secure_files=[self.no_yaml],
        )
        region = c._get_region(cloud='_test_cloud_regions', region_name='')
        self.assertEqual(
            region,
            {
                'name': 'region1',
                'values': {'external_network': 'region1-network'},
            },
        )

    def test_get_region_many_regions(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml],
            vendor_files=[self.vendor_yaml],
            secure_files=[self.no_yaml],
        )
        region = c._get_region(
            cloud='_test_cloud_regions', region_name='region2'
        )
        self.assertEqual(
            region,
            {'name': 'region2', 'values': {'external_network': 'my-network'}},
        )

    def test_get_region_by_name_no_value(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        region = c._get_region(
            cloud='_test_cloud_regions', region_name='region-no-value'
        )
        self.assertEqual(region, {'name': 'region-no-value', 'values': {}})

    def test_get_region_invalid_region(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml],
            vendor_files=[self.vendor_yaml],
            secure_files=[self.no_yaml],
        )
        self.assertRaises(
            exceptions.ConfigException,
            c._get_region,
            cloud='_test_cloud_regions',
            region_name='invalid-region',
        )

    def test_get_region_no_cloud(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml],
            vendor_files=[self.vendor_yaml],
            secure_files=[self.no_yaml],
        )
        region = c._get_region(region_name='no-cloud-region')
        self.assertEqual(region, {'name': 'no-cloud-region', 'values': {}})

    def test_get_region_invalid_keys(self):
        invalid_conf = base._write_yaml(
            {
                'clouds': {
                    '_test_cloud': {
                        'profile': '_test_cloud_in_our_cloud',
                        'auth': {
                            'auth_url': 'http://example.com/v2',
                            'username': 'testuser',
                            'password': 'testpass',
                        },
                        'regions': [
                            {'name': 'region1', 'foo': 'bar'},
                        ],
                    }
                }
            }
        )
        c = config.OpenStackConfig(
            config_files=[invalid_conf], vendor_files=[self.vendor_yaml]
        )
        self.assertRaises(
            exceptions.ConfigException,
            c._get_region,
            cloud='_test_cloud',
            region_name='region1',
        )

    @mock.patch('openstack.config.cloud_region.keyring')
    @mock.patch(
        'keystoneauth1.identity.base.BaseIdentityPlugin.set_auth_state'
    )
    def test_load_auth_cache_not_found(self, ks_mock, kr_mock):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], secure_files=[]
        )
        c._cache_auth = True

        kr_mock.get_password = mock.Mock(side_effect=[RuntimeError])

        region = c.get_one('_test-cloud_')
        assert region._auth is not None
        kr_mock.get_password.assert_called_with(
            'openstacksdk', region._auth.get_cache_id()
        )
        ks_mock.assert_not_called()

    @mock.patch('openstack.config.cloud_region.keyring')
    @mock.patch(
        'keystoneauth1.identity.base.BaseIdentityPlugin.set_auth_state'
    )
    def test_load_auth_cache_found(self, ks_mock, kr_mock):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], secure_files=[]
        )
        c._cache_auth = True
        fake_auth = {'a': 'b'}

        kr_mock.get_password = mock.Mock(return_value=fake_auth)

        region = c.get_one('_test-cloud_')
        assert region._auth is not None
        kr_mock.get_password.assert_called_with(
            'openstacksdk', region._auth.get_cache_id()
        )
        ks_mock.assert_called_with(fake_auth)

    @mock.patch('openstack.config.cloud_region.keyring')
    def test_set_auth_cache_empty_auth(self, kr_mock):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], secure_files=[]
        )
        c._cache_auth = True

        kr_mock.get_password = mock.Mock(side_effect=[RuntimeError])
        kr_mock.set_password = mock.Mock()

        region = c.get_one('_test-cloud_')

        region.set_auth_cache()
        kr_mock.set_password.assert_not_called()

    @mock.patch('openstack.config.cloud_region.keyring')
    def test_set_auth_cache(self, kr_mock):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], secure_files=[]
        )
        c._cache_auth = True

        kr_mock.get_password = mock.Mock(side_effect=[RuntimeError])
        kr_mock.set_password = mock.Mock()

        region = c.get_one('_test-cloud_')
        assert region._auth is not None
        region._auth.set_auth_state(
            '{"auth_token":"foo", "body":{"token":"bar"}}'
        )

        region.set_auth_cache()
        kr_mock.set_password.assert_called_with(
            'openstacksdk',
            region._auth.get_cache_id(),
            region._auth.get_auth_state(),
        )

    def _region_with_unscoped_auth(self, kr_mock, interactive=True):
        """Return a region and the stand-in plugin installed on it.

        The password plugin the test config produces has no unscoped
        credential of its own, so a federated one is stood in for it.
        """
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], secure_files=[]
        )
        c._cache_auth = True
        kr_mock.get_password = mock.Mock(return_value=None)
        kr_mock.set_password = mock.Mock()

        region = c.get_one('_test-cloud_')
        auth = mock.Mock(wraps=region.get_auth())
        auth.get_unscoped_cache_id.return_value = 'abc123'
        auth.get_unscoped_auth_state.return_value = (
            '{"auth_token": "unscoped"}'
        )
        # Would otherwise reach the base plugin, which does not implement it.
        auth.set_unscoped_auth_state.return_value = None
        auth.interactive_unscoped_auth = interactive
        self.useFixture(fixtures.MockPatchObject(region, '_auth', auth))
        return region, auth

    @staticmethod
    def _keyring_holding_unscoped(state):
        """Only the unscoped entry is present, as it would be on first use."""

        def get_password(service, cache_id):
            return state if cache_id == 'unscoped-abc123' else None

        return mock.Mock(side_effect=get_password)

    @mock.patch('openstack.config.cloud_region.keyring')
    def test_load_unscoped_auth_cache(self, kr_mock):
        region, auth = self._region_with_unscoped_auth(kr_mock)
        kr_mock.get_password = self._keyring_holding_unscoped(
            '{"auth_token": "x"}'
        )

        region.load_auth_from_cache()

        kr_mock.get_password.assert_any_call('openstacksdk', 'unscoped-abc123')
        auth.set_unscoped_auth_state.assert_called_with('{"auth_token": "x"}')

    @mock.patch('openstack.config.cloud_region.keyring')
    def test_load_unscoped_auth_cache_without_a_scoped_token(self, kr_mock):
        # The federated plugins return None from get_cache_id, so the unscoped
        # credential has to be loaded without depending on a scoped entry.
        region, auth = self._region_with_unscoped_auth(kr_mock)
        auth.get_cache_id.return_value = None
        kr_mock.get_password = self._keyring_holding_unscoped(
            '{"auth_token": "x"}'
        )

        region.load_auth_from_cache()

        auth.set_unscoped_auth_state.assert_called_with('{"auth_token": "x"}')

    @mock.patch('openstack.config.cloud_region.keyring')
    def test_set_unscoped_auth_cache(self, kr_mock):
        region, _auth = self._region_with_unscoped_auth(kr_mock)

        region.set_auth_cache()

        kr_mock.set_password.assert_any_call(
            'openstacksdk', 'unscoped-abc123', '{"auth_token": "unscoped"}'
        )

    @mock.patch('openstack.config.cloud_region.keyring')
    def test_set_unscoped_auth_cache_skipped_when_not_interactive(
        self, kr_mock
    ):
        # Storing it would expose a credential to no purpose when the plugin
        # can authenticate again without the user.
        region, _auth = self._region_with_unscoped_auth(
            kr_mock, interactive=False
        )

        region.set_auth_cache()

        for call in kr_mock.set_password.call_args_list:
            self.assertNotIn('unscoped-abc123', call.args)

    @mock.patch('openstack.config.cloud_region.keyring')
    def test_clear_auth_cache(self, kr_mock):
        region, auth = self._region_with_unscoped_auth(kr_mock)
        kr_mock.delete_password = mock.Mock()

        region.clear_auth_cache()

        kr_mock.delete_password.assert_any_call(
            'openstacksdk', auth.get_cache_id()
        )
        kr_mock.delete_password.assert_any_call(
            'openstacksdk', 'unscoped-abc123'
        )

    @mock.patch('openstack.config.cloud_region.keyring')
    def test_clear_auth_cache_with_nothing_stored(self, kr_mock):
        # Asking for it to be gone when it already is, is not an error.
        region, _auth = self._region_with_unscoped_auth(kr_mock)
        kr_mock.errors.PasswordDeleteError = keyring.errors.PasswordDeleteError
        kr_mock.delete_password = mock.Mock(
            side_effect=keyring.errors.PasswordDeleteError
        )

        region.clear_auth_cache()

    @mock.patch('openstack.config.cloud_region.keyring')
    def test_clear_auth_cache_prevents_repersist(self, kr_mock):
        # Clearing has to drop the in-memory credential too, or a connection
        # already built from this region would write it straight back when it
        # closed and called set_auth_cache.
        region, auth = self._region_with_unscoped_auth(kr_mock)
        kr_mock.delete_password = mock.Mock()
        # Make the stand-in behave like a real plugin: once its unscoped
        # state is unset, it no longer offers one to store.
        auth.set_unscoped_auth_state.side_effect = lambda data: setattr(
            auth.get_unscoped_auth_state, 'return_value', data
        )

        region.clear_auth_cache()
        region.set_auth_cache()

        auth.invalidate.assert_called_once_with()
        auth.set_unscoped_auth_state.assert_called_once_with(None)
        for call in kr_mock.set_password.call_args_list:
            self.assertNotIn('unscoped-abc123', call.args)

    @mock.patch('openstack.config.cloud_region.keyring')
    def test_clear_auth_cache_ignores_disabled_caching(self, kr_mock):
        # Removing a credential is a recovery path, so it must work even after
        # cache.auth has been turned off; otherwise a stale entry would
        # survive to be reused the next time caching is enabled.
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], secure_files=[]
        )
        kr_mock.get_password = mock.Mock(return_value=None)
        kr_mock.delete_password = mock.Mock()

        region = c.get_one('_test-cloud_')
        self.assertTrue(region.skip_auth_cache())  # caching is off
        region.clear_auth_cache()

        assert region._auth is not None
        kr_mock.delete_password.assert_called_once_with(
            'openstacksdk', region._auth.get_cache_id()
        )

    @mock.patch('openstack.config.cloud_region.keyring', None)
    def test_clear_auth_cache_without_a_keyring(self):
        # Not every client keeps a keyring, but the in-memory credential
        # still has to be dropped so the connection stops using it.
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], secure_files=[]
        )
        region = c.get_one('_test-cloud_')
        auth = mock.Mock(wraps=region.get_auth())
        auth.get_unscoped_cache_id.return_value = None
        self.useFixture(fixtures.MockPatchObject(region, '_auth', auth))

        region.clear_auth_cache()

        auth.invalidate.assert_called_once_with()

    @mock.patch('openstack.config.cloud_region.keyring')
    def test_unscoped_auth_cache_skipped_without_an_unscoped_credential(
        self, kr_mock
    ):
        # Most plugins have no unscoped credential and answer None, so there
        # is nothing to store and nothing to look for.
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], secure_files=[]
        )
        c._cache_auth = True
        kr_mock.get_password = mock.Mock(return_value=None)
        kr_mock.set_password = mock.Mock()

        region = c.get_one('_test-cloud_')

        region.load_auth_from_cache()
        region.set_auth_cache()

        self.assertIsNone(region._unscoped_auth_cache_id())

    def test_metrics_global(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml],
            vendor_files=[self.vendor_yaml],
            secure_files=[self.secure_yaml],
        )
        self.assertIsInstance(c.cloud_config, dict)
        cc = c.get_one('_test-cloud_')
        statsd = {
            'host': '127.0.0.1',
            'port': '1234',
        }
        # NOTE(ianw) we don't test/call get_<stat>_client() because we
        # don't want to instantiate the client, which tries to
        # connect / do hostname lookups.
        self.assertEqual(statsd['host'], cc._statsd_host)
        self.assertEqual(statsd['port'], cc._statsd_port)
        self.assertEqual('openstack.api', cc.get_statsd_prefix())
        influxdb = {
            'use_udp': True,
            'host': '127.0.0.1',
            'port': '1234',
            'username': 'username',
            'password': 'password',
            'database': 'database',
            'measurement': 'measurement.name',
            'timeout': 10,
        }
        self.assertEqual(influxdb, cc._influxdb_config)

    def test_metrics_override(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml],
            vendor_files=[self.vendor_yaml],
            secure_files=[self.secure_yaml],
        )
        self.assertIsInstance(c.cloud_config, dict)
        cc = c.get_one('_test-cloud-override-metrics')
        statsd = {
            'host': '127.0.0.1',
            'port': '4321',
            'prefix': 'statsd.override.prefix',
        }
        self.assertEqual(statsd['host'], cc._statsd_host)
        self.assertEqual(statsd['port'], cc._statsd_port)
        self.assertEqual(statsd['prefix'], cc.get_statsd_prefix())
        influxdb = {
            'use_udp': True,
            'host': '127.0.0.1',
            'port': '1234',
            'username': 'override-username',
            'password': 'override-password',
            'database': 'override-database',
            'measurement': 'measurement.name',
            'timeout': 10,
        }
        self.assertEqual(influxdb, cc._influxdb_config)


class TestExcludedFormattedConfigValue(base.TestCase):
    # verify https://storyboard.openstack.org/#!/story/1635696
    #
    # get_one_cloud() and get_one_cloud_osc() iterate over config
    # values and try to expand any variables in those values by
    # calling value.format(), however some config values
    # (e.g. password) should never have format() applied to them, not
    # only might that change the password but it will also cause the
    # format() function to raise an exception if it can not parse the
    # format string. Examples would be single brace (e.g. 'foo{')
    # which raises an ValueError because it's looking for a matching
    # end brace or a brace pair with a key value that cannot be found
    # (e.g. 'foo{bar}') which raises a KeyError.

    def setUp(self):
        super().setUp()

        self.args: dict[str, Any] = dict(
            auth_url='http://example.com/v2',
            username='user',
            project_name='project',
            region_name='region2',
            snack_type='cookie',
            os_auth_token='no-good-things',
        )

        self.options = argparse.Namespace(**self.args)

    def test_get_one_cloud_password_brace(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        password = 'foo{'  # Would raise ValueError, single brace
        self.options.password = password
        cc = c.get_one_cloud(
            cloud='_test_cloud_regions', argparse=self.options, validate=False
        )
        self.assertEqual(cc.password, password)

        password = 'foo{bar}'  # Would raise KeyError, 'bar' not found
        self.options.password = password
        cc = c.get_one_cloud(
            cloud='_test_cloud_regions', argparse=self.options, validate=False
        )
        self.assertEqual(cc.password, password)

    def test_get_one_cloud_osc_password_brace(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        password = 'foo{'  # Would raise ValueError, single brace
        self.options.password = password
        cc = c.get_one_cloud_osc(
            cloud='_test_cloud_regions', argparse=self.options, validate=False
        )
        self.assertEqual(cc.password, password)

        password = 'foo{bar}'  # Would raise KeyError, 'bar' not found
        self.options.password = password
        cc = c.get_one_cloud_osc(
            cloud='_test_cloud_regions', argparse=self.options, validate=False
        )
        self.assertEqual(cc.password, password)


class TestConfigArgparse(base.TestCase):
    def setUp(self):
        super().setUp()

        self.args: dict[str, Any] = dict(
            auth_url='http://example.com/v2',
            username='user',
            password='password',
            project_name='project',
            region_name='region2',
            snack_type='cookie',
            os_auth_token='no-good-things',
        )

        self.options = argparse.Namespace(**self.args)

    def test_get_one_bad_region_argparse(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        self.assertRaises(
            exceptions.ConfigException,
            c.get_one,
            cloud='_test-cloud_',
            argparse=self.options,
        )

    def test_get_one_argparse(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        cc = c.get_one(
            cloud='_test_cloud_regions', argparse=self.options, validate=False
        )
        self.assertEqual(cc.region_name, 'region2')
        self.assertEqual(cc.snack_type, 'cookie')

    def test_get_one_precedence(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        kwargs: dict[str, Any] = {
            'auth': {
                'username': 'testuser',
                'password': 'authpass',
                'project-id': 'testproject',
                'auth_url': 'http://example.com/v2',
            },
            'region_name': 'kwarg_region',
            'password': 'ansible_password',
            'arbitrary': 'value',
        }

        args = dict(
            auth_url='http://example.com/v2',
            username='user',
            password='argpass',
            project_name='project',
            region_name='region2',
            snack_type='cookie',
        )

        options = argparse.Namespace(**args)
        cc = c.get_one(argparse=options, **kwargs)
        self.assertEqual(cc.region_name, 'region2')
        self.assertEqual(cc.auth['password'], 'authpass')
        self.assertEqual(cc.snack_type, 'cookie')

    def test_get_one_cloud_precedence_osc(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml],
            vendor_files=[self.vendor_yaml],
        )

        kwargs: dict[str, Any] = {
            'auth': {
                'username': 'testuser',
                'password': 'authpass',
                'project-id': 'testproject',
                'auth_url': 'http://example.com/v2',
            },
            'region_name': 'kwarg_region',
            'password': 'ansible_password',
            'arbitrary': 'value',
        }

        args = dict(
            auth_url='http://example.com/v2',
            username='user',
            password='argpass',
            project_name='project',
            region_name='region2',
            snack_type='cookie',
        )

        options = argparse.Namespace(**args)
        cc = c.get_one_cloud_osc(argparse=options, **kwargs)
        self.assertEqual(cc.region_name, 'region2')
        self.assertEqual(cc.auth['password'], 'argpass')
        self.assertEqual(cc.snack_type, 'cookie')

    def test_get_one_precedence_no_argparse(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        kwargs: dict[str, Any] = {
            'auth': {
                'username': 'testuser',
                'password': 'authpass',
                'project-id': 'testproject',
                'auth_url': 'http://example.com/v2',
            },
            'region_name': 'kwarg_region',
            'password': 'ansible_password',
            'arbitrary': 'value',
        }

        cc = c.get_one(**kwargs)
        self.assertEqual(cc.region_name, 'kwarg_region')
        self.assertEqual(cc.auth['password'], 'authpass')
        self.assertIsNone(cc.password)

    def test_get_one_just_argparse(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        cc = c.get_one(argparse=self.options, validate=False)
        self.assertIsNone(cc.cloud)
        self.assertEqual(cc.region_name, 'region2')
        self.assertEqual(cc.snack_type, 'cookie')

    def test_get_one_just_kwargs(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        cc = c.get_one(validate=False, **self.args)
        self.assertIsNone(cc.cloud)
        self.assertEqual(cc.region_name, 'region2')
        self.assertEqual(cc.snack_type, 'cookie')

    def test_get_one_dash_kwargs(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        args: dict[str, Any] = {
            'auth-url': 'http://example.com/v2',
            'username': 'user',
            'password': 'password',
            'project_name': 'project',
            'region_name': 'other-test-region',
            'snack_type': 'cookie',
        }
        cc = c.get_one(**args)
        self.assertIsNone(cc.cloud)
        self.assertEqual(cc.region_name, 'other-test-region')
        self.assertEqual(cc.snack_type, 'cookie')

    def test_get_one_no_argparse(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        cc = c.get_one(cloud='_test-cloud_', argparse=None)
        self._assert_cloud_details(cc)
        self.assertEqual(cc.region_name, 'test-region')
        self.assertIsNone(cc.snack_type)

    def test_get_one_no_argparse_regions(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        cc = c.get_one(cloud='_test_cloud_regions', argparse=None)
        self._assert_cloud_details(cc)
        self.assertEqual(cc.region_name, 'region1')
        self.assertIsNone(cc.snack_type)

    def test_get_one_bad_region(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        self.assertRaises(
            exceptions.ConfigException,
            c.get_one,
            cloud='_test_cloud_regions',
            region_name='bad',
        )

    def test_get_one_bad_region_no_regions(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        self.assertRaises(
            exceptions.ConfigException,
            c.get_one,
            cloud='_test-cloud_',
            region_name='bad_region',
        )

    def test_get_one_no_argparse_region2(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        cc = c.get_one(
            cloud='_test_cloud_regions', region_name='region2', argparse=None
        )
        self._assert_cloud_details(cc)
        self.assertEqual(cc.region_name, 'region2')
        self.assertIsNone(cc.snack_type)

    def test_get_one_network(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        cc = c.get_one(
            cloud='_test_cloud_regions', region_name='region1', argparse=None
        )
        self._assert_cloud_details(cc)
        self.assertEqual(cc.region_name, 'region1')
        self.assertEqual('region1-network', cc.config['external_network'])

    def test_get_one_per_region_network(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        cc = c.get_one(
            cloud='_test_cloud_regions', region_name='region2', argparse=None
        )
        self._assert_cloud_details(cc)
        self.assertEqual(cc.region_name, 'region2')
        self.assertEqual('my-network', cc.config['external_network'])

    def test_get_one_no_yaml_no_cloud(self):
        c = config.OpenStackConfig(load_yaml_config=False)

        self.assertRaises(
            exceptions.ConfigException,
            c.get_one,
            cloud='_test_cloud_regions',
            region_name='region2',
            argparse=None,
        )

    def test_get_one_no_yaml(self):
        c = config.OpenStackConfig(load_yaml_config=False)

        cc = c.get_one(
            region_name='region2',
            argparse=None,
            **base.USER_CONF['clouds']['_test_cloud_regions'],
        )
        # Not using assert_cloud_details because of cache settings which
        # are not present without the file
        self.assertIsInstance(cc, cloud_region.CloudRegion)
        self.assertTrue(hasattr(cc, 'auth'))
        self.assertIsInstance(cc.auth, dict)
        self.assertIsNone(cc.cloud)
        self.assertIn('username', cc.auth)
        self.assertEqual('testuser', cc.auth['username'])
        self.assertEqual('testpass', cc.auth['password'])
        self.assertFalse(cc.config['image_api_use_tasks'])
        self.assertTrue('project_name' in cc.auth or 'project_id' in cc.auth)
        if 'project_name' in cc.auth:
            self.assertEqual('testproject', cc.auth['project_name'])
        elif 'project_id' in cc.auth:
            self.assertEqual('testproject', cc.auth['project_id'])
        self.assertEqual(cc.region_name, 'region2')

    def test_fix_env_args(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        env_args = {'os-compute-api-version': 1}
        fixed_args = c._fix_args(env_args)

        self.assertDictEqual({'compute_api_version': 1}, fixed_args)

    def test_extra_config(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        defaults = {'use_hostnames': False, 'other-value': 'something'}
        ansible_options = c.get_extra_config('ansible', defaults)

        # This should show that the default for use_hostnames above is
        # overridden by the value in the config file defined in base.py
        # It should also show that other-value key is normalized and passed
        # through even though there is no corresponding value in the config
        # file, and that expand-hostvars key is normalized and the value
        # from the config comes through even though there is no default.
        self.assertDictEqual(
            {
                'expand_hostvars': False,
                'use_hostnames': True,
                'other_value': 'something',
            },
            ansible_options,
        )

    def test_get_client_config(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        cc = c.get_one(cloud='_test_cloud_regions')

        defaults = {
            'use_hostnames': False,
            'other-value': 'something',
            'force_ipv4': False,
        }
        ansible_options = cc.get_client_config('ansible', defaults)
        assert ansible_options is not None

        # This should show that the default for use_hostnames and force_ipv4
        # above is overridden by the value in the config file defined in
        # base.py
        # It should also show that other-value key is normalized and passed
        # through even though there is no corresponding value in the config
        # file, and that expand-hostvars key is normalized and the value
        # from the config comes through even though there is no default.
        self.assertDictEqual(
            {
                'expand_hostvars': False,
                'use_hostnames': True,
                'other_value': 'something',
                'force_ipv4': True,
            },
            ansible_options,
        )

    def test_register_argparse_cloud(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        parser = argparse.ArgumentParser()
        c.register_argparse_arguments(parser, [])
        opts, _remain = parser.parse_known_args(['--os-cloud', 'foo'])
        self.assertEqual(opts.os_cloud, 'foo')

    def test_env_argparse_precedence(self):
        self.useFixture(
            fixtures.EnvironmentVariable('OS_TENANT_NAME', 'tenants-are-bad')
        )
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        cc = c.get_one(cloud='envvars', argparse=self.options, validate=False)
        self.assertEqual(cc.auth['project_name'], 'project')

    def test_argparse_default_no_token(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        parser = argparse.ArgumentParser()
        c.register_argparse_arguments(parser, [])
        # novaclient will add this
        parser.add_argument('--os-auth-token')
        opts, _remain = parser.parse_known_args()
        cc = c.get_one(cloud='_test_cloud_regions', argparse=opts)
        self.assertEqual(cc.config['auth_type'], 'password')
        self.assertNotIn('token', cc.config['auth'])

    def test_argparse_token(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )

        parser = argparse.ArgumentParser()
        c.register_argparse_arguments(parser, [])
        # novaclient will add this
        parser.add_argument('--os-auth-token')
        opts, _remain = parser.parse_known_args(
            ['--os-auth-token', 'very-bad-things', '--os-auth-type', 'token']
        )
        cc = c.get_one(argparse=opts, validate=False)
        self.assertEqual(cc.config['auth_type'], 'token')
        self.assertEqual(cc.config['auth']['token'], 'very-bad-things')

    def test_argparse_underscores(self):
        c = config.OpenStackConfig(
            config_files=[self.no_yaml],
            vendor_files=[self.no_yaml],
            secure_files=[self.no_yaml],
        )
        parser = argparse.ArgumentParser()
        parser.add_argument('--os_username')
        argv = [
            '--os_username',
            'user',
            '--os_password',
            'pass',
            '--os-auth-url',
            'auth-url',
            '--os-project-name',
            'project',
        ]
        c.register_argparse_arguments(parser, argv=argv)
        opts, _remain = parser.parse_known_args(argv)
        cc = c.get_one(argparse=opts)
        self.assertEqual(cc.config['auth']['username'], 'user')
        self.assertEqual(cc.config['auth']['password'], 'pass')
        self.assertEqual(cc.config['auth']['auth_url'], 'auth-url')

    def test_argparse_action_append_no_underscore(self):
        c = config.OpenStackConfig(
            config_files=[self.no_yaml],
            vendor_files=[self.no_yaml],
            secure_files=[self.no_yaml],
        )
        parser = argparse.ArgumentParser()
        parser.add_argument('--foo', action='append')
        argv = ['--foo', '1', '--foo', '2']
        c.register_argparse_arguments(parser, argv=argv)
        opts, _remain = parser.parse_known_args(argv)
        self.assertEqual(opts.foo, ['1', '2'])

    def test_argparse_underscores_duplicate(self):
        c = config.OpenStackConfig(
            config_files=[self.no_yaml],
            vendor_files=[self.no_yaml],
            secure_files=[self.no_yaml],
        )
        parser = argparse.ArgumentParser()
        parser.add_argument('--os_username')
        argv = [
            '--os_username',
            'user',
            '--os_password',
            'pass',
            '--os-username',
            'user1',
            '--os-password',
            'pass1',
            '--os-auth-url',
            'auth-url',
            '--os-project-name',
            'project',
        ]
        self.assertRaises(
            exceptions.ConfigException,
            c.register_argparse_arguments,
            parser=parser,
            argv=argv,
        )

    def test_register_argparse_bad_plugin(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        parser = argparse.ArgumentParser()
        self.assertRaises(
            exceptions.ConfigException,
            c.register_argparse_arguments,
            parser,
            ['--os-auth-type', 'foo'],
        )

    def test_register_argparse_not_password(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        parser = argparse.ArgumentParser()
        args = [
            '--os-auth-type',
            'v3token',
            '--os-token',
            'some-secret',
        ]
        c.register_argparse_arguments(parser, args)
        opts, _remain = parser.parse_known_args(args)
        self.assertEqual(opts.os_token, 'some-secret')

    def test_register_argparse_password(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        parser = argparse.ArgumentParser()
        args = [
            '--os-password',
            'some-secret',
        ]
        c.register_argparse_arguments(parser, args)
        opts, _remain = parser.parse_known_args(args)
        self.assertEqual(opts.os_password, 'some-secret')
        with self.assertRaises(AttributeError):
            opts.os_token

    def test_register_argparse_service_type(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        parser = argparse.ArgumentParser()
        args = [
            '--os-service-type',
            'network',
            '--os-endpoint-type',
            'admin',
            '--http-timeout',
            '20',
        ]
        c.register_argparse_arguments(parser, args)
        opts, _remain = parser.parse_known_args(args)
        self.assertEqual(opts.os_service_type, 'network')
        self.assertEqual(opts.os_endpoint_type, 'admin')
        self.assertEqual(opts.http_timeout, '20')
        with self.assertRaises(AttributeError):
            opts.os_network_service_type
        cloud = c.get_one(argparse=opts, validate=False)
        self.assertEqual(cloud.config['service_type'], 'network')
        self.assertEqual(cloud.config['interface'], 'admin')
        self.assertEqual(cloud.config['api_timeout'], '20')
        self.assertNotIn('http_timeout', cloud.config)

    def test_register_argparse_network_service_type(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        parser = argparse.ArgumentParser()
        args = [
            '--os-endpoint-type',
            'admin',
            '--network-api-version',
            '4',
        ]
        c.register_argparse_arguments(parser, args, ['network'])
        opts, _remain = parser.parse_known_args(args)
        self.assertEqual(opts.os_service_type, 'network')
        self.assertEqual(opts.os_endpoint_type, 'admin')
        self.assertIsNone(opts.os_network_service_type)
        self.assertIsNone(opts.os_network_api_version)
        self.assertEqual(opts.network_api_version, '4')
        cloud = c.get_one(argparse=opts, validate=False)
        self.assertEqual(cloud.config['service_type'], 'network')
        self.assertEqual(cloud.config['interface'], 'admin')
        self.assertEqual(cloud.config['network_api_version'], '4')
        self.assertNotIn('http_timeout', cloud.config)

    def test_register_argparse_network_service_types(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        parser = argparse.ArgumentParser()
        args = [
            '--os-compute-service-name',
            'cloudServers',
            '--os-network-service-type',
            'badtype',
            '--os-endpoint-type',
            'admin',
            '--network-api-version',
            '4',
        ]
        c.register_argparse_arguments(
            parser, args, ['compute', 'network', 'volume']
        )
        opts, _remain = parser.parse_known_args(args)
        self.assertEqual(opts.os_network_service_type, 'badtype')
        self.assertIsNone(opts.os_compute_service_type)
        self.assertIsNone(opts.os_volume_service_type)
        self.assertEqual(opts.os_service_type, 'compute')
        self.assertEqual(opts.os_compute_service_name, 'cloudServers')
        self.assertEqual(opts.os_endpoint_type, 'admin')
        self.assertIsNone(opts.os_network_api_version)
        self.assertEqual(opts.network_api_version, '4')
        cloud = c.get_one(argparse=opts, validate=False)
        self.assertEqual(cloud.config['service_type'], 'compute')
        self.assertEqual(cloud.config['network_service_type'], 'badtype')
        self.assertEqual(cloud.config['interface'], 'admin')
        self.assertEqual(cloud.config['network_api_version'], '4')
        self.assertNotIn('volume_service_type', cloud.config)
        self.assertNotIn('http_timeout', cloud.config)


class TestConfigPrompt(base.TestCase):
    def setUp(self):
        super().setUp()

        self.args = dict(
            auth_url='http://example.com/v2',
            username='user',
            project_name='project',
            # region_name='region2',
            auth_type='password',
        )

        self.options = argparse.Namespace(**self.args)

    def test_get_one_prompt(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml],
            vendor_files=[self.vendor_yaml],
            pw_func=prompt_for_password,
        )

        # This needs a cloud definition without a password.
        # If this starts failing unexpectedly check that the cloud_yaml
        # and/or vendor_yaml do not have a password in the selected cloud.
        cc = c.get_one(
            cloud='_test_cloud_no_vendor',
            argparse=self.options,
        )
        self.assertEqual('promptpass', cc.auth['password'])


class TestConfigDefault(base.TestCase):
    def setUp(self):
        super().setUp()

        # Reset defaults after each test so that other tests are
        # not affected by any changes.
        self.addCleanup(self._reset_defaults)

    def _reset_defaults(self):
        defaults._defaults = None

    def test_set_no_default(self):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        cc = c.get_one(cloud='_test-cloud_', argparse=None)
        self._assert_cloud_details(cc)
        self.assertEqual('password', cc.auth_type)


class TestMagicFixes(base.TestCase):
    def _test_magic_fixes(self, cloud, expected):
        c = config.OpenStackConfig(
            config_files=[self.cloud_yaml], vendor_files=[self.vendor_yaml]
        )
        result = c.magic_fixes(cloud)
        self.assertEqual(expected, result)

    def test_set_no_default(self):
        cloud = {
            'auth': {},
            'identity_endpoint_type': 'admin',
            'compute_endpoint_type': 'private',
            'endpoint_type': 'public',
            'auth_type': 'v3password',
        }
        expected = {
            'identity_interface': 'admin',
            'compute_interface': 'private',
            'interface': 'public',
            'auth_type': 'v3password',
            'auth': {},
            'networks': [],
        }
        self._test_magic_fixes(cloud, expected)

    def test_project_v2password(self):
        cloud = {
            'auth_type': 'v2password',
            'auth': {
                'project-name': 'my_project_name',
                'project-id': 'my_project_id',
            },
        }
        expected = {
            'auth_type': 'v2password',
            'auth': {
                'tenant_name': 'my_project_name',
                'tenant_id': 'my_project_id',
            },
            'networks': [],
        }
        self._test_magic_fixes(cloud, expected)

    def test_project_password(self):
        cloud = {
            'auth_type': 'password',
            'auth': {
                'project-name': 'my_project_name',
                'project-id': 'my_project_id',
            },
        }
        expected = {
            'auth_type': 'password',
            'auth': {
                'project_name': 'my_project_name',
                'project_id': 'my_project_id',
            },
            'networks': [],
        }
        self._test_magic_fixes(cloud, expected)

    def test_project_conflict_priority(self):
        """The order of priority should be
        1: env or cli settings
        2: setting from 'auth' section of clouds.yaml

        The ordering of #1 is important so that operators can use domain-wide
        inherited credentials in clouds.yaml.
        """

        cloud = {
            'auth_type': 'password',
            'auth': {
                'project_id': 'my_project_id',
            },
        }
        expected = {
            'auth_type': 'password',
            'auth': {
                'project_id': 'my_project_id',
            },
            'networks': [],
        }
        self._test_magic_fixes(cloud, expected)

        cloud = {
            'auth_type': 'password',
            'auth': {
                'project_id': 'my_project_id',
            },
            'project_id': 'different_project_id',
        }
        expected = {
            'auth_type': 'password',
            'auth': {
                'project_id': 'different_project_id',
            },
            'networks': [],
        }
        self._test_magic_fixes(cloud, expected)

    def test_backwards_network_fail(self):
        cloud = {
            'auth': {},
            'external_network': 'public',
            'networks': [
                {'name': 'private', 'routes_externally': False},
            ],
        }
        self.assertRaises(
            exceptions.ConfigException, self._test_magic_fixes, cloud, {}
        )

    def test_backwards_network(self):
        cloud = {
            'auth': {},
            'external_network': 'public',
            'internal_network': 'private',
        }
        expected: dict[str, Any] = {
            'auth': {},
            'auth_type': None,
            'external_network': 'public',
            'internal_network': 'private',
            'networks': [
                {
                    'name': 'public',
                    'routes_externally': True,
                    'nat_destination': False,
                    'default_interface': True,
                },
                {
                    'name': 'private',
                    'routes_externally': False,
                    'nat_destination': True,
                    'default_interface': False,
                },
            ],
        }
        self._test_magic_fixes(cloud, expected)

    def test_normalize_network(self):
        cloud = {'auth': {}, 'networks': [{'name': 'private'}]}
        expected: dict[str, Any] = {
            'auth': {},
            'auth_type': None,
            'networks': [
                {
                    'name': 'private',
                    'routes_externally': False,
                    'nat_destination': False,
                    'default_interface': False,
                    'nat_source': False,
                    'routes_ipv4_externally': False,
                    'routes_ipv6_externally': False,
                },
            ],
        }
        self._test_magic_fixes(cloud, expected)

    def test_single_default_interface(self):
        cloud = {
            'auth': {},
            'networks': [
                {'name': 'blue', 'default_interface': True},
                {'name': 'purple', 'default_interface': True},
            ],
        }
        self.assertRaises(
            exceptions.ConfigException, self._test_magic_fixes, cloud, {}
        )

    def test_token_auth(self):
        expected = {
            "auth_type": "v3token",
            "auth": {
                "token": "my_token",
            },
            'networks': [],
        }

        cloud = {
            "auth_type": "v3token",
            "auth": {
                "token": "my_token",
            },
        }
        self._test_magic_fixes(cloud, expected)

        cloud = {
            "auth_type": "v3token",
            "auth": {
                "auth_token": "my_token",
            },
        }
        self._test_magic_fixes(cloud, expected)

        cloud = {
            "auth_type": "v3token",
            "auth": {
                "auth-token": "my_token",
            },
        }
        self._test_magic_fixes(cloud, expected)

        cloud = {
            "auth_type": "v3token",
            "auth": {},
            "token": "my_token",
        }
        self._test_magic_fixes(cloud, expected)

        cloud = {
            "auth_type": "v3token",
            "auth": {},
            "auth_token": "my_token",
        }
        self._test_magic_fixes(cloud, expected)

        cloud = {
            "auth_type": "v3token",
            "auth": {},
            "auth-token": "my_token",
        }
        self._test_magic_fixes(cloud, expected)

        # test priority
        cloud = {
            "auth_type": "v3token",
            "auth": {
                "token": "I will be ignored",
            },
            "token": "my_token",
        }
        self._test_magic_fixes(cloud, expected)

    def test_passcode(self):
        cloud = {
            "auth": {},
            "passcode": "totp",
        }
        expected: dict[str, Any] = {
            "auth": {
                "passcode": "totp",
            },
            'auth_type': None,
            'networks': [],
        }
        self._test_magic_fixes(cloud, expected)

    def test_endpoint_type_to_interface(self):
        cloud = {
            'auth': {},
            "endpoint_type": "public",
            "foo_endpoint_type": "internal",
        }
        expected: dict[str, Any] = {
            "auth": {},
            'auth_type': None,
            'networks': [],
            "interface": "public",
            "foo_interface": "internal",
        }
        self._test_magic_fixes(cloud, expected)

    def test_bool_keys(self):
        cloud: dict[str, Any] = {"auth": {}}
        cloud.update({k: "True" for k in loader.BOOL_KEYS})
        expected: dict[str, Any] = {
            "auth_type": None,
            "auth": {},
            "networks": [],
        }
        expected.update({k: True for k in loader.BOOL_KEYS})
        self._test_magic_fixes(cloud, expected)

    def test_csv_keys(self):
        cloud: dict[str, Any] = {"auth": {}}
        cloud.update({k: "spam,ham" for k in loader.CSV_KEYS})
        expected: dict[str, Any] = {
            "auth_type": None,
            "auth": {},
            "networks": [],
        }
        expected.update({k: ["spam", "ham"] for k in loader.CSV_KEYS})
        self._test_magic_fixes(cloud, expected)

    def test_auth_url_formatting(self):
        cloud = {
            "auth": {
                "auth_url": "https://my.cloud/{region_id}",
            },
            "region_id": "RegionOne",
        }
        expected: dict[str, Any] = {
            "auth_type": None,
            "auth": {
                "auth_url": "https://my.cloud/RegionOne",
            },
            "region_id": "RegionOne",
            "networks": [],
        }
        self._test_magic_fixes(cloud, expected)
