Support remote vendor profiles

Maintaining vendor profile information inside of the sdk is great and
all, but it winds up being problematic from a release management
perspective since the data should always be current but people may not
be in a position to upgrade their version of openstacksdk. It's also a
less pleasing experience for people running or using clouds that the
openstacksdk developers don't know about.

RFC 5785 defines a scheme for serving data at well known URL locations.
Use it to allow specifying a profile by URL instead of by name. For
instance, for the cloud Example, a user could list profile:
https://example.com and openstacksdk will fetch the profile from
https://example.com/.well-known/openstack/api. It should be noted that
sub-urls are not allowed, so it MUST be served off of a root domain.
(That is, https://example.com/cloud is not allowed by the RFC)

Clouds are not required to serve one of these.

Change-Id: I884f62b35da5f29aa6e72e2dde9b8ec3ef48ad60
This commit is contained in:
Monty Taylor 2018-11-07 09:49:07 -06:00
parent 1da367b537
commit 3d08643c43
No known key found for this signature in database
GPG Key ID: 7BAE94BC7141A594
6 changed files with 89 additions and 15 deletions
doc/source/user/config
openstack
releasenotes/notes

@ -84,7 +84,7 @@ An example config file is probably helpful:
clouds: clouds:
mtvexx: mtvexx:
profile: vexxhost profile: https://vexxhost.com
auth: auth:
username: mordred@inaugust.com username: mordred@inaugust.com
password: XXXXXXXXX password: XXXXXXXXX
@ -111,7 +111,8 @@ An example config file is probably helpful:
You may note a few things. First, since `auth_url` settings are silly You may note a few things. First, since `auth_url` settings are silly
and embarrassingly ugly, known cloud vendor profile information is included and and embarrassingly ugly, known cloud vendor profile information is included and
may be referenced by name. One of the benefits of that is that `auth_url` may be referenced by name or by base URL to the cloud in question if the
cloud serves a vendor profile. One of the benefits of that is that `auth_url`
isn't the only thing the vendor defaults contain. For instance, since isn't the only thing the vendor defaults contain. For instance, since
Rackspace lists `rax:database` as the service type for trove, `openstacksdk` Rackspace lists `rax:database` as the service type for trove, `openstacksdk`
knows that so that you don't have to. In case the cloud vendor profile is not knows that so that you don't have to. In case the cloud vendor profile is not

@ -462,6 +462,11 @@ class OpenStackConfig(object):
else: else:
profile_data = vendors.get_profile(profile_name) profile_data = vendors.get_profile(profile_name)
if profile_data: if profile_data:
nested_profile = profile_data.pop('profile', None)
if nested_profile:
nested_profile_data = vendors.get_profile(nested_profile)
if nested_profile_data:
profile_data = nested_profile_data
status = profile_data.pop('status', 'active') status = profile_data.pop('status', 'active')
message = profile_data.pop('message', '') message = profile_data.pop('message', '')
if status == 'deprecated': if status == 'deprecated':

@ -16,10 +16,16 @@ import glob
import json import json
import os import os
from six.moves import urllib
import requests
import yaml import yaml
from openstack.config import _util
from openstack import exceptions
_VENDORS_PATH = os.path.dirname(os.path.realpath(__file__)) _VENDORS_PATH = os.path.dirname(os.path.realpath(__file__))
_VENDOR_DEFAULTS = {} _VENDOR_DEFAULTS = {}
_WELL_KNOWN_PATH = "{scheme}://{netloc}/.well-known/openstack/api"
def _get_vendor_defaults(): def _get_vendor_defaults():
@ -40,3 +46,37 @@ def get_profile(profile_name):
vendor_defaults = _get_vendor_defaults() vendor_defaults = _get_vendor_defaults()
if profile_name in vendor_defaults: if profile_name in vendor_defaults:
return vendor_defaults[profile_name].copy() return vendor_defaults[profile_name].copy()
profile_url = urllib.parse.urlparse(profile_name)
if not profile_url.netloc:
# This isn't a url, and we already don't have it.
return
well_known_url = _WELL_KNOWN_PATH.format(
scheme=profile_url.scheme,
netloc=profile_url.netloc,
)
response = requests.get(well_known_url)
if not response.ok:
raise exceptions.ConfigException(
"{profile_name} is a remote profile that could not be fetched:"
" ({status_code) {reason}".format(
profile_name=profile_name,
status_code=response.status_code,
reason=response.reason))
vendor_defaults[profile_name] = None
return
vendor_data = response.json()
name = vendor_data['name']
# Merge named and url cloud config, but make named config override the
# config from the cloud so that we can supply local overrides if needed.
profile = _util.merge_clouds(
vendor_data['profile'],
vendor_defaults.get(name, {}))
# If there is (or was) a profile listed in a named config profile, it
# might still be here. We just merged in content from a URL though, so
# pop the key to prevent doing it again in the future.
profile.pop('profile', None)
# Save the data under both names so we don't reprocess this, no matter
# how we're called.
vendor_defaults[profile_name] = profile
vendor_defaults[name] = profile
return profile

@ -1,18 +1,6 @@
{ {
"name": "vexxhost", "name": "vexxhost",
"profile": { "profile": {
"auth_type": "v3password", "profile": "https://vexxhost.com"
"auth": {
"auth_url": "https://auth.vexxhost.net/v3"
},
"regions": [
"ca-ymq-1",
"sjc1"
],
"dns_api_version": "1",
"identity_api_version": "3",
"image_format": "raw",
"floating_ip_source": "None",
"requires_floating_ip": false
} }
} }

@ -96,6 +96,39 @@ class TestConfig(base.TestCase):
cc = c.get_one() cc = c.get_one()
self.assertEqual(cc.name, 'single') 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): def test_get_one_auth_defaults(self):
c = config.OpenStackConfig(config_files=[self.cloud_yaml]) c = config.OpenStackConfig(config_files=[self.cloud_yaml])
cc = c.get_one(cloud='_test-cloud_', auth={'username': 'user'}) cc = c.get_one(cloud='_test-cloud_', auth={'username': 'user'})

@ -0,0 +1,7 @@
---
features:
- |
Vendor profiles can now be fetched from an RFC 5785 compliant URL on a
cloud, namely, ``https://example.com/.well-known/openstack/api``. A cloud
can manage their own vendor profile and serve it from that URL, allowing
a user to simply list ``https://example.com`` as the profile name.