From 9fe769c512a44de9772fd79b6a5bc12998c758eb Mon Sep 17 00:00:00 2001 From: Oliver Walsh Date: Tue, 2 Aug 2022 17:42:18 +0100 Subject: [PATCH] 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 --- .../nova_api_ensure_default_cells.py | 161 +++++++++++++++++ .../test_nova_api_ensure_default_cells.py | 168 ++++++++++++++++++ .../nova/nova-api-container-puppet.yaml | 89 ++-------- 3 files changed, 341 insertions(+), 77 deletions(-) create mode 100644 container_config_scripts/nova_api_ensure_default_cells.py create mode 100644 container_config_scripts/tests/test_nova_api_ensure_default_cells.py diff --git a/container_config_scripts/nova_api_ensure_default_cells.py b/container_config_scripts/nova_api_ensure_default_cells.py new file mode 100644 index 0000000000..8e41e3eb7b --- /dev/null +++ b/container_config_scripts/nova_api_ensure_default_cells.py @@ -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) + ) diff --git a/container_config_scripts/tests/test_nova_api_ensure_default_cells.py b/container_config_scripts/tests/test_nova_api_ensure_default_cells.py new file mode 100644 index 0000000000..18ee8ff183 --- /dev/null +++ b/container_config_scripts/tests/test_nova_api_ensure_default_cells.py @@ -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¶m=bar#blah' + expected_url = 'scheme://{username}:{password}@example.com:12345/?param=foo¶m=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¶m=bar#blah' + expected_url = 'scheme://{username1}:{password1}@example.com:12345,{username2}:{password2}@example2.com:6789,{username3}:{password3}@example3.com:4321/?param=foo¶m=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¶m=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¶m=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) diff --git a/deployment/nova/nova-api-container-puppet.yaml b/deployment/nova/nova-api-container-puppet.yaml index f8ae81efaf..9a0f580f38 100644 --- a/deployment/nova/nova-api-container-puppet.yaml +++ b/deployment/nova/nova-api-container-puppet.yaml @@ -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: