diff --git a/releasenotes/notes/dhcp_start_stop_optional_for_remote_subnets-805b7d2ed7ed0863.yaml b/releasenotes/notes/dhcp_start_stop_optional_for_remote_subnets-805b7d2ed7ed0863.yaml new file mode 100644 index 000000000..355bd902d --- /dev/null +++ b/releasenotes/notes/dhcp_start_stop_optional_for_remote_subnets-805b7d2ed7ed0863.yaml @@ -0,0 +1,33 @@ +--- +features: + - | + The ``dhcp_start`` and ``dhcp_end`` options are now optional for subnet + definitions in the Undercloud configuration (``undercloud.conf``). + The the allocation_pools are calculated by removing the ``local_ip``, + ``gateway``, ``undercloud_admin_host``, ``undercloud_public_host`` and + ``inspection_iprange`` from the subnets full IP range. Allocation pools for + all remaining ranges will be configured. Additionally the new option + ``dhcp_exlcude`` can be used to exclude additional IP addresses and/or + IP address ranges, for example to exclude ``172.20.0.105`` and the range + ``172.20.0.210-172.20.0.219``:: + + dhcp_exclude = 172.20.0.105,172.20.0.210-172.20.0.219 + + * When ``dhcp_start`` is defined any addresses prior to this address is + also removed from the allocation pools. + * When ``dhcp_end`` is defined any addresses after this address is also + removed from the allocation pools. + + .. Note:: If the default cidr (``192.168.24.0/24``) is used for the local + subnet the ``dhcp_start`` and ``dhcp_end`` cannot simply be + removed to utilize the full address space of the subnet. This due + to the default values of ``dhcp_start`` and ``dhcp_end``. + - | + It is now possible to configure non-contiguous allocation pools for the + Undercloud ctlplane subnets. The ``dhcp_start`` and ``dhcp_end`` options + have been extended to allow a list of start and end address pairs. For + example to create allocation pools ``172.20.0.100-172.20.0.150`` and + ``172.20.0.200-172.20.0.250``:: + + dhcp_start = 172.20.0.100,172.20.0.200 + dhcp_end = 172.20.0.150,172.20.0.250 diff --git a/tripleoclient/config/undercloud.py b/tripleoclient/config/undercloud.py index cd6d03a36..d7dda298e 100644 --- a/tripleoclient/config/undercloud.py +++ b/tripleoclient/config/undercloud.py @@ -27,6 +27,27 @@ CONF = cfg.CONF # Control plane network name SUBNETS_DEFAULT = ['ctlplane-subnet'] +CIDR_HELP_STR = _( + 'Network CIDR for the Neutron-managed subnet for Overcloud instances.') +DHCP_START_HELP_STR = _( + 'Start of DHCP allocation range for PXE and DHCP of Overcloud instances ' + 'on this network.') +DHCP_END_HELP_STR = _( + 'End of DHCP allocation range for PXE and DHCP of Overcloud instances on ' + 'this network.') +DHCP_EXCLUDE_HELP_STR = _( + 'List of IP addresses or IP ranges to exclude from the subnets allocation ' + 'pool. Example: 192.168.24.50,192.168.24.80-192.168.24.90') +INSPECTION_IPRANGE_HELP_STR = _( + 'Temporary IP range that will be given to nodes on this network during ' + 'the inspection process. Should not overlap with the range defined by ' + 'dhcp_start and dhcp_end, but should be in the same ip subnet.') +GATEWAY_HELP_STR = _( + 'Network gateway for the Neutron-managed network for Overcloud instances ' + 'on this network.') +MASQUERADE_HELP_STR = _( + 'The network will be masqueraded for external access.') + # Deprecated options _deprecated_opt_network_gateway = [cfg.DeprecatedOpt( 'network_gateway', group='DEFAULT')] @@ -314,46 +335,57 @@ class UndercloudConfig(StandaloneConfig): _service_opts = self.get_undercloud_service_opts() return self.sort_opts(_base_opts + _service_opts) - def get_subnet_opts(self): + def get_local_subnet_opts(self): _subnets_opts = [ cfg.StrOpt('cidr', - default='192.168.24.0/24', + default=constants.CTLPLANE_CIDR_DEFAULT, deprecated_opts=_deprecated_opt_network_cidr, - help=_( - 'Network CIDR for the Neutron-managed subnet for ' - 'Overcloud instances.')), - cfg.StrOpt('dhcp_start', - default='192.168.24.5', - deprecated_opts=_deprecated_opt_dhcp_start, - help=_( - 'Start of DHCP allocation range for PXE and DHCP ' - 'of Overcloud instances on this network.')), - cfg.StrOpt('dhcp_end', - default='192.168.24.24', - deprecated_opts=_deprecated_opt_dhcp_end, - help=_('End of DHCP allocation range for PXE and DHCP ' - 'of Overcloud instances on this network.')), + help=CIDR_HELP_STR), + cfg.ListOpt('dhcp_start', + default=constants.CTLPLANE_DHCP_START_DEFAULT, + deprecated_opts=_deprecated_opt_dhcp_start, + help=DHCP_START_HELP_STR), + cfg.ListOpt('dhcp_end', + default=constants.CTLPLANE_DHCP_END_DEFAULT, + deprecated_opts=_deprecated_opt_dhcp_end, + help=DHCP_END_HELP_STR), + cfg.ListOpt('dhcp_exclude', + default=[], + help=DHCP_EXCLUDE_HELP_STR), cfg.StrOpt('inspection_iprange', - default='192.168.24.100,192.168.24.120', + default=constants.CTLPLANE_INSPECTION_IPRANGE_DEFAULT, deprecated_opts=_deprecated_opt_inspection_iprange, - help=_( - 'Temporary IP range that will be given to nodes on ' - 'this network during the inspection process. ' - 'Should not overlap with the range defined by ' - 'dhcp_start and dhcp_end, but should be in the ' - 'same ip subnet.' - )), + help=INSPECTION_IPRANGE_HELP_STR), cfg.StrOpt('gateway', - default='192.168.24.1', + default=constants.CTLPLANE_GATEWAY_DEFAULT, deprecated_opts=_deprecated_opt_network_gateway, - help=_( - 'Network gateway for the Neutron-managed network ' - 'for Overcloud instances on this network.')), + help=GATEWAY_HELP_STR), cfg.BoolOpt('masquerade', default=False, - help=_( - 'The network will be masqueraded for external ' - 'access.')), + help=MASQUERADE_HELP_STR), + ] + return self.sort_opts(_subnets_opts) + + def get_remote_subnet_opts(self): + _subnets_opts = [ + cfg.StrOpt('cidr', + help=CIDR_HELP_STR), + cfg.ListOpt('dhcp_start', + default=[], + help=DHCP_START_HELP_STR), + cfg.ListOpt('dhcp_end', + default=[], + help=DHCP_END_HELP_STR), + cfg.ListOpt('dhcp_exclude', + default=[], + help=DHCP_EXCLUDE_HELP_STR), + cfg.StrOpt('inspection_iprange', + help=INSPECTION_IPRANGE_HELP_STR), + cfg.StrOpt('gateway', + help=GATEWAY_HELP_STR), + cfg.BoolOpt('masquerade', + default=False, + help=MASQUERADE_HELP_STR), ] return self.sort_opts(_subnets_opts) @@ -363,7 +395,8 @@ def list_opts(): config = UndercloudConfig() _opts = config.get_opts() return [(None, copy.deepcopy(_opts)), - (SUBNETS_DEFAULT[0], copy.deepcopy(config.get_subnet_opts()))] + (SUBNETS_DEFAULT[0], + copy.deepcopy(config.get_local_subnet_opts()))] def load_global_config(): diff --git a/tripleoclient/constants.py b/tripleoclient/constants.py index f3d4c3224..f332eea19 100644 --- a/tripleoclient/constants.py +++ b/tripleoclient/constants.py @@ -82,3 +82,10 @@ ANSIBLE_VALIDATION_DIR = '/usr/share/openstack-tripleo-validations/validations' # The path to the local CA certificate installed on the undercloud LOCAL_CACERT_PATH = '/etc/pki/ca-trust/source/anchors/cm-local-ca.pem' + +# ctlplane network defaults +CTLPLANE_CIDR_DEFAULT = '192.168.24.0/24' +CTLPLANE_DHCP_START_DEFAULT = ['192.168.24.5'] +CTLPLANE_DHCP_END_DEFAULT = ['192.168.24.24'] +CTLPLANE_INSPECTION_IPRANGE_DEFAULT = '192.168.24.100,192.168.24.120' +CTLPLANE_GATEWAY_DEFAULT = '192.168.24.1' diff --git a/tripleoclient/tests/config/test_config_undercloud.py b/tripleoclient/tests/config/test_config_undercloud.py index 3d11ece78..657ce01d5 100644 --- a/tripleoclient/tests/config/test_config_undercloud.py +++ b/tripleoclient/tests/config/test_config_undercloud.py @@ -138,13 +138,18 @@ class TestUndercloudConfig(base.TestCase): self.assertEqual(expected, [x.name for x in ret]) def test_get_subnet_opts(self): - ret = self.config.get_subnet_opts() expected = ['cidr', 'dhcp_end', + 'dhcp_exclude', 'dhcp_start', 'gateway', 'inspection_iprange', 'masquerade'] + + ret = self.config.get_local_subnet_opts() + self.assertEqual(expected, [x.name for x in ret]) + + ret = self.config.get_remote_subnet_opts() self.assertEqual(expected, [x.name for x in ret]) def test_get_undercloud_service_opts(self): diff --git a/tripleoclient/tests/v1/undercloud/test_config.py b/tripleoclient/tests/v1/undercloud/test_config.py index 8a6b97fe5..64d20f6dd 100644 --- a/tripleoclient/tests/v1/undercloud/test_config.py +++ b/tripleoclient/tests/v1/undercloud/test_config.py @@ -123,8 +123,9 @@ class TestNetworkSettings(base.TestCase): self.grp0 = cfg.OptGroup(name='ctlplane-subnet', title='ctlplane-subnet') self.opts = [cfg.StrOpt('cidr'), - cfg.StrOpt('dhcp_start'), - cfg.StrOpt('dhcp_end'), + cfg.ListOpt('dhcp_start'), + cfg.ListOpt('dhcp_end'), + cfg.ListOpt('dhcp_exclude'), cfg.StrOpt('inspection_iprange'), cfg.StrOpt('gateway'), cfg.BoolOpt('masquerade')] @@ -134,6 +135,7 @@ class TestNetworkSettings(base.TestCase): self.conf.config(cidr='192.168.24.0/24', dhcp_start='192.168.24.5', dhcp_end='192.168.24.24', + dhcp_exclude=[], inspection_iprange='192.168.24.100,192.168.24.120', gateway='192.168.24.1', masquerade=False, @@ -153,12 +155,180 @@ class TestNetworkSettings(base.TestCase): 'MasqueradeNetworks': {}, 'UndercloudCtlplaneSubnets': { 'ctlplane-subnet': { + 'AllocationPools': [ + {'start': '192.168.24.5', 'end': '192.168.24.24'}], 'DhcpRangeEnd': '192.168.24.24', 'DhcpRangeStart': '192.168.24.5', 'NetworkCidr': '192.168.24.0/24', 'NetworkGateway': '192.168.24.1'}}} self.assertEqual(expected, env) + def test_start_end_all_addresses(self): + self.conf.config(dhcp_start='192.168.24.0', + dhcp_end='192.168.24.255', + group='ctlplane-subnet') + env = {} + undercloud_config._process_network_args(env) + expected = { + 'ControlPlaneStaticRoutes': [], + 'DnsServers': '', + 'IronicInspectorSubnets': [ + {'gateway': '192.168.24.1', + 'ip_range': '192.168.24.100,192.168.24.120', + 'netmask': '255.255.255.0', + 'tag': 'ctlplane-subnet'}], + 'MasqueradeNetworks': {}, + 'UndercloudCtlplaneSubnets': { + 'ctlplane-subnet': { + 'AllocationPools': [ + {'start': '192.168.24.4', 'end': '192.168.24.99'}, + {'start': '192.168.24.121', 'end': '192.168.24.254'}], + 'DhcpRangeEnd': '192.168.24.255', + 'DhcpRangeStart': '192.168.24.0', + 'NetworkCidr': '192.168.24.0/24', + 'NetworkGateway': '192.168.24.1'}}} + self.assertEqual(expected, env) + + def test_ignore_dhcp_start_end_if_default_but_cidr_not_default(self): + self.conf.config(cidr='192.168.10.0/24', + inspection_iprange='192.168.10.100,192.168.10.120', + gateway='192.168.10.1', + group='ctlplane-subnet') + env = {} + undercloud_config._process_network_args(env) + expected = { + 'ControlPlaneStaticRoutes': [], + 'DnsServers': '', + 'IronicInspectorSubnets': [ + {'gateway': '192.168.10.1', + 'ip_range': '192.168.10.100,192.168.10.120', + 'netmask': '255.255.255.0', + 'tag': 'ctlplane-subnet'}], + 'MasqueradeNetworks': {}, + 'UndercloudCtlplaneSubnets': { + 'ctlplane-subnet': { + 'AllocationPools': [ + {'start': '192.168.10.2', 'end': '192.168.10.99'}, + {'start': '192.168.10.121', 'end': '192.168.10.254'}], + 'DhcpRangeEnd': '192.168.24.24', + 'DhcpRangeStart': '192.168.24.5', + 'NetworkCidr': '192.168.10.0/24', + 'NetworkGateway': '192.168.10.1'}}} + self.assertEqual(expected, env) + + def test_dhcp_exclude(self): + self.conf.config(cidr='192.168.10.0/24', + inspection_iprange='192.168.10.100,192.168.10.120', + gateway='192.168.10.1', + dhcp_exclude=['192.168.10.50', + '192.168.10.80-192.168.10.89'], + group='ctlplane-subnet') + env = {} + undercloud_config._process_network_args(env) + expected = { + 'ControlPlaneStaticRoutes': [], + 'DnsServers': '', + 'IronicInspectorSubnets': [ + {'gateway': '192.168.10.1', + 'ip_range': '192.168.10.100,192.168.10.120', + 'netmask': '255.255.255.0', + 'tag': 'ctlplane-subnet'}], + 'MasqueradeNetworks': {}, + 'UndercloudCtlplaneSubnets': { + 'ctlplane-subnet': { + 'AllocationPools': [ + {'start': '192.168.10.2', 'end': '192.168.10.49'}, + {'start': '192.168.10.51', 'end': '192.168.10.79'}, + {'start': '192.168.10.90', 'end': '192.168.10.99'}, + {'start': '192.168.10.121', 'end': '192.168.10.254'}], + 'DhcpRangeEnd': '192.168.24.24', + 'DhcpRangeStart': '192.168.24.5', + 'NetworkCidr': '192.168.10.0/24', + 'NetworkGateway': '192.168.10.1'}}} + self.assertEqual(expected, env) + + def test_no_dhcp_start_no_dhcp_end(self): + self.conf.config(dhcp_start=[], + dhcp_end=[], + group='ctlplane-subnet') + env = {} + undercloud_config._process_network_args(env) + expected = { + 'ControlPlaneStaticRoutes': [], + 'DnsServers': '', + 'IronicInspectorSubnets': [ + {'gateway': '192.168.24.1', + 'ip_range': '192.168.24.100,192.168.24.120', + 'netmask': '255.255.255.0', + 'tag': 'ctlplane-subnet'}], + 'MasqueradeNetworks': {}, + 'UndercloudCtlplaneSubnets': { + 'ctlplane-subnet': { + 'AllocationPools': [ + {'start': '192.168.24.4', 'end': '192.168.24.99'}, + {'start': '192.168.24.121', 'end': '192.168.24.254'}], + 'NetworkCidr': '192.168.24.0/24', + 'NetworkGateway': '192.168.24.1'}}} + self.assertEqual(expected, env) + + def test_dhcp_start_no_dhcp_end(self): + self.conf.config(dhcp_start='192.168.24.10', + dhcp_end=[], + group='ctlplane-subnet') + env = {} + undercloud_config._process_network_args(env) + expected = { + 'ControlPlaneStaticRoutes': [], + 'DnsServers': '', + 'IronicInspectorSubnets': [ + {'gateway': '192.168.24.1', + 'ip_range': '192.168.24.100,192.168.24.120', + 'netmask': '255.255.255.0', + 'tag': 'ctlplane-subnet'}], + 'MasqueradeNetworks': {}, + 'UndercloudCtlplaneSubnets': { + 'ctlplane-subnet': { + 'AllocationPools': [ + {'start': '192.168.24.10', 'end': '192.168.24.99'}, + {'start': '192.168.24.121', 'end': '192.168.24.254'}], + # TODO(hjensas): Remove DhcpRangeStart and DhcpRangeEnd + # once change: Ifdf3e9d22766c1b5ede151979b93754a3d244cc3 is + # merged and THT uses AllocationPools. + 'DhcpRangeStart': '192.168.24.10', + 'NetworkCidr': '192.168.24.0/24', + 'NetworkGateway': '192.168.24.1'}} + } + self.assertEqual(expected, env) + + def test_dhcp_end_no_dhcp_start(self): + self.conf.config(dhcp_start=[], + dhcp_end='192.168.24.220', + group='ctlplane-subnet') + env = {} + undercloud_config._process_network_args(env) + expected = { + 'ControlPlaneStaticRoutes': [], + 'DnsServers': '', + 'IronicInspectorSubnets': [ + {'gateway': '192.168.24.1', + 'ip_range': '192.168.24.100,192.168.24.120', + 'netmask': '255.255.255.0', + 'tag': 'ctlplane-subnet'}], + 'MasqueradeNetworks': {}, + 'UndercloudCtlplaneSubnets': { + 'ctlplane-subnet': { + 'AllocationPools': [ + {'start': '192.168.24.4', 'end': '192.168.24.99'}, + {'start': '192.168.24.121', 'end': '192.168.24.220'}], + # TODO(hjensas): Remove DhcpRangeStart and DhcpRangeEnd + # once change: Ifdf3e9d22766c1b5ede151979b93754a3d244cc3 is + # merged and THT uses AllocationPools. + 'DhcpRangeEnd': '192.168.24.220', + 'NetworkCidr': '192.168.24.0/24', + 'NetworkGateway': '192.168.24.1'}} + } + self.assertEqual(expected, env) + def test_routed_network(self): self.conf.config(subnets=['ctlplane-subnet', 'subnet1', 'subnet2']) self.conf.register_opts(self.opts, group=self.grp1) @@ -168,6 +338,7 @@ class TestNetworkSettings(base.TestCase): self.conf.config(cidr='192.168.10.0/24', dhcp_start='192.168.10.10', dhcp_end='192.168.10.99', + dhcp_exclude=[], inspection_iprange='192.168.10.100,192.168.10.189', gateway='192.168.10.254', masquerade=True, @@ -175,6 +346,7 @@ class TestNetworkSettings(base.TestCase): self.conf.config(cidr='192.168.20.0/24', dhcp_start='192.168.20.10', dhcp_end='192.168.20.99', + dhcp_exclude=[], inspection_iprange='192.168.20.100,192.168.20.189', gateway='192.168.20.254', masquerade=True, @@ -213,16 +385,25 @@ class TestNetworkSettings(base.TestCase): 'UndercloudCtlplaneSubnets': { # The ctlplane-subnet subnet have defaults 'ctlplane-subnet': { + 'AllocationPools': [ + {'start': '192.168.24.5', 'end': '192.168.24.24'}], + # TODO(hjensas): Remove DhcpRangeStart and DhcpRangeEnd + # once change: Ifdf3e9d22766c1b5ede151979b93754a3d244cc3 is + # merged and THT uses AllocationPools. 'DhcpRangeEnd': '192.168.24.24', 'DhcpRangeStart': '192.168.24.5', 'NetworkCidr': '192.168.24.0/24', 'NetworkGateway': '192.168.24.1'}, 'subnet1': { + 'AllocationPools': [ + {'start': '192.168.10.10', 'end': '192.168.10.99'}], 'DhcpRangeEnd': '192.168.10.99', 'DhcpRangeStart': '192.168.10.10', 'NetworkCidr': '192.168.10.0/24', 'NetworkGateway': '192.168.10.254'}, 'subnet2': { + 'AllocationPools': [ + {'start': '192.168.20.10', 'end': '192.168.20.99'}], 'DhcpRangeEnd': '192.168.20.99', 'DhcpRangeStart': '192.168.20.10', 'NetworkCidr': '192.168.20.0/24', @@ -238,12 +419,14 @@ class TestNetworkSettings(base.TestCase): self.conf.config(cidr='192.168.10.0/24', dhcp_start='192.168.10.10', dhcp_end='192.168.10.99', + dhcp_exclude=[], inspection_iprange='192.168.10.100,192.168.10.189', gateway='192.168.10.254', group='subnet1') self.conf.config(cidr='192.168.20.0/24', dhcp_start='192.168.20.10', dhcp_end='192.168.20.99', + dhcp_exclude=[], inspection_iprange='192.168.20.100,192.168.20.189', gateway='192.168.20.254', group='subnet2') @@ -272,16 +455,25 @@ class TestNetworkSettings(base.TestCase): 'UndercloudCtlplaneSubnets': { # The ctlplane-subnet subnet have defaults 'ctlplane-subnet': { + 'AllocationPools': [ + {'start': '192.168.24.5', 'end': '192.168.24.24'}], + # TODO(hjensas): Remove DhcpRangeStart and DhcpRangeEnd + # once change: Ifdf3e9d22766c1b5ede151979b93754a3d244cc3 is + # merged and THT uses AllocationPools. 'DhcpRangeEnd': '192.168.24.24', 'DhcpRangeStart': '192.168.24.5', 'NetworkCidr': '192.168.24.0/24', 'NetworkGateway': '192.168.24.1'}, 'subnet1': { + 'AllocationPools': [ + {'start': '192.168.10.10', 'end': '192.168.10.99'}], 'DhcpRangeEnd': '192.168.10.99', 'DhcpRangeStart': '192.168.10.10', 'NetworkCidr': '192.168.10.0/24', 'NetworkGateway': '192.168.10.254'}, 'subnet2': { + 'AllocationPools': [ + {'start': '192.168.20.10', 'end': '192.168.20.99'}], 'DhcpRangeEnd': '192.168.20.99', 'DhcpRangeStart': '192.168.20.10', 'NetworkCidr': '192.168.20.0/24', @@ -290,6 +482,102 @@ class TestNetworkSettings(base.TestCase): } self.assertEqual(expected, env) + def test_no_allocation_pool_on_remote_network(self): + self.conf.config(subnets=['ctlplane-subnet', 'subnet1']) + self.conf.register_opts(self.opts, group=self.grp1) + self.conf.config(cidr='192.168.10.0/24', + dhcp_exclude=[], + inspection_iprange='192.168.10.200,192.168.10.254', + gateway='192.168.10.254', + masquerade=False, + group='subnet1') + env = {} + undercloud_config._process_network_args(env) + expected = { + 'ControlPlaneStaticRoutes': [ + {'ip_netmask': '192.168.10.0/24', 'next_hop': '192.168.24.1'}], + 'DnsServers': '', + 'IronicInspectorSubnets': [ + {'gateway': '192.168.24.1', + 'ip_range': '192.168.24.100,192.168.24.120', + 'netmask': '255.255.255.0', + 'tag': 'ctlplane-subnet'}, + {'gateway': '192.168.10.254', + 'ip_range': '192.168.10.200,192.168.10.254', + 'netmask': '255.255.255.0', + 'tag': 'subnet1'}, + ], + 'MasqueradeNetworks': {}, + 'UndercloudCtlplaneSubnets': { + # The ctlplane-subnet subnet have defaults + 'ctlplane-subnet': { + 'AllocationPools': [ + {'start': '192.168.24.5', 'end': '192.168.24.24'}], + # TODO(hjensas): Remove DhcpRangeStart and DhcpRangeEnd + # once change: Ifdf3e9d22766c1b5ede151979b93754a3d244cc3 is + # merged and THT uses AllocationPools. + 'DhcpRangeEnd': '192.168.24.24', + 'DhcpRangeStart': '192.168.24.5', + 'NetworkCidr': '192.168.24.0/24', + 'NetworkGateway': '192.168.24.1'}, + 'subnet1': { + 'AllocationPools': [ + {'start': '192.168.10.1', 'end': '192.168.10.199'}], + 'NetworkCidr': '192.168.10.0/24', + 'NetworkGateway': '192.168.10.254'} + } + } + self.assertEqual(expected, env) + + def test_no_allocation_pool_on_remote_network_three_pools(self): + self.conf.config(subnets=['ctlplane-subnet', 'subnet1']) + self.conf.register_opts(self.opts, group=self.grp1) + self.conf.config(cidr='192.168.10.0/24', + dhcp_exclude=[], + inspection_iprange='192.168.10.100,192.168.10.199', + gateway='192.168.10.222', + masquerade=False, + group='subnet1') + env = {} + undercloud_config._process_network_args(env) + expected = { + 'ControlPlaneStaticRoutes': [ + {'ip_netmask': '192.168.10.0/24', 'next_hop': '192.168.24.1'}], + 'DnsServers': '', + 'IronicInspectorSubnets': [ + {'gateway': '192.168.24.1', + 'ip_range': '192.168.24.100,192.168.24.120', + 'netmask': '255.255.255.0', + 'tag': 'ctlplane-subnet'}, + {'gateway': '192.168.10.222', + 'ip_range': '192.168.10.100,192.168.10.199', + 'netmask': '255.255.255.0', + 'tag': 'subnet1'}, + ], + 'MasqueradeNetworks': {}, + 'UndercloudCtlplaneSubnets': { + # The ctlplane-subnet subnet have defaults + 'ctlplane-subnet': { + 'AllocationPools': [ + {'start': '192.168.24.5', 'end': '192.168.24.24'}], + # TODO(hjensas): Remove DhcpRangeStart and DhcpRangeEnd + # once change: Ifdf3e9d22766c1b5ede151979b93754a3d244cc3 is + # merged and THT uses AllocationPools. + 'DhcpRangeEnd': '192.168.24.24', + 'DhcpRangeStart': '192.168.24.5', + 'NetworkCidr': '192.168.24.0/24', + 'NetworkGateway': '192.168.24.1'}, + 'subnet1': { + 'AllocationPools': [ + {'start': '192.168.10.1', 'end': '192.168.10.99'}, + {'start': '192.168.10.200', 'end': '192.168.10.221'}, + {'start': '192.168.10.223', 'end': '192.168.10.254'}], + 'NetworkCidr': '192.168.10.0/24', + 'NetworkGateway': '192.168.10.222'} + } + } + self.assertEqual(expected, env) + class TestTLSSettings(base.TestCase): def test_public_host_with_ip_should_give_ip_endpoint_environment(self): diff --git a/tripleoclient/tests/v1/undercloud/test_install_upgrade.py b/tripleoclient/tests/v1/undercloud/test_install_upgrade.py index f49366db3..ba862e361 100644 --- a/tripleoclient/tests/v1/undercloud/test_install_upgrade.py +++ b/tripleoclient/tests/v1/undercloud/test_install_upgrade.py @@ -199,6 +199,7 @@ class TestUndercloudInstall(TestPluginV1): self.conf.config(local_mtu='1234') self.conf.config(undercloud_nameservers=['8.8.8.8', '8.8.4.4']) self.conf.config(subnets='foo') + self.conf.config(local_subnet='foo') mock_masq.return_value = {'1.1.1.1/11': ['2.2.2.2/22']} mock_sroutes.return_value = {'ip_netmask': '1.1.1.1/11', 'next_hop': '1.1.1.1'} diff --git a/tripleoclient/v1/undercloud_config.py b/tripleoclient/v1/undercloud_config.py index 35696e8fe..e87fcfd25 100644 --- a/tripleoclient/v1/undercloud_config.py +++ b/tripleoclient/v1/undercloud_config.py @@ -77,8 +77,6 @@ PARAMETER_MAPPING = { SUBNET_PARAMETER_MAPPING = { 'cidr': 'NetworkCidr', 'gateway': 'NetworkGateway', - 'dhcp_start': 'DhcpRangeStart', - 'dhcp_end': 'DhcpRangeEnd', } THT_HOME = os.environ.get('THT_HOME', @@ -108,7 +106,10 @@ load_global_config() def _load_subnets_config_groups(): for group in CONF.subnets: g = cfg.OptGroup(name=group, title=group) - CONF.register_opts(config.get_subnet_opts(), group=g) + if group == CONF.local_subnet: + CONF.register_opts(config.get_local_subnet_opts(), group=g) + else: + CONF.register_opts(config.get_remote_subnet_opts(), group=g) LOG = logging.getLogger(__name__ + ".undercloud_config") @@ -248,6 +249,62 @@ def _generate_masquerade_networks(): return masqurade_networks +def _calculate_allocation_pools(subnet): + """Calculate subnet allocation pools + + Remove the gateway address, the inspection IP range and the undercloud IP's + from the subnets full IP range and return all remaining address ranges as + allocation pools. If dhcp_start and/or dhcp_end is defined, also remove + addresses before dhcp_start and addresses after dhcp_end. + """ + ip_network = netaddr.IPNetwork(subnet.cidr) + # NOTE(hjensas): Ignore the default dhcp_start and dhcp_end if cidr is not + # the default as well. I.e allow not specifying dhcp_start and dhcp_end. + if (subnet.cidr != constants.CTLPLANE_CIDR_DEFAULT + and subnet.dhcp_start == constants.CTLPLANE_DHCP_START_DEFAULT + and subnet.dhcp_end == constants.CTLPLANE_DHCP_END_DEFAULT): + subnet.dhcp_start, subnet.dhcp_end = None, None + if subnet.dhcp_start and subnet.dhcp_end: + ip_set = netaddr.IPSet() + for a, b in zip(subnet.dhcp_start, subnet.dhcp_end): + ip_set.add(netaddr.IPRange(netaddr.IPAddress(a), + netaddr.IPAddress(b))) + else: + ip_set = netaddr.IPSet(ip_network) + # Remove addresses before dhcp_start if defined + if subnet.dhcp_start: + a = netaddr.IPAddress(ip_network.first) + b = netaddr.IPAddress(subnet.dhcp_start[0]) - 1 + ip_set.remove(netaddr.IPRange(a, b)) + # Remove addresses after dhcp_end if defined + if subnet.dhcp_end: + a = netaddr.IPAddress(subnet.dhcp_end[0]) + 1 + b = netaddr.IPAddress(ip_network.last) + ip_set.remove(netaddr.IPRange(a, b)) + # Remove network address and broadcast address + ip_set.remove(ip_network.first) + ip_set.remove(ip_network.last) + # Remove gateway, local_ip, admin_host and public_host addresses + ip_set.remove(netaddr.IPAddress(subnet.get('gateway'))) + ip_set.remove(netaddr.IPNetwork(CONF.local_ip).ip) + ip_set.remove(netaddr.IPNetwork(CONF.undercloud_admin_host)) + ip_set.remove(netaddr.IPNetwork(CONF.undercloud_public_host)) + # Remove addresses in the inspection_iprange + inspect_start, inspect_end = subnet.get('inspection_iprange').split(',') + ip_set.remove(netaddr.IPRange(inspect_start, inspect_end)) + # Remove dhcp_exclude addresses and ip ranges + for exclude in subnet.dhcp_exclude: + if '-' in exclude: + exclude_start, exclude_end = exclude.split('-') + ip_set.remove(netaddr.IPRange(exclude_start, exclude_end)) + else: + ip_set.remove(netaddr.IPAddress(exclude)) + + return [{'start': netaddr.IPAddress(ip_range.first).format(), + 'end': netaddr.IPAddress(ip_range.last).format()} + for ip_range in list(ip_set.iter_ipranges())] + + def _process_network_args(env): """Populate the environment with network configuration.""" @@ -256,10 +313,22 @@ def _process_network_args(env): env['UndercloudCtlplaneSubnets'] = {} for subnet in CONF.subnets: s = CONF.get(subnet) - env['UndercloudCtlplaneSubnets'][subnet] = {} - for param_key, param_value in SUBNET_PARAMETER_MAPPING.items(): + env['UndercloudCtlplaneSubnets'][subnet] = { + 'AllocationPools': _calculate_allocation_pools(s) + } + # TODO(hjensas): Remove DhcpRangeStart and DhcpRangeEnd once change: + # Ifdf3e9d22766c1b5ede151979b93754a3d244cc3 is merged and THT uses + # AllocationPools. + if s.get('dhcp_start'): env['UndercloudCtlplaneSubnets'][subnet].update( - {param_value: s[param_key]}) + {'DhcpRangeStart': s.get('dhcp_start')[0]}) + if s.get('dhcp_end'): + env['UndercloudCtlplaneSubnets'][subnet].update( + {'DhcpRangeEnd': s.get('dhcp_end')[0]}) + for param_key, param_value in SUBNET_PARAMETER_MAPPING.items(): + if param_value: + env['UndercloudCtlplaneSubnets'][subnet].update( + {param_value: s[param_key]}) env['MasqueradeNetworks'] = _generate_masquerade_networks() env['DnsServers'] = ','.join(CONF['undercloud_nameservers']) diff --git a/tripleoclient/v1/undercloud_preflight.py b/tripleoclient/v1/undercloud_preflight.py index 8905ef155..c88d62c96 100644 --- a/tripleoclient/v1/undercloud_preflight.py +++ b/tripleoclient/v1/undercloud_preflight.py @@ -288,8 +288,15 @@ def _validate_in_cidr(subnet_props, subnet_name): raise FailedValidation(message) validate_addr_in_cidr(subnet_props.gateway, 'gateway') - validate_addr_in_cidr(subnet_props.dhcp_start, 'dhcp_start') - validate_addr_in_cidr(subnet_props.dhcp_end, 'dhcp_end') + # NOTE(hjensas): Ignore the default dhcp_start and dhcp_end if cidr is not + # the default as well. I.e allow not specifying dhcp_start and dhcp_end. + if not (subnet_props.cidr != constants.CTLPLANE_CIDR_DEFAULT and + subnet_props.dhcp_start == constants.CTLPLANE_DHCP_START_DEFAULT + and subnet_props.dhcp_end == constants.CTLPLANE_DHCP_END_DEFAULT): + for start in subnet_props.dhcp_start: + validate_addr_in_cidr(start, 'dhcp_start') + for end in subnet_props.dhcp_end: + validate_addr_in_cidr(end, 'dhcp_end') if subnet_name == CONF.local_subnet: validate_addr_in_cidr(str(netaddr.IPNetwork(CONF.local_ip).ip), 'local_ip') @@ -308,14 +315,26 @@ def _validate_in_cidr(subnet_props, subnet_name): require_ip=False) -def _validate_dhcp_range(subnet_props): - start = netaddr.IPAddress(subnet_props.dhcp_start) - end = netaddr.IPAddress(subnet_props.dhcp_end) - if start >= end: - message = (_('Invalid dhcp range specified, dhcp_start "{0}" does ' - 'not come before dhcp_end "{1}"').format(start, end)) +def _validate_dhcp_range(subnet_props, subnet_name): + len_dhcp_start = len(subnet_props.dhcp_start) + len_dhcp_end = len(subnet_props.dhcp_end) + if (len_dhcp_start > 1 or len_dhcp_end > 1 and + len_dhcp_start != len_dhcp_end): + message = (_('Number of elements in dhcp_start and dhcp_end must be ' + 'identical. Subnet "{0}" have "{1}" dhcp_start elements ' + 'and "{2}" dhcp_end elements.').format(subnet_name, + len_dhcp_start, + len_dhcp_end)) LOG.error(message) raise FailedValidation(message) + for a, b in zip(subnet_props.dhcp_start, subnet_props.dhcp_end): + start = netaddr.IPAddress(a) + end = netaddr.IPAddress(b) + if start >= end: + message = (_('Invalid dhcp range specified, dhcp_start "{0}" does ' + 'not come before dhcp_end "{1}"').format(start, end)) + LOG.error(message) + raise FailedValidation(message) def _validate_inspection_range(subnet_props): @@ -328,23 +347,6 @@ def _validate_inspection_range(subnet_props): raise FailedValidation(message) -def _validate_no_overlap(subnet_props): - """Validate the provisioning and inspection ip ranges do not overlap""" - dhcp_set = netaddr.IPSet(netaddr.IPRange(subnet_props.dhcp_start, - subnet_props.dhcp_end)) - inspection_set = netaddr.IPSet(netaddr.IPRange( - subnet_props.inspection_iprange.split(',')[0], - subnet_props.inspection_iprange.split(',')[1])) - if dhcp_set.intersection(inspection_set): - message = (_('Inspection DHCP range "{0}-{1} overlaps provisioning ' - 'DHCP range "{2}-{3}".') % - (subnet_props.inspection_iprange.split(',')[0], - subnet_props.inspection_iprange.split(',')[1], - subnet_props.dhcp_start, subnet_props.dhcp_end)) - LOG.error(message) - raise FailedValidation(message) - - def _validate_interface_exists(): """Validate the provided local interface exists""" if (not CONF.net_config_override @@ -528,11 +530,9 @@ def check(verbose_level, upgrade=False): _checking_status('Subnet "%s" is in CIDR' % subnet) _validate_in_cidr(s, subnet) _checking_status('DHCP range is in subnet "%s"' % subnet) - _validate_dhcp_range(s) + _validate_dhcp_range(s, subnet) _checking_status('Inspection range for subnet "%s"' % subnet) _validate_inspection_range(s) - _checking_status('Subnet "%s" has no overlap' % subnet) - _validate_no_overlap(s) _checking_status('IP addresses') _validate_ips() _checking_status('Network interfaces')