Allow templated cell_mapping URLs
The way we store DB and MQ URLs in the API database causes issues for some deployments (and deployment tools) which want to use per-host credentials or remote hostnames. Since all the URLs loaded from the database are the same on all systems, this becomes very difficult and some have even resorted to using client-based aliasing underneath Nova and just providing URLs that reference those aliases. This makes our CellMapping object load the URLs out of the database, and apply variable substitution from the CONF-resident base URLs for any fields provided. Such functionality will let operators define per-host credentials in [database]/connection, for example, and have those applied to the database_connection URLs loaded from CellMapping records. Change-Id: Iab296c27bcd56162e2efca5fb232cae0aea1160e
This commit is contained in:
parent
7b41bac2f1
commit
50658eee4f
@ -337,6 +337,100 @@ instances. Any time you add more compute hosts to a cell, you need to
|
||||
re-run this command to map them from the top-level so they can be
|
||||
utilized.
|
||||
|
||||
Template URLs in Cell Mappings
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Starting in the Rocky release, the URLs provided in the cell mappings
|
||||
for ``--database_connection`` and ``--transport-url`` can contain
|
||||
variables which are evaluated each time they are loaded from the
|
||||
database, and the values of which are taken from the corresponding
|
||||
base options in the host's configuration file. The base URL is parsed
|
||||
and the following elements may be substituted into the cell mapping
|
||||
URL (using ``rabbit://bob:s3kret@myhost:123/nova?sync=true#extra``):
|
||||
|
||||
.. list-table:: Cell Mapping URL Variables
|
||||
:header-rows: 1
|
||||
:widths: 15, 50, 15
|
||||
|
||||
* - Variable
|
||||
- Meaning
|
||||
- Part of example URL
|
||||
* - ``scheme``
|
||||
- The part before the `://`
|
||||
- ``rabbit``
|
||||
* - ``username``
|
||||
- The username part of the credentials
|
||||
- ``bob``
|
||||
* - ``password``
|
||||
- The password part of the credentials
|
||||
- ``s3kret``
|
||||
* - ``hostname``
|
||||
- The hostname or address
|
||||
- ``myhost``
|
||||
* - ``port``
|
||||
- The port number (must be specified)
|
||||
- ``123``
|
||||
* - ``path``
|
||||
- The "path" part of the URL (without leading slash)
|
||||
- ``nova``
|
||||
* - ``query``
|
||||
- The full query string arguments (without leading question mark)
|
||||
- ``sync=true``
|
||||
* - ``fragment``
|
||||
- Everything after the first hash mark
|
||||
- ``extra``
|
||||
|
||||
Variables are provided in curly brackets, like ``{username}``. A simple template
|
||||
of ``rabbit://{username}:{password}@otherhost/{path}`` will generate a full URL
|
||||
of ``rabbit://bob:s3kret@otherhost/nova`` when used with the above example.
|
||||
|
||||
.. note:: The ``[database]/connection`` and
|
||||
``[DEFAULT]/transport_url`` values are not reloaded from the
|
||||
configuration file during a SIGHUP, which means that a full service
|
||||
restart will be required to notice changes in a cell mapping record
|
||||
if variables are changed.
|
||||
|
||||
.. note:: The ``[DEFAULT]/transport_url`` option can contain an
|
||||
extended syntax for the "netloc" part of the url
|
||||
(i.e. `userA:passwordA@hostA:portA,userB:passwordB:hostB:portB`). In this
|
||||
case, substitions of the form ``username1``, ``username2``, etc will be
|
||||
honored and can be used in the template URL.
|
||||
|
||||
The templating of these URLs may be helpful in order to provide each service host
|
||||
with its own credentials for, say, the database. Without templating, all hosts
|
||||
will use the same URL (and thus credentials) for accessing services like the
|
||||
database and message queue. By using a URL with a template that results in the
|
||||
credentials being taken from the host-local configuration file, each host will
|
||||
use different values for those connections.
|
||||
|
||||
Assuming you have two service hosts that are normally configured with the cell0
|
||||
database as their primary connection, their (abbreviated) configurations would
|
||||
look like this::
|
||||
|
||||
[database]
|
||||
connection = mysql+pymysql://service1:foo@myapidbhost/nova_cell0
|
||||
|
||||
and::
|
||||
|
||||
[database]
|
||||
connection = mysql+pymysql://service2:bar@myapidbhost/nova_cell0
|
||||
|
||||
Without cell mapping template URLs, they would still use the same credentials
|
||||
(as stored in the mapping) to connect to the cell databases. However, consider
|
||||
template URLs like the following::
|
||||
|
||||
mysql+pymysql://{username}:{password}@mycell1dbhost/nova
|
||||
|
||||
and::
|
||||
|
||||
mysql+pymysql://{username}:{password}@mycell2dbhost/nova
|
||||
|
||||
Using the first service and cell1 mapping, the calculated URL that will actually
|
||||
be used for connecting to that database will be::
|
||||
|
||||
mysql+pymysql://service1:foo@mycell1dbhost/nova
|
||||
|
||||
|
||||
References
|
||||
~~~~~~~~~~
|
||||
|
||||
|
@ -10,18 +10,60 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import versionutils
|
||||
import six.moves.urllib.parse as urlparse
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.sql.expression import asc
|
||||
from sqlalchemy.sql import false
|
||||
from sqlalchemy.sql import true
|
||||
|
||||
import nova.conf
|
||||
from nova.db.sqlalchemy import api as db_api
|
||||
from nova.db.sqlalchemy import api_models
|
||||
from nova import exception
|
||||
from nova.objects import base
|
||||
from nova.objects import fields
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _parse_netloc(netloc):
|
||||
"""Parse a user:pass@host:port and return a dict suitable for formatting
|
||||
a cell mapping template.
|
||||
"""
|
||||
these = {
|
||||
'username': None,
|
||||
'password': None,
|
||||
'hostname': None,
|
||||
'port': None,
|
||||
}
|
||||
|
||||
if '@' in netloc:
|
||||
userpass, hostport = netloc.split('@', 1)
|
||||
else:
|
||||
hostport = netloc
|
||||
userpass = ''
|
||||
|
||||
if hostport.startswith('['):
|
||||
host_end = hostport.find(']')
|
||||
if host_end < 0:
|
||||
raise ValueError('Invalid IPv6 URL')
|
||||
these['hostname'] = hostport[1:host_end]
|
||||
these['port'] = hostport[host_end + 1:]
|
||||
elif ':' in hostport:
|
||||
these['hostname'], these['port'] = hostport.split(':', 1)
|
||||
else:
|
||||
these['hostname'] = hostport
|
||||
|
||||
if ':' in userpass:
|
||||
these['username'], these['password'] = userpass.split(':', 1)
|
||||
else:
|
||||
these['username'] = userpass
|
||||
|
||||
return these
|
||||
|
||||
|
||||
@base.NovaObjectRegistry.register
|
||||
class CellMapping(base.NovaTimestampObject, base.NovaObject):
|
||||
@ -54,10 +96,72 @@ class CellMapping(base.NovaTimestampObject, base.NovaObject):
|
||||
else:
|
||||
return self.uuid
|
||||
|
||||
@staticmethod
|
||||
def _format_url(url, default):
|
||||
default_url = urlparse.urlparse(default)
|
||||
|
||||
subs = {
|
||||
'username': default_url.username,
|
||||
'password': default_url.password,
|
||||
'hostname': default_url.hostname,
|
||||
'port': default_url.port,
|
||||
'scheme': default_url.scheme,
|
||||
'query': default_url.query,
|
||||
'fragment': default_url.fragment,
|
||||
'path': default_url.path.lstrip('/'),
|
||||
}
|
||||
|
||||
# NOTE(danms): oslo.messaging has an extended format for the URL
|
||||
# which we need to support:
|
||||
# scheme://user:pass@host:port[,user1:pass@host1:port, ...]/path
|
||||
# Encode these values, if they exist, as indexed keys like
|
||||
# username1, password1, hostname1, port1.
|
||||
if ',' in default_url.netloc:
|
||||
netlocs = default_url.netloc.split(',')
|
||||
index = 0
|
||||
for netloc in netlocs:
|
||||
index += 1
|
||||
these = _parse_netloc(netloc)
|
||||
for key in these:
|
||||
subs['%s%i' % (key, index)] = these[key]
|
||||
|
||||
return url.format(**subs)
|
||||
|
||||
@staticmethod
|
||||
def _format_db_url(url):
|
||||
if CONF.database.connection is None and '{' in url:
|
||||
LOG.error('Cell mapping database_connection is a template, but '
|
||||
'[database]/connection is not set')
|
||||
return url
|
||||
try:
|
||||
return CellMapping._format_url(url, CONF.database.connection)
|
||||
except Exception:
|
||||
LOG.exception('Failed to parse [database]/connection to '
|
||||
'format cell mapping')
|
||||
return url
|
||||
|
||||
@staticmethod
|
||||
def _format_mq_url(url):
|
||||
if CONF.transport_url is None and '{' in url:
|
||||
LOG.error('Cell mapping transport_url is a template, but '
|
||||
'[DEFAULT]/transport_url is not set')
|
||||
return url
|
||||
try:
|
||||
return CellMapping._format_url(url, CONF.transport_url)
|
||||
except Exception:
|
||||
LOG.exception('Failed to parse [DEFAULT]/transport_url to '
|
||||
'format cell mapping')
|
||||
return url
|
||||
|
||||
@staticmethod
|
||||
def _from_db_object(context, cell_mapping, db_cell_mapping):
|
||||
for key in cell_mapping.fields:
|
||||
setattr(cell_mapping, key, db_cell_mapping[key])
|
||||
val = db_cell_mapping[key]
|
||||
if key == 'database_connection':
|
||||
val = cell_mapping._format_db_url(val)
|
||||
elif key == 'transport_url':
|
||||
val = cell_mapping._format_mq_url(val)
|
||||
setattr(cell_mapping, key, val)
|
||||
cell_mapping.obj_reset_changes()
|
||||
cell_mapping._context = context
|
||||
return cell_mapping
|
||||
|
@ -128,6 +128,125 @@ class _TestCellMappingObject(object):
|
||||
self.assertEqual(uuids.cell, obj.uuid)
|
||||
self.assertNotIn('disabled', obj)
|
||||
|
||||
@mock.patch.object(cell_mapping.CellMapping, '_get_by_uuid_from_db')
|
||||
def test_formatted_db_url(self, mock_get):
|
||||
url = 'sqlite://bob:s3kret@localhost:123/nova?munchies=doritos#baz'
|
||||
varurl = ('{scheme}://not{username}:{password}@'
|
||||
'{hostname}:1{port}/{path}?{query}&flavor=coolranch'
|
||||
'#{fragment}')
|
||||
self.flags(connection=url, group='database')
|
||||
db_mapping = get_db_mapping(database_connection=varurl)
|
||||
mock_get.return_value = db_mapping
|
||||
mapping_obj = objects.CellMapping().get_by_uuid(self.context,
|
||||
db_mapping['uuid'])
|
||||
self.assertEqual(('sqlite://notbob:s3kret@localhost:1123/nova?'
|
||||
'munchies=doritos&flavor=coolranch#baz'),
|
||||
mapping_obj.database_connection)
|
||||
|
||||
@mock.patch.object(cell_mapping.CellMapping, '_get_by_uuid_from_db')
|
||||
def test_formatted_mq_url(self, mock_get):
|
||||
url = 'rabbit://bob:s3kret@localhost:123/nova?munchies=doritos#baz'
|
||||
varurl = ('{scheme}://not{username}:{password}@'
|
||||
'{hostname}:1{port}/{path}?{query}&flavor=coolranch'
|
||||
'#{fragment}')
|
||||
self.flags(transport_url=url)
|
||||
db_mapping = get_db_mapping(transport_url=varurl)
|
||||
mock_get.return_value = db_mapping
|
||||
mapping_obj = objects.CellMapping().get_by_uuid(self.context,
|
||||
db_mapping['uuid'])
|
||||
self.assertEqual(('rabbit://notbob:s3kret@localhost:1123/nova?'
|
||||
'munchies=doritos&flavor=coolranch#baz'),
|
||||
mapping_obj.transport_url)
|
||||
|
||||
@mock.patch.object(cell_mapping.CellMapping, '_get_by_uuid_from_db')
|
||||
def test_formatted_mq_url_multi_netloc1(self, mock_get):
|
||||
# Multiple netlocs, each with all parameters
|
||||
url = ('rabbit://alice:n0ts3kret@otherhost:456,'
|
||||
'bob:s3kret@localhost:123'
|
||||
'/nova?munchies=doritos#baz')
|
||||
varurl = ('{scheme}://not{username2}:{password1}@'
|
||||
'{hostname2}:1{port1}/{path}?{query}&flavor=coolranch'
|
||||
'#{fragment}')
|
||||
self.flags(transport_url=url)
|
||||
db_mapping = get_db_mapping(transport_url=varurl)
|
||||
mock_get.return_value = db_mapping
|
||||
mapping_obj = objects.CellMapping().get_by_uuid(self.context,
|
||||
db_mapping['uuid'])
|
||||
self.assertEqual(('rabbit://notbob:n0ts3kret@localhost:1456/nova?'
|
||||
'munchies=doritos&flavor=coolranch#baz'),
|
||||
mapping_obj.transport_url)
|
||||
|
||||
@mock.patch.object(cell_mapping.CellMapping, '_get_by_uuid_from_db')
|
||||
def test_formatted_mq_url_multi_netloc1_but_ipv6(self, mock_get):
|
||||
# Multiple netlocs, each with all parameters
|
||||
url = ('rabbit://alice:n0ts3kret@otherhost:456,'
|
||||
'bob:s3kret@[1:2::7]:123'
|
||||
'/nova?munchies=doritos#baz')
|
||||
varurl = ('{scheme}://not{username2}:{password1}@'
|
||||
'[{hostname2}]:1{port1}/{path}?{query}&flavor=coolranch'
|
||||
'#{fragment}')
|
||||
self.flags(transport_url=url)
|
||||
db_mapping = get_db_mapping(transport_url=varurl)
|
||||
mock_get.return_value = db_mapping
|
||||
mapping_obj = objects.CellMapping().get_by_uuid(self.context,
|
||||
db_mapping['uuid'])
|
||||
self.assertEqual(('rabbit://notbob:n0ts3kret@[1:2::7]:1456/nova?'
|
||||
'munchies=doritos&flavor=coolranch#baz'),
|
||||
mapping_obj.transport_url)
|
||||
|
||||
@mock.patch.object(cell_mapping.CellMapping, '_get_by_uuid_from_db')
|
||||
def test_formatted_mq_url_multi_netloc2(self, mock_get):
|
||||
# Multiple netlocs, without optional password and port
|
||||
url = ('rabbit://alice@otherhost,'
|
||||
'bob:s3kret@localhost:123'
|
||||
'/nova?munchies=doritos#baz')
|
||||
varurl = ('{scheme}://not{username1}:{password2}@'
|
||||
'{hostname2}:1{port2}/{path}?{query}&flavor=coolranch'
|
||||
'#{fragment}')
|
||||
self.flags(transport_url=url)
|
||||
db_mapping = get_db_mapping(transport_url=varurl)
|
||||
mock_get.return_value = db_mapping
|
||||
mapping_obj = objects.CellMapping().get_by_uuid(self.context,
|
||||
db_mapping['uuid'])
|
||||
self.assertEqual(('rabbit://notalice:s3kret@localhost:1123/nova?'
|
||||
'munchies=doritos&flavor=coolranch#baz'),
|
||||
mapping_obj.transport_url)
|
||||
|
||||
@mock.patch.object(cell_mapping.CellMapping, '_get_by_uuid_from_db')
|
||||
def test_formatted_mq_url_multi_netloc3(self, mock_get):
|
||||
# Multiple netlocs, without optional args
|
||||
url = ('rabbit://otherhost,'
|
||||
'bob:s3kret@localhost:123'
|
||||
'/nova?munchies=doritos#baz')
|
||||
varurl = ('{scheme}://not{username2}:{password2}@'
|
||||
'{hostname1}:1{port2}/{path}?{query}&flavor=coolranch'
|
||||
'#{fragment}')
|
||||
self.flags(transport_url=url)
|
||||
db_mapping = get_db_mapping(transport_url=varurl)
|
||||
mock_get.return_value = db_mapping
|
||||
mapping_obj = objects.CellMapping().get_by_uuid(self.context,
|
||||
db_mapping['uuid'])
|
||||
self.assertEqual(('rabbit://notbob:s3kret@otherhost:1123/nova?'
|
||||
'munchies=doritos&flavor=coolranch#baz'),
|
||||
mapping_obj.transport_url)
|
||||
|
||||
@mock.patch.object(cell_mapping.CellMapping, '_get_by_uuid_from_db')
|
||||
def test_formatted_url_without_base_set(self, mock_get):
|
||||
# Make sure we just pass through the template URL if the base
|
||||
# URLs are not set
|
||||
varurl = ('{scheme}://not{username2}:{password2}@'
|
||||
'{hostname1}:1{port2}/{path}?{query}&flavor=coolranch'
|
||||
'#{fragment}')
|
||||
self.flags(transport_url=None)
|
||||
self.flags(connection=None, group='database')
|
||||
db_mapping = get_db_mapping(transport_url=varurl,
|
||||
database_connection=varurl)
|
||||
mock_get.return_value = db_mapping
|
||||
mapping_obj = objects.CellMapping().get_by_uuid(self.context,
|
||||
db_mapping['uuid'])
|
||||
self.assertEqual(varurl, mapping_obj.database_connection)
|
||||
self.assertEqual(varurl, mapping_obj.transport_url)
|
||||
|
||||
|
||||
class TestCellMappingObject(test_objects._LocalTest,
|
||||
_TestCellMappingObject):
|
||||
|
@ -0,0 +1,11 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
The URLs in cell mapping records may now include variables that are filled
|
||||
from the corresponding default URL specified in the host's configuration
|
||||
file. This allows per-host credentials, as well as other values to be set
|
||||
in the config file which will affect the URL of a cell, as calculated when
|
||||
loading the record. For ``database_connection``, the ``[database]/connection``
|
||||
URL is used as the base. For ``transport_url``, the ``[DEFAULT]/transport_url``
|
||||
is used. For more information, see the cells configuration docs:
|
||||
https://docs.openstack.org/nova/latest/user/cells.html
|
Loading…
x
Reference in New Issue
Block a user