Use python to template cell urls

Heat and Puppet cannot handle it properly due to url_parse()
not escaping url encoded special characters.

In future, this script could be reworked into an ansible plugin.
We need it as is to close the related multi-cells regression
sneaked in after Train for Wallaby.

This doesn't raise the bar for standalone roles migration plans
as we have already a plenty of container config scripts to be
migrated as well. Adding another one won't make things (much)
worse.

Related: rhbz#2089512
Change-Id: I0216dd555eacbcb37abd03a67fdd39f9244285db
This commit is contained in:
Oliver Walsh 2022-08-02 17:42:18 +01:00
parent f664129d68
commit 9fe769c512
3 changed files with 341 additions and 77 deletions

View File

@ -0,0 +1,161 @@
#!/usr/bin/env python
#
# Copyright 2022 Red Hat Inc.
#
# 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 configparser import ConfigParser
import logging
import os
import subprocess
import sys
from urllib import parse as urlparse
config = ConfigParser(strict=False)
debug = os.getenv('__OS_DEBUG', 'false')
if debug.lower() == 'true':
loglevel = logging.DEBUG
else:
loglevel = logging.INFO
logging.basicConfig(stream=sys.stdout, level=loglevel)
LOG = logging.getLogger('nova_api_ensure_default_cells')
NOVA_CFG = '/etc/nova/nova.conf'
CELL0_ID = '00000000-0000-0000-0000-000000000000'
DEFAULT_CELL_NAME = 'default'
def template_netloc_credentials(netloc, index=None):
if '@' in netloc:
userpass, hostport = netloc.split('@', 1)
has_pass = ':' in userpass
if index is None:
cred_template = '{username}'
if has_pass:
cred_template += ':{password}'
else:
cred_template = '{{username{index}}}'.format(index=index)
if has_pass:
cred_template += ':{{password{index}}}'.format(index=index)
return '@'.join((cred_template, hostport))
else:
return netloc
def template_url(url):
parsed = urlparse.urlparse(url)
if ',' in parsed.netloc:
orig_netlocs = parsed.netloc.split(',')
templ_netlocs = []
index = 0
for netloc in orig_netlocs:
index += 1
templ_netlocs.append(template_netloc_credentials(netloc, index))
new_netloc = ','.join(templ_netlocs)
else:
new_netloc = template_netloc_credentials(parsed.netloc)
return parsed._replace(netloc=new_netloc).geturl()
def parse_list_cells(list_cells_output):
list_cells_lines = list_cells_output.split('\n')
if len(list_cells_lines) < 5:
raise ValueError('Invalid nova-manage cell_v2 list_cells output')
data_rows = list_cells_lines[3:-2]
by_name = {}
by_uuid = {}
for row in data_rows:
parts = row.split('|')
entry = {
'name': parts[1].strip(),
'uuid': parts[2].strip(),
'transport_url': parts[3].strip(),
'database_connection': parts[4].strip(),
}
by_name[entry['name']] = entry
by_uuid[entry['uuid']] = entry
return by_name, by_uuid
def create_or_update_default_cells(cell0_db, default_db, default_transport_url):
list_cells_cmd = ['/usr/bin/nova-manage', 'cell_v2', 'list_cells', '--verbose']
list_cells_output = subprocess.check_output(list_cells_cmd, encoding='utf-8')
cells_by_name, cells_by_uuid = parse_list_cells(list_cells_output)
if CELL0_ID in cells_by_uuid:
LOG.info('Setting cell0 database connection to \'{}\''.format(cell0_db))
cmd = [
'/usr/bin/nova-manage', 'cell_v2', 'update_cell',
'--cell_uuid', CELL0_ID,
'--database_connection', cell0_db,
'--transport-url', 'none:///'
]
else:
LOG.info('Creating cell0 with database connection \'{}\''.format(cell0_db))
cmd = [
'/usr/bin/nova-manage', 'cell_v2', 'map_cell0',
'--database_connection', cell0_db
]
subprocess.check_call(cmd)
if DEFAULT_CELL_NAME in cells_by_name:
LOG.info('Setting default cell database connection to \'{}\' and transport url to \'{}\''.format(
default_db, default_transport_url))
cmd = [
'/usr/bin/nova-manage', 'cell_v2', 'update_cell',
'--cell_uuid', cells_by_name[DEFAULT_CELL_NAME]['uuid'],
'--database_connection', default_db,
'--transport-url', default_transport_url
]
else:
LOG.info('Creating default cell with database connection \'{}\' and transport url \'{}\''.format(
default_db, default_transport_url))
cmd = [
'/usr/bin/nova-manage', 'cell_v2', 'create_cell',
'--name', DEFAULT_CELL_NAME,
'--database_connection', default_db,
'--transport-url', default_transport_url
]
subprocess.check_call(cmd)
def replace_db_name(db_url, db_name):
return urlparse.urlparse(db_url)._replace(path=db_name).geturl()
if __name__ == '__main__':
if os.path.isfile(NOVA_CFG):
try:
config.read(NOVA_CFG)
except Exception:
LOG.exception('Error while reading nova.conf:')
sys.exit(1)
else:
LOG.error('Nova configuration file %s does not exist', NOVA_CFG)
sys.exit(1)
default_database_connection = config.get('database', 'connection')
cell0_database_connection = replace_db_name(default_database_connection, 'nova_cell0')
default_transport_url = config.get('DEFAULT', 'transport_url')
create_or_update_default_cells(
template_url(cell0_database_connection),
template_url(default_database_connection),
template_url(default_transport_url)
)

View File

@ -0,0 +1,168 @@
#
# Copyright 2022 Red Hat Inc.
#
# 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 container_config_scripts.nova_api_ensure_default_cells import parse_list_cells
from container_config_scripts.nova_api_ensure_default_cells import replace_db_name
from container_config_scripts.nova_api_ensure_default_cells import template_netloc_credentials
from container_config_scripts.nova_api_ensure_default_cells import template_url
from oslotest import base
class TemplateNetlocCredentialsCase(base.BaseTestCase):
def test_host(self):
test_netloc = 'example.com'
expected_netloc = test_netloc
templated_netloc = template_netloc_credentials(test_netloc)
self.assertEqual(expected_netloc, templated_netloc)
def test_host_port(self):
test_netloc = 'example.com:1234'
expected_netloc = test_netloc
templated_netloc = template_netloc_credentials(test_netloc)
self.assertEqual(expected_netloc, templated_netloc)
def test_host_port_ipv6(self):
test_netloc = '[dead:beef::1]:1234'
expected_netloc = test_netloc
templated_netloc = template_netloc_credentials(test_netloc)
self.assertEqual(expected_netloc, templated_netloc)
def test_username(self):
test_netloc = 'foo@example.com'
expected_netloc = '{username}@example.com'
templated_netloc = template_netloc_credentials(test_netloc)
self.assertEqual(expected_netloc, templated_netloc)
def test_userpass(self):
test_netloc = 'foo:bar@example.com'
expected_netloc = '{username}:{password}@example.com'
templated_netloc = template_netloc_credentials(test_netloc)
self.assertEqual(expected_netloc, templated_netloc)
def test_username_index(self):
test_netloc = 'foo@example.com'
expected_netloc = '{username5}@example.com'
templated_netloc = template_netloc_credentials(test_netloc, index=5)
self.assertEqual(expected_netloc, templated_netloc)
def test_userpass_index(self):
test_netloc = 'foo:bar@example.com'
expected_netloc = '{username5}:{password5}@example.com'
templated_netloc = template_netloc_credentials(test_netloc, index=5)
self.assertEqual(expected_netloc, templated_netloc)
class TemplateUrlCase(base.BaseTestCase):
def test_simple_url(self):
test_url = 'scheme://foo:bar@example.com:12345/?param=foo&param=bar#blah'
expected_url = 'scheme://{username}:{password}@example.com:12345/?param=foo&param=bar#blah'
templated_url = template_url(test_url)
self.assertEqual(expected_url, templated_url)
def test_ha_url(self):
test_url = 'scheme://foo:bar@example.com:12345,foo2:bar2@example2.com:6789,foo3:bar3@example3.com:4321/?param=foo&param=bar#blah'
expected_url = 'scheme://{username1}:{password1}@example.com:12345,{username2}:{password2}@example2.com:6789,{username3}:{password3}@example3.com:4321/?param=foo&param=bar#blah'
templated_url = template_url(test_url)
self.assertEqual(expected_url, templated_url)
def test_ha_ipv6_url(self):
test_url = 'scheme://foo:bar@[dead:beef::1]:12345,foo2:bar2@[dead:beef::2]:6789,foo3:bar3@[dead:beef::3]:4321/?param=foo&param=bar#blah'
expected_url = 'scheme://{username1}:{password1}@[dead:beef::1]:12345,{username2}:{password2}@[dead:beef::2]:6789,{username3}:{password3}@[dead:beef::3]:4321/?param=foo&param=bar#blah'
templated_url = template_url(test_url)
self.assertEqual(expected_url, templated_url)
class ParseListCellsCase(base.BaseTestCase):
def test_no_output(self):
test_output = ''
self.assertRaises(ValueError, parse_list_cells, test_output)
def test_no_cells(self):
test_output = '''\
+------+------+---------------+---------------------+----------+
| Name | UUID | Transport URL | Database Connection | Disabled |
+------+------+---------------+---------------------+----------+
+------+------+---------------+---------------------+----------+
'''
expected_cell_dicts = ({}, {})
cell_dicts = parse_list_cells(test_output)
self.assertEqual(expected_cell_dicts, cell_dicts)
def test_cell0(self):
test_output = '''\
+-------+--------------------------------------+---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+
| Name | UUID | Transport URL | Database Connection | Disabled |
+-------+--------------------------------------+---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+
| cell0 | 00000000-0000-0000-0000-000000000000 | none:/// | mysql+pymysql://nova:GsrvXnnW6Oam6Uz1CraPS46PV@overcloud.internalapi.redhat.local/nova_cell0?read_default_file=/etc/my.cnf.d/tripleo.cnf&read_default_group=tripleo | False |
+-------+--------------------------------------+---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+
'''
expected_cell0_dict = {
'name': 'cell0',
'uuid': '00000000-0000-0000-0000-000000000000',
'transport_url': 'none:///',
'database_connection': 'mysql+pymysql://nova:GsrvXnnW6Oam6Uz1CraPS46PV@overcloud.internalapi.redhat.local/nova_cell0?read_default_file=/etc/my.cnf.d/tripleo.cnf&read_default_group=tripleo'
}
expected_cell_dicts = (
{
'cell0': expected_cell0_dict
},
{
'00000000-0000-0000-0000-000000000000': expected_cell0_dict
}
)
cell_dicts = parse_list_cells(test_output)
self.assertEqual(expected_cell_dicts, cell_dicts)
def test_default_cells(self):
test_output = '''\
+---------+--------------------------------------+--------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+
| Name | UUID | Transport URL | Database Connection | Disabled |
+---------+--------------------------------------+--------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+
| cell0 | 00000000-0000-0000-0000-000000000000 | none:/// | mysql+pymysql://nova:GsrvXnnW6Oam6Uz1CraPS46PV@overcloud.internalapi.redhat.local/nova_cell0?read_default_file=/etc/my.cnf.d/tripleo.cnf&read_default_group=tripleo | False |
| default | 541ca4e9-15f7-4178-95de-8af9e3659daf | rabbit://guest:oLniT3uE12BLP4VsyoFt29k3U@controller-0.internalapi.redhat.local:5672/?ssl=1 | mysql+pymysql://nova:GsrvXnnW6Oam6Uz1CraPS46PV@overcloud.internalapi.redhat.local/nova?read_default_file=/etc/my.cnf.d/tripleo.cnf&read_default_group=tripleo | False |
+---------+--------------------------------------+--------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+
'''
expected_cell0_dict = {
'name': 'cell0',
'uuid': '00000000-0000-0000-0000-000000000000',
'transport_url': 'none:///',
'database_connection': 'mysql+pymysql://nova:GsrvXnnW6Oam6Uz1CraPS46PV@overcloud.internalapi.redhat.local/nova_cell0?read_default_file=/etc/my.cnf.d/tripleo.cnf&read_default_group=tripleo'
}
expected_default_dict = {
'name': 'default',
'uuid': '541ca4e9-15f7-4178-95de-8af9e3659daf',
'transport_url': 'rabbit://guest:oLniT3uE12BLP4VsyoFt29k3U@controller-0.internalapi.redhat.local:5672/?ssl=1',
'database_connection': 'mysql+pymysql://nova:GsrvXnnW6Oam6Uz1CraPS46PV@overcloud.internalapi.redhat.local/nova?read_default_file=/etc/my.cnf.d/tripleo.cnf&read_default_group=tripleo'
}
expected_cell_dicts = (
{
'cell0': expected_cell0_dict,
'default': expected_default_dict
},
{
'00000000-0000-0000-0000-000000000000': expected_cell0_dict,
'541ca4e9-15f7-4178-95de-8af9e3659daf': expected_default_dict
}
)
cell_dicts = parse_list_cells(test_output)
self.assertEqual(expected_cell_dicts, cell_dicts)
class ReplaceDbNameCase(base.BaseTestCase):
def test_replace_db_name(self):
test_db_url = 'mysql+pymysql://nova:GsrvXnnW6Oam6Uz1CraPS46PV@overcloud.internalapi.redhat.local/nova?read_default_file=/etc/my.cnf.d/tripleo.cnf&read_default_group=tripleo'
expected_db_url = 'mysql+pymysql://nova:GsrvXnnW6Oam6Uz1CraPS46PV@overcloud.internalapi.redhat.local/foobar?read_default_file=/etc/my.cnf.d/tripleo.cnf&read_default_group=tripleo'
db_url = replace_db_name(test_db_url, 'foobar')
self.assertEqual(expected_db_url, db_url)

View File

@ -482,7 +482,7 @@ outputs:
config_files: *nova_api_db_sync
permissions: *nova_api_permissions
/var/lib/kolla/config_files/nova_api_ensure_default_cells.json:
command: "/usr/bin/bootstrap_host_exec nova_api /nova_api_ensure_default_cells.sh"
command: "/usr/bin/bootstrap_host_exec nova_api su nova -s /bin/bash -c '/container-config-scripts/pyshim.sh /container-config-scripts/nova_api_ensure_default_cells.py'"
config_files: *nova_api_db_sync
permissions: *nova_api_permissions
/var/lib/kolla/config_files/nova_api_cron.json:
@ -504,80 +504,9 @@ outputs:
- nova_wait_for_api_service.py:
mode: "0755"
content: { get_file: ../../container_config_scripts/nova_wait_for_api_service.py }
nova_api_ensure_default_cells.sh:
mode: "0700"
content:
str_replace:
template: |
#!/bin/bash
set -e
CELL0_ID='00000000-0000-0000-0000-000000000000'
CELL0_EXISTS=$(su nova -s /bin/bash -c "nova-manage cell_v2 list_cells" | sed -e '1,3d' -e '$d' | awk -F ' *| *' '$4 == "'$CELL0_ID'" {print $4}')
if [ "$CELL0_EXISTS" ]; then
echo "(cellv2) Updating cell_v2 cell0 database uri"
su nova -s /bin/bash -c "/usr/bin/nova-manage cell_v2 update_cell --cell_uuid $CELL0_ID --database_connection='CELL0DB' --transport-url='none:///'"
else
echo "(cellv2) Creating cell_v2 cell0"
su nova -s /bin/bash -c "/usr/bin/nova-manage cell_v2 map_cell0 --database_connection='CELL0DB'"
fi
DEFID=$(su nova -s /bin/bash -c "nova-manage cell_v2 list_cells" | sed -e '1,3d' -e '$d' | awk -F ' *| *' '$2 == "default" {print $4}')
if [ "$DEFID" ]; then
echo "(cellv2) Updating default cell_v2 cell $DEFID"
su nova -s /bin/bash -c "/usr/bin/nova-manage cell_v2 update_cell --cell_uuid $DEFID --name=default --database_connection='CELLDB' --transport-url='TRANSPORTURL'"
else
echo "(cellv2) Creating default cell_v2 cell"
su nova -s /bin/bash -c "/usr/bin/nova-manage cell_v2 create_cell --name=default --database_connection='CELLDB' --transport-url='TRANSPORTURL'"
fi
params:
CELL0DB:
list_join:
- ''
- - '{scheme}'
- '://'
- '{username}'
- ':'
- '{password}'
- '@'
- if:
- mysql_ipv6_use_ip_address
- '[{hostname}]'
- '{hostname}'
- '/'
- 'nova_cell0'
- '?'
- '{query}'
CELLDB:
list_join:
- ''
- - '{scheme}'
- '://'
- '{username}'
- ':'
- '{password}'
- '@'
- if:
- mysql_ipv6_use_ip_address
- '[{hostname}]'
- '{hostname}'
- '/'
- 'nova'
- '?'
- '{query}'
TRANSPORTURL:
list_join:
- ''
- - '{scheme}'
- '://'
- '{username}'
- ':'
- '{password}'
- '@'
- '{hostname}'
- ':'
- '{port}'
- '/'
- '?'
- '{query}'
nova_api_ensure_default_cells.py:
mode: "0755"
content: { get_file: ../../container_config_scripts/nova_api_ensure_default_cells.py }
docker_config:
step_2:
get_attr: [NovaApiLogging, docker_config, step_2]
@ -600,7 +529,9 @@ outputs:
nova_api_ensure_default_cells:
start_order: 1 # Runs before nova-conductor dbsync
image: *nova_api_image
user: root
net: host
privileged: false
detach: false
volumes:
list_concat:
@ -608,9 +539,13 @@ outputs:
- {get_attr: [NovaApiLogging, volumes]}
- - /var/lib/kolla/config_files/nova_api_ensure_default_cells.json:/var/lib/kolla/config_files/config.json:ro
- /var/lib/config-data/puppet-generated/nova:/var/lib/kolla/config_files/src:ro
- /var/lib/container-config-scripts/nova_api_ensure_default_cells.sh:/nova_api_ensure_default_cells.sh:ro
user: root
- /var/lib/container-config-scripts:/container-config-scripts:z
environment:
__OS_DEBUG:
yaql:
expression: str($.data.debug)
data:
debug: {get_attr: [NovaBase, role_data, config_settings, 'nova::logging::debug']}
KOLLA_CONFIG_STRATEGY: COPY_ALWAYS
step_4:
nova_api: