diff options
Diffstat (limited to 'apex/network')
-rw-r--r-- | apex/network/__init__.py | 0 | ||||
-rw-r--r-- | apex/network/ip_utils.py | 230 | ||||
-rw-r--r-- | apex/network/jumphost.py | 172 | ||||
-rw-r--r-- | apex/network/network_environment.py | 218 |
4 files changed, 620 insertions, 0 deletions
diff --git a/apex/network/__init__.py b/apex/network/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/apex/network/__init__.py diff --git a/apex/network/ip_utils.py b/apex/network/ip_utils.py new file mode 100644 index 00000000..ae60b705 --- /dev/null +++ b/apex/network/ip_utils.py @@ -0,0 +1,230 @@ +############################################################################## +# Copyright (c) 2016 Feng Pan (fpan@redhat.com) and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +import ipaddress +import subprocess +import re +import logging + + +def get_ip_range(start_offset=None, count=None, end_offset=None, + cidr=None, interface=None): + """ + Generate IP range for a network (cidr) or an interface. + + If CIDR is provided, it will take precedence over interface. In this case, + The entire CIDR IP address space is considered usable. start_offset will be + calculated from the network address, and end_offset will be calculated from + the last address in subnet. + + If interface is provided, the interface IP will be used to calculate + offsets: + - If the interface IP is in the first half of the address space, + start_offset will be calculated from the interface IP, and end_offset + will be calculated from end of address space. + - If the interface IP is in the second half of the address space, + start_offset will be calculated from the network address in the address + space, and end_offset will be calculated from the interface IP. + + 2 of start_offset, end_offset and count options must be provided: + - If start_offset and end_offset are provided, a range from + start_offset to end_offset will be returned. + - If count is provided, a range from either start_offset to + (start_offset+count) or (end_offset-count) to end_offset will be + returned. The IP range returned will be of size <count>. + Both start_offset and end_offset must be greater than 0. + + Returns IP range in the format of "first_addr,second_addr" or exception + is raised. + """ + if cidr: + if count and start_offset and not end_offset: + start_index = start_offset + end_index = start_offset + count - 1 + elif count and end_offset and not start_offset: + end_index = -1 - end_offset + start_index = -1 - end_index - count + 1 + elif start_offset and end_offset and not count: + start_index = start_offset + end_index = -1 - end_offset + else: + raise IPUtilsException("Argument error: must pass in exactly 2 of" + " start_offset, end_offset and count") + + start_ip = cidr[start_index] + end_ip = cidr[end_index] + network = cidr + elif interface: + network = interface.network + number_of_addr = network.num_addresses + if interface.ip < network[int(number_of_addr / 2)]: + if count and start_offset and not end_offset: + start_ip = interface.ip + start_offset + end_ip = start_ip + count - 1 + elif count and end_offset and not start_offset: + end_ip = network[-1 - end_offset] + start_ip = end_ip - count + 1 + elif start_offset and end_offset and not count: + start_ip = interface.ip + start_offset + end_ip = network[-1 - end_offset] + else: + raise IPUtilsException( + "Argument error: must pass in exactly 2 of" + " start_offset, end_offset and count") + else: + if count and start_offset and not end_offset: + start_ip = network[start_offset] + end_ip = start_ip + count - 1 + elif count and end_offset and not start_offset: + end_ip = interface.ip - end_offset + start_ip = end_ip - count + 1 + elif start_offset and end_offset and not count: + start_ip = network[start_offset] + end_ip = interface.ip - end_offset + else: + raise IPUtilsException( + "Argument error: must pass in exactly 2 of" + " start_offset, end_offset and count") + + else: + raise IPUtilsException("Must pass in cidr or interface to generate" + "ip range") + + range_result = _validate_ip_range(start_ip, end_ip, network) + if range_result: + ip_range = "{},{}".format(start_ip, end_ip) + return ip_range + else: + raise IPUtilsException("Invalid IP range: {},{} for network {}" + .format(start_ip, end_ip, network)) + + +def get_ip(offset, cidr=None, interface=None): + """ + Returns an IP in a network given an offset. + + Either cidr or interface must be provided, cidr takes precedence. + + If cidr is provided, offset is calculated from network address. + If interface is provided, offset is calculated from interface IP. + + offset can be positive or negative, but the resulting IP address must also + be contained in the same subnet, otherwise an exception will be raised. + + returns a IP address object. + """ + if cidr: + ip = cidr[0 + offset] + network = cidr + elif interface: + ip = interface.ip + offset + network = interface.network + else: + raise IPUtilsException("Must pass in cidr or interface to generate IP") + + if ip not in network: + raise IPUtilsException("IP {} not in network {}".format(ip, network)) + else: + return str(ip) + + +def get_interface(nic, address_family=4): + """ + Returns interface object for a given NIC name in the system + + Only global address will be returned at the moment. + + Returns interface object if an address is found for the given nic, + otherwise returns None. + """ + if not nic.strip(): + logging.error("empty nic name specified") + return None + output = subprocess.getoutput("/usr/sbin/ip -{} addr show {} scope global" + .format(address_family, nic)) + if address_family == 4: + pattern = re.compile("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}") + elif address_family == 6: + pattern = re.compile("([0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}/\d{1,3}") + else: + raise IPUtilsException("Invalid address family: {}" + .format(address_family)) + match = re.search(pattern, output) + if match: + logging.info("found interface {} ip: {}".format(nic, match.group())) + return ipaddress.ip_interface(match.group()) + else: + logging.info("interface ip not found! ip address output:\n{}" + .format(output)) + return None + + +def find_gateway(interface): + """ + Validate gateway on the system + + Ensures that the provided interface object is in fact configured as default + route on the system. + + Returns gateway IP (reachable from interface) if default route is found, + otherwise returns None. + """ + + address_family = interface.version + output = subprocess.getoutput("/usr/sbin/ip -{} route".format( + address_family)) + + pattern = re.compile("default\s+via\s+(\S+)\s+") + match = re.search(pattern, output) + + if match: + gateway_ip = match.group(1) + reverse_route_output = subprocess.getoutput("/usr/sbin/ip route get {}" + .format(gateway_ip)) + pattern = re.compile("{}.+src\s+{}".format(gateway_ip, interface.ip)) + if not re.search(pattern, reverse_route_output): + logging.warning("Default route doesn't match interface specified: " + "{}".format(reverse_route_output)) + return None + else: + return gateway_ip + else: + logging.warning("Can't find gateway address on system") + return None + + +def _validate_ip_range(start_ip, end_ip, cidr): + """ + Validates an IP range is in good order and the range is part of cidr. + + Returns True if validation succeeds, False otherwise. + """ + ip_range = "{},{}".format(start_ip, end_ip) + if end_ip <= start_ip: + logging.warning("IP range {} is invalid: end_ip should be greater " + "than starting ip".format(ip_range)) + return False + if start_ip not in ipaddress.ip_network(cidr): + logging.warning('start_ip {} is not in network {}' + .format(start_ip, cidr)) + return False + if end_ip not in ipaddress.ip_network(cidr): + logging.warning('end_ip {} is not in network {}'.format(end_ip, cidr)) + return False + + return True + + +class IPUtilsException(Exception): + def __init__(self, value): + self.value = value + + def __str__(self): + return self.value diff --git a/apex/network/jumphost.py b/apex/network/jumphost.py new file mode 100644 index 00000000..81562c7a --- /dev/null +++ b/apex/network/jumphost.py @@ -0,0 +1,172 @@ +############################################################################## +# Copyright (c) 2017 Tim Rozet (trozet@redhat.com) and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import logging +import os +import re +import shutil +import subprocess + +from apex.common.exceptions import ApexDeployException +from apex.network import ip_utils + +NET_MAP = { + 'admin': 'br-admin', + 'tenant': 'br-tenant', + 'external': 'br-external', + 'storage': 'br-storage', + 'api': 'br-api' +} + + +def configure_bridges(ns): + """ + Configures IP on jumphost bridges + :param ns: network_settings + :return: None + """ + bridge_networks = ['admin'] + if 'external' in ns.enabled_network_list: + bridge_networks.append('external') + for network in bridge_networks: + if network == 'external': + net_config = ns['networks'][network][0] + else: + net_config = ns['networks'][network] + cidr = net_config['cidr'] + interface = ip_utils.get_interface(NET_MAP[network], cidr.version) + + if interface: + logging.info("Bridge {} already configured with IP: {}".format( + NET_MAP[network], interface.ip)) + else: + logging.info("Will configure IP for {}".format(NET_MAP[network])) + ovs_ip = net_config['overcloud_ip_range'][1] + if cidr.version == 6: + ipv6_br_path = "/proc/sys/net/ipv6/conf/{}/disable_" \ + "ipv6".format(NET_MAP[network]) + try: + subprocess.check_call('echo', 0, '>', ipv6_br_path) + except subprocess.CalledProcessError: + logging.error("Unable to enable ipv6 on " + "bridge {}".format(NET_MAP[network])) + raise + try: + ip_prefix = "{}/{}".format(ovs_ip, cidr.prefixlen) + subprocess.check_call(['ip', 'addr', 'add', ip_prefix, 'dev', + NET_MAP[network]]) + subprocess.check_call(['ip', 'link', 'set', 'up', NET_MAP[ + network]]) + logging.info("IP configured: {} on bridge {}".format(ovs_ip, + NET_MAP[network])) + except subprocess.CalledProcessError: + logging.error("Unable to configure IP address on " + "bridge {}".format(NET_MAP[network])) + + +def attach_interface_to_ovs(bridge, interface, network): + """ + Attaches jumphost interface to OVS for baremetal deployments + :param bridge: bridge to attach to + :param interface: interface to attach to bridge + :param network: Apex network type for these interfaces + :return: None + """ + + net_cfg_path = '/etc/sysconfig/network-scripts' + if_file = os.path.join(net_cfg_path, "ifcfg-{}".format(interface)) + ovs_file = os.path.join(net_cfg_path, "ifcfg-{}".format(bridge)) + + logging.info("Attaching interface: {} to bridge: {} on network {}".format( + bridge, interface, network + )) + + try: + output = subprocess.check_output(['ovs-vsctl', 'list-ports', bridge], + stderr=subprocess.STDOUT) + if bridge in output: + logging.debug("Interface already attached to bridge") + return + except subprocess.CalledProcessError as e: + logging.error("Unable to dump ports for bridge: {}".format(bridge)) + logging.error("Error output: {}".format(e.output)) + raise + + if not os.path.isfile(if_file): + logging.error("Interface ifcfg not found: {}".format(if_file)) + raise FileNotFoundError("Interface file missing: {}".format(if_file)) + + ifcfg_params = { + 'IPADDR': '', + 'NETMASK': '', + 'GATEWAY': '', + 'METRIC': '', + 'DNS1': '', + 'DNS2': '', + 'PREFIX': '' + } + with open(if_file, 'r') as fh: + interface_output = fh.read() + + for param in ifcfg_params.keys(): + match = re.search("{}=(.*)\n".format(param), interface_output) + if match: + ifcfg_params[param] = match.group(1) + + if not ifcfg_params['IPADDR']: + logging.error("IPADDR missing in {}".format(if_file)) + raise ApexDeployException("IPADDR missing in {}".format(if_file)) + if not (ifcfg_params['NETMASK'] or ifcfg_params['PREFIX']): + logging.error("NETMASK/PREFIX missing in {}".format(if_file)) + raise ApexDeployException("NETMASK/PREFIX missing in {}".format( + if_file)) + if network == 'external' and not ifcfg_params['GATEWAY']: + logging.error("GATEWAY is required to be in {} for external " + "network".format(if_file)) + raise ApexDeployException("GATEWAY is required to be in {} for " + "external network".format(if_file)) + + shutil.move(if_file, "{}.orig".format(if_file)) + if_content = """DEVICE={} +DEVICETYPE=ovs +TYPE=OVSPort +PEERDNS=no +BOOTPROTO=static +NM_CONTROLLED=no +ONBOOT=yes +OVS_BRIDGE={} +PROMISC=yes""".format(interface, bridge) + + bridge_content = """DEVICE={} +DEVICETYPE=ovs +BOOTPROTO=static +ONBOOT=yes +TYPE=OVSBridge +PROMISC=yes""".format(bridge) + peer_dns = 'no' + for param, value in ifcfg_params.items(): + if value: + bridge_content += "\n{}={}".format(param, value) + if param == 'DNS1' or param == 'DNS2': + peer_dns = 'yes' + bridge_content += "\n{}={}".format('PEERDNS', peer_dns) + + logging.debug("New interface file content:\n{}".format(if_content)) + logging.debug("New bridge file content:\n{}".format(bridge_content)) + with open(if_file, 'w') as fh: + fh.write(if_content) + with open(ovs_file, 'w') as fh: + fh.write(bridge_content) + logging.info("New network ifcfg files written") + logging.info("Restarting Linux networking") + try: + subprocess.check_call(['systemctl', 'restart', 'network']) + except subprocess.CalledProcessError: + logging.error("Failed to restart Linux networking") + raise diff --git a/apex/network/network_environment.py b/apex/network/network_environment.py new file mode 100644 index 00000000..c2e9991a --- /dev/null +++ b/apex/network/network_environment.py @@ -0,0 +1,218 @@ +############################################################################## +# Copyright (c) 2016 Tim Rozet (trozet@redhat.com) and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import re + +import yaml + +from apex.settings.network_settings import NetworkSettings +from apex.common.constants import ( + CONTROLLER, + COMPUTE, + ADMIN_NETWORK, + TENANT_NETWORK, + STORAGE_NETWORK, + EXTERNAL_NETWORK, + API_NETWORK +) + +HEAT_NONE = 'OS::Heat::None' +PORTS = '/ports' +# Resources defined by <resource name>: <prefix> +EXTERNAL_RESOURCES = {'OS::TripleO::Network::External': None, + 'OS::TripleO::Network::Ports::ExternalVipPort': PORTS, + 'OS::TripleO::Controller::Ports::ExternalPort': PORTS, + 'OS::TripleO::Compute::Ports::ExternalPort': PORTS} +TENANT_RESOURCES = {'OS::TripleO::Network::Tenant': None, + 'OS::TripleO::Controller::Ports::TenantPort': PORTS, + 'OS::TripleO::Compute::Ports::TenantPort': PORTS} +STORAGE_RESOURCES = {'OS::TripleO::Network::Storage': None, + 'OS::TripleO::Network::Ports::StorageVipPort': PORTS, + 'OS::TripleO::Controller::Ports::StoragePort': PORTS, + 'OS::TripleO::Compute::Ports::StoragePort': PORTS} +API_RESOURCES = {'OS::TripleO::Network::InternalApi': None, + 'OS::TripleO::Network::Ports::InternalApiVipPort': PORTS, + 'OS::TripleO::Controller::Ports::InternalApiPort': PORTS, + 'OS::TripleO::Compute::Ports::InternalApiPort': PORTS} + +# A list of flags that will be set to true when IPv6 is enabled +IPV6_FLAGS = ["NovaIPv6", "MongoDbIPv6", "CorosyncIPv6", "CephIPv6", + "RabbitIPv6", "MemcachedIPv6"] + +reg = 'resource_registry' +param_def = 'parameter_defaults' + + +class NetworkEnvironment(dict): + """ + This class creates a Network Environment to be used in TripleO Heat + Templates. + + The class builds upon an existing network-environment file and modifies + based on a NetworkSettings object. + """ + def __init__(self, net_settings, filename, compute_pre_config=False, + controller_pre_config=False): + """ + Create Network Environment according to Network Settings + """ + init_dict = {} + if isinstance(filename, str): + with open(filename, 'r') as net_env_fh: + init_dict = yaml.safe_load(net_env_fh) + + super().__init__(init_dict) + if not isinstance(net_settings, NetworkSettings): + raise NetworkEnvException('Invalid Network Settings object') + + self._set_tht_dir() + + nets = net_settings['networks'] + + admin_cidr = nets[ADMIN_NETWORK]['cidr'] + admin_prefix = str(admin_cidr.prefixlen) + self[param_def]['ControlPlaneSubnetCidr'] = admin_prefix + self[param_def]['ControlPlaneDefaultRoute'] = \ + nets[ADMIN_NETWORK]['installer_vm']['ip'] + self[param_def]['EC2MetadataIp'] = \ + nets[ADMIN_NETWORK]['installer_vm']['ip'] + self[param_def]['DnsServers'] = net_settings['dns_servers'] + + if EXTERNAL_NETWORK in net_settings.enabled_network_list: + external_cidr = net_settings.get_network(EXTERNAL_NETWORK)['cidr'] + self[param_def]['ExternalNetCidr'] = str(external_cidr) + external_vlan = self._get_vlan(net_settings.get_network( + EXTERNAL_NETWORK)) + if isinstance(external_vlan, int): + self[param_def]['NeutronExternalNetworkBridge'] = '""' + self[param_def]['ExternalNetworkVlanID'] = external_vlan + external_range = net_settings.get_network(EXTERNAL_NETWORK)[ + 'overcloud_ip_range'] + self[param_def]['ExternalAllocationPools'] = \ + [{'start': str(external_range[0]), + 'end': str(external_range[1])}] + self[param_def]['ExternalInterfaceDefaultRoute'] = \ + net_settings.get_network(EXTERNAL_NETWORK)['gateway'] + + if external_cidr.version == 6: + postfix = '/external_v6.yaml' + else: + postfix = '/external.yaml' + else: + postfix = '/noop.yaml' + + # apply resource registry update for EXTERNAL_RESOURCES + self._config_resource_reg(EXTERNAL_RESOURCES, postfix) + + if TENANT_NETWORK in net_settings.enabled_network_list: + tenant_range = nets[TENANT_NETWORK]['overcloud_ip_range'] + self[param_def]['TenantAllocationPools'] = \ + [{'start': str(tenant_range[0]), + 'end': str(tenant_range[1])}] + tenant_cidr = nets[TENANT_NETWORK]['cidr'] + self[param_def]['TenantNetCidr'] = str(tenant_cidr) + if tenant_cidr.version == 6: + postfix = '/tenant_v6.yaml' + # set overlay_ip_version option in Neutron ML2 config + self[param_def]['NeutronOverlayIPVersion'] = "6" + else: + postfix = '/tenant.yaml' + + tenant_vlan = self._get_vlan(nets[TENANT_NETWORK]) + if isinstance(tenant_vlan, int): + self[param_def]['TenantNetworkVlanID'] = tenant_vlan + else: + postfix = '/noop.yaml' + + # apply resource registry update for TENANT_RESOURCES + self._config_resource_reg(TENANT_RESOURCES, postfix) + + if STORAGE_NETWORK in net_settings.enabled_network_list: + storage_range = nets[STORAGE_NETWORK]['overcloud_ip_range'] + self[param_def]['StorageAllocationPools'] = \ + [{'start': str(storage_range[0]), + 'end': str(storage_range[1])}] + storage_cidr = nets[STORAGE_NETWORK]['cidr'] + self[param_def]['StorageNetCidr'] = str(storage_cidr) + if storage_cidr.version == 6: + postfix = '/storage_v6.yaml' + else: + postfix = '/storage.yaml' + storage_vlan = self._get_vlan(nets[STORAGE_NETWORK]) + if isinstance(storage_vlan, int): + self[param_def]['StorageNetworkVlanID'] = storage_vlan + else: + postfix = '/noop.yaml' + + # apply resource registry update for STORAGE_RESOURCES + self._config_resource_reg(STORAGE_RESOURCES, postfix) + + if API_NETWORK in net_settings.enabled_network_list: + api_range = nets[API_NETWORK]['overcloud_ip_range'] + self[param_def]['InternalApiAllocationPools'] = \ + [{'start': str(api_range[0]), + 'end': str(api_range[1])}] + api_cidr = nets[API_NETWORK]['cidr'] + self[param_def]['InternalApiNetCidr'] = str(api_cidr) + if api_cidr.version == 6: + postfix = '/internal_api_v6.yaml' + else: + postfix = '/internal_api.yaml' + api_vlan = self._get_vlan(nets[API_NETWORK]) + if isinstance(api_vlan, int): + self[param_def]['InternalApiNetworkVlanID'] = api_vlan + else: + postfix = '/noop.yaml' + + # apply resource registry update for API_RESOURCES + self._config_resource_reg(API_RESOURCES, postfix) + + # Set IPv6 related flags to True. Not that we do not set those to False + # when IPv4 is configured, we'll use the default or whatever the user + # may have set. + if net_settings.get_ip_addr_family() == 6: + for flag in IPV6_FLAGS: + self[param_def][flag] = True + + def _get_vlan(self, network): + if isinstance(network['nic_mapping'][CONTROLLER]['vlan'], int): + return network['nic_mapping'][CONTROLLER]['vlan'] + elif isinstance(network['nic_mapping'][COMPUTE]['vlan'], int): + return network['nic_mapping'][COMPUTE]['vlan'] + else: + return 'native' + + def _set_tht_dir(self): + self.tht_dir = None + for key, prefix in TENANT_RESOURCES.items(): + if prefix is None: + prefix = '' + m = re.split('%s/\w+\.yaml' % prefix, self[reg][key]) + if m is not None and len(m) > 1: + self.tht_dir = m[0] + break + if not self.tht_dir: + raise NetworkEnvException('Unable to parse THT Directory') + + def _config_resource_reg(self, resources, postfix): + for key, prefix in resources.items(): + if prefix is None: + if postfix == '/noop.yaml': + self[reg][key] = HEAT_NONE + continue + prefix = '' + self[reg][key] = self.tht_dir + prefix + postfix + + +class NetworkEnvException(Exception): + def __init__(self, value): + self.value = value + + def __str__(self): + return self.value |