Generalize subcloud network reconfiguration

This commit refactors the subcloud network reconfiguration,
allowing for a more flexible and generalized approach, adding
the option to fallback to the management network as well.

Test Plan:
PASS: Run dcmanager subcloud update with network paramaters
(dcmanager subcloud update --sysadmin-password <password>
--management-subnet <network-subnet>
--management-gateway-ip <network-gateway-ip>
--management-start-ip <network-start-ip>
--management-end-ip <network-end-ip>
--bootstrap-address <bootstrap-address> <subcloud_name>)
- The update_playbook will be called and update the subcloud
(subcloud route to systemcontroller and admin endpoints)
- A new route to the subcloud is created on the system controller.
- Subcloud service endpoint URLs are updated in keystone
(openstack endpoint list|grep <subcloud-name>) on the system controller.
PASS: verify successful deployment of a new subcloud
PASS: verify successful reconfiguration of a subcloud from mgmt to
admin network

Depends-On: https://review.opendev.org/c/starlingx/ansible-playbooks/+/878504

Story: 2010319
Task: 47706

Signed-off-by: Hugo Brito <hugo.brito@windriver.com>
Change-Id: I1df57a206e21fa2444bd645c456c4d5d1b539066
This commit is contained in:
Hugo Brito 2023-03-23 21:58:26 -03:00
parent ad1f05ac5f
commit 3d7cb75e22
6 changed files with 195 additions and 145 deletions

View File

@ -319,13 +319,13 @@ The attributes of a subcloud which are modifiable:
- group_id
- admin-subnet
- management-subnet
- admin-gateway-ip
- management-gateway-ip
- admin-node-0-address
- management-start-ip
- admin-node-1-address
- management-end-ip
**Normal response codes**
@ -346,10 +346,12 @@ serviceUnavailable (503)
- location: subcloud_location
- management-state: subcloud_management_state
- group_id: subcloud_group_id
- admin-subnet: subcloud_admin_subnet
- admin-gateway-ip: subcloud_admin_gateway_ip
- admin-node-0-address: subcloud_admin_node_0_address
- admin-node-1-address: subcloud_admin_node_1_address
- management-subnet: subcloud_management_subnet
- management-gateway-ip: subcloud_management_gateway_ip
- management-start-ip: subcloud_management_start_ip
- management-end-ip: subcloud_management_end_ip
- bootstrap-address: bootstrap_address
- sysadmin-password: sysadmin_password
Request Example
----------------

View File

@ -398,30 +398,6 @@ strategy_steps:
in: body
required: false
type: array
subcloud_admin_gateway_ip:
description: |
The admin gateway ip of a subcloud.
in: body
required: false
type: string
subcloud_admin_node_0_address:
description: |
The admin node-0 address of a subcloud.
in: body
required: false
type: string
subcloud_admin_node_1_address:
description: |
The admin node-1 address of a subcloud.
in: body
required: false
type: string
subcloud_admin_subnet:
description: |
The admin subnet of a subcloud.
in: body
required: false
type: string
subcloud_apply_type:
description: |
The apply type for the update. `serial` or `parallel`.
@ -555,12 +531,36 @@ subcloud_location:
in: body
required: false
type: string
subcloud_management_end_ip:
description: |
The management end ip of a subcloud.
in: body
required: false
type: string
subcloud_management_gateway_ip:
description: |
The management gateway ip of a subcloud.
in: body
required: false
type: string
subcloud_management_start_ip:
description: |
The management start ip of a subcloud.
in: body
required: false
type: string
subcloud_management_state:
description: |
Management state of the subcloud.
in: body
required: false
type: string
subcloud_management_subnet:
description: |
The management subnet of a subcloud.
in: body
required: false
type: string
subcloud_name:
description: |
The name of a subcloud.

View File

@ -96,6 +96,11 @@ INSTALL_VALUES_ADDRESSES = [
'network_address'
]
SUBCLOUD_MANDATORY_NETWORK_PARAMS = [
'management_subnet', 'management_gateway_ip',
'management_start_ip', 'management_end_ip'
]
ANSIBLE_BOOTSTRAP_VALIDATE_CONFIG_VARS = \
consts.ANSIBLE_CURRENT_VERSION_BASE_PATH + \
'/roles/bootstrap/validate-config/vars/main.yml'
@ -452,7 +457,7 @@ class SubcloudsController(object):
# Ensure systemcontroller gateway is in the management subnet
# for the systemcontroller region.
management_address_pool = self._get_management_address_pool(context)
management_address_pool = self._get_network_address_pool()
systemcontroller_subnet_str = "%s/%d" % (
management_address_pool.network,
management_address_pool.prefix)
@ -592,6 +597,39 @@ class SubcloudsController(object):
{'start': subcloud_admin_address_start,
'end': subcloud_admin_address_end})
# TODO(nicodemos): Check if subcloud is online and network already exist in the
# subcloud when the lock/unlock is not required for network reconfiguration
def _validate_network_reconfiguration(self, payload, subcloud):
if payload.get('management-state'):
pecan.abort(422, _("Management state and network reconfiguration must "
"be updated separately"))
if subcloud.management_state != dccommon_consts.MANAGEMENT_UNMANAGED:
pecan.abort(422, _("A subcloud must be unmanaged to perform network "
"reconfiguration"))
if not payload.get('bootstrap_address'):
pecan.abort(422, _("The bootstrap_address parameter is required for "
"network reconfiguration"))
# Check if all parameters exist
if not all(payload.get(value) is not None for value in (
SUBCLOUD_MANDATORY_NETWORK_PARAMS)):
mandatory_params = ', '.join('--{}'.format(param.replace(
'_', '-')) for param in SUBCLOUD_MANDATORY_NETWORK_PARAMS)
abort_msg = (
"The following parameters are necessary for "
"subcloud network reconfiguration: {}".format(mandatory_params)
)
pecan.abort(422, _(abort_msg))
# Check if any network values are already in use
for param in SUBCLOUD_MANDATORY_NETWORK_PARAMS:
if payload.get(param) == getattr(subcloud, param):
pecan.abort(422, _("%s already in use by the subcloud.") % param)
# Check if password is valid
valid, msg = utils.is_password_valid(payload)
if not valid:
pecan.abort(400, _(msg))
def _format_ip_address(self, payload):
"""Format IP addresses in 'bootstrap_values' and 'install_values'.
@ -857,13 +895,17 @@ class SubcloudsController(object):
if patches[patch_id]['patchstate'] in [patching_v1.PATCH_STATE_PARTIAL_APPLY, patching_v1.PATCH_STATE_PARTIAL_REMOVE]:
pecan.abort(422, _('Subcloud add is not allowed while system controller patching is still in progress.'))
def _get_management_address_pool(self, context):
"""Get the system controller's management address pool"""
ks_client = self.get_ks_client()
def _get_network_address_pool(
self, network='management',
region_name=dccommon_consts.DEFAULT_REGION_NAME):
"""Get the region network address pool"""
ks_client = self.get_ks_client(region_name)
endpoint = ks_client.endpoint_cache.get_endpoint('sysinv')
sysinv_client = SysinvClient(dccommon_consts.DEFAULT_REGION_NAME,
sysinv_client = SysinvClient(region_name,
ks_client.session,
endpoint=endpoint)
if network == 'admin':
return sysinv_client.get_admin_address_pool()
return sysinv_client.get_management_address_pool()
# TODO(gsilvatr): refactor to use implementation from common/utils and test
@ -1260,7 +1302,6 @@ class SubcloudsController(object):
subcloud_ref)
except exceptions.SubcloudNameNotFound:
pecan.abort(404, _('Subcloud not found'))
subcloud_id = subcloud.id
if verb is None:
@ -1269,39 +1310,26 @@ class SubcloudsController(object):
if not payload:
pecan.abort(400, _('Body required'))
# Check if exist any network reconfiguration parameters
reconfigure_network = any(payload.get(value) is not None for value in (
SUBCLOUD_MANDATORY_NETWORK_PARAMS))
if reconfigure_network:
system_controller_mgmt_pool = self._get_network_address_pool()
# Required parameters
payload['name'] = subcloud.name
payload['system_controller_network'] = (
system_controller_mgmt_pool.network)
payload['system_controller_network_prefix'] = (
system_controller_mgmt_pool.prefix
)
# Validation
self._validate_network_reconfiguration(payload, subcloud)
management_state = payload.get('management-state')
group_id = payload.get('group_id')
description = payload.get('description')
location = payload.get('location')
admin_subnet_str = payload.get('admin_subnet')
admin_start_ip_str = payload.get('admin_start_address')
admin_end_ip_str = payload.get('admin_end_address')
admin_gateway_ip_str = payload.get('admin_gateway_ip')
# Syntax checking
if (admin_subnet_str and admin_gateway_ip_str and
admin_start_ip_str and admin_end_ip_str):
# Required parameters
payload['name'] = subcloud.name
payload['systemcontroller_gateway_ip'] = (
subcloud.systemcontroller_gateway_ip)
# Parse/validate the admin subnet
subcloud_subnets = []
subclouds = db_api.subcloud_get_all(context)
for subcloud in subclouds:
subcloud_subnets.append(IPNetwork(subcloud.management_subnet))
self._validate_admin_network_config(admin_subnet_str,
admin_start_ip_str,
admin_end_ip_str,
admin_gateway_ip_str,
subcloud_subnets)
# Password only required when update admin network
valid, msg = utils.is_password_valid(payload)
if not valid:
pecan.abort(400, _(msg))
# Syntax checking
if management_state and \
@ -1335,27 +1363,16 @@ class SubcloudsController(object):
if self._validate_install_values(payload, subcloud):
payload['data_install'] = json.dumps(payload[INSTALL_VALUES])
try:
# Inform dcmanager that subcloud has been updated.
# It will do all the real work...
if payload.get('admin_subnet'):
if payload.get('management-state'):
pecan.abort(422, _('Management state and network configuration must be updated separately'))
if subcloud.management_state != dccommon_consts.MANAGEMENT_UNMANAGED:
pecan.abort(422, _("Subcloud must be unmanaged to update admin network"))
subcloud = db_api.subcloud_update(
context, subcloud_id,
deploy_status=consts.DEPLOY_STATE_RECONFIGURING_NETWORK)
if reconfigure_network:
self.dcmanager_rpc_client.update_subcloud_with_network_reconfig(
context, subcloud_id, payload)
return db_api.subcloud_db_model_to_dict(subcloud)
else:
subcloud = self.dcmanager_rpc_client.update_subcloud(
context, subcloud_id, management_state=management_state,
description=description, location=location,
group_id=group_id, data_install=payload.get('data_install'),
force=force_flag)
return subcloud
subcloud = self.dcmanager_rpc_client.update_subcloud(
context, subcloud_id, management_state=management_state,
description=description, location=location,
group_id=group_id, data_install=payload.get('data_install'),
force=force_flag)
return subcloud
except RemoteError as e:
pecan.abort(422, e.value)
except Exception as e:

View File

@ -26,6 +26,7 @@ import shutil
import threading
import time
from cgtsclient.exc import HTTPConflict
from eventlet import greenpool
from fm_api import constants as fm_const
from fm_api import fm_api
@ -1741,6 +1742,11 @@ class SubcloudManager(manager.Manager):
return db_api.subcloud_db_model_to_dict(subcloud)
def update_subcloud_with_network_reconfig(self, context, subcloud_id, payload):
subcloud = db_api.subcloud_get(context, subcloud_id)
subcloud = db_api.subcloud_update(
context, subcloud.id,
deploy_status=consts.DEPLOY_STATE_RECONFIGURING_NETWORK
)
subcloud_name = payload['name']
try:
self._create_intermediate_ca_cert(payload)
@ -1754,22 +1760,25 @@ class SubcloudManager(manager.Manager):
update_command = self.compose_update_command(
subcloud_name, subcloud_inventory_file)
except Exception:
LOG.exception("Failed to prepare subcloud %s for update."
% subcloud_name)
LOG.exception(
"Failed to prepare subcloud %s for update." % subcloud_name)
return
try:
apply_thread = threading.Thread(
target=self._run_admin_network_update_playbook,
args=(subcloud_name, update_command, overrides_file, payload, context, subcloud_id))
target=self._run_network_reconfig_playbook,
args=(subcloud_name, update_command, overrides_file,
payload, context, subcloud))
apply_thread.start()
except Exception:
LOG.exception("Failed to update subcloud %s" % subcloud_name)
def _run_admin_network_update_playbook(
self, subcloud_name, update_command, overrides_file, payload, context, subcloud_id):
def _run_network_reconfig_playbook(
self, subcloud_name, update_command, overrides_file,
payload, context, subcloud
):
log_file = (os.path.join(consts.DC_ANSIBLE_LOG_DIR, subcloud_name) +
'_playbook_output.log')
subcloud = db_api.subcloud_get(context, subcloud_id)
subcloud_id = subcloud.id
try:
run_playbook(log_file, update_command)
utils.delete_subcloud_inventory(overrides_file)
@ -1787,9 +1796,14 @@ class SubcloudManager(manager.Manager):
m_ks_client = OpenStackDriver(
region_name=dccommon_consts.DEFAULT_REGION_NAME,
region_clients=None).keystone_client
self._create_subcloud_admin_route(payload, m_ks_client)
self._create_subcloud_route(payload, m_ks_client, subcloud)
except HTTPConflict:
# The route already exists
LOG.warning(
"Failed to create route to subcloud %s" % subcloud_name)
except Exception:
LOG.exception("Failed to create route to admin")
LOG.exception(
"Failed to create route to subcloud %s." % subcloud_name)
db_api.subcloud_update(
context, subcloud_id,
deploy_status=consts.DEPLOY_STATE_RECONFIGURING_NETWORK_FAILED,
@ -1797,7 +1811,8 @@ class SubcloudManager(manager.Manager):
)
return
try:
self._update_services_endpoint(context, payload, m_ks_client)
self._update_services_endpoint(
context, payload, subcloud_name, m_ks_client)
except Exception:
LOG.exception("Failed to update subcloud %s endpoints" % subcloud_name)
db_api.subcloud_update(
@ -1810,45 +1825,46 @@ class SubcloudManager(manager.Manager):
self._delete_subcloud_routes(m_ks_client, subcloud)
db_api.subcloud_update(
context, subcloud_id,
deploy_status=consts.DEPLOY_STATE_DONE
context, subcloud_id, deploy_status=consts.DEPLOY_STATE_DONE
)
subcloud = db_api.subcloud_update(
context,
subcloud_id,
description=payload.get('description', subcloud.description),
management_subnet=payload.get('admin_subnet'),
management_gateway_ip=payload.get('admin_gateway_ip'),
management_start_ip=payload.get('admin_start_address'),
management_end_ip=payload.get('admin_end_address'),
management_subnet=payload.get('management_subnet'),
management_gateway_ip=payload.get('management_gateway_ip'),
management_start_ip=payload.get('management_start_ip'),
management_end_ip=payload.get('management_end_ip'),
location=payload.get('location', subcloud.location),
group_id=payload.get('group_id', subcloud.group_id),
data_install=payload.get('data_install', subcloud.data_install)
)
def _create_subcloud_admin_route(self, payload, keystone_client):
subcloud_subnet = netaddr.IPNetwork(utils.get_management_subnet(payload))
# Regenerate the addn_hosts_dc file
self._create_addn_hosts_dc(context)
def _create_subcloud_route(self, payload, keystone_client, subcloud):
subcloud_subnet = netaddr.IPNetwork(payload.get('management_subnet'))
endpoint = keystone_client.endpoint_cache.get_endpoint('sysinv')
sysinv_client = SysinvClient(dccommon_consts.DEFAULT_REGION_NAME,
keystone_client.session,
endpoint=endpoint)
systemcontroller_gateway_ip = payload.get('systemcontroller_gateway_ip')
# TODO(nicodemos) delete old route
cached_regionone_data = self._get_cached_regionone_data(
keystone_client, sysinv_client)
for mgmt_if_uuid in cached_regionone_data['mgmt_interface_uuids']:
sysinv_client.create_route(mgmt_if_uuid,
str(subcloud_subnet.ip),
subcloud_subnet.prefixlen,
systemcontroller_gateway_ip,
subcloud.systemcontroller_gateway_ip,
1)
def _update_services_endpoint(self, context, payload, m_ks_client):
endpoint_ip = str(ipaddress.ip_network(payload.get('admin_subnet'))[2])
subcloud_name = payload.get('name')
def _update_services_endpoint(
self, context, payload, subcloud_name, m_ks_client):
endpoint_ip = str(ipaddress.ip_network(
payload.get('management_subnet'))[2])
if netaddr.IPAddress(endpoint_ip).version == 6:
endpoint_ip = '[' + endpoint_ip + ']'
endpoint_ip = f"[{endpoint_ip}]"
services_endpoints = {
"keystone": "https://{}:5001/v3".format(endpoint_ip),
@ -1911,14 +1927,17 @@ class SubcloudManager(manager.Manager):
payload['sysadmin_password'])
payload['override_values']['ansible_become_pass'] = (
payload['sysadmin_password'])
payload['override_values']['admin_gateway_address'] = (
payload['admin_gateway_ip'])
payload['override_values']['admin_floating_address'] = (
payload['admin_start_address']
)
payload['override_values']['admin_subnet'] = (
payload['admin_subnet']
)
payload['override_values']['sc_gateway_address'] = (
payload['management_gateway_ip'])
payload['override_values']['sc_floating_address'] = (
payload['management_start_ip'])
payload['override_values']['system_controller_network'] = (
payload['system_controller_network'])
payload['override_values']['system_controller_network_prefix'] = (
payload['system_controller_network_prefix'])
payload['override_values']['sc_subnet'] = payload['management_subnet']
payload['override_values']['dc_root_ca_cert'] = payload['dc_root_ca_cert']
payload['override_values']['sc_ca_cert'] = payload['sc_ca_cert']
payload['override_values']['sc_ca_key'] = payload['sc_ca_key']

View File

@ -278,9 +278,9 @@ class TestSubcloudPost(testroot.DCManagerApiTest,
'192.168.204.100')
p = mock.patch.object(subclouds.SubcloudsController,
'_get_management_address_pool')
self.mock_get_management_address_pool = p.start()
self.mock_get_management_address_pool.return_value = \
'_get_network_address_pool')
self.mock_get_network_address_pool = p.start()
self.mock_get_network_address_pool.return_value = \
self.management_address_pool
self.addCleanup(p.stop)
@ -1234,21 +1234,30 @@ class TestSubcloudAPIOther(testroot.DCManagerApiTest):
self.assertEqual(response.status_int, 200)
@mock.patch.object(rpc_client, 'ManagerClient')
@mock.patch.object(subclouds.SubcloudsController, '_validate_admin_network_config')
@mock.patch.object(subclouds.SubcloudsController, '_get_network_address_pool')
@mock.patch.object(subclouds.SubcloudsController,
'_validate_network_reconfiguration')
@mock.patch.object(subclouds.SubcloudsController, '_get_patch_data')
def test_patch_subcloud_admin_values(self, mock_get_patch_data,
mock_validate_admin_network_config,
mock_rpc_client):
def test_patch_subcloud_network_values(
self, mock_get_patch_data, mock_validate_network_reconfiguration,
mock_mgmt_address_pool, mock_rpc_client):
subcloud = fake_subcloud.create_fake_subcloud(self.ctx)
db_api.subcloud_update(self.ctx, subcloud.id,
availability_status=dccommon_consts.AVAILABILITY_ONLINE)
db_api.subcloud_update(
self.ctx, subcloud.id,
availability_status=dccommon_consts.AVAILABILITY_ONLINE)
fake_password = (
base64.b64encode('testpass'.encode("utf-8"))).decode('ascii')
payload = {'sysadmin_password': fake_password,
'admin_subnet': "192.168.102.0/24",
'admin_start_address': "192.168.102.5",
'admin_end_address': "192.168.102.49",
'admin_gateway_ip': "192.168.102.1"}
'bootstrap_address': "192.168.102.2",
'management_subnet': "192.168.102.0/24",
'management_start_ip': "192.168.102.5",
'management_end_ip': "192.168.102.49",
'management_gateway_ip': "192.168.102.1"}
fake_management_address_pool = FakeAddressPool('192.168.204.0', 24,
'192.168.204.2',
'192.168.204.100')
mock_mgmt_address_pool.return_value = fake_management_address_pool
mock_rpc_client().update_subcloud_with_network_reconfig.return_value = True
mock_get_patch_data.return_value = payload
@ -1256,7 +1265,7 @@ class TestSubcloudAPIOther(testroot.DCManagerApiTest):
headers=FAKE_HEADERS,
params=payload)
self.assertEqual(response.status_int, 200)
mock_validate_admin_network_config.assert_called_once()
mock_validate_network_reconfiguration.assert_called_once()
mock_rpc_client().update_subcloud_with_network_reconfig.assert_called_once_with(
mock.ANY,
subcloud.id,

View File

@ -661,17 +661,19 @@ class TestSubcloudManager(base.DCManagerTestCase):
self.assertEqual("subcloud new location",
updated_subcloud.location)
@mock.patch.object(subcloud_manager.SubcloudManager,
'_create_addn_hosts_dc')
@mock.patch.object(subcloud_manager.SubcloudManager,
'_delete_subcloud_routes')
@mock.patch.object(subcloud_manager.SubcloudManager,
'_update_services_endpoint')
@mock.patch.object(subcloud_manager.SubcloudManager,
'_create_subcloud_admin_route')
'_create_subcloud_route')
@mock.patch.object(subcloud_manager, 'OpenStackDriver')
@mock.patch.object(subcloud_manager, 'run_playbook')
def test_update_subcloud_with_admin_values(
def test_update_subcloud_network_reconfiguration(
self, mock_run_playbook, mock_keystone_client, mock_create_route,
mock_update_endpoints, mock_delete_route):
mock_update_endpoints, mock_delete_route, mock_addn_hosts_dc):
subcloud = self.create_subcloud_static(
self.ctx,
name='subcloud1',
@ -683,10 +685,10 @@ class TestSubcloudManager(base.DCManagerTestCase):
payload = {'name': subcloud.name,
'description': "subcloud description",
'location': "subcloud location",
'admin_subnet': "192.168.102.0/24",
'admin_start_address': "192.168.102.5",
'admin_end_address': "192.168.102.49",
'admin_gateway_ip': "192.168.102.1"}
'management_subnet': "192.168.102.0/24",
'management_start_ip': "192.168.102.5",
'management_end_ip': "192.168.102.49",
'management_gateway_ip': "192.168.102.1"}
fake_dcmanager_notification = FakeDCManagerNotifications()
@ -695,14 +697,15 @@ class TestSubcloudManager(base.DCManagerTestCase):
mock_dcmanager_api.return_value = fake_dcmanager_notification
sm = subcloud_manager.SubcloudManager()
sm._run_admin_network_update_playbook(
subcloud.name, mock.ANY, None, payload, self.ctx, subcloud.id)
sm._run_network_reconfig_playbook(
subcloud.name, mock.ANY, None, payload, self.ctx, subcloud)
mock_run_playbook.assert_called_once()
mock_keystone_client.assert_called_once()
mock_create_route.assert_called_once()
mock_update_endpoints.assert_called_once()
mock_delete_route.assert_called_once()
mock_addn_hosts_dc.assert_called_once()
# Verify subcloud was updated with correct values
updated_subcloud = db_api.subcloud_get_by_name(self.ctx, subcloud.name)
@ -710,13 +713,13 @@ class TestSubcloudManager(base.DCManagerTestCase):
updated_subcloud.description)
self.assertEqual(payload['location'],
updated_subcloud.location)
self.assertEqual(payload['admin_subnet'],
self.assertEqual(payload['management_subnet'],
updated_subcloud.management_subnet)
self.assertEqual(payload['admin_gateway_ip'],
self.assertEqual(payload['management_gateway_ip'],
updated_subcloud.management_gateway_ip)
self.assertEqual(payload['admin_start_address'],
self.assertEqual(payload['management_start_ip'],
updated_subcloud.management_start_ip)
self.assertEqual(payload['admin_end_address'],
self.assertEqual(payload['management_end_ip'],
updated_subcloud.management_end_ip)
def test_update_subcloud_with_install_values(self):