diff --git a/api-ref/source/api-ref-dcmanager-v1.rst b/api-ref/source/api-ref-dcmanager-v1.rst index 5fcf85d63..191a97176 100644 --- a/api-ref/source/api-ref-dcmanager-v1.rst +++ b/api-ref/source/api-ref-dcmanager-v1.rst @@ -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 ---------------- diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 7b79caa9f..12071bcbf 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -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. diff --git a/distributedcloud/dcmanager/api/controllers/v1/subclouds.py b/distributedcloud/dcmanager/api/controllers/v1/subclouds.py index e32ab23ac..6c6155f74 100644 --- a/distributedcloud/dcmanager/api/controllers/v1/subclouds.py +++ b/distributedcloud/dcmanager/api/controllers/v1/subclouds.py @@ -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: diff --git a/distributedcloud/dcmanager/manager/subcloud_manager.py b/distributedcloud/dcmanager/manager/subcloud_manager.py index 0dcf09bb5..9e14cad2b 100644 --- a/distributedcloud/dcmanager/manager/subcloud_manager.py +++ b/distributedcloud/dcmanager/manager/subcloud_manager.py @@ -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'] diff --git a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py index 5d3c61002..1e5b3a563 100644 --- a/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py +++ b/distributedcloud/dcmanager/tests/unit/api/v1/controllers/test_subclouds.py @@ -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, diff --git a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py index 90e925837..5a17953ad 100644 --- a/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py +++ b/distributedcloud/dcmanager/tests/unit/manager/test_subcloud_manager.py @@ -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):