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:
		| @@ -84,7 +84,7 @@ An example config file is probably helpful: | ||||
|  | ||||
|   clouds: | ||||
|     mtvexx: | ||||
|       profile: vexxhost | ||||
|       profile: https://vexxhost.com | ||||
|       auth: | ||||
|         username: mordred@inaugust.com | ||||
|         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 | ||||
| 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 | ||||
| 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 | ||||
|   | ||||
| @@ -462,6 +462,11 @@ class OpenStackConfig(object): | ||||
|         else: | ||||
|             profile_data = vendors.get_profile(profile_name) | ||||
|             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') | ||||
|                 message = profile_data.pop('message', '') | ||||
|                 if status == 'deprecated': | ||||
|   | ||||
							
								
								
									
										40
									
								
								openstack/config/vendors/__init__.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										40
									
								
								openstack/config/vendors/__init__.py
									
									
									
									
										vendored
									
									
								
							| @@ -16,10 +16,16 @@ import glob | ||||
| import json | ||||
| import os | ||||
|  | ||||
| from six.moves import urllib | ||||
| import requests | ||||
| import yaml | ||||
|  | ||||
| from openstack.config import _util | ||||
| from openstack import exceptions | ||||
|  | ||||
| _VENDORS_PATH = os.path.dirname(os.path.realpath(__file__)) | ||||
| _VENDOR_DEFAULTS = {} | ||||
| _WELL_KNOWN_PATH = "{scheme}://{netloc}/.well-known/openstack/api" | ||||
|  | ||||
|  | ||||
| def _get_vendor_defaults(): | ||||
| @@ -40,3 +46,37 @@ def get_profile(profile_name): | ||||
|     vendor_defaults = _get_vendor_defaults() | ||||
|     if profile_name in vendor_defaults: | ||||
|         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 | ||||
|   | ||||
							
								
								
									
										14
									
								
								openstack/config/vendors/vexxhost.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								openstack/config/vendors/vexxhost.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,18 +1,6 @@ | ||||
| { | ||||
|   "name": "vexxhost", | ||||
|   "profile": { | ||||
|     "auth_type": "v3password", | ||||
|     "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 | ||||
|     "profile": "https://vexxhost.com" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -96,6 +96,39 @@ class TestConfig(base.TestCase): | ||||
|         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'}) | ||||
|   | ||||
							
								
								
									
										7
									
								
								releasenotes/notes/remote-profile-100218d08b25019d.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								releasenotes/notes/remote-profile-100218d08b25019d.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -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. | ||||
		Reference in New Issue
	
	Block a user
	 Monty Taylor
					Monty Taylor