Initial integration of DC with admin network

Because the management network and its parameters are
embedded in many parts of the system, having a separate
admin network makes it much easier to change the parameters
of this network (subnet, gateway, etc) after a subcloud has
been provisioned.

The admin network will take precedence over the existing
management network for communication between the
subcloud and system controller if it is defined.

The management network will still exist on the subcloud, but
will be a private network.

This commit contains logic to choose the most appropriate
keystone auth url and admin endpoint required for subcloud
administration depending on whether the admin network is
present or not.

Note: Corresponding puppet review:
https://review.opendev.org/c/starlingx/stx-puppet/+/865288

Test Plan:

- Bootstrap and install DC subcloud with admin network defined.
  PASS: Ensure the openstack admin endpoints on both the subcloud
        and system controller for the affected services use the
        admin subnet of the subcloud
  PASS: Ensure the subcloud can become online and in-sync using
        the admin network.

Regression:

- AIO-SX: On a non-DC system, ensure the openstack endpoints for
          the various services are not impacted by the change.

- Bootstrap and install DC subcloud with no admin network defined.
  PASS: Ensure the openstack admin endpoints on both the subcloud
        and system controller for the affected services use the
        management subnet of the subcloud (no impact)
  PASS: Ensure the subcloud can become online and in-sync with the
        management network (no impact).

Depends-On: https://review.opendev.org/c/starlingx/config/+/863033

Story: 2010319
Task: 46910

Signed-off-by: Steven Webster <steven.webster@windriver.com>
Change-Id: Icf4c7c97ed69c74e6827c63614cb44abca28e38a
This commit is contained in:
Steven Webster 2022-11-22 14:48:35 +00:00
parent 1c056d966d
commit d0c8907104
15 changed files with 191 additions and 37 deletions

View File

@ -16,7 +16,7 @@
# License for the specific language governing permissions and limitations
# under the License.
#
# Copyright (c) 2013-2022 Wind River Systems, Inc.
# Copyright (c) 2013-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -194,7 +194,10 @@ class InterfaceNetworkController(rest.RestController):
ethernet_port_mac = tmp_interface['imac']
_update_host_mgmt_mac(host, ethernet_port_mac)
cutils.perform_distributed_cloud_config(pecan.request.dbapi,
interface_id)
interface_id)
elif network_type == constants.NETWORK_TYPE_ADMIN:
cutils.perform_distributed_cloud_config(pecan.request.dbapi,
interface_id)
elif network_type == constants.NETWORK_TYPE_OAM:
pecan.request.rpcapi.initialize_oam_config(pecan.request.context, host)
@ -328,16 +331,17 @@ class InterfaceNetworkController(rest.RestController):
raise wsme.exc.ClientSideError(msg)
def _check_network_type_and_interface_type(self, interface, network_type):
# Make sure network type 'mgmt', with if type 'ae',
# Make sure network type 'mgmt' or 'admin', with if type 'ae',
# can only be in ae mode 'active_standby' or '802.3ad'
if (network_type == constants.NETWORK_TYPE_MGMT):
valid_mgmt_aemode = [constants.AE_MODE_LACP,
constants.AE_MODE_ACTIVE_STANDBY]
if (network_type in [constants.NETWORK_TYPE_MGMT,
constants.NETWORK_TYPE_ADMIN]):
valid_aemode = [constants.AE_MODE_LACP,
constants.AE_MODE_ACTIVE_STANDBY]
if (interface.iftype == constants.INTERFACE_TYPE_AE and
interface.aemode not in valid_mgmt_aemode):
interface.aemode not in valid_aemode):
msg = _("Device interface with network type {}, and interface "
"type 'aggregated ethernet' must be in mode {}").format(
network_type, ', '.join(valid_mgmt_aemode))
network_type, ', '.join(valid_aemode))
raise wsme.exc.ClientSideError(msg)
# Make sure network type 'oam' or 'cluster-host', with if type 'ae',
# can only be in ae mode 'active_standby' or 'balanced' or '802.3ad'

View File

@ -15,7 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
#
# Copyright (c) 2015-2022 Wind River Systems, Inc.
# Copyright (c) 2015-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -165,6 +165,13 @@ class NetworkController(rest.RestController):
networks = pecan.request.dbapi.networks_get_by_type(networktype)
if networks:
raise exception.NetworkAlreadyExists(type=networktype)
if (networktype == constants.NETWORK_TYPE_ADMIN and
utils.get_distributed_cloud_role() !=
constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD):
msg = _("Network of type {} restricted to distributed cloud "
"role of {}."
.format(networktype, constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD))
raise wsme.exc.ClientSideError(msg)
def _check_network_pool(self, pool):
# ensure address pool exists and is not already inuse

View File

@ -18,7 +18,7 @@
# License for the specific language governing permissions and limitations
# under the License.
#
# Copyright (c) 2013-2022 Wind River Systems, Inc.
# Copyright (c) 2013-2023 Wind River Systems, Inc.
#
@ -1801,10 +1801,18 @@ def perform_distributed_cloud_config(dbapi, mgmt_iface_id):
# for local & reachable gateway etc, as config_subcloud
# will have already done these checks before allowing
# the system controller gateway into the database.
cc_gtwy_addr_name = '%s-%s' % (
constants.SYSTEM_CONTROLLER_GATEWAY_IP_NAME,
constants.NETWORK_TYPE_MGMT)
try:
# Prefer admin network
dbapi.network_get_by_type(
constants.NETWORK_TYPE_ADMIN)
cc_gtwy_addr_name = '%s-%s' % (
constants.SYSTEM_CONTROLLER_GATEWAY_IP_NAME,
constants.NETWORK_TYPE_ADMIN)
except exception.NetworkTypeNotFound:
cc_gtwy_addr_name = '%s-%s' % (
constants.SYSTEM_CONTROLLER_GATEWAY_IP_NAME,
constants.NETWORK_TYPE_MGMT)
pass
try:
cc_gtwy_addr = dbapi.address_get_by_name(

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2018 Wind River Systems, Inc.
# Copyright (c) 2018-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -89,7 +89,13 @@ class BarbicanPuppet(openstack.OpenstackBasePuppet):
return self._format_private_endpoint(self.SERVICE_PORT)
def get_admin_url(self):
return self._format_admin_endpoint(self.SERVICE_PORT)
if (self._distributed_cloud_role() ==
constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD):
return self._format_admin_endpoint(
self.SERVICE_PORT,
address=self._get_subcloud_endpoint_address())
else:
return self._format_admin_endpoint(self.SERVICE_PORT)
def get_region_name(self):
return self._get_service_region_name(self.SERVICE_NAME)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2017-2018 Wind River Systems, Inc.
# Copyright (c) 2017-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -196,6 +196,11 @@ class BasePuppet(object):
constants.CONTROLLER_HOSTNAME, constants.NETWORK_TYPE_OAM)
return address.address
def _get_admin_address(self):
address = self._get_address_by_name(
constants.CONTROLLER_HOSTNAME, constants.NETWORK_TYPE_ADMIN)
return address.address
def _get_cluster_host_address(self):
address = self._get_address_by_name(
constants.CONTROLLER_HOSTNAME, constants.NETWORK_TYPE_CLUSTER_HOST)
@ -207,6 +212,14 @@ class BasePuppet(object):
subnet = address.address + '/' + address.prefix
return subnet
def _get_subcloud_endpoint_address(self):
try:
address = self._format_url_address(self._get_admin_address())
except exception.AddressNotFoundByName:
address = self._format_url_address(self._get_management_address())
pass
return address
def _get_host_cpu_list(self, host, function=None, threads=False):
"""
Retreive a list of CPUs for the host, filtered by function and thread

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2019-2022 Wind River Systems, Inc.
# Copyright (c) 2019-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -153,8 +153,15 @@ class DCDBsyncPuppet(openstack.OpenstackBasePuppet):
path=self.SERVICE_PATH)
def get_admin_url(self):
return self._format_admin_endpoint(self.SERVICE_PORT,
path=self.SERVICE_PATH)
if (self._distributed_cloud_role() ==
constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD):
return self._format_admin_endpoint(
self.SERVICE_PORT,
address=self._get_subcloud_endpoint_address(),
path=self.SERVICE_PATH)
else:
return self._format_admin_endpoint(self.SERVICE_PORT,
path=self.SERVICE_PATH)
def get_region_name(self):
return self._get_service_region_name(self.SERVICE_NAME)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2018-2021 Wind River Systems, Inc.
# Copyright (c) 2018-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -114,7 +114,13 @@ class FmPuppet(openstack.OpenstackBasePuppet):
return self._format_private_endpoint(self.SERVICE_PORT)
def get_admin_url(self):
return self._format_admin_endpoint(self.SERVICE_PORT)
if (self._distributed_cloud_role() ==
constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD):
return self._format_admin_endpoint(
self.SERVICE_PORT,
address=self._get_subcloud_endpoint_address())
else:
return self._format_admin_endpoint(self.SERVICE_PORT)
def get_region_name(self):
return self._get_service_region_name(self.SERVICE_NAME)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2017 Wind River Systems, Inc.
# Copyright (c) 2017-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -120,8 +120,15 @@ class SystemInventoryPuppet(openstack.OpenstackBasePuppet):
path=self.SERVICE_PATH)
def get_admin_url(self):
return self._format_admin_endpoint(self.SERVICE_PORT,
path=self.SERVICE_PATH)
if (self._distributed_cloud_role() ==
constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD):
return self._format_admin_endpoint(
self.SERVICE_PORT,
address=self._get_subcloud_endpoint_address(),
path=self.SERVICE_PATH)
else:
return self._format_admin_endpoint(self.SERVICE_PORT,
path=self.SERVICE_PATH)
def get_region_name(self):
return self._get_service_region_name(self.SERVICE_NAME)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2017 Wind River Systems, Inc.
# Copyright (c) 2017-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -258,7 +258,13 @@ class KeystonePuppet(openstack.OpenstackBasePuppet):
self.SERVICE_TYPE in self._get_shared_services()):
return self._get_admin_url_from_service_config(self.SERVICE_NAME)
else:
return self._format_admin_endpoint(self.SERVICE_PORT)
if (self._distributed_cloud_role() ==
constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD):
return self._format_admin_endpoint(
self.SERVICE_PORT,
address=self._get_subcloud_endpoint_address())
else:
return self._format_admin_endpoint(self.SERVICE_PORT)
def get_auth_address(self):
if self._region_config():

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2017-2022 Wind River Systems, Inc.
# Copyright (c) 2017-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -323,4 +323,10 @@ class NfvPuppet(openstack.OpenstackBasePuppet):
return self._format_private_endpoint(self.SERVICE_PORT)
def get_admin_url(self):
return self._format_admin_endpoint(self.SERVICE_PORT)
if (self._distributed_cloud_role() ==
constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD):
return self._format_admin_endpoint(
self.SERVICE_PORT,
address=self._get_subcloud_endpoint_address())
else:
return self._format_admin_endpoint(self.SERVICE_PORT)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2017 Wind River Systems, Inc.
# Copyright (c) 2017-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -88,7 +88,13 @@ class PatchingPuppet(openstack.OpenstackBasePuppet):
return self._format_private_endpoint(self.SERVICE_PORT)
def get_admin_url(self):
return self._format_admin_endpoint(self.SERVICE_PORT)
if (self._distributed_cloud_role() ==
constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD):
return self._format_admin_endpoint(
self.SERVICE_PORT,
address=self._get_subcloud_endpoint_address())
else:
return self._format_admin_endpoint(self.SERVICE_PORT)
def get_region_name(self):
return self._get_service_region_name(self.SERVICE_NAME)

View File

@ -1,4 +1,4 @@
# Copyright (c) 2017-2022 Wind River Systems, Inc.
# Copyright (c) 2017-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -208,6 +208,14 @@ class PlatformPuppet(base.BasePuppet):
constants.CONTROLLER, constants.NETWORK_TYPE_OAM)
private_address = self._get_address_by_name(
constants.CONTROLLER, constants.NETWORK_TYPE_MGMT)
try:
private_dc_address = self._get_address_by_name(
constants.CONTROLLER, constants.NETWORK_TYPE_ADMIN)
except exception.AddressNotFoundByName:
private_dc_address = self._get_address_by_name(
constants.CONTROLLER, constants.NETWORK_TYPE_MGMT)
pass
public_address_url = self._format_url_address(public_address.address)
https_enabled = self._https_enabled()
@ -218,6 +226,8 @@ class PlatformPuppet(base.BasePuppet):
public_address_url,
'platform::haproxy::params::private_ip_address':
private_address.address,
'platform::haproxy::params::private_dc_ip_address':
private_dc_address.address,
'platform::haproxy::params::enable_https':
https_enabled,
}

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2022 Wind River Systems, Inc.
# Copyright (c) 2022-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -152,12 +152,18 @@ class SssdPuppet(base.BasePuppet):
% parameter_name)
return None
def _get_network_type(self):
return self.dbapi.network_get_by_type(constants.NETWORK_TYPE_MGMT)
def _get_network_type(self, network_type):
return self.dbapi.network_get_by_type(network_type)
def _is_host_address_ipv6(self):
addr_pool = self.dbapi.address_pool_get(self._get_network_type().pool_uuid)
try:
# Subclouds may be using the optional admin network
network = self._get_network_type(constants.NETWORK_TYPE_ADMIN)
except exception.NetworkTypeNotFound:
network = self._get_network_type(constants.NETWORK_TYPE_MGMT)
pass
addr_pool = self.dbapi.address_pool_get(network.pool_uuid)
if addr_pool.family == constants.IPV6_FAMILY:
return True
else:

View File

@ -107,6 +107,19 @@ class InterfaceNetworkTestCase(base.FunctionalTest):
prefix=24,
name='controller-0-pxeboot',
address_pool_id=self.address_pool_pxeboot.id)
self.address_pool_admin = dbutils.create_test_address_pool(
id=5,
network='192.168.208.0',
name='admin',
ranges=[['192.168.208.2', '192.168.208.254']],
prefix=24)
self.admin_network = dbutils.create_test_network(
id=5,
name='admin',
type=constants.NETWORK_TYPE_ADMIN,
link_capacity=10000,
vlan_id=8,
address_pool_id=self.address_pool_admin.id)
def _post_and_check(self, ndict, expect_errors=False):
response = self.post_json('%s' % self._get_path(), ndict,
@ -376,3 +389,18 @@ class InterfaceNetworkCreateTestCase(InterfaceNetworkTestCase):
interface_uuid=controller_interface.uuid,
network_uuid=self.mgmt_network.uuid)
self._post_and_check(controller_interface_network, expect_errors=True)
# Expected error: Device interface with network type ___, and interface type
# 'aggregated ethernet' must be in mode '802.3ad'
def test_aemode_invalid_admin(self):
controller_interface = dbutils.create_test_interface(
ifname='name',
forihostid=self.controller.id,
ifclass=constants.INTERFACE_CLASS_PLATFORM,
iftype=constants.INTERFACE_TYPE_AE,
aemode='balanced',
txhashpolicy='layer2')
controller_interface_network = dbutils.post_get_test_interface_network(
interface_uuid=controller_interface.uuid,
network_uuid=self.admin_network.uuid)
self._post_and_check(controller_interface_network, expect_errors=True)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2020-2022 Wind River Systems, Inc.
# Copyright (c) 2020-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -183,6 +183,22 @@ class TestPostMixin(NetworkTestCase):
self.assertIn("Network of type %s already exists." % network_type,
response.json['error_message'])
def _test_create_network_fail_subcloud_only(self, name, network_type, subnet):
address_pool_id = self._create_test_address_pool(name, subnet)['uuid']
ndict = self.get_post_object(network_type, address_pool_id)
response = self.post_json(self.API_PREFIX,
ndict,
headers=self.API_HEADERS,
expect_errors=True)
# Check HTTP response is failed
self.assertEqual('application/json', response.content_type)
self.assertEqual(response.status_code, http_client.BAD_REQUEST)
self.assertIn("Network of type %s restricted to distributed cloud "
"role of subcloud." % network_type,
response.json['error_message'])
def test_create_success_system_controller_oam(self):
self._create_test_host(constants.CONTROLLER)
m = mock.Mock()
@ -272,11 +288,23 @@ class TestPostMixin(NetworkTestCase):
self.storage_subnet)
def test_create_success_admin(self):
p = mock.patch('sysinv.api.controllers.v1.utils.get_distributed_cloud_role')
self.mock_utils_get_distributed_cloud_role = p.start()
self.mock_utils_get_distributed_cloud_role.return_value = \
constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD
self.addCleanup(p.stop)
self._test_create_network_success(
'admin',
constants.NETWORK_TYPE_ADMIN,
self.admin_subnet)
def test_create_failure_admin_non_subcloud(self):
self._test_create_network_fail_subcloud_only(
'admin',
constants.NETWORK_TYPE_ADMIN,
self.admin_subnet)
def test_create_fail_duplicate_pxeboot(self):
self._test_create_network_fail_duplicate(
'pxeboot',
@ -320,6 +348,12 @@ class TestPostMixin(NetworkTestCase):
self.storage_subnet)
def test_create_fail_duplicate_admin(self):
p = mock.patch('sysinv.api.controllers.v1.utils.get_distributed_cloud_role')
self.mock_utils_get_distributed_cloud_role = p.start()
self.mock_utils_get_distributed_cloud_role.return_value = \
constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD
self.addCleanup(p.stop)
self._test_create_network_fail_duplicate(
'admin',
constants.NETWORK_TYPE_ADMIN,