Put env vars into their own cloud config

The semantics around mixing environment variables and config file values
are confusing at best and no reasonable usecase has been expressed as to
why doing so is desirable.

Move the logic around environment variable processing to always provide
an "envvars" cloud if any envvars are set. The cloud will only exist in
the presence of OS_ env vars.

get_one_cloud() will default to returning the envvars cloud if it
exists.

Change-Id: I6c3a54997c3278feedfdf93cc4d1e74b6235700a
Closes-Bug: #1439927
This commit is contained in:
Monty Taylor 2015-04-11 08:27:05 -04:00
parent 2ac9258563
commit 7e682d3bf0
7 changed files with 181 additions and 109 deletions

View File

@ -16,14 +16,13 @@ os-client-config honors all of the normal `OS_*` variables. It does not
provide backwards compatibility to service-specific variables such as
`NOVA_USERNAME`.
If you have environment variables and no config files, os-client-config
will produce a cloud config object named "openstack" containing your
values from the environment.
If you have OpenStack environment variables seet and no config files,
os-client-config will produce a cloud config object named "envvars" containing
your values from the environment.
Service specific settings, like the nova service type, are set with the
default service type as a prefix. For instance, to set a special service_type
for trove (because you're using Rackspace) set:
::
for trove set::
export OS_DATABASE_SERVICE_TYPE=rax:database
@ -40,7 +39,7 @@ locations:
The first file found wins.
The keys are all of the keys you'd expect from `OS_*` - except lower case
and without the OS prefix. So, username is set with `username`.
and without the OS prefix. So, region name is set with `region_name`.
Service specific settings, like the nova service type, are set with the
default service type as a prefix. For instance, to set a special service_type
@ -119,6 +118,10 @@ behaviors:
* `cache.max_age` and nothing else gets you memory cache.
* Otherwise, `cache.class` and `cache.arguments` are passed in
`os-client-config` does not actually cache anything itself, but it collects
and presents the cache information so that your various applications that
are connecting to OpenStack can share a cache should you desire.
::
cache:

View File

@ -15,7 +15,7 @@
class CloudConfig(object):
def __init__(self, name, region, config):
self.name = name or 'openstack'
self.name = name
self.region = region
self.config = config

View File

@ -54,10 +54,12 @@ def get_boolean(value):
def _get_os_environ():
ret = dict(defaults._defaults)
for (k, v) in os.environ.items():
if k.startswith('OS_'):
newkey = k[3:].lower()
ret[newkey] = v
environkeys = [k for k in os.environ.keys() if k.startswith('OS_')]
if not environkeys:
return None
for k in environkeys:
newkey = k[3:].lower()
ret[newkey] = os.environ[k]
return ret
@ -80,7 +82,7 @@ class OpenStackConfig(object):
self._config_files = config_files or CONFIG_FILES
self._vendor_files = vendor_files or VENDOR_FILES
self.defaults = _get_os_environ()
self.defaults = dict(defaults._defaults)
# use a config file if it exists where expected
self.cloud_config = self._load_config_file()
@ -88,6 +90,14 @@ class OpenStackConfig(object):
self.cloud_config = dict(
clouds=dict(openstack=dict(self.defaults)))
envvars = _get_os_environ()
if envvars:
if 'envvars' in self.cloud_config['clouds']:
raise exceptions.OpenStackConfigException(
'clouds.yaml defines a cloud named envvars, and OS_'
' env vars are set')
self.cloud_config['clouds']['envvars'] = envvars
self._cache_max_age = None
self._cache_path = CACHE_PATH
self._cache_class = 'dogpile.cache.null'
@ -109,6 +119,7 @@ class OpenStackConfig(object):
if os.path.exists(path):
with open(path, 'r') as f:
return yaml.safe_load(f)
return dict(clouds=dict())
def _load_vendor_file(self):
for path in self._vendor_files:
@ -135,7 +146,7 @@ class OpenStackConfig(object):
# No region configured
return ''
def _get_region(self, cloud):
def _get_region(self, cloud=None):
return self._get_regions(cloud).split(',')[0]
def _get_cloud_sections(self):
@ -152,7 +163,7 @@ class OpenStackConfig(object):
our_cloud = self.cloud_config['clouds'].get(name, dict())
# Get the defaults (including env vars) first
# Get the defaults
cloud.update(self.defaults)
# yes, I know the next line looks silly
@ -197,7 +208,8 @@ class OpenStackConfig(object):
if key in cloud['auth']:
target = cloud['auth'][key]
del cloud['auth'][key]
cloud['auth'][target_key] = target
if target:
cloud['auth'][target_key] = target
return cloud
def _fix_backwards_auth_plugin(self, cloud):
@ -321,6 +333,9 @@ class OpenStackConfig(object):
:param kwargs: Additional configuration options
"""
if cloud is None and 'envvars' in self._get_cloud_sections():
cloud = 'envvars'
args = self._fix_args(kwargs, argparse=argparse)
if 'region_name' not in args or args['region_name'] is None:

View File

@ -15,9 +15,87 @@
# License for the specific language governing permissions and limitations
# under the License.
import os
import tempfile
from os_client_config import cloud_config
import extras
import fixtures
from oslotest import base
import yaml
VENDOR_CONF = {
'public-clouds': {
'_test_cloud_in_our_cloud': {
'auth': {
'username': 'testotheruser',
'project_name': 'testproject',
},
},
}
}
USER_CONF = {
'clouds': {
'_test_cloud_': {
'cloud': '_test_cloud_in_our_cloud',
'auth': {
'username': 'testuser',
'password': 'testpass',
},
'region_name': 'test-region',
},
'_test_cloud_no_vendor': {
'cloud': '_test_non_existant_cloud',
'auth': {
'username': 'testuser',
'password': 'testpass',
'project_name': 'testproject',
},
'region_name': 'test-region',
},
},
'cache': {'max_age': 1},
}
def _write_yaml(obj):
# Assume NestedTempfile so we don't have to cleanup
with tempfile.NamedTemporaryFile(delete=False) as obj_yaml:
obj_yaml.write(yaml.safe_dump(obj).encode('utf-8'))
return obj_yaml.name
class TestCase(base.BaseTestCase):
"""Test case base class for all unit tests."""
def setUp(self):
super(TestCase, self).setUp()
self.useFixture(fixtures.NestedTempfile())
conf = dict(USER_CONF)
tdir = self.useFixture(fixtures.TempDir())
conf['cache']['path'] = tdir.path
self.cloud_yaml = _write_yaml(conf)
self.vendor_yaml = _write_yaml(VENDOR_CONF)
# Isolate the test runs from the environment
# Do this as two loops because you can't modify the dict in a loop
# over the dict in 3.4
keys_to_isolate = []
for env in os.environ.keys():
if env.startswith('OS_'):
keys_to_isolate.append(env)
for env in keys_to_isolate:
self.useFixture(fixtures.EnvironmentVariable(env))
def _assert_cloud_details(self, cc):
self.assertIsInstance(cc, cloud_config.CloudConfig)
self.assertTrue(extras.safe_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('testproject', cc.auth['project_name'])

View File

@ -12,66 +12,12 @@
# License for the specific language governing permissions and limitations
# under the License.
import tempfile
import extras
import fixtures
import testtools
import yaml
from os_client_config import cloud_config
from os_client_config import config
VENDOR_CONF = {
'public-clouds': {
'_test_cloud_in_our_cloud': {
'auth': {
'username': 'testotheruser',
'project_name': 'testproject',
},
},
}
}
USER_CONF = {
'clouds': {
'_test_cloud_': {
'cloud': '_test_cloud_in_our_cloud',
'auth': {
'username': 'testuser',
'password': 'testpass',
},
'region_name': 'test-region',
},
'_test_cloud_no_vendor': {
'cloud': '_test_non_existant_cloud',
'auth': {
'username': 'testuser',
'password': 'testpass',
'project_name': 'testproject',
},
'region_name': 'test-region',
},
},
'cache': {'max_age': 1},
}
from os_client_config.tests import base
def _write_yaml(obj):
# Assume NestedTempfile so we don't have to cleanup
with tempfile.NamedTemporaryFile(delete=False) as obj_yaml:
obj_yaml.write(yaml.safe_dump(obj).encode('utf-8'))
return obj_yaml.name
class TestConfig(testtools.TestCase):
def setUp(self):
super(TestConfig, self).setUp()
self.useFixture(fixtures.NestedTempfile())
conf = dict(USER_CONF)
tdir = self.useFixture(fixtures.TempDir())
conf['cache']['path'] = tdir.path
self.cloud_yaml = _write_yaml(conf)
self.vendor_yaml = _write_yaml(VENDOR_CONF)
class TestConfig(base.TestCase):
def test_get_one_cloud(self):
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
@ -90,12 +36,3 @@ class TestConfig(testtools.TestCase):
self._assert_cloud_details(cc)
cc = c.get_one_cloud('_test_cloud_no_vendor')
self._assert_cloud_details(cc)
def _assert_cloud_details(self, cc):
self.assertIsInstance(cc, cloud_config.CloudConfig)
self.assertTrue(extras.safe_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('testproject', cc.auth['project_name'])

View File

@ -0,0 +1,67 @@
# 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.
from os_client_config import cloud_config
from os_client_config import config
from os_client_config import exceptions
from os_client_config.tests import base
import fixtures
class TestConfig(base.TestCase):
def test_get_one_cloud(self):
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml])
self.assertIsInstance(c.get_one_cloud(), cloud_config.CloudConfig)
def test_no_environ(self):
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml])
self.assertRaises(
exceptions.OpenStackConfigException, c.get_one_cloud, 'envvars')
def test_environ_exists(self):
self.useFixture(
fixtures.EnvironmentVariable('OS_AUTH_URL', 'https://example.com'))
self.useFixture(
fixtures.EnvironmentVariable('OS_USERNAME', 'testuser'))
self.useFixture(
fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'testproject'))
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml])
cc = c.get_one_cloud('envvars')
self._assert_cloud_details(cc)
self.assertNotIn('auth_url', cc.config)
self.assertIn('auth_url', cc.config['auth'])
self.assertNotIn('auth_url', cc.config)
cc = c.get_one_cloud('_test_cloud_')
self._assert_cloud_details(cc)
cc = c.get_one_cloud('_test_cloud_no_vendor')
self._assert_cloud_details(cc)
def test_get_one_cloud_with_config_files(self):
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_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_cloud('_test_cloud_')
self._assert_cloud_details(cc)
cc = c.get_one_cloud('_test_cloud_no_vendor')
self._assert_cloud_details(cc)

View File

@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
# 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.
"""
test_os_client_config
----------------------------------
Tests for `os_client_config` module.
"""
from os_client_config.tests import base
class TestOs_client_config(base.TestCase):
def test_something(self):
pass