diff --git a/octavia/amphorae/backends/agent/api_server/osutils.py b/octavia/amphorae/backends/agent/api_server/osutils.py index f808a71338..ac779db5cc 100644 --- a/octavia/amphorae/backends/agent/api_server/osutils.py +++ b/octavia/amphorae/backends/agent/api_server/osutils.py @@ -66,7 +66,7 @@ class BaseOS(object): def write_vip_interface_file(self, interface, vip, ip_version, prefixlen, gateway, mtu, vrrp_ip, - host_routes): + host_routes, fixed_ips=None): vip_interface = interface_file.VIPInterfaceFile( name=interface, mtu=mtu, @@ -76,6 +76,7 @@ class BaseOS(object): gateway=gateway, vrrp_ip=vrrp_ip, host_routes=host_routes, + fixed_ips=fixed_ips, topology=CONF.controller_worker.loadbalancer_topology) vip_interface.write() @@ -94,7 +95,8 @@ class BaseOS(object): try: out = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT) - LOG.debug(out) + for line in out.decode('utf-8').split('\n'): + LOG.debug(line) except subprocess.CalledProcessError as e: LOG.error('Failed to set up %s due to error: %s %s', interface, e, e.output) diff --git a/octavia/amphorae/backends/agent/api_server/plug.py b/octavia/amphorae/backends/agent/api_server/plug.py index 8b30eb4ee1..21b2a75bfe 100644 --- a/octavia/amphorae/backends/agent/api_server/plug.py +++ b/octavia/amphorae/backends/agent/api_server/plug.py @@ -132,16 +132,52 @@ class Plug(object): except socket.error: socket.inet_pton(socket.AF_INET6, ip.get('ip_address')) - def plug_network(self, mac_address, fixed_ips, mtu=None): + def plug_network(self, mac_address, fixed_ips, mtu=None, + vip_net_info=None): # Check if the interface is already in the network namespace # Do not attempt to re-plug the network if it is already in the - # network namespace + # network namespace, just ensure all fixed_ips are up if self._netns_interface_exists(mac_address): - return webob.Response(json=dict( - message="Interface already exists"), status=409) + # Get the existing interface name and path + existing_interface = self._netns_interface_by_mac(mac_address) - # This is the interface as it was initially plugged into the - # default network namespace, this will likely always be eth1 + # If we have net_info, this is the special case of plugging a new + # subnet on the vrrp port, which is essentially a re-vip-plug + if vip_net_info: + ip = ipaddress.ip_address(vip_net_info['vip']) + network = ipaddress.ip_network(vip_net_info['subnet_cidr']) + vip = ip.exploded + prefixlen = network.prefixlen + + vrrp_ip = vip_net_info.get('vrrp_ip') + gateway = vip_net_info['gateway'] + host_routes = vip_net_info.get('host_routes', ()) + + self._osutils.write_vip_interface_file( + interface=existing_interface, + vip=vip, + ip_version=ip.version, + prefixlen=prefixlen, + gateway=gateway, + vrrp_ip=vrrp_ip, + host_routes=host_routes, + mtu=mtu, + fixed_ips=fixed_ips) + self._osutils.bring_interface_up(existing_interface, 'vip') + # Otherwise, we are just plugging a run-of-the-mill network + else: + # Write an updated config + self._osutils.write_port_interface_file( + interface=existing_interface, + fixed_ips=fixed_ips, + mtu=mtu) + self._osutils.bring_interface_up(existing_interface, 'network') + return webob.Response(json=dict( + message="OK", + details="Updated existing interface {interface}".format( + # TODO(rm_work): Everything in this should probably use + # HTTP code 200, but continuing to use 202 for consistency. + interface=existing_interface)), status=202) try: self._check_ip_addresses(fixed_ips=fixed_ips) @@ -149,6 +185,8 @@ class Plug(object): return webob.Response(json=dict( message="Invalid network port"), status=400) + # This is the interface as it was initially plugged into the + # default network namespace, this will likely always be eth1 default_netns_interface = self._interface_by_mac(mac_address) # We need to determine the interface name when inside the namespace @@ -222,11 +260,14 @@ class Plug(object): text_file.write("{mac_address} {interface}\n".format( mac_address=mac_address, interface=interface)) - def _netns_interface_exists(self, mac_address): + def _netns_interface_by_mac(self, mac_address): with pyroute2.NetNS(consts.AMPHORA_NAMESPACE, flags=os.O_CREAT) as netns: for link in netns.get_links(): - for attr in link['attrs']: - if attr[0] == 'IFLA_ADDRESS' and attr[1] == mac_address: - return True - return False + attr_dict = dict(link['attrs']) + if attr_dict.get('IFLA_ADDRESS') == mac_address: + return attr_dict.get('IFLA_IFNAME') + return None + + def _netns_interface_exists(self, mac_address): + return self._netns_interface_by_mac(mac_address) is not None diff --git a/octavia/amphorae/backends/agent/api_server/server.py b/octavia/amphorae/backends/agent/api_server/server.py index 45b2cc0caf..d11595a868 100644 --- a/octavia/amphorae/backends/agent/api_server/server.py +++ b/octavia/amphorae/backends/agent/api_server/server.py @@ -214,7 +214,8 @@ class Server(object): description='Invalid port information') from e return self._plug.plug_network(port_info['mac_address'], port_info.get('fixed_ips'), - port_info.get('mtu')) + port_info.get('mtu'), + port_info.get('vip_net_info')) def upload_cert(self): return certificate_update.upload_server_cert() diff --git a/octavia/amphorae/drivers/driver_base.py b/octavia/amphorae/drivers/driver_base.py index 413efed2f2..8a4505e55c 100644 --- a/octavia/amphorae/drivers/driver_base.py +++ b/octavia/amphorae/drivers/driver_base.py @@ -162,6 +162,8 @@ class AmphoraLoadBalancerDriver(object, metaclass=abc.ABCMeta): :param amphorae_network_config: A data model containing information about the subnets and ports that an amphorae owns. + :type amphorae_network_config: octavia.network.data_models. + AmphoraNetworkConfig :param vrrp_port: VRRP port associated with the load balancer :type vrrp_port: octavia.network.data_models.Port @@ -175,13 +177,18 @@ class AmphoraLoadBalancerDriver(object, metaclass=abc.ABCMeta): the vip, such as bring up interfaces. """ - def post_network_plug(self, amphora, port): + def post_network_plug(self, amphora, port, amphora_network_config): """Called after amphora added to network :param amphora: amphora object, needs id and network ip(s) :type amphora: octavia.db.models.Amphora :param port: contains information of the plugged port :type port: octavia.network.data_models.Port + :param amphora_network_config: A data model containing information + about the subnets and ports that an + amphorae owns. + :type amphora_network_config: octavia.network.data_models. + AmphoraNetworkConfig This method is optional to implement. After adding an amphora to a network, there may be steps necessary on the amphora to allow it to diff --git a/octavia/amphorae/drivers/haproxy/rest_api_driver.py b/octavia/amphorae/drivers/haproxy/rest_api_driver.py index 60d4a0b2e7..09615e3526 100644 --- a/octavia/amphorae/drivers/haproxy/rest_api_driver.py +++ b/octavia/amphorae/drivers/haproxy/rest_api_driver.py @@ -39,6 +39,7 @@ from octavia.common.tls_utils import cert_parser from octavia.common import utils from octavia.db import api as db_apis from octavia.db import repositories as repo +from octavia.network import data_models as network_models LOG = logging.getLogger(__name__) @@ -381,23 +382,33 @@ class HaproxyAmphoraLoadBalancerDriver( def finalize_amphora(self, amphora): pass + def _build_net_info(self, port, amphora, subnet, mtu=None): + # NOTE(blogan): using the vrrp port here because that + # is what the allowed address pairs network driver sets + # this particular port to. This does expose a bit of + # tight coupling between the network driver and amphora + # driver. We will need to revisit this to try and remove + # this tight coupling. + # NOTE (johnsom): I am loading the vrrp_ip into the + # net_info structure here so that I don't break + # compatibility with old amphora agent versions. + host_routes = [{'nexthop': hr[consts.NEXTHOP], + 'destination': hr[consts.DESTINATION]} + for hr in subnet[consts.HOST_ROUTES]] + net_info = {'subnet_cidr': subnet[consts.CIDR], + 'gateway': subnet[consts.GATEWAY_IP], + 'mac_address': port[consts.MAC_ADDRESS], + 'vrrp_ip': amphora[consts.VRRP_IP], + 'mtu': mtu or port[consts.NETWORK][consts.MTU], + 'host_routes': host_routes} + return net_info + def post_vip_plug(self, amphora, load_balancer, amphorae_network_config, vrrp_port=None, vip_subnet=None): if amphora.status != consts.DELETED: self._populate_amphora_api_version(amphora) if vip_subnet is None: - subnet = amphorae_network_config.get(amphora.id).vip_subnet - else: - subnet = vip_subnet - # NOTE(blogan): using the vrrp port here because that - # is what the allowed address pairs network driver sets - # this particular port to. This does expose a bit of - # tight coupling between the network driver and amphora - # driver. We will need to revisit this to try and remove - # this tight coupling. - # NOTE (johnsom): I am loading the vrrp_ip into the - # net_info structure here so that I don't break - # compatibility with old amphora agent versions. + vip_subnet = amphorae_network_config.get(amphora.id).vip_subnet if vrrp_port is None: port = amphorae_network_config.get(amphora.id).vrrp_port mtu = port.network.mtu @@ -406,15 +417,9 @@ class HaproxyAmphoraLoadBalancerDriver( mtu = port.network['mtu'] LOG.debug("Post-VIP-Plugging with vrrp_ip %s vrrp_port %s", amphora.vrrp_ip, port.id) - host_routes = [{'nexthop': hr.nexthop, - 'destination': hr.destination} - for hr in subnet.host_routes] - net_info = {'subnet_cidr': subnet.cidr, - 'gateway': subnet.gateway_ip, - 'mac_address': port.mac_address, - 'vrrp_ip': amphora.vrrp_ip, - 'mtu': mtu, - 'host_routes': host_routes} + net_info = self._build_net_info( + port.to_dict(recurse=True), amphora.to_dict(), + vip_subnet.to_dict(recurse=True), mtu) try: self.clients[amphora.api_version].plug_vip( amphora, load_balancer.vip.ip_address, net_info) @@ -423,7 +428,7 @@ class HaproxyAmphoraLoadBalancerDriver( 'skipping post_vip_plug', {'mac': port.mac_address}) - def post_network_plug(self, amphora, port): + def post_network_plug(self, amphora, port, amphora_network_config): fixed_ips = [] for fixed_ip in port.fixed_ips: host_routes = [{'nexthop': hr.nexthop, @@ -431,11 +436,25 @@ class HaproxyAmphoraLoadBalancerDriver( for hr in fixed_ip.subnet.host_routes] ip = {'ip_address': fixed_ip.ip_address, 'subnet_cidr': fixed_ip.subnet.cidr, - 'host_routes': host_routes} + 'host_routes': host_routes, + 'gateway': fixed_ip.subnet.gateway_ip} fixed_ips.append(ip) port_info = {'mac_address': port.mac_address, 'fixed_ips': fixed_ips, 'mtu': port.network.mtu} + if port.id == amphora.vrrp_port_id: + if isinstance(amphora_network_config, + network_models.AmphoraNetworkConfig): + amphora_network_config = amphora_network_config.to_dict( + recurse=True) + # We have to special-case sharing the vrrp port and pass through + # enough extra information to populate the whole VIP port + net_info = self._build_net_info( + port.to_dict(recurse=True), amphora.to_dict(), + amphora_network_config[consts.VIP_SUBNET], + port.network.mtu) + net_info['vip'] = amphora.ha_ip + port_info['vip_net_info'] = net_info try: self._populate_amphora_api_version(amphora) self.clients[amphora.api_version].plug_network(amphora, port_info) diff --git a/octavia/amphorae/drivers/noop_driver/driver.py b/octavia/amphorae/drivers/noop_driver/driver.py index 57cb3525cf..8d6bc52def 100644 --- a/octavia/amphorae/drivers/noop_driver/driver.py +++ b/octavia/amphorae/drivers/noop_driver/driver.py @@ -85,9 +85,10 @@ class NoopManager(object): self.__class__.__name__, amphora.id) self.amphoraconfig[amphora.id] = (amphora.id, 'finalize amphora') - def post_network_plug(self, amphora, port): - LOG.debug("Amphora %s no-op, post network plug amphora %s, port %s", - self.__class__.__name__, amphora.id, port.id) + def post_network_plug(self, amphora, port, amphora_network_config): + LOG.debug("Amphora %s no-op, post network plug amphora %s, port %s, " + "amphora_network_config %s", self.__class__.__name__, + amphora.id, port.id, amphora_network_config) self.amphoraconfig[amphora.id, port.id] = (amphora.id, port.id, 'post_network_plug') @@ -160,9 +161,9 @@ class NoopAmphoraLoadBalancerDriver( self.driver.finalize_amphora(amphora) - def post_network_plug(self, amphora, port): + def post_network_plug(self, amphora, port, amphora_network_config): - self.driver.post_network_plug(amphora, port) + self.driver.post_network_plug(amphora, port, amphora_network_config) def post_vip_plug(self, amphora, load_balancer, amphorae_network_config, vrrp_port=None, vip_subnet=None): diff --git a/octavia/common/constants.py b/octavia/common/constants.py index a698b4716e..8091ac3157 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -298,7 +298,7 @@ SUPPORTED_TASKFLOW_ENGINE_TYPES = ['serial', 'parallel'] # Task/Flow constants ACTIVE_CONNECTIONS = 'active_connections' ADD_NICS = 'add_nics' -ADDED_PORTS = 'added_ports' +ADD_SUBNETS = 'add_subnets' ADMIN_STATE_UP = 'admin_state_up' ALLOWED_ADDRESS_PAIRS = 'allowed_address_pairs' AMP_DATA = 'amp_data' @@ -330,9 +330,11 @@ CREATED_AT = 'created_at' CRL_CONTAINER_ID = 'crl_container_id' DEFAULT_TLS_CONTAINER_DATA = 'default_tls_container_data' DELETE_NICS = 'delete_nics' +DELETE_SUBNETS = 'delete_subnets' DELTA = 'delta' DELTAS = 'deltas' DESCRIPTION = 'description' +DESTINATION = 'destination' DEVICE_OWNER = 'device_owner' ENABLED = 'enabled' FAILED_AMP_VRRP_PORT_ID = 'failed_amp_vrrp_port_id' @@ -342,6 +344,7 @@ FAILOVER_AMPHORA_ID = 'failover_amphora_id' FIELDS = 'fields' FIXED_IPS = 'fixed_ips' FLAVOR_ID = 'flavor_id' +GATEWAY_IP = 'gateway_ip' HA_IP = 'ha_ip' HA_PORT_ID = 'ha_port_id' HEALTH_MON = 'health_mon' @@ -349,6 +352,7 @@ HEALTH_MONITOR = 'health_monitor' HEALTH_MONITOR_ID = 'health_monitor_id' HEALTHMONITOR_ID = 'healthmonitor_id' HEALTH_MONITOR_UPDATES = 'health_monitor_updates' +HOST_ROUTES = 'host_routes' ID = 'id' IMAGE_ID = 'image_id' IP_ADDRESS = 'ip_address' @@ -367,6 +371,7 @@ LOADBALANCER = 'loadbalancer' LOADBALANCER_ID = 'loadbalancer_id' LOAD_BALANCER_ID = 'load_balancer_id' LOAD_BALANCER_UPDATES = 'load_balancer_updates' +MAC_ADDRESS = 'mac_address' MANAGEMENT_NETWORK = 'management_network' MEMBER = 'member' MEMBER_ID = 'member_id' @@ -376,6 +381,7 @@ MESSAGE = 'message' NAME = 'name' NETWORK = 'network' NETWORK_ID = 'network_id' +NEXTHOP = 'nexthop' NICS = 'nics' OBJECT = 'object' ORIGINAL_HEALTH_MONITOR = 'original_health_monitor' @@ -423,10 +429,12 @@ TOPOLOGY = 'topology' TOTAL_CONNECTIONS = 'total_connections' UPDATED_AT = 'updated_at' UPDATE_DICT = 'update_dict' +UPDATED_PORTS = 'updated_ports' VALID_VIP_NETWORKS = 'valid_vip_networks' VIP = 'vip' VIP_ADDRESS = 'vip_address' VIP_NETWORK = 'vip_network' +VIP_NETWORK_ID = 'vip_network_id' VIP_PORT_ID = 'vip_port_id' VIP_QOS_POLICY_ID = 'vip_qos_policy_id' VIP_SG_ID = 'vip_sg_id' diff --git a/octavia/controller/worker/v1/controller_worker.py b/octavia/controller/worker/v1/controller_worker.py index 4c5b4f0c1a..9fd0dea7b5 100644 --- a/octavia/controller/worker/v1/controller_worker.py +++ b/octavia/controller/worker/v1/controller_worker.py @@ -429,6 +429,7 @@ class ControllerWorker(base_taskflow.BaseTaskFlowEngine): constants.MEMBER: member, constants.LISTENERS: listeners, constants.LOADBALANCER: load_balancer, + constants.LOADBALANCER_ID: load_balancer.id, constants.POOL: pool} if load_balancer.availability_zone: store[constants.AVAILABILITY_ZONE] = ( @@ -461,6 +462,7 @@ class ControllerWorker(base_taskflow.BaseTaskFlowEngine): constants.MEMBER: member, constants.LISTENERS: listeners, constants.LOADBALANCER: load_balancer, + constants.LOADBALANCER_ID: load_balancer.id, constants.POOL: pool} if load_balancer.availability_zone: store[constants.AVAILABILITY_ZONE] = ( diff --git a/octavia/controller/worker/v1/flows/amphora_flows.py b/octavia/controller/worker/v1/flows/amphora_flows.py index 11fc986f51..92e558cf96 100644 --- a/octavia/controller/worker/v1/flows/amphora_flows.py +++ b/octavia/controller/worker/v1/flows/amphora_flows.py @@ -443,11 +443,11 @@ class AmphoraFlows(object): amp_for_failover_flow.add(network_tasks.HandleNetworkDelta( name=prefix + '-' + constants.HANDLE_NETWORK_DELTA, requires=(constants.AMPHORA, constants.DELTA), - provides=constants.ADDED_PORTS)) + provides=constants.UPDATED_PORTS)) amp_for_failover_flow.add(amphora_driver_tasks.AmphoraePostNetworkPlug( name=prefix + '-' + constants.AMPHORAE_POST_NETWORK_PLUG, - requires=(constants.LOADBALANCER, constants.ADDED_PORTS))) + requires=(constants.LOADBALANCER, constants.UPDATED_PORTS))) return amp_for_failover_flow diff --git a/octavia/controller/worker/v1/flows/load_balancer_flows.py b/octavia/controller/worker/v1/flows/load_balancer_flows.py index 681e17ced0..f12d03e7e3 100644 --- a/octavia/controller/worker/v1/flows/load_balancer_flows.py +++ b/octavia/controller/worker/v1/flows/load_balancer_flows.py @@ -202,12 +202,12 @@ class LoadBalancerFlows(object): ) flows.append( network_tasks.HandleNetworkDeltas( - requires=constants.DELTAS, provides=constants.ADDED_PORTS + requires=constants.DELTAS, provides=constants.UPDATED_PORTS ) ) flows.append( amphora_driver_tasks.AmphoraePostNetworkPlug( - requires=(constants.LOADBALANCER, constants.ADDED_PORTS) + requires=(constants.LOADBALANCER, constants.UPDATED_PORTS) ) ) flows.append( diff --git a/octavia/controller/worker/v1/flows/member_flows.py b/octavia/controller/worker/v1/flows/member_flows.py index 854e50f57a..c879f5fa0d 100644 --- a/octavia/controller/worker/v1/flows/member_flows.py +++ b/octavia/controller/worker/v1/flows/member_flows.py @@ -43,10 +43,14 @@ class MemberFlows(object): requires=(constants.LOADBALANCER, constants.AVAILABILITY_ZONE), provides=constants.DELTAS)) create_member_flow.add(network_tasks.HandleNetworkDeltas( - requires=constants.DELTAS, provides=constants.ADDED_PORTS)) + requires=(constants.DELTAS, constants.LOADBALANCER), + provides=constants.UPDATED_PORTS)) + create_member_flow.add(network_tasks.GetAmphoraeNetworkConfigs( + requires=constants.LOADBALANCER_ID, + provides=constants.AMPHORAE_NETWORK_CONFIG)) create_member_flow.add(amphora_driver_tasks.AmphoraePostNetworkPlug( - requires=(constants.LOADBALANCER, constants.ADDED_PORTS) - )) + requires=(constants.LOADBALANCER, constants.UPDATED_PORTS, + constants.AMPHORAE_NETWORK_CONFIG))) create_member_flow.add(amphora_driver_tasks.ListenersUpdate( requires=constants.LOADBALANCER)) create_member_flow.add(database_tasks.MarkMemberActiveInDB( @@ -73,6 +77,18 @@ class MemberFlows(object): constants.POOL])) delete_member_flow.add(database_tasks.MarkMemberPendingDeleteInDB( requires=constants.MEMBER)) + delete_member_flow.add(network_tasks.CalculateDelta( + requires=(constants.LOADBALANCER, constants.AVAILABILITY_ZONE), + provides=constants.DELTAS)) + delete_member_flow.add(network_tasks.HandleNetworkDeltas( + requires=(constants.DELTAS, constants.LOADBALANCER), + provides=constants.UPDATED_PORTS)) + delete_member_flow.add(network_tasks.GetAmphoraeNetworkConfigs( + requires=constants.LOADBALANCER_ID, + provides=constants.AMPHORAE_NETWORK_CONFIG)) + delete_member_flow.add(amphora_driver_tasks.AmphoraePostNetworkPlug( + requires=(constants.LOADBALANCER, constants.UPDATED_PORTS, + constants.AMPHORAE_NETWORK_CONFIG))) delete_member_flow.add(model_tasks. DeleteModelObject(rebind={constants.OBJECT: constants.MEMBER})) @@ -188,10 +204,15 @@ class MemberFlows(object): requires=(constants.LOADBALANCER, constants.AVAILABILITY_ZONE), provides=constants.DELTAS)) batch_update_members_flow.add(network_tasks.HandleNetworkDeltas( - requires=constants.DELTAS, provides=constants.ADDED_PORTS)) + requires=(constants.DELTAS, constants.LOADBALANCER), + provides=constants.UPDATED_PORTS)) + batch_update_members_flow.add(network_tasks.GetAmphoraeNetworkConfigs( + requires=constants.LOADBALANCER_ID, + provides=constants.AMPHORAE_NETWORK_CONFIG)) batch_update_members_flow.add( amphora_driver_tasks.AmphoraePostNetworkPlug( - requires=(constants.LOADBALANCER, constants.ADDED_PORTS))) + requires=(constants.LOADBALANCER, constants.UPDATED_PORTS, + constants.AMPHORAE_NETWORK_CONFIG))) # Update the Listener (this makes the changes active on the Amp) batch_update_members_flow.add(amphora_driver_tasks.ListenersUpdate( diff --git a/octavia/controller/worker/v1/tasks/amphora_driver_tasks.py b/octavia/controller/worker/v1/tasks/amphora_driver_tasks.py index 52c5ba01ea..44c55af2b2 100644 --- a/octavia/controller/worker/v1/tasks/amphora_driver_tasks.py +++ b/octavia/controller/worker/v1/tasks/amphora_driver_tasks.py @@ -196,10 +196,11 @@ class AmphoraFinalize(BaseAmphoraTask): class AmphoraPostNetworkPlug(BaseAmphoraTask): """Task to notify the amphora post network plug.""" - def execute(self, amphora, ports): + def execute(self, amphora, ports, amphora_network_config): """Execute post_network_plug routine.""" for port in ports: - self.amphora_driver.post_network_plug(amphora, port) + self.amphora_driver.post_network_plug( + amphora, port, amphora_network_config) LOG.debug("post_network_plug called on compute instance " "%(compute_id)s for port %(port_id)s", {"compute_id": amphora.compute_id, "port_id": port.id}) @@ -215,7 +216,7 @@ class AmphoraPostNetworkPlug(BaseAmphoraTask): class AmphoraePostNetworkPlug(BaseAmphoraTask): """Task to notify the amphorae post network plug.""" - def execute(self, loadbalancer, added_ports): + def execute(self, loadbalancer, updated_ports, amphorae_network_config): """Execute post_network_plug routine.""" amp_post_plug = AmphoraPostNetworkPlug() # We need to make sure we have the fresh list of amphora @@ -223,10 +224,11 @@ class AmphoraePostNetworkPlug(BaseAmphoraTask): db_apis.get_session(), load_balancer_id=loadbalancer.id, status=constants.AMPHORA_ALLOCATED)[0] for amphora in amphorae: - if amphora.id in added_ports: - amp_post_plug.execute(amphora, added_ports[amphora.id]) + if amphora.id in updated_ports: + amp_post_plug.execute(amphora, updated_ports[amphora.id], + amphorae_network_config[amphora.id]) - def revert(self, result, loadbalancer, added_ports, *args, **kwargs): + def revert(self, result, loadbalancer, updated_ports, *args, **kwargs): """Handle a failed post network plug.""" if isinstance(result, failure.Failure): return diff --git a/octavia/controller/worker/v1/tasks/network_tasks.py b/octavia/controller/worker/v1/tasks/network_tasks.py index 53abc698b2..6547435adf 100644 --- a/octavia/controller/worker/v1/tasks/network_tasks.py +++ b/octavia/controller/worker/v1/tasks/network_tasks.py @@ -58,37 +58,101 @@ class CalculateAmphoraDelta(BaseNetworkTask): LOG.debug("Calculating network delta for amphora id: %s", amphora.id) if vrrp_port is None: - vrrp_port = self.network_driver.get_port(amphora.vrrp_port_id) + vrrp_port = self.network_driver.get_port( + amphora.vrrp_port_id) + + vip_subnet_to_net_map = { + loadbalancer.vip.subnet_id: + loadbalancer.vip.network_id, + } + + # Figure out what networks we want + # seed with lb network(s) if (availability_zone and availability_zone.get(constants.MANAGEMENT_NETWORK)): - management_nets = [availability_zone.get( - constants.MANAGEMENT_NETWORK)] + management_nets = [ + availability_zone.get(constants.MANAGEMENT_NETWORK)] else: management_nets = CONF.controller_worker.amp_boot_network_list - desired_network_ids = {vrrp_port.network_id}.union(management_nets) + + desired_subnet_to_net_map = { + subnet: mgmt_net_id + for mgmt_net_id in management_nets + for subnet in self.network_driver.get_network( + mgmt_net_id).subnets + } + desired_subnet_to_net_map.update(vip_subnet_to_net_map) for pool in loadbalancer.pools: - member_networks = [ - self.network_driver.get_subnet(member.subnet_id).network_id - for member in pool.members - if member.subnet_id - ] - desired_network_ids.update(member_networks) + for member in pool.members: + if (member.subnet_id and + member.provisioning_status != + constants.PENDING_DELETE): + member_network = self.network_driver.get_subnet( + member.subnet_id).network_id + desired_subnet_to_net_map[member.subnet_id] = ( + member_network) - nics = self.network_driver.get_plugged_networks(amphora.compute_id) + desired_network_ids = set(desired_subnet_to_net_map.values()) + desired_subnet_ids = set(desired_subnet_to_net_map) + + # Calculate Network deltas + nics = self.network_driver.get_plugged_networks( + amphora.compute_id) # assume we don't have two nics in the same network - actual_network_nics = dict((nic.network_id, nic) for nic in nics) + # TODO(rm_work): how accurate is this assumption really? + network_to_nic_map = {nic.network_id: nic for nic in nics} - del_ids = set(actual_network_nics) - desired_network_ids - delete_nics = list( - actual_network_nics[net_id] for net_id in del_ids) + del_ids = set(network_to_nic_map) - desired_network_ids + delete_nics = [n_data_models.Interface( + network_id=net_id, + port_id=network_to_nic_map[net_id].port_id) + for net_id in del_ids] + + add_ids = desired_network_ids - set(network_to_nic_map) + add_nics = [n_data_models.Interface( + network_id=net_id, + fixed_ips=[ + n_data_models.FixedIP( + subnet_id=subnet_id) + for subnet_id in desired_subnet_to_net_map + if desired_subnet_to_net_map[subnet_id] == net_id]) + for net_id in add_ids] + + # Calculate member Subnet deltas + plugged_subnets = { + fixed_ip.subnet_id: nic.network_id + for nic in network_to_nic_map.values() + for fixed_ip in nic.fixed_ips or [] + } + + del_subnet_ids = set(plugged_subnets) - desired_subnet_ids + add_subnet_ids = desired_subnet_ids - set(plugged_subnets) + + def _subnet_updates(subnet_ids, subnets): + updates = [] + for s in subnet_ids: + network_id = subnets[s] + nic = network_to_nic_map.get(network_id) + port_id = nic.port_id if nic else None + updates.append({ + constants.SUBNET_ID: s, + constants.NETWORK_ID: network_id, + constants.PORT_ID: port_id + }) + return updates + + add_subnets = _subnet_updates(add_subnet_ids, + desired_subnet_to_net_map) + del_subnets = _subnet_updates(del_subnet_ids, + plugged_subnets) - add_ids = desired_network_ids - set(actual_network_nics) - add_nics = list(n_data_models.Interface( - network_id=net_id) for net_id in add_ids) delta = n_data_models.Delta( - amphora_id=amphora.id, compute_id=amphora.compute_id, - add_nics=add_nics, delete_nics=delete_nics) + amphora_id=amphora.id, + compute_id=amphora.compute_id, + add_nics=add_nics, delete_nics=delete_nics, + add_subnets=add_subnets, + delete_subnets=del_subnets) return delta @@ -234,28 +298,81 @@ class HandleNetworkDelta(BaseNetworkTask): Plug or unplug networks based on delta """ + def _fill_port_info(self, port): + port.network = self.network_driver.get_network(port.network_id) + for fixed_ip in port.fixed_ips: + fixed_ip.subnet = self.network_driver.get_subnet( + fixed_ip.subnet_id) + def execute(self, amphora, delta): """Handle network plugging based off deltas.""" - added_ports = {} - added_ports[amphora.id] = [] + updated_ports = {} for nic in delta.add_nics: - interface = self.network_driver.plug_network(delta.compute_id, - nic.network_id) + subnet_id = [ + fixed_ip.subnet_id + for fixed_ip in nic.fixed_ips][0] + interface = self.network_driver.plug_network( + amphora.compute_id, nic.network_id) port = self.network_driver.get_port(interface.port_id) - port.network = self.network_driver.get_network(port.network_id) + self._fill_port_info(port) + updated_ports[port.network_id] = port + + for update in delta.add_subnets: + network_id = update[constants.NETWORK_ID] + # Get already existing port from Deltas or + # newly created port from updated_ports dict + port_id = (update[constants.PORT_ID] or + updated_ports[network_id].id) + subnet_id = update[constants.SUBNET_ID] + # Avoid duplicated subnets + has_subnet = False + if network_id in updated_ports: + for fixed_ip in updated_ports[network_id].fixed_ips: + if fixed_ip.subnet_id == subnet_id: + has_subnet = True + if not has_subnet: + port = self.network_driver.plug_fixed_ip( + port_id=port_id, subnet_id=subnet_id) + self._fill_port_info(port) + updated_ports[network_id] = port + + for update in delta.delete_subnets: + network_id = update[constants.NETWORK_ID] + port_id = update[constants.PORT_ID] + subnet_id = update[constants.SUBNET_ID] + port = self.network_driver.unplug_fixed_ip( + port_id=port_id, subnet_id=subnet_id) + self._fill_port_info(port) + # In neutron, when removing an ipv6 subnet (with slaac) from a + # port, it just ignores it. + # https://bugs.launchpad.net/neutron/+bug/1945156 + # When it happens, don't add the port to the updated_ports dict + has_subnet = False for fixed_ip in port.fixed_ips: - fixed_ip.subnet = self.network_driver.get_subnet( - fixed_ip.subnet_id) - added_ports[amphora.id].append(port) + if fixed_ip.subnet_id == subnet_id: + has_subnet = True + break + if not has_subnet: + updated_ports[network_id] = port + for nic in delta.delete_nics: + network_id = nic.network_id try: - self.network_driver.unplug_network(delta.compute_id, - nic.network_id) + self.network_driver.unplug_network( + amphora.compute_id, network_id) except base.NetworkNotFound: - LOG.debug("Network %d not found ", nic.network_id) + LOG.debug("Network %s not found", network_id) except Exception: LOG.exception("Unable to unplug network") - return added_ports + + port_id = nic.port_id + try: + self.network_driver.delete_port(port_id) + except Exception: + LOG.exception("Unable to delete the port") + + updated_ports.pop(network_id, None) + return {amphora.id: list(updated_ports.values())} def revert(self, result, amphora, delta, *args, **kwargs): """Handle a network plug or unplug failures.""" @@ -276,6 +393,12 @@ class HandleNetworkDelta(BaseNetworkTask): except Exception: pass + port_id = nic.port_id + try: + self.network_driver.delete_port(port_id) + except Exception: + pass + class HandleNetworkDeltas(BaseNetworkTask): """Task to plug and unplug networks @@ -284,35 +407,28 @@ class HandleNetworkDeltas(BaseNetworkTask): networks based on delta """ - def execute(self, deltas): + def execute(self, deltas, loadbalancer): """Handle network plugging based off deltas.""" - added_ports = {} + amphorae = {amp.id: amp for amp in loadbalancer.amphorae} + + updated_ports = {} + handle_delta = HandleNetworkDelta() + for amp_id, delta in deltas.items(): - added_ports[amp_id] = [] - for nic in delta.add_nics: - interface = self.network_driver.plug_network(delta.compute_id, - nic.network_id) - port = self.network_driver.get_port(interface.port_id) - port.network = self.network_driver.get_network(port.network_id) - for fixed_ip in port.fixed_ips: - fixed_ip.subnet = self.network_driver.get_subnet( - fixed_ip.subnet_id) - added_ports[amp_id].append(port) - for nic in delta.delete_nics: - try: - self.network_driver.unplug_network(delta.compute_id, - nic.network_id) - except base.NetworkNotFound: - LOG.debug("Network %d not found ", nic.network_id) - except Exception: - LOG.exception("Unable to unplug network") - return added_ports + ret = handle_delta.execute(amphorae[amp_id], delta) + updated_ports.update(ret) + + return updated_ports def revert(self, result, deltas, *args, **kwargs): """Handle a network plug or unplug failures.""" if isinstance(result, failure.Failure): return + + if not deltas: + return + for amp_id, delta in deltas.items(): LOG.warning("Unable to plug networks for amp id %s", delta.amphora_id) @@ -323,7 +439,13 @@ class HandleNetworkDeltas(BaseNetworkTask): try: self.network_driver.unplug_network(delta.compute_id, nic.network_id) - except base.NetworkNotFound: + except Exception: + pass + + port_id = nic.port_id + try: + self.network_driver.delete_port(port_id) + except Exception: pass diff --git a/octavia/controller/worker/v2/flows/amphora_flows.py b/octavia/controller/worker/v2/flows/amphora_flows.py index ee8bfaacd2..9453ab3e30 100644 --- a/octavia/controller/worker/v2/flows/amphora_flows.py +++ b/octavia/controller/worker/v2/flows/amphora_flows.py @@ -410,11 +410,11 @@ class AmphoraFlows(object): amp_for_failover_flow.add(network_tasks.HandleNetworkDelta( name=prefix + '-' + constants.HANDLE_NETWORK_DELTA, requires=(constants.AMPHORA, constants.DELTA), - provides=constants.ADDED_PORTS)) + provides=constants.UPDATED_PORTS)) amp_for_failover_flow.add(amphora_driver_tasks.AmphoraePostNetworkPlug( name=prefix + '-' + constants.AMPHORAE_POST_NETWORK_PLUG, - requires=(constants.LOADBALANCER, constants.ADDED_PORTS))) + requires=(constants.LOADBALANCER, constants.UPDATED_PORTS))) return amp_for_failover_flow diff --git a/octavia/controller/worker/v2/flows/load_balancer_flows.py b/octavia/controller/worker/v2/flows/load_balancer_flows.py index 2c9eb049a8..33915ba7a7 100644 --- a/octavia/controller/worker/v2/flows/load_balancer_flows.py +++ b/octavia/controller/worker/v2/flows/load_balancer_flows.py @@ -196,12 +196,12 @@ class LoadBalancerFlows(object): ) flows.append( network_tasks.HandleNetworkDeltas( - requires=constants.DELTAS, provides=constants.ADDED_PORTS + requires=constants.DELTAS, provides=constants.UPDATED_PORTS ) ) flows.append( amphora_driver_tasks.AmphoraePostNetworkPlug( - requires=(constants.LOADBALANCER, constants.ADDED_PORTS) + requires=(constants.LOADBALANCER, constants.UPDATED_PORTS) ) ) flows.append( diff --git a/octavia/controller/worker/v2/flows/member_flows.py b/octavia/controller/worker/v2/flows/member_flows.py index b1927c83d9..0f6f8c0dab 100644 --- a/octavia/controller/worker/v2/flows/member_flows.py +++ b/octavia/controller/worker/v2/flows/member_flows.py @@ -42,10 +42,14 @@ class MemberFlows(object): requires=(constants.LOADBALANCER, constants.AVAILABILITY_ZONE), provides=constants.DELTAS)) create_member_flow.add(network_tasks.HandleNetworkDeltas( - requires=constants.DELTAS, provides=constants.ADDED_PORTS)) + requires=(constants.DELTAS, constants.LOADBALANCER), + provides=constants.UPDATED_PORTS)) + create_member_flow.add(network_tasks.GetAmphoraeNetworkConfigs( + requires=constants.LOADBALANCER_ID, + provides=constants.AMPHORAE_NETWORK_CONFIG)) create_member_flow.add(amphora_driver_tasks.AmphoraePostNetworkPlug( - requires=(constants.LOADBALANCER, constants.ADDED_PORTS) - )) + requires=(constants.LOADBALANCER, constants.UPDATED_PORTS, + constants.AMPHORAE_NETWORK_CONFIG))) create_member_flow.add(amphora_driver_tasks.ListenersUpdate( requires=constants.LOADBALANCER_ID)) create_member_flow.add(database_tasks.MarkMemberActiveInDB( @@ -72,6 +76,18 @@ class MemberFlows(object): constants.POOL_ID])) delete_member_flow.add(database_tasks.MarkMemberPendingDeleteInDB( requires=constants.MEMBER)) + delete_member_flow.add(network_tasks.CalculateDelta( + requires=(constants.LOADBALANCER, constants.AVAILABILITY_ZONE), + provides=constants.DELTAS)) + delete_member_flow.add(network_tasks.HandleNetworkDeltas( + requires=(constants.DELTAS, constants.LOADBALANCER), + provides=constants.UPDATED_PORTS)) + delete_member_flow.add(network_tasks.GetAmphoraeNetworkConfigs( + requires=constants.LOADBALANCER_ID, + provides=constants.AMPHORAE_NETWORK_CONFIG)) + delete_member_flow.add(amphora_driver_tasks.AmphoraePostNetworkPlug( + requires=(constants.LOADBALANCER, constants.UPDATED_PORTS, + constants.AMPHORAE_NETWORK_CONFIG))) delete_member_flow.add(amphora_driver_tasks.ListenersUpdate( requires=constants.LOADBALANCER_ID)) delete_member_flow.add(database_tasks.DeleteMemberInDB( @@ -183,10 +199,15 @@ class MemberFlows(object): requires=(constants.LOADBALANCER, constants.AVAILABILITY_ZONE), provides=constants.DELTAS)) batch_update_members_flow.add(network_tasks.HandleNetworkDeltas( - requires=constants.DELTAS, provides=constants.ADDED_PORTS)) + requires=(constants.DELTAS, constants.LOADBALANCER), + provides=constants.UPDATED_PORTS)) + batch_update_members_flow.add(network_tasks.GetAmphoraeNetworkConfigs( + requires=constants.LOADBALANCER_ID, + provides=constants.AMPHORAE_NETWORK_CONFIG)) batch_update_members_flow.add( amphora_driver_tasks.AmphoraePostNetworkPlug( - requires=(constants.LOADBALANCER, constants.ADDED_PORTS))) + requires=(constants.LOADBALANCER, constants.UPDATED_PORTS, + constants.AMPHORAE_NETWORK_CONFIG))) # Update the Listener (this makes the changes active on the Amp) batch_update_members_flow.add(amphora_driver_tasks.ListenersUpdate( diff --git a/octavia/controller/worker/v2/tasks/amphora_driver_tasks.py b/octavia/controller/worker/v2/tasks/amphora_driver_tasks.py index fdf91d23d1..77a6560fb6 100644 --- a/octavia/controller/worker/v2/tasks/amphora_driver_tasks.py +++ b/octavia/controller/worker/v2/tasks/amphora_driver_tasks.py @@ -259,7 +259,7 @@ class AmphoraFinalize(BaseAmphoraTask): class AmphoraPostNetworkPlug(BaseAmphoraTask): """Task to notify the amphora post network plug.""" - def execute(self, amphora, ports): + def execute(self, amphora, ports, amphora_network_config): """Execute post_network_plug routine.""" db_amp = self.amphora_repo.get(db_apis.get_session(), id=amphora[constants.ID]) @@ -279,8 +279,9 @@ class AmphoraPostNetworkPlug(BaseAmphoraTask): fixed_ips.append(data_models.FixedIP( subnet=data_models.Subnet(**subnet_arg), **ip)) self.amphora_driver.post_network_plug( - db_amp, data_models.Port(network=net, fixed_ips=fixed_ips, - **port)) + db_amp, + data_models.Port(network=net, fixed_ips=fixed_ips, **port), + amphora_network_config) LOG.debug("post_network_plug called on compute instance " "%(compute_id)s for port %(port_id)s", @@ -298,17 +299,18 @@ class AmphoraPostNetworkPlug(BaseAmphoraTask): class AmphoraePostNetworkPlug(BaseAmphoraTask): """Task to notify the amphorae post network plug.""" - def execute(self, loadbalancer, added_ports): + def execute(self, loadbalancer, updated_ports, amphorae_network_config): """Execute post_network_plug routine.""" amp_post_plug = AmphoraPostNetworkPlug() db_lb = self.loadbalancer_repo.get( db_apis.get_session(), id=loadbalancer[constants.LOADBALANCER_ID]) for amphora in db_lb.amphorae: - if amphora.id in added_ports: + if amphora.id in updated_ports: amp_post_plug.execute(amphora.to_dict(), - added_ports[amphora.id]) + updated_ports[amphora.id], + amphorae_network_config[amphora.id]) - def revert(self, result, loadbalancer, added_ports, *args, **kwargs): + def revert(self, result, loadbalancer, updated_ports, *args, **kwargs): """Handle a failed post network plug.""" if isinstance(result, failure.Failure): return diff --git a/octavia/controller/worker/v2/tasks/network_tasks.py b/octavia/controller/worker/v2/tasks/network_tasks.py index 6e715dff9c..eae08749b7 100644 --- a/octavia/controller/worker/v2/tasks/network_tasks.py +++ b/octavia/controller/worker/v2/tasks/network_tasks.py @@ -55,17 +55,16 @@ class CalculateAmphoraDelta(BaseNetworkTask): default_provides = constants.DELTA + # TODO(gthiemonge) ensure we no longer need vrrp_port def execute(self, loadbalancer, amphora, availability_zone, vrrp_port=None): LOG.debug("Calculating network delta for amphora id: %s", amphora.get(constants.ID)) - if vrrp_port is None: - vrrp_port = self.network_driver.get_port( - amphora[constants.VRRP_PORT_ID]) - vrrp_port_network_id = vrrp_port.network_id - else: - vrrp_port_network_id = vrrp_port[constants.NETWORK_ID] + vip_subnet_to_net_map = { + loadbalancer[constants.VIP_SUBNET_ID]: + loadbalancer[constants.VIP_NETWORK_ID] + } # Figure out what networks we want # seed with lb network(s) @@ -75,33 +74,88 @@ class CalculateAmphoraDelta(BaseNetworkTask): availability_zone.get(constants.MANAGEMENT_NETWORK)] else: management_nets = CONF.controller_worker.amp_boot_network_list - desired_network_ids = {vrrp_port_network_id}.union(management_nets) + db_lb = self.loadbalancer_repo.get( db_apis.get_session(), id=loadbalancer[constants.LOADBALANCER_ID]) - for pool in db_lb.pools: - member_networks = [ - self.network_driver.get_subnet(member.subnet_id).network_id - for member in pool.members - if member.subnet_id - ] - desired_network_ids.update(member_networks) + desired_subnet_to_net_map = { + subnet: mgmt_net_id + for mgmt_net_id in management_nets + for subnet in self.network_driver.get_network( + mgmt_net_id).subnets + } + desired_subnet_to_net_map.update(vip_subnet_to_net_map) + + for pool in db_lb.pools: + for member in pool.members: + if (member.subnet_id and + member.provisioning_status != + constants.PENDING_DELETE): + member_network = self.network_driver.get_subnet( + member.subnet_id).network_id + desired_subnet_to_net_map[member.subnet_id] = ( + member_network) + + desired_network_ids = set(desired_subnet_to_net_map.values()) + desired_subnet_ids = set(desired_subnet_to_net_map) + + # Calculate Network deltas nics = self.network_driver.get_plugged_networks( amphora[constants.COMPUTE_ID]) # assume we don't have two nics in the same network - actual_network_nics = dict((nic.network_id, nic) for nic in nics) + # TODO(rm_work): how accurate is this assumption really? + network_to_nic_map = {nic.network_id: nic for nic in nics} - del_ids = set(actual_network_nics) - desired_network_ids - delete_nics = list( - n_data_models.Interface(network_id=net_id) for net_id in del_ids) + del_ids = set(network_to_nic_map) - desired_network_ids + delete_nics = [n_data_models.Interface( + network_id=net_id, + port_id=network_to_nic_map[net_id].port_id) + for net_id in del_ids] + + add_ids = desired_network_ids - set(network_to_nic_map) + add_nics = [n_data_models.Interface( + network_id=net_id, + fixed_ips=[ + n_data_models.FixedIP( + subnet_id=subnet_id) + for subnet_id in desired_subnet_to_net_map + if desired_subnet_to_net_map[subnet_id] == net_id]) + for net_id in add_ids] + + # Calculate member Subnet deltas + plugged_subnets = { + fixed_ip.subnet_id: nic.network_id + for nic in network_to_nic_map.values() + for fixed_ip in nic.fixed_ips or [] + } + + del_subnet_ids = set(plugged_subnets) - desired_subnet_ids + add_subnet_ids = desired_subnet_ids - set(plugged_subnets) + + def _subnet_updates(subnet_ids, subnets): + updates = [] + for s in subnet_ids: + network_id = subnets[s] + nic = network_to_nic_map.get(network_id) + port_id = nic.port_id if nic else None + updates.append({ + constants.SUBNET_ID: s, + constants.NETWORK_ID: network_id, + constants.PORT_ID: port_id + }) + return updates + + add_subnets = _subnet_updates(add_subnet_ids, + desired_subnet_to_net_map) + del_subnets = _subnet_updates(del_subnet_ids, + plugged_subnets) - add_ids = desired_network_ids - set(actual_network_nics) - add_nics = list(n_data_models.Interface( - network_id=net_id) for net_id in add_ids) delta = n_data_models.Delta( amphora_id=amphora[constants.ID], compute_id=amphora[constants.COMPUTE_ID], - add_nics=add_nics, delete_nics=delete_nics) + add_nics=add_nics, delete_nics=delete_nics, + add_subnets=add_subnets, + delete_subnets=del_subnets) return delta.to_dict(recurse=True) @@ -256,29 +310,86 @@ class HandleNetworkDelta(BaseNetworkTask): Plug or unplug networks based on delta """ + def _fill_port_info(self, port): + port.network = self.network_driver.get_network(port.network_id) + for fixed_ip in port.fixed_ips: + fixed_ip.subnet = self.network_driver.get_subnet( + fixed_ip.subnet_id) + def execute(self, amphora, delta): """Handle network plugging based off deltas.""" - added_ports = {} - added_ports[amphora[constants.ID]] = [] + db_amp = self.amphora_repo.get(db_apis.get_session(), + id=amphora.get(constants.ID)) + updated_ports = {} for nic in delta[constants.ADD_NICS]: + subnet_id = [ + fixed_ip[constants.SUBNET_ID] + for fixed_ip in nic[constants.FIXED_IPS]][0] interface = self.network_driver.plug_network( - delta[constants.COMPUTE_ID], nic[constants.NETWORK_ID]) + db_amp.compute_id, nic[constants.NETWORK_ID]) port = self.network_driver.get_port(interface.port_id) - port.network = self.network_driver.get_network(port.network_id) + self._fill_port_info(port) + updated_ports[port.network_id] = port.to_dict(recurse=True) + + for update in delta.get(constants.ADD_SUBNETS, []): + network_id = update[constants.NETWORK_ID] + # Get already existing port from Deltas or + # newly created port from updated_ports dict + port_id = (update[constants.PORT_ID] or + updated_ports[network_id][constants.ID]) + subnet_id = update[constants.SUBNET_ID] + # Avoid duplicated subnets + has_subnet = False + if network_id in updated_ports: + for fixed_ip in updated_ports[network_id][ + constants.FIXED_IPS]: + if fixed_ip[constants.SUBNET_ID] == subnet_id: + has_subnet = True + if not has_subnet: + port = self.network_driver.plug_fixed_ip( + port_id=port_id, subnet_id=subnet_id) + self._fill_port_info(port) + updated_ports[network_id] = ( + port.to_dict(recurse=True)) + + for update in delta.get(constants.DELETE_SUBNETS, []): + network_id = update[constants.NETWORK_ID] + port_id = update[constants.PORT_ID] + subnet_id = update[constants.SUBNET_ID] + port = self.network_driver.unplug_fixed_ip( + port_id=port_id, subnet_id=subnet_id) + self._fill_port_info(port) + # In neutron, when removing an ipv6 subnet (with slaac) from a + # port, it just ignores it. + # https://bugs.launchpad.net/neutron/+bug/1945156 + # When it happens, don't add the port to the updated_ports dict + has_subnet = False for fixed_ip in port.fixed_ips: - fixed_ip.subnet = self.network_driver.get_subnet( - fixed_ip.subnet_id) - added_ports[amphora[constants.ID]].append(port.to_dict( - recurse=True)) + if fixed_ip.subnet_id == subnet_id: + has_subnet = True + break + if not has_subnet: + updated_ports[network_id] = ( + port.to_dict(recurse=True)) + for nic in delta[constants.DELETE_NICS]: + network_id = nic[constants.NETWORK_ID] try: self.network_driver.unplug_network( - delta[constants.COMPUTE_ID], nic[constants.NETWORK_ID]) + db_amp.compute_id, network_id) except base.NetworkNotFound: - LOG.debug("Network %d not found ", nic[constants.NETWORK_ID]) + LOG.debug("Network %s not found", network_id) except Exception: LOG.exception("Unable to unplug network") - return added_ports + + port_id = nic[constants.PORT_ID] + try: + self.network_driver.delete_port(port_id) + except Exception: + LOG.exception("Unable to delete the port") + + updated_ports.pop(network_id, None) + return {amphora[constants.ID]: list(updated_ports.values())} def revert(self, result, amphora, delta, *args, **kwargs): """Handle a network plug or unplug failures.""" @@ -299,6 +410,12 @@ class HandleNetworkDelta(BaseNetworkTask): except Exception: pass + port_id = nic[constants.PORT_ID] + try: + self.network_driver.delete_port(port_id) + except Exception: + pass + class HandleNetworkDeltas(BaseNetworkTask): """Task to plug and unplug networks @@ -307,49 +424,45 @@ class HandleNetworkDeltas(BaseNetworkTask): networks based on delta """ - def execute(self, deltas): + def execute(self, deltas, loadbalancer): """Handle network plugging based off deltas.""" - added_ports = {} + db_lb = self.loadbalancer_repo.get( + db_apis.get_session(), id=loadbalancer[constants.LOADBALANCER_ID]) + amphorae = {amp.id: amp for amp in db_lb.amphorae} + + updated_ports = {} + handle_delta = HandleNetworkDelta() + for amp_id, delta in deltas.items(): - added_ports[amp_id] = [] - for nic in delta[constants.ADD_NICS]: - interface = self.network_driver.plug_network( - delta[constants.COMPUTE_ID], nic[constants.NETWORK_ID]) - port = self.network_driver.get_port(interface.port_id) - port.network = self.network_driver.get_network(port.network_id) - for fixed_ip in port.fixed_ips: - fixed_ip.subnet = self.network_driver.get_subnet( - fixed_ip.subnet_id) - added_ports[amp_id].append(port.to_dict(recurse=True)) - for nic in delta[constants.DELETE_NICS]: - try: - self.network_driver.unplug_network( - delta[constants.COMPUTE_ID], - nic[constants.NETWORK_ID]) - except base.NetworkNotFound: - LOG.debug("Network %d not found ", - nic[constants.NETWORK_ID]) - except Exception: - LOG.exception("Unable to unplug network") - return added_ports + ret = handle_delta.execute(amphorae[amp_id].to_dict(), delta) + updated_ports.update(ret) + + return updated_ports def revert(self, result, deltas, *args, **kwargs): """Handle a network plug or unplug failures.""" if isinstance(result, failure.Failure): return + + if not deltas: + return + for amp_id, delta in deltas.items(): LOG.warning("Unable to plug networks for amp id %s", delta[constants.AMPHORA_ID]) - if not delta: - return - for nic in delta[constants.ADD_NICS]: try: self.network_driver.unplug_network( delta[constants.COMPUTE_ID], nic[constants.NETWORK_ID]) - except base.NetworkNotFound: + except Exception: + pass + + port_id = nic[constants.PORT_ID] + try: + self.network_driver.delete_port(port_id) + except Exception: pass diff --git a/octavia/network/base.py b/octavia/network/base.py index cf129f89d1..9e068b1eea 100644 --- a/octavia/network/base.py +++ b/octavia/network/base.py @@ -45,6 +45,10 @@ class UnplugNetworkException(NetworkException): pass +class AllocateNetworkException(NetworkException): + pass + + class VIPInUseException(NetworkException): pass @@ -169,7 +173,7 @@ class AbstractNetworkDriver(object, metaclass=abc.ABCMeta): """ @abc.abstractmethod - def plug_network(self, compute_id, network_id, ip_address=None): + def plug_network(self, amphora, network_id, subnet_id=None): """Connects an existing amphora to an existing network. :param compute_id: id of an amphora in the compute service @@ -180,10 +184,10 @@ class AbstractNetworkDriver(object, metaclass=abc.ABCMeta): """ @abc.abstractmethod - def unplug_network(self, compute_id, network_id, ip_address=None): + def unplug_network(self, compute_id, network_id): """Disconnects an existing amphora from an existing network. - If ip_address is not specificed, all the interfaces plugged on + If ip_address is not specified, all the interfaces plugged on network_id should be unplugged. :param compute_id: id of an amphora in the compute service @@ -194,6 +198,29 @@ class AbstractNetworkDriver(object, metaclass=abc.ABCMeta): NetworkException """ + @abc.abstractmethod + def plug_fixed_ip(self, port_id, subnet_id, ip_address=None): + """Plug a fixed ip to an existing port. + + If ip_address is not specified, one will be auto-assigned. + + :param port_id: id of a port to add a fixed ip + :param subnet_id: id of a subnet + :param ip_address: specific ip_address to add + :return: octavia.network.data_models.Port + :raises: NetworkException, PortNotFound + """ + + @abc.abstractmethod + def unplug_fixed_ip(self, port_id, subnet_id): + """Unplug a fixed ip from an existing port. + + :param port_id: id of a port to remove the fixed ip from + :param subnet_id: id of a subnet + :return: octavia.network.data_models.Port + :raises: NetworkException, PortNotFound + """ + @abc.abstractmethod def get_plugged_networks(self, compute_id): """Retrieves the current plugged networking configuration. diff --git a/octavia/network/data_models.py b/octavia/network/data_models.py index 7dd061f249..d2b612adbd 100644 --- a/octavia/network/data_models.py +++ b/octavia/network/data_models.py @@ -29,11 +29,14 @@ class Interface(data_models.BaseDataModel): class Delta(data_models.BaseDataModel): def __init__(self, amphora_id=None, compute_id=None, - add_nics=None, delete_nics=None): + add_nics=None, delete_nics=None, + add_subnets=None, delete_subnets=None): self.compute_id = compute_id self.amphora_id = amphora_id self.add_nics = add_nics self.delete_nics = delete_nics + self.add_subnets = add_subnets + self.delete_subnets = delete_subnets class Network(data_models.BaseDataModel): diff --git a/octavia/network/drivers/neutron/allowed_address_pairs.py b/octavia/network/drivers/neutron/allowed_address_pairs.py index e7fa1c501f..f2867258f6 100644 --- a/octavia/network/drivers/neutron/allowed_address_pairs.py +++ b/octavia/network/drivers/neutron/allowed_address_pairs.py @@ -547,7 +547,7 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver): amphora.compute_id) return try: - self.unplug_network(amphora.compute_id, subnet.network_id) + self.unplug_network(amphora, subnet.network_id) except Exception: pass try: @@ -610,15 +610,14 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver): return self._nova_interface_to_octavia_interface(compute_id, interface) - def unplug_network(self, compute_id, network_id, ip_address=None): + def unplug_network(self, compute_id, network_id): interfaces = self.get_plugged_networks(compute_id) if not interfaces: msg = ('Amphora with compute id {compute_id} does not have any ' 'plugged networks').format(compute_id=compute_id) raise base.NetworkNotFound(msg) - unpluggers = self._get_interfaces_to_unplug(interfaces, network_id, - ip_address=ip_address) + unpluggers = self._get_interfaces_to_unplug(interfaces, network_id) for index, unplugger in enumerate(unpluggers): self.compute.detach_port( compute_id=compute_id, port_id=unplugger.port_id) diff --git a/octavia/network/drivers/neutron/base.py b/octavia/network/drivers/neutron/base.py index 93eb7730e6..83dc921482 100644 --- a/octavia/network/drivers/neutron/base.py +++ b/octavia/network/drivers/neutron/base.py @@ -275,3 +275,35 @@ class BaseNeutronDriver(base.AbstractNetworkDriver): def get_network_ip_availability(self, network): return self._get_resource('network_ip_availability', network.id) + + def plug_fixed_ip(self, port_id, subnet_id, ip_address=None): + port = self.get_port(port_id).to_dict(recurse=True) + fixed_ips = port['fixed_ips'] + + new_fixed_ip = {'subnet_id': subnet_id} + if ip_address: + new_fixed_ip['ip_address'] = ip_address + + fixed_ips.append(new_fixed_ip) + + body = {'port': {'fixed_ips': fixed_ips}} + try: + updated_port = self.neutron_client.update_port(port_id, body) + return utils.convert_port_dict_to_model(updated_port) + except Exception as e: + raise base.NetworkException(str(e)) + + def unplug_fixed_ip(self, port_id, subnet_id): + port = self.get_port(port_id) + fixed_ips = [ + fixed_ip.to_dict() + for fixed_ip in port.fixed_ips + if fixed_ip.subnet_id != subnet_id + ] + + body = {'port': {'fixed_ips': fixed_ips}} + try: + updated_port = self.neutron_client.update_port(port_id, body) + return utils.convert_port_dict_to_model(updated_port) + except Exception as e: + raise base.NetworkException(str(e)) diff --git a/octavia/network/drivers/noop_driver/driver.py b/octavia/network/drivers/noop_driver/driver.py index a3d945c02f..c59eb6d99d 100644 --- a/octavia/network/drivers/noop_driver/driver.py +++ b/octavia/network/drivers/noop_driver/driver.py @@ -21,8 +21,12 @@ from octavia.network import data_models as network_models LOG = logging.getLogger(__name__) +_PLUGGED_NETWORKS = {} +_PORTS = {} + class NoopManager(object): + def __init__(self): super().__init__() self.networkconfigconfig = {} @@ -110,33 +114,41 @@ class NoopManager(object): vip.ip_address)] = (vip, amphora, subnet, 'unplug_aap_port') - def plug_network(self, compute_id, network_id, ip_address=None): + def plug_network(self, compute_id, network_id, subnet_id=None): LOG.debug("Network %s no-op, plug_network compute_id %s, network_id " - "%s, ip_address %s", self.__class__.__name__, compute_id, - network_id, ip_address) - self.networkconfigconfig[(compute_id, network_id, ip_address)] = ( - compute_id, network_id, ip_address, 'plug_network') - return network_models.Interface( + "%s, subnet %s", self.__class__.__name__, compute_id, + network_id, subnet_id) + self.networkconfigconfig[(compute_id, network_id)] = ( + compute_id, network_id, subnet_id, 'plug_network') + interface = network_models.Interface( id=uuidutils.generate_uuid(), compute_id=compute_id, network_id=network_id, - fixed_ips=[], + fixed_ips=[network_models.FixedIP( + subnet_id=subnet_id)], port_id=uuidutils.generate_uuid() ) + _PORTS[interface.port_id] = network_models.Port( + id=interface.port_id, + network_id=network_id) + _PLUGGED_NETWORKS[(network_id, compute_id)] = interface + return interface - def unplug_network(self, compute_id, network_id, ip_address=None): + def unplug_network(self, compute_id, network_id): LOG.debug("Network %s no-op, unplug_network compute_id %s, " "network_id %s", self.__class__.__name__, compute_id, network_id) - self.networkconfigconfig[(compute_id, network_id, ip_address)] = ( - compute_id, network_id, ip_address, 'unplug_network') + self.networkconfigconfig[(compute_id, network_id)] = ( + compute_id, network_id, 'unplug_network') + _PLUGGED_NETWORKS.pop((network_id, compute_id), None) def get_plugged_networks(self, compute_id): LOG.debug("Network %s no-op, get_plugged_networks amphora_id %s", self.__class__.__name__, compute_id) self.networkconfigconfig[compute_id] = ( compute_id, 'get_plugged_networks') - return [] + return [pn for pn in _PLUGGED_NETWORKS.values() + if pn.compute_id == compute_id] def update_vip(self, loadbalancer, for_delete=False): LOG.debug("Network %s no-op, update_vip loadbalancer %s " @@ -175,7 +187,10 @@ class NoopManager(object): LOG.debug("Port %s no-op, get_port port_id %s", self.__class__.__name__, port_id) self.networkconfigconfig[port_id] = (port_id, 'get_port') - return network_models.Port(id=uuidutils.generate_uuid()) + if port_id in _PORTS: + return _PORTS[port_id] + return network_models.Port(id=uuidutils.generate_uuid(), + network_id=uuidutils.generate_uuid()) def get_network_by_name(self, network_name): LOG.debug("Network %s no-op, get_network_by_name network_name %s", @@ -337,6 +352,27 @@ class NoopManager(object): admin_state_up=admin_state_up, fixed_ips=fixed_ip_obj_list, qos_policy_id=qos_policy_id, security_group_ids=security_group_ids) + def plug_fixed_ip(self, port_id, subnet_id, ip_address=None): + LOG.debug("Network %s no-op, plug_fixed_ip port_id %s, subnet_id " + "%s, ip_address %s", self.__class__.__name__, port_id, + subnet_id, ip_address) + self.networkconfigconfig[(port_id, subnet_id)] = ( + port_id, subnet_id, ip_address, 'plug_fixed_ip') + + port = network_models.Port(id=port_id, + network_id=uuidutils.generate_uuid()) + _PORTS[port.id] = port + return port + + def unplug_fixed_ip(self, port_id, subnet_id): + LOG.debug("Network %s no-op, unplug_fixed_ip port_id %s, subnet_id " + "%s", self.__class__.__name__, port_id, + subnet_id) + self.networkconfigconfig[(port_id, subnet_id)] = ( + port_id, subnet_id, 'unplug_fixed_ip') + + return _PORTS.get(port_id) + class NoopNetworkDriver(driver_base.AbstractNetworkDriver): def __init__(self): @@ -355,15 +391,15 @@ class NoopNetworkDriver(driver_base.AbstractNetworkDriver): def unplug_vip(self, loadbalancer, vip): self.driver.unplug_vip(loadbalancer, vip) - def plug_network(self, amphora_id, network_id, ip_address=None): - return self.driver.plug_network(amphora_id, network_id, ip_address) + def plug_network(self, compute_id, network_id, subnet_id=None): + return self.driver.plug_network(compute_id, network_id, + subnet_id=subnet_id) - def unplug_network(self, amphora_id, network_id, ip_address=None): - self.driver.unplug_network(amphora_id, network_id, - ip_address=ip_address) + def unplug_network(self, compute_id, network_id): + self.driver.unplug_network(compute_id, network_id) - def get_plugged_networks(self, amphora_id): - return self.driver.get_plugged_networks(amphora_id) + def get_plugged_networks(self, compute_id): + return self.driver.get_plugged_networks(compute_id) def update_vip(self, loadbalancer, for_delete=False): self.driver.update_vip(loadbalancer, for_delete) @@ -437,3 +473,9 @@ class NoopNetworkDriver(driver_base.AbstractNetworkDriver): return self.driver.create_port( network_id, name, fixed_ips, secondary_ips, security_group_ids, admin_state_up, qos_policy_id) + + def plug_fixed_ip(self, port_id, subnet_id, ip_address=None): + return self.driver.plug_fixed_ip(port_id, subnet_id, ip_address) + + def unplug_fixed_ip(self, port_id, subnet_id): + return self.driver.unplug_fixed_ip(port_id, subnet_id) diff --git a/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py b/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py index 59fab933d1..f3c3cf7fc1 100644 --- a/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py +++ b/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py @@ -967,10 +967,12 @@ class TestServerTestCase(base.TestCase): @mock.patch('subprocess.check_output') @mock.patch('octavia.amphorae.backends.agent.api_server.' 'plug.Plug._netns_interface_exists') + @mock.patch('octavia.amphorae.backends.agent.api_server.' + 'plug.Plug._netns_interface_by_mac') @mock.patch('os.path.isfile') - def _test_plug_network(self, distro, mock_isfile, mock_int_exists, - mock_check_output, mock_netns, mock_pyroute2, - mock_os_chmod): + def _test_plug_network(self, distro, mock_isfile, mock_int_by_mac, + mock_int_exists, mock_check_output, mock_netns, + mock_pyroute2, mock_os_chmod): mock_ipr = mock.MagicMock() mock_ipr_instance = mock.MagicMock() mock_ipr_instance.link_lookup.side_effect = [ @@ -992,21 +994,22 @@ class TestServerTestCase(base.TestCase): test_int_num = str(test_int_num) # Interface already plugged - mock_int_exists.return_value = True - if distro == consts.UBUNTU: - rv = self.ubuntu_app.post('/' + api_server.VERSION + - "/plug/network", - content_type='application/json', - data=jsonutils.dumps(port_info)) - elif distro == consts.CENTOS: - rv = self.centos_app.post('/' + api_server.VERSION + - "/plug/network", - content_type='application/json', - data=jsonutils.dumps(port_info)) - self.assertEqual(409, rv.status_code) - self.assertEqual(dict(message="Interface already exists"), - jsonutils.loads(rv.data.decode('utf-8'))) - mock_int_exists.return_value = False + # TODO(gthiemonge) FIXME + # mock_int_exists.return_value = True + # if distro == consts.UBUNTU: + # rv = self.ubuntu_app.post('/' + api_server.VERSION + + # "/plug/network", + # content_type='application/json', + # data=jsonutils.dumps(port_info)) + # elif distro == consts.CENTOS: + # rv = self.centos_app.post('/' + api_server.VERSION + + # "/plug/network", + # content_type='application/json', + # data=jsonutils.dumps(port_info)) + # self.assertEqual(409, rv.status_code) + # self.assertEqual(dict(message="Interface already exists"), + # jsonutils.loads(rv.data.decode('utf-8'))) + # mock_int_exists.return_value = False # No interface at all file_name = '/sys/bus/pci/rescan' diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py index 658551087b..fe5a51eb70 100644 --- a/octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py +++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py @@ -96,6 +96,20 @@ class TestOSUtils(base.TestCase): package_name)) self.assertEqual(centos_cmd, returned_centos_cmd) + @mock.patch('octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile') + def test_write_interface_file(self, mock_interface_file): + mock_interface = mock.MagicMock() + mock_interface_file.return_value = mock_interface + + self.ubuntu_os_util.write_interface_file('eth1', + '1.2.3.4', 16) + + mock_interface_file.assert_called_once_with( + name='eth1', + addresses=[{"address": "1.2.3.4", "prefixlen": 16}]) + mock_interface.write.assert_called_once() + @mock.patch('octavia.amphorae.backends.utils.interface_file.' 'VIPInterfaceFile') def test_write_vip_interface_file(self, mock_vip_interface_file): @@ -142,6 +156,7 @@ class TestOSUtils(base.TestCase): mtu=MTU, vrrp_ip=None, host_routes=host_routes, + fixed_ips=None, topology="SINGLE") mock_vip_interface_file.return_value.write.assert_called_once() @@ -167,6 +182,7 @@ class TestOSUtils(base.TestCase): mtu=MTU, vrrp_ip=None, host_routes=host_routes, + fixed_ips=None, topology="SINGLE") @mock.patch('octavia.amphorae.backends.utils.interface_file.' diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py index 7e74f260fb..6cabc1f942 100644 --- a/octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py +++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py @@ -193,16 +193,167 @@ class TestPlug(base.TestCase): mock_webob.Response.assert_any_call(json={'message': 'Invalid VIP'}, status=400) + @mock.patch("octavia.amphorae.backends.agent.api_server.osutils." + "BaseOS.write_interface_file") + def test_plug_lo(self, mock_write_interface): + m = mock.mock_open() + with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): + self.test_plug.plug_lo() + mock_write_interface.assert_called_once_with(interface='lo', + ip_address='127.0.0.1', + prefixlen=8) + @mock.patch('pyroute2.NetNS', create=True) def test__netns_interface_exists(self, mock_netns): netns_handle = mock_netns.return_value.__enter__.return_value netns_handle.get_links.return_value = [{ - 'attrs': [['IFLA_ADDRESS', '123']]}] + 'attrs': [['IFLA_ADDRESS', '123'], + ['IFLA_IFNAME', 'eth0']]}] # Interface is found in netns self.assertTrue(self.test_plug._netns_interface_exists('123')) # Interface is not found in netns self.assertFalse(self.test_plug._netns_interface_exists('321')) + + @mock.patch.object(plug, "webob") + @mock.patch('octavia.amphorae.backends.agent.api_server.plug.Plug.' + '_netns_interface_exists', return_value=False) + @mock.patch('octavia.amphorae.backends.agent.api_server.plug.Plug.' + '_interface_by_mac', return_value=FAKE_INTERFACE) + @mock.patch('pyroute2.IPRoute', create=True) + @mock.patch('pyroute2.netns.create', create=True) + @mock.patch('pyroute2.NetNS', create=True) + @mock.patch("octavia.amphorae.backends.agent.api_server.osutils." + "BaseOS.write_port_interface_file") + @mock.patch("octavia.amphorae.backends.agent.api_server.osutils." + "BaseOS.bring_interface_up") + def test_plug_network(self, mock_if_up, mock_write_port_interface, + mock_netns, mock_netns_create, mock_iproute, + mock_by_mac, mock_interface_exists, mock_webob): + fixed_ips = [ + {'ip_address': FAKE_IP_IPV4, + 'subnet_cidr': FAKE_CIDR_IPV4, + 'gateway': FAKE_GATEWAY_IPV4, + 'host_routes': [ + {'destination': '10.1.0.0/16', + 'nexthop': '10.0.1.254'}] + }] + mtu = 1400 + m = mock.mock_open() + with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): + self.test_plug.plug_network(FAKE_MAC_ADDRESS, fixed_ips, 1400) + + mock_write_port_interface.assert_called_once_with( + interface='eth0', fixed_ips=fixed_ips, mtu=mtu) + mock_if_up.assert_called_once_with('eth0', 'network') + + mock_webob.Response.assert_any_call( + json={'message': 'OK', + 'details': 'Plugged on interface eth0'}, + status=202) + + @mock.patch.object(plug, "webob") + @mock.patch('octavia.amphorae.backends.agent.api_server.plug.Plug.' + '_netns_interface_exists', return_value=True) + @mock.patch('octavia.amphorae.backends.agent.api_server.plug.Plug.' + '_netns_interface_by_mac', return_value=FAKE_INTERFACE) + @mock.patch('pyroute2.NetNS', create=True) + @mock.patch("octavia.amphorae.backends.agent.api_server.osutils." + "BaseOS.write_port_interface_file") + @mock.patch("octavia.amphorae.backends.agent.api_server.osutils." + "BaseOS.bring_interface_up") + def test_plug_network_existing_interface(self, mock_if_up, + mock_write_port_interface, + mock_netns, mock_by_mac, + mock_interface_exists, + mock_webob): + fixed_ips = [ + {'ip_address': FAKE_IP_IPV4, + 'subnet_cidr': FAKE_CIDR_IPV4, + 'gateway': FAKE_GATEWAY_IPV4, + 'host_routes': [ + {'destination': '10.1.0.0/16', + 'nexthop': '10.0.1.254'}] + }, {'ip_address': FAKE_IP_IPV6, + 'subnet_cidr': FAKE_CIDR_IPV6, + 'gateway': FAKE_GATEWAY_IPV6, + 'host_routes': [ + {'destination': '2001:1::/64', + 'nexthop': '1001:1::ffff'}] + }] + mtu = 1400 + m = mock.mock_open() + with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): + self.test_plug.plug_network(FAKE_MAC_ADDRESS, fixed_ips, 1400) + + mock_write_port_interface.assert_called_once_with( + interface=FAKE_INTERFACE, fixed_ips=fixed_ips, mtu=mtu) + mock_if_up.assert_called_once_with(FAKE_INTERFACE, 'network') + + mock_webob.Response.assert_any_call( + json={'message': 'OK', + 'details': 'Updated existing interface {}'.format( + FAKE_INTERFACE)}, + status=202) + + @mock.patch.object(plug, "webob") + @mock.patch('octavia.amphorae.backends.agent.api_server.plug.Plug.' + '_netns_interface_exists', return_value=True) + @mock.patch('octavia.amphorae.backends.agent.api_server.plug.Plug.' + '_netns_interface_by_mac', return_value=FAKE_INTERFACE) + @mock.patch('pyroute2.NetNS', create=True) + @mock.patch("octavia.amphorae.backends.agent.api_server.osutils." + "BaseOS.write_vip_interface_file") + @mock.patch("octavia.amphorae.backends.agent.api_server.osutils." + "BaseOS.bring_interface_up") + def test_plug_network_on_vip( + self, mock_if_up, mock_write_vip_interface, + mock_netns, mock_by_mac, mock_interface_exists, mock_webob): + fixed_ips = [ + {'ip_address': FAKE_IP_IPV4, + 'subnet_cidr': FAKE_CIDR_IPV4, + 'gateway': FAKE_GATEWAY_IPV4, + 'host_routes': [ + {'destination': '10.1.0.0/16', + 'nexthop': '10.0.1.254'}] + }, {'ip_address': FAKE_IP_IPV6, + 'subnet_cidr': FAKE_CIDR_IPV6, + 'gateway': FAKE_GATEWAY_IPV6, + 'host_routes': [ + {'destination': '2001:1::/64', + 'nexthop': '1001:1::ffff'}] + }] + mtu = 1400 + vip_net_info = { + 'vip': '10.2.1.1', + 'subnet_cidr': '10.2.0.0/16', + 'vrrp_ip': '10.2.1.2', + 'gateway': '10.2.255.254', + 'host_routes': [] + } + + m = mock.mock_open() + with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): + self.test_plug.plug_network(FAKE_MAC_ADDRESS, fixed_ips, mtu=1400, + vip_net_info=vip_net_info) + + mock_write_vip_interface.assert_called_once_with( + interface=FAKE_INTERFACE, + vip=vip_net_info['vip'], + ip_version=4, + prefixlen=16, + gateway=vip_net_info['gateway'], + vrrp_ip=vip_net_info['vrrp_ip'], + host_routes=[], + fixed_ips=fixed_ips, mtu=mtu) + + mock_if_up.assert_called_once_with(FAKE_INTERFACE, 'vip') + + mock_webob.Response.assert_any_call( + json={'message': 'OK', + 'details': 'Updated existing interface {}'.format( + FAKE_INTERFACE)}, + status=202) diff --git a/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_0_5.py b/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_0_5.py index 5e592bc288..547f31d2a7 100644 --- a/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_0_5.py +++ b/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_0_5.py @@ -94,6 +94,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): self.fixed_ip = mock.MagicMock() self.fixed_ip.ip_address = '198.51.100.5' self.fixed_ip.subnet.cidr = '198.51.100.0/24' + self.fixed_ip.subnet.gateway_ip = FAKE_GATEWAY self.network = network_models.Network(mtu=FAKE_MTU) self.port = network_models.Port(mac_address=FAKE_MAC_ADDRESS, fixed_ips=[self.fixed_ip], @@ -116,6 +117,9 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): constants.REQ_READ_TIMEOUT: 2, constants.CONN_MAX_RETRIES: 3, constants.CONN_RETRY_INTERVAL: 4} + self.amp_net_config = network_models.AmphoraNetworkConfig( + vip_subnet=self.lb.vip.subnet_id + ) @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' 'HaproxyAmphoraLoadBalancerDriver._process_secret') @@ -589,6 +593,13 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): amphorae_network_config.get().vip_subnet.cidr = FAKE_CIDR amphorae_network_config.get().vip_subnet.gateway_ip = FAKE_GATEWAY amphorae_network_config.get().vip_subnet.host_routes = self.host_routes + amphorae_network_config.get().vip_subnet.to_dict.return_value = { + 'cidr': FAKE_CIDR, + 'gateway_ip': FAKE_GATEWAY, + 'host_routes': [ + hr.to_dict(recurse=True) + for hr in self.host_routes] + } amphorae_network_config.get().vrrp_port = self.port self.driver.post_vip_plug(self.amp, self.lb, amphorae_network_config) self.driver.clients[API_VERSION].plug_vip.assert_called_once_with( @@ -599,7 +610,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): port = network_models.Port(mac_address=FAKE_MAC_ADDRESS, fixed_ips=[], network=self.network) - self.driver.post_network_plug(self.amp, port) + self.driver.post_network_plug(self.amp, port, self.amp_net_config) self.driver.clients[API_VERSION].plug_network.assert_called_once_with( self.amp, dict(mac_address=FAKE_MAC_ADDRESS, fixed_ips=[], @@ -608,12 +619,13 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): self.driver.clients[API_VERSION].plug_network.reset_mock() # Test fixed IP path - self.driver.post_network_plug(self.amp, self.port) + self.driver.post_network_plug(self.amp, self.port, self.amp_net_config) self.driver.clients[API_VERSION].plug_network.assert_called_once_with( self.amp, dict(mac_address=FAKE_MAC_ADDRESS, fixed_ips=[dict(ip_address='198.51.100.5', subnet_cidr='198.51.100.0/24', - host_routes=[])], + host_routes=[], + gateway=FAKE_GATEWAY)], mtu=FAKE_MTU)) def test_post_network_plug_with_host_routes(self): @@ -629,6 +641,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): network_models.HostRoute(destination=DEST2, nexthop=NEXTHOP)] subnet = network_models.Subnet(id=SUBNET_ID, cidr=SUBNET_CIDR, + gateway_ip=FAKE_GATEWAY, ip_version=4, host_routes=host_routes) fixed_ips = [ network_models.FixedIP(subnet_id=subnet.id, ip_address=FIXED_IP1, @@ -639,12 +652,14 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): port = network_models.Port(mac_address=FAKE_MAC_ADDRESS, fixed_ips=fixed_ips, network=self.network) - self.driver.post_network_plug(self.amp, port) + self.driver.post_network_plug(self.amp, port, self.amp_net_config) expected_fixed_ips = [ {'ip_address': FIXED_IP1, 'subnet_cidr': SUBNET_CIDR, + 'gateway': FAKE_GATEWAY, 'host_routes': [{'destination': DEST1, 'nexthop': NEXTHOP}, {'destination': DEST2, 'nexthop': NEXTHOP}]}, {'ip_address': FIXED_IP2, 'subnet_cidr': SUBNET_CIDR, + 'gateway': FAKE_GATEWAY, 'host_routes': [{'destination': DEST1, 'nexthop': NEXTHOP}, {'destination': DEST2, 'nexthop': NEXTHOP}]} ] diff --git a/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_1_0.py b/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_1_0.py index 60ee82bb0d..1d977ef55a 100644 --- a/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_1_0.py +++ b/octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_1_0.py @@ -94,6 +94,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): self.fixed_ip = mock.MagicMock() self.fixed_ip.ip_address = '198.51.100.5' self.fixed_ip.subnet.cidr = '198.51.100.0/24' + self.fixed_ip.subnet.gateway_ip = FAKE_GATEWAY self.network = network_models.Network(mtu=FAKE_MTU) self.port = network_models.Port(mac_address=FAKE_MAC_ADDRESS, fixed_ips=[self.fixed_ip], @@ -116,6 +117,11 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): constants.REQ_READ_TIMEOUT: 2, constants.CONN_MAX_RETRIES: 3, constants.CONN_RETRY_INTERVAL: 4} + self.amp_net_config = network_models.AmphoraNetworkConfig( + vip_subnet=network_models.Subnet( + id=self.lb.vip.subnet_id, + cidr='10.1.0.0/16', + host_routes=[])) @mock.patch('octavia.amphorae.drivers.haproxy.rest_api_driver.' 'HaproxyAmphoraLoadBalancerDriver._process_secret') @@ -684,6 +690,13 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): amphorae_network_config.get().vip_subnet.cidr = FAKE_CIDR amphorae_network_config.get().vip_subnet.gateway_ip = FAKE_GATEWAY amphorae_network_config.get().vip_subnet.host_routes = self.host_routes + amphorae_network_config.get().vip_subnet.to_dict.return_value = { + 'cidr': FAKE_CIDR, + 'gateway_ip': FAKE_GATEWAY, + 'host_routes': [ + hr.to_dict(recurse=True) + for hr in self.host_routes] + } amphorae_network_config.get().vrrp_port = self.port self.driver.post_vip_plug(self.amp, self.lb, amphorae_network_config) self.driver.clients[API_VERSION].plug_vip.assert_called_once_with( @@ -694,7 +707,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): port = network_models.Port(mac_address=FAKE_MAC_ADDRESS, fixed_ips=[], network=self.network) - self.driver.post_network_plug(self.amp, port) + self.driver.post_network_plug(self.amp, port, self.amp_net_config) self.driver.clients[API_VERSION].plug_network.assert_called_once_with( self.amp, dict(mac_address=FAKE_MAC_ADDRESS, fixed_ips=[], @@ -703,14 +716,40 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): self.driver.clients[API_VERSION].plug_network.reset_mock() # Test fixed IP path - self.driver.post_network_plug(self.amp, self.port) + self.driver.post_network_plug(self.amp, self.port, self.amp_net_config) self.driver.clients[API_VERSION].plug_network.assert_called_once_with( self.amp, dict(mac_address=FAKE_MAC_ADDRESS, fixed_ips=[dict(ip_address='198.51.100.5', subnet_cidr='198.51.100.0/24', - host_routes=[])], + host_routes=[], + gateway=FAKE_GATEWAY)], mtu=FAKE_MTU)) + self.driver.clients[API_VERSION].plug_network.reset_mock() + + # Test member network on vip port + port = network_models.Port(id=self.amp.vrrp_port_id, + mac_address=FAKE_MAC_ADDRESS, + fixed_ips=[self.fixed_ip], + network=self.network) + self.driver.post_network_plug(self.amp, port, self.amp_net_config) + self.driver.clients[API_VERSION].plug_network.assert_called_once_with( + self.amp, dict(mac_address=FAKE_MAC_ADDRESS, + fixed_ips=[dict(ip_address='198.51.100.5', + subnet_cidr='198.51.100.0/24', + host_routes=[], + gateway=FAKE_GATEWAY)], + mtu=FAKE_MTU, + vip_net_info=dict( + vip=self.amp.ha_ip, + subnet_cidr='10.1.0.0/16', + mac_address=FAKE_MAC_ADDRESS, + gateway=None, + vrrp_ip=self.amp.vrrp_ip, + host_routes=[], + mtu=FAKE_MTU + ))) + def test_post_network_plug_with_host_routes(self): SUBNET_ID = 'SUBNET_ID' FIXED_IP1 = '192.0.2.2' @@ -724,6 +763,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): network_models.HostRoute(destination=DEST2, nexthop=NEXTHOP)] subnet = network_models.Subnet(id=SUBNET_ID, cidr=SUBNET_CIDR, + gateway_ip=FAKE_GATEWAY, ip_version=4, host_routes=host_routes) fixed_ips = [ network_models.FixedIP(subnet_id=subnet.id, ip_address=FIXED_IP1, @@ -734,12 +774,14 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase): port = network_models.Port(mac_address=FAKE_MAC_ADDRESS, fixed_ips=fixed_ips, network=self.network) - self.driver.post_network_plug(self.amp, port) + self.driver.post_network_plug(self.amp, port, self.amp_net_config) expected_fixed_ips = [ {'ip_address': FIXED_IP1, 'subnet_cidr': SUBNET_CIDR, + 'gateway': FAKE_GATEWAY, 'host_routes': [{'destination': DEST1, 'nexthop': NEXTHOP}, {'destination': DEST2, 'nexthop': NEXTHOP}]}, {'ip_address': FIXED_IP2, 'subnet_cidr': SUBNET_CIDR, + 'gateway': FAKE_GATEWAY, 'host_routes': [{'destination': DEST1, 'nexthop': NEXTHOP}, {'destination': DEST2, 'nexthop': NEXTHOP}]} ] diff --git a/octavia/tests/unit/amphorae/drivers/noop_driver/test_driver.py b/octavia/tests/unit/amphorae/drivers/noop_driver/test_driver.py index 2d8050019b..8f05d05389 100644 --- a/octavia/tests/unit/amphorae/drivers/noop_driver/test_driver.py +++ b/octavia/tests/unit/amphorae/drivers/noop_driver/test_driver.py @@ -115,7 +115,9 @@ class TestNoopAmphoraLoadBalancerDriver(base.TestCase): self.amphora.id]) def test_post_network_plug(self): - self.driver.post_network_plug(self.amphora, self.port) + self.driver.post_network_plug( + self.amphora, self.port, + self.amphorae_net_configs[self.amphora.id]) self.assertEqual((self.amphora.id, self.port.id, 'post_network_plug'), self.driver.driver.amphoraconfig[( self.amphora.id, self.port.id)]) diff --git a/octavia/tests/unit/common/sample_configs/sample_configs_combined.py b/octavia/tests/unit/common/sample_configs/sample_configs_combined.py index fc35cc79e2..5fe40b9550 100644 --- a/octavia/tests/unit/common/sample_configs/sample_configs_combined.py +++ b/octavia/tests/unit/common/sample_configs/sample_configs_combined.py @@ -24,16 +24,20 @@ from octavia.tests.common import sample_certs CONF = cfg.CONF +class AmphoraTuple(collections.namedtuple( + 'amphora', 'id, lb_network_ip, vrrp_ip, ha_ip, vrrp_port_id, ' + 'ha_port_id, role, status, vrrp_interface,' + 'vrrp_priority, api_version')): + def to_dict(self): + return self._asdict() + + def sample_amphora_tuple(id='sample_amphora_id_1', lb_network_ip='10.0.1.1', vrrp_ip='10.1.1.1', ha_ip='192.168.10.1', vrrp_port_id='1234', ha_port_id='1234', role=None, status='ACTIVE', vrrp_interface=None, vrrp_priority=None, api_version='1.0'): - in_amphora = collections.namedtuple( - 'amphora', 'id, lb_network_ip, vrrp_ip, ha_ip, vrrp_port_id, ' - 'ha_port_id, role, status, vrrp_interface,' - 'vrrp_priority, api_version') - return in_amphora( + amp = AmphoraTuple( id=id, lb_network_ip=lb_network_ip, vrrp_ip=vrrp_ip, @@ -45,6 +49,7 @@ def sample_amphora_tuple(id='sample_amphora_id_1', lb_network_ip='10.0.1.1', vrrp_interface=vrrp_interface, vrrp_priority=vrrp_priority, api_version=api_version) + return amp RET_PERSISTENCE = { @@ -648,9 +653,9 @@ def sample_vrrp_group_tuple(): smtp_connect_timeout='') -def sample_vip_tuple(): - vip = collections.namedtuple('vip', 'ip_address') - return vip(ip_address='10.0.0.2') +def sample_vip_tuple(ip_address='10.0.0.2', subnet_id='vip_subnet_uuid'): + vip = collections.namedtuple('vip', ('ip_address', 'subnet_id')) + return vip(ip_address=ip_address, subnet_id=subnet_id) def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, diff --git a/octavia/tests/unit/common/sample_configs/sample_configs_split.py b/octavia/tests/unit/common/sample_configs/sample_configs_split.py index f628bed949..ec33daa642 100644 --- a/octavia/tests/unit/common/sample_configs/sample_configs_split.py +++ b/octavia/tests/unit/common/sample_configs/sample_configs_split.py @@ -23,16 +23,20 @@ from octavia.tests.common import sample_certs CONF = cfg.CONF +class AmphoraTuple(collections.namedtuple( + 'amphora', 'id, lb_network_ip, vrrp_ip, ha_ip, vrrp_port_id, ' + 'ha_port_id, role, status, vrrp_interface,' + 'vrrp_priority, api_version')): + def to_dict(self): + return self._asdict() + + def sample_amphora_tuple(id='sample_amphora_id_1', lb_network_ip='10.0.1.1', vrrp_ip='10.1.1.1', ha_ip='192.168.10.1', vrrp_port_id='1234', ha_port_id='1234', role=None, status='ACTIVE', vrrp_interface=None, vrrp_priority=None, api_version='0.5'): - in_amphora = collections.namedtuple( - 'amphora', 'id, lb_network_ip, vrrp_ip, ha_ip, vrrp_port_id, ' - 'ha_port_id, role, status, vrrp_interface,' - 'vrrp_priority, api_version') - return in_amphora( + return AmphoraTuple( id=id, lb_network_ip=lb_network_ip, vrrp_ip=vrrp_ip, @@ -599,9 +603,9 @@ def sample_vrrp_group_tuple(): smtp_connect_timeout='') -def sample_vip_tuple(): - vip = collections.namedtuple('vip', 'ip_address') - return vip(ip_address='10.0.0.2') +def sample_vip_tuple(ip_address='10.0.0.2', subnet_id='vip_subnet_uuid'): + vip = collections.namedtuple('vip', ('ip_address', 'subnet_id')) + return vip(ip_address=ip_address, subnet_id=subnet_id) def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, diff --git a/octavia/tests/unit/controller/worker/v1/flows/test_amphora_flows.py b/octavia/tests/unit/controller/worker/v1/flows/test_amphora_flows.py index cd1c385698..e9f61abf92 100644 --- a/octavia/tests/unit/controller/worker/v1/flows/test_amphora_flows.py +++ b/octavia/tests/unit/controller/worker/v1/flows/test_amphora_flows.py @@ -238,7 +238,7 @@ class TestAmphoraFlows(base.TestCase): self.assertIn(constants.LOADBALANCER_ID, amp_flow.requires) self.assertIn(constants.VIP, amp_flow.requires) - self.assertIn(constants.ADDED_PORTS, amp_flow.provides) + self.assertIn(constants.UPDATED_PORTS, amp_flow.provides) self.assertIn(constants.AMP_VRRP_INT, amp_flow.provides) self.assertIn(constants.AMPHORA, amp_flow.provides) self.assertIn(constants.AMPHORA_ID, amp_flow.provides) @@ -272,7 +272,7 @@ class TestAmphoraFlows(base.TestCase): self.assertIn(constants.LOADBALANCER_ID, amp_flow.requires) self.assertIn(constants.VIP, amp_flow.requires) - self.assertIn(constants.ADDED_PORTS, amp_flow.provides) + self.assertIn(constants.UPDATED_PORTS, amp_flow.provides) self.assertIn(constants.AMPHORA, amp_flow.provides) self.assertIn(constants.AMPHORA_ID, amp_flow.provides) self.assertIn(constants.AMPHORAE, amp_flow.provides) @@ -430,7 +430,7 @@ class TestAmphoraFlows(base.TestCase): self.assertIn(constants.VIP, get_amp_flow.requires) self.assertIn(constants.VIP_SG_ID, get_amp_flow.requires) - self.assertIn(constants.ADDED_PORTS, get_amp_flow.provides) + self.assertIn(constants.UPDATED_PORTS, get_amp_flow.provides) self.assertIn(constants.AMPHORA, get_amp_flow.provides) self.assertIn(constants.AMPHORA_ID, get_amp_flow.provides) self.assertIn(constants.AMPHORAE_NETWORK_CONFIG, get_amp_flow.provides) @@ -460,7 +460,7 @@ class TestAmphoraFlows(base.TestCase): self.assertIn(constants.VIP, get_amp_flow.requires) self.assertIn(constants.VIP_SG_ID, get_amp_flow.requires) - self.assertIn(constants.ADDED_PORTS, get_amp_flow.provides) + self.assertIn(constants.UPDATED_PORTS, get_amp_flow.provides) self.assertIn(constants.AMPHORA, get_amp_flow.provides) self.assertIn(constants.AMPHORA_ID, get_amp_flow.provides) self.assertIn(constants.AMPHORAE_NETWORK_CONFIG, get_amp_flow.provides) diff --git a/octavia/tests/unit/controller/worker/v1/flows/test_load_balancer_flows.py b/octavia/tests/unit/controller/worker/v1/flows/test_load_balancer_flows.py index 75fa3f136e..46ffc522c8 100644 --- a/octavia/tests/unit/controller/worker/v1/flows/test_load_balancer_flows.py +++ b/octavia/tests/unit/controller/worker/v1/flows/test_load_balancer_flows.py @@ -183,22 +183,28 @@ class TestLoadBalancerFlows(base.TestCase): self.assertIsInstance(create_flow, flow.Flow) self.assertIn(constants.LOADBALANCER_ID, create_flow.requires) self.assertIn(constants.UPDATE_DICT, create_flow.requires) + self.assertIn(constants.BUILD_TYPE_PRIORITY, create_flow.requires) + self.assertIn(constants.FLAVOR, create_flow.requires) + self.assertIn(constants.AMPHORAE_NETWORK_CONFIG, create_flow.requires) + self.assertIn(constants.AVAILABILITY_ZONE, create_flow.requires) + self.assertIn(constants.SERVER_GROUP_ID, create_flow.requires) self.assertIn(constants.LISTENERS, create_flow.provides) + self.assertIn(constants.SUBNET, create_flow.provides) self.assertIn(constants.AMPHORA, create_flow.provides) self.assertIn(constants.AMPHORA_ID, create_flow.provides) self.assertIn(constants.COMPUTE_ID, create_flow.provides) self.assertIn(constants.COMPUTE_OBJ, create_flow.provides) self.assertIn(constants.LOADBALANCER, create_flow.provides) self.assertIn(constants.DELTAS, create_flow.provides) - self.assertIn(constants.ADDED_PORTS, create_flow.provides) + self.assertIn(constants.UPDATED_PORTS, create_flow.provides) self.assertIn(constants.VIP, create_flow.provides) self.assertIn(constants.AMP_DATA, create_flow.provides) + self.assertIn(constants.SERVER_PEM, create_flow.provides) self.assertIn(constants.AMPHORA_NETWORK_CONFIG, create_flow.provides) - self.assertEqual(6, len(create_flow.requires)) - self.assertEqual(13, len(create_flow.provides), - create_flow.provides) + self.assertEqual(7, len(create_flow.requires)) + self.assertEqual(13, len(create_flow.provides)) def test_get_create_load_balancer_flows_active_standby_listeners( self, mock_get_net_driver): @@ -218,7 +224,7 @@ class TestLoadBalancerFlows(base.TestCase): self.assertIn(constants.COMPUTE_OBJ, create_flow.provides) self.assertIn(constants.LOADBALANCER, create_flow.provides) self.assertIn(constants.DELTAS, create_flow.provides) - self.assertIn(constants.ADDED_PORTS, create_flow.provides) + self.assertIn(constants.UPDATED_PORTS, create_flow.provides) self.assertIn(constants.VIP, create_flow.provides) self.assertIn(constants.AMP_DATA, create_flow.provides) self.assertIn(constants.AMPHORAE_NETWORK_CONFIG, @@ -243,7 +249,7 @@ class TestLoadBalancerFlows(base.TestCase): self.assertIn(constants.LOADBALANCER, failover_flow.requires) self.assertIn(constants.LOADBALANCER_ID, failover_flow.requires) - self.assertIn(constants.ADDED_PORTS, failover_flow.provides) + self.assertIn(constants.UPDATED_PORTS, failover_flow.provides) self.assertIn(constants.AMPHORA, failover_flow.provides) self.assertIn(constants.AMPHORA_ID, failover_flow.provides) self.assertIn(constants.AMPHORAE_NETWORK_CONFIG, @@ -312,7 +318,7 @@ class TestLoadBalancerFlows(base.TestCase): self.assertIn(constants.LOADBALANCER, failover_flow.requires) self.assertIn(constants.LOADBALANCER_ID, failover_flow.requires) - self.assertIn(constants.ADDED_PORTS, failover_flow.provides) + self.assertIn(constants.UPDATED_PORTS, failover_flow.provides) self.assertIn(constants.AMP_VRRP_INT, failover_flow.provides) self.assertIn(constants.AMPHORA, failover_flow.provides) self.assertIn(constants.AMPHORA_ID, failover_flow.provides) diff --git a/octavia/tests/unit/controller/worker/v1/flows/test_member_flows.py b/octavia/tests/unit/controller/worker/v1/flows/test_member_flows.py index cfd6affb8e..68780355b6 100644 --- a/octavia/tests/unit/controller/worker/v1/flows/test_member_flows.py +++ b/octavia/tests/unit/controller/worker/v1/flows/test_member_flows.py @@ -37,6 +37,7 @@ class TestMemberFlows(base.TestCase): self.assertIsInstance(member_flow, flow.Flow) + self.assertIn(constants.MEMBER, member_flow.requires) self.assertIn(constants.LISTENERS, member_flow.requires) self.assertIn(constants.LOADBALANCER, member_flow.requires) self.assertIn(constants.POOL, member_flow.requires) @@ -44,10 +45,11 @@ class TestMemberFlows(base.TestCase): self.assertIn(constants.AVAILABILITY_ZONE, member_flow.requires) self.assertIn(constants.DELTAS, member_flow.provides) - self.assertIn(constants.ADDED_PORTS, member_flow.provides) + self.assertIn(constants.AMPHORAE_NETWORK_CONFIG, member_flow.provides) + self.assertIn(constants.UPDATED_PORTS, member_flow.provides) - self.assertEqual(5, len(member_flow.requires)) - self.assertEqual(2, len(member_flow.provides)) + self.assertEqual(6, len(member_flow.requires)) + self.assertEqual(3, len(member_flow.provides)) def test_get_delete_member_flow(self, mock_get_net_driver): @@ -58,10 +60,16 @@ class TestMemberFlows(base.TestCase): self.assertIn(constants.MEMBER, member_flow.requires) self.assertIn(constants.LISTENERS, member_flow.requires) self.assertIn(constants.LOADBALANCER, member_flow.requires) + self.assertIn(constants.LOADBALANCER_ID, member_flow.requires) self.assertIn(constants.POOL, member_flow.requires) + self.assertIn(constants.AVAILABILITY_ZONE, member_flow.requires) - self.assertEqual(4, len(member_flow.requires)) - self.assertEqual(0, len(member_flow.provides)) + self.assertIn(constants.AMPHORAE_NETWORK_CONFIG, member_flow.provides) + self.assertIn(constants.DELTAS, member_flow.provides) + self.assertIn(constants.UPDATED_PORTS, member_flow.provides) + + self.assertEqual(6, len(member_flow.requires)) + self.assertEqual(3, len(member_flow.provides)) def test_get_update_member_flow(self, mock_get_net_driver): @@ -91,7 +99,8 @@ class TestMemberFlows(base.TestCase): self.assertIn(constants.AVAILABILITY_ZONE, member_flow.requires) self.assertIn(constants.DELTAS, member_flow.provides) - self.assertIn(constants.ADDED_PORTS, member_flow.provides) + self.assertIn(constants.UPDATED_PORTS, member_flow.provides) + self.assertIn(constants.AMPHORAE_NETWORK_CONFIG, member_flow.provides) - self.assertEqual(4, len(member_flow.requires)) - self.assertEqual(2, len(member_flow.provides)) + self.assertEqual(5, len(member_flow.requires)) + self.assertEqual(3, len(member_flow.provides)) diff --git a/octavia/tests/unit/controller/worker/v1/tasks/test_amphora_driver_tasks.py b/octavia/tests/unit/controller/worker/v1/tasks/test_amphora_driver_tasks.py index fa92aae320..dcd9078882 100644 --- a/octavia/tests/unit/controller/worker/v1/tasks/test_amphora_driver_tasks.py +++ b/octavia/tests/unit/controller/worker/v1/tasks/test_amphora_driver_tasks.py @@ -50,6 +50,9 @@ _vip_mock = mock.MagicMock() _load_balancer_mock.vip = _vip_mock _LB_mock = mock.MagicMock() _amphorae_mock = [_amphora_mock] +_amphora_network_config_mock = mock.MagicMock() +_amphorae_network_config_mock = { + _amphora_mock.id: _amphora_network_config_mock} _network_mock = mock.MagicMock() _port_mock = mock.MagicMock() _ports_mock = [_port_mock] @@ -379,10 +382,12 @@ class TestAmphoraDriverTasks(base.TestCase): amphora_post_network_plug_obj = (amphora_driver_tasks. AmphoraPostNetworkPlug()) - amphora_post_network_plug_obj.execute(_amphora_mock, _ports_mock) + amphora_post_network_plug_obj.execute(_amphora_mock, _ports_mock, + _amphora_network_config_mock) (mock_driver.post_network_plug. - assert_called_once_with)(_amphora_mock, _port_mock) + assert_called_once_with)(_amphora_mock, _port_mock, + _amphora_network_config_mock) # Test revert amp = amphora_post_network_plug_obj.revert(None, _amphora_mock) @@ -428,17 +433,20 @@ class TestAmphoraDriverTasks(base.TestCase): port_mock = mock.Mock() _deltas_mock = {_amphora_mock.id: [port_mock]} - amphora_post_network_plug_obj.execute(_LB_mock, _deltas_mock) + amphora_post_network_plug_obj.execute(_LB_mock, _deltas_mock, + _amphorae_network_config_mock) (mock_driver.post_network_plug. - assert_called_once_with(_amphora_mock, port_mock)) + assert_called_once_with(_amphora_mock, port_mock, + _amphora_network_config_mock)) # Test with no ports to plug mock_driver.post_network_plug.reset_mock() _deltas_mock = {'0': [port_mock]} - amphora_post_network_plug_obj.execute(_LB_mock, _deltas_mock) + amphora_post_network_plug_obj.execute(_LB_mock, _deltas_mock, + _amphora_network_config_mock) mock_driver.post_network_plug.assert_not_called() # Test revert diff --git a/octavia/tests/unit/controller/worker/v1/tasks/test_network_tasks.py b/octavia/tests/unit/controller/worker/v1/tasks/test_network_tasks.py index 0832c0ac04..01240af26c 100644 --- a/octavia/tests/unit/controller/worker/v1/tasks/test_network_tasks.py +++ b/octavia/tests/unit/controller/worker/v1/tasks/test_network_tasks.py @@ -34,6 +34,8 @@ COMPUTE_ID = uuidutils.generate_uuid() PORT_ID = uuidutils.generate_uuid() SUBNET_ID = uuidutils.generate_uuid() NETWORK_ID = uuidutils.generate_uuid() +MGMT_NETWORK_ID = uuidutils.generate_uuid() +MGMT_SUBNET_ID = uuidutils.generate_uuid() SG_ID = uuidutils.generate_uuid() IP_ADDRESS = "172.24.41.1" VIP = o_data_models.Vip(port_id=t_constants.MOCK_PORT_ID, @@ -76,23 +78,27 @@ class TestNetworkTasks(base.TestCase): self.load_balancer_mock = mock.MagicMock() self.vip_mock = mock.MagicMock() self.vip_mock.subnet_id = SUBNET_ID + self.vip_mock.network_id = NETWORK_ID self.load_balancer_mock.vip = self.vip_mock self.load_balancer_mock.amphorae = [] self.amphora_mock.id = AMPHORA_ID self.amphora_mock.compute_id = COMPUTE_ID self.amphora_mock.status = constants.AMPHORA_ALLOCATED - self.boot_net_id = NETWORK_ID + self.mgmt_net_id = MGMT_NETWORK_ID + self.mgmt_subnet_id = MGMT_SUBNET_ID conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) conf.config(group="controller_worker", - amp_boot_network_list=[self.boot_net_id]) + amp_boot_network_list=[MGMT_NETWORK_ID]) conf.config(group="networking", max_retries=1) super().setUp() def test_calculate_amphora_delta(self, mock_get_net_driver): + VRRP_PORT_ID = uuidutils.generate_uuid() + VIP_NETWORK_ID = uuidutils.generate_uuid() + VIP_SUBNET_ID = uuidutils.generate_uuid() DELETE_NETWORK_ID = uuidutils.generate_uuid() MEMBER_NETWORK_ID = uuidutils.generate_uuid() MEMBER_SUBNET_ID = uuidutils.generate_uuid() - VRRP_PORT_ID = uuidutils.generate_uuid() mock_driver = mock.MagicMock() mock_get_net_driver.return_value = mock_driver member_mock = mock.MagicMock() @@ -101,23 +107,55 @@ class TestNetworkTasks(base.TestCase): pool_mock.members = [member_mock] lb_mock = mock.MagicMock() lb_mock.pools = [pool_mock] + lb_mock.vip = mock.MagicMock() + lb_mock.vip.subnet_id = VIP_SUBNET_ID + lb_mock.vip.network_id = VIP_NETWORK_ID amphora_mock = mock.MagicMock() amphora_mock.id = AMPHORA_ID amphora_mock.compute_id = COMPUTE_ID amphora_mock.vrrp_port_id = VRRP_PORT_ID - vrrp_port_mock = mock.MagicMock() - vrrp_port_mock.network_id = self.boot_net_id - mock_subnet = mock.MagicMock() - mock_subnet.network_id = MEMBER_NETWORK_ID - nic1_delete_mock = mock.MagicMock() - nic1_delete_mock.network_id = DELETE_NETWORK_ID - nic2_keep_mock = mock.MagicMock() - nic2_keep_mock.network_id = self.boot_net_id - mock_driver.get_port.return_value = vrrp_port_mock - mock_driver.get_subnet.return_value = mock_subnet - mock_driver.get_plugged_networks.return_value = [nic1_delete_mock, - nic2_keep_mock] + mgmt_subnet = data_models.Subnet( + id=MGMT_SUBNET_ID, + network_id=MGMT_NETWORK_ID) + mgmt_net = data_models.Network( + id=MGMT_NETWORK_ID, + subnets=[mgmt_subnet.id]) + mgmt_interface = data_models.Interface( + network_id=mgmt_net.id, + fixed_ips=[ + data_models.FixedIP( + subnet_id=mgmt_subnet.id)]) + + vrrp_subnet = data_models.Subnet( + id=VIP_SUBNET_ID, + network_id=VIP_NETWORK_ID) + vrrp_port = data_models.Port( + id=VRRP_PORT_ID, + network_id=VIP_NETWORK_ID, + fixed_ips=[ + data_models.FixedIP( + subnet=vrrp_subnet, + subnet_id=vrrp_subnet.id)]) + vrrp_interface = data_models.Interface( + network_id=VIP_NETWORK_ID, + fixed_ips=vrrp_port.fixed_ips) + + member_subnet = data_models.Subnet( + id=MEMBER_SUBNET_ID, + network_id=MEMBER_NETWORK_ID) + + to_be_deleted_interface = data_models.Interface( + id=mock.Mock(), + network_id=DELETE_NETWORK_ID) + + mock_driver.get_port.return_value = vrrp_port + mock_driver.get_subnet.return_value = member_subnet + mock_driver.get_network.return_value = mgmt_net + mock_driver.get_plugged_networks.return_value = [ + mgmt_interface, + vrrp_interface, + to_be_deleted_interface] calc_amp_delta = network_tasks.CalculateAmphoraDelta() @@ -130,15 +168,21 @@ class TestNetworkTasks(base.TestCase): self.assertEqual(MEMBER_NETWORK_ID, result.add_nics[0].network_id) self.assertEqual(1, len(result.delete_nics)) self.assertEqual(DELETE_NETWORK_ID, result.delete_nics[0].network_id) - mock_driver.get_port.assert_called_once_with(VRRP_PORT_ID) mock_driver.get_subnet.assert_called_once_with(MEMBER_SUBNET_ID) mock_driver.get_plugged_networks.assert_called_once_with(COMPUTE_ID) # Test with vrrp_port_id mock_driver.reset_mock() + mock_driver.get_port.return_value = vrrp_port + mock_driver.get_subnet.return_value = member_subnet + mock_driver.get_network.return_value = mgmt_net + mock_driver.get_plugged_networks.return_value = [ + mgmt_interface, + vrrp_interface, + to_be_deleted_interface] result = calc_amp_delta.execute(lb_mock, amphora_mock, {}, - vrrp_port=vrrp_port_mock) + vrrp_port=vrrp_port) self.assertEqual(AMPHORA_ID, result.amphora_id) self.assertEqual(COMPUTE_ID, result.compute_id) @@ -146,7 +190,6 @@ class TestNetworkTasks(base.TestCase): self.assertEqual(MEMBER_NETWORK_ID, result.add_nics[0].network_id) self.assertEqual(1, len(result.delete_nics)) self.assertEqual(DELETE_NETWORK_ID, result.delete_nics[0].network_id) - mock_driver.get_port.assert_not_called() mock_driver.get_subnet.assert_called_once_with(MEMBER_SUBNET_ID) mock_driver.get_plugged_networks.assert_called_once_with(COMPUTE_ID) @@ -158,105 +201,443 @@ class TestNetworkTasks(base.TestCase): amphora_id=self.amphora_mock.id, compute_id=self.amphora_mock.compute_id, add_nics=[], - delete_nics=[])} + delete_nics=[], + add_subnets=[], + delete_subnets=[], + )} + + mgmt_subnet = data_models.Subnet( + id=self.mgmt_subnet_id, network_id=self.mgmt_net_id) + mgmt_net = data_models.Network( + id=self.mgmt_net_id, + subnets=[mgmt_subnet.id]) + mgmt_ip_address = mock.MagicMock() + mgmt_interface = data_models.Interface( + network_id=self.mgmt_net_id, + fixed_ips=[ + data_models.FixedIP( + subnet=mgmt_subnet, + subnet_id=self.mgmt_subnet_id, + ip_address=mgmt_ip_address + ) + ]) + vrrp_subnet = data_models.Subnet( + id=self.vip_mock.subnet_id, network_id=self.vip_mock.network_id, + name='vrrp_subnet') + member_vip_subnet = data_models.Subnet( + id=uuidutils.generate_uuid(), network_id=self.vip_mock.network_id, + name='member_vip_subnet') + vip_net = data_models.Network( + id=self.vip_mock.network_id, + subnets=[member_vip_subnet, vrrp_subnet], + name='flat_network') + vrrp_port = data_models.Port( + id=uuidutils.generate_uuid(), + network_id=vip_net.id, network=vip_net, + fixed_ips=[ + data_models.FixedIP( + subnet=vrrp_subnet, subnet_id=vrrp_subnet.id, + ip_address=t_constants.MOCK_IP_ADDRESS) + ], + name='vrrp_port') + + member_private_net_id = uuidutils.generate_uuid() + member_private_subnet = data_models.Subnet( + id=uuidutils.generate_uuid(), network_id=member_private_net_id, + name='member_private_subnet') + member_private_subnet2 = data_models.Subnet( + id=uuidutils.generate_uuid(), network_id=member_private_net_id, + name='member_private_subnet2') + member_private_net = data_models.Network( + id=member_private_subnet.network_id, + subnets=[member_private_subnet, member_private_subnet2], + name='member_private_net') + member_private_subnet_port = data_models.Port( + id=uuidutils.generate_uuid(), + network_id=member_private_net.id, network=member_private_net, + fixed_ips=[ + data_models.FixedIP( + subnet=member_private_subnet, + subnet_id=member_private_subnet.id, + ip_address=t_constants.MOCK_IP_ADDRESS2) + ], + name='member_private_net_port') + member_private_subnet2_port = data_models.Port( + id=uuidutils.generate_uuid(), + network_id=member_private_net.id, network=member_private_net, + fixed_ips=[ + data_models.FixedIP( + subnet=member_private_subnet2, + subnet_id=member_private_subnet2.id, + ip_address=t_constants.MOCK_IP_ADDRESS2) + ], + name='member_private_net_port') + + # Pretend the VIP is on the member network, so already plugged + mock_driver.get_plugged_networks.return_value = [ + mgmt_interface, + data_models.Interface( + network_id=vip_net.id, port_id=vrrp_port.id, + fixed_ips=vrrp_port.fixed_ips)] + mock_driver.get_port.return_value = vrrp_port + mock_driver.get_subnet.return_value = vrrp_subnet + mock_driver.get_network.return_value = mgmt_net calc_delta = network_tasks.CalculateDelta() - self.assertEqual(EMPTY, - calc_delta.execute(self.load_balancer_mock, {})) + # Test with no amps or anything at all + self.assertEqual(EMPTY, calc_delta.execute( + self.load_balancer_mock, {})) - # Test with one amp and no pools, nothing plugged + # Test with one amp and no pools, only the base network plugged # Delta should be empty mock_driver.reset_mock() self.amphora_mock.load_balancer = self.load_balancer_mock self.load_balancer_mock.amphorae = [self.amphora_mock] self.load_balancer_mock.pools = [] - self.assertEqual(empty_deltas, calc_delta.execute(self.load_balancer_mock, {})) mock_driver.get_plugged_networks.assert_called_once_with(COMPUTE_ID) - # Pool mock should be configured explicitly for each test - pool_mock = mock.MagicMock() - self.load_balancer_mock.pools = [pool_mock] - # Test with one amp and one pool but no members, nothing plugged # Delta should be empty + mock_driver.reset_mock() + pool_mock = mock.MagicMock() pool_mock.members = [] + self.load_balancer_mock.pools = [pool_mock] self.assertEqual(empty_deltas, calc_delta.execute(self.load_balancer_mock, {})) - # Test with one amp and one pool and one member, nothing plugged - # Delta should be one additional subnet to plug + # Test with one amp/pool and one member (on a distinct member subnet) + # Dummy AZ is provided + # Only the base network is already plugged + # Delta should be one additional network/subnet to plug mock_driver.reset_mock() member_mock = mock.MagicMock() - member_mock.subnet_id = 1 + member_mock.subnet_id = member_private_subnet.id + member2_mock = mock.MagicMock() + member2_mock.subnet_id = member_private_subnet2.id pool_mock.members = [member_mock] - mock_driver.get_subnet.return_value = data_models.Subnet(id=2, - network_id=3) + az = { + constants.COMPUTE_ZONE: 'foo' + } + mock_driver.get_subnet.return_value = data_models.Subnet( + id=2, network_id=3) - ndm = data_models.Delta(amphora_id=self.amphora_mock.id, - compute_id=self.amphora_mock.compute_id, - add_nics=[ - data_models.Interface(network_id=2)], - delete_nics=[]) + ndm = data_models.Delta( + amphora_id=self.amphora_mock.id, + compute_id=self.amphora_mock.compute_id, + add_nics=[ + data_models.Interface( + network_id=3, + fixed_ips=[ + data_models.FixedIP( + subnet_id=member_private_subnet.id)])], + delete_nics=[], + add_subnets=[{ + 'subnet_id': member_private_subnet.id, + 'network_id': 3, + 'port_id': None}], + delete_subnets=[]) self.assertEqual({self.amphora_mock.id: ndm}, - calc_delta.execute(self.load_balancer_mock, {})) + calc_delta.execute(self.load_balancer_mock, az)) - vrrp_port_call = mock.call(self.amphora_mock.vrrp_port_id) - mock_driver.get_port.assert_has_calls([vrrp_port_call]) - self.assertEqual(1, mock_driver.get_port.call_count) + mock_driver.get_subnet.assert_called_once_with( + member_mock.subnet_id) - member_subnet_call = mock.call(member_mock.subnet_id) - mock_driver.get_subnet.assert_has_calls([member_subnet_call]) - self.assertEqual(1, mock_driver.get_subnet.call_count) - - # Test with one amp and one pool and one member, already plugged + # Test with one amp/pool and one member (not plugged) that is being + # deleted + # Only the base network is already plugged # Delta should be empty mock_driver.reset_mock() member_mock = mock.MagicMock() - member_mock.subnet_id = 1 + member_mock.subnet_id = member_private_subnet.id + member_mock.provisioning_status = constants.PENDING_DELETE pool_mock.members = [member_mock] - mock_driver.get_plugged_networks.return_value = [ - data_models.Interface(network_id=2)] self.assertEqual(empty_deltas, calc_delta.execute(self.load_balancer_mock, {})) - # Test with one amp and one pool and one member, wrong network plugged - # Delta should be one network to add and one to remove + # Test with one amp/pool and one member (without any subnets) + # Only the base network is already plugged + # No delta mock_driver.reset_mock() member_mock = mock.MagicMock() - member_mock.subnet_id = 1 + member_mock.subnet_id = None pool_mock.members = [member_mock] - mock_driver.get_plugged_networks.return_value = [ - data_models.Interface(network_id=3)] - ndm = data_models.Delta(amphora_id=self.amphora_mock.id, - compute_id=self.amphora_mock.compute_id, - add_nics=[ - data_models.Interface(network_id=2)], - delete_nics=[ - data_models.Interface(network_id=3)]) - self.assertEqual({self.amphora_mock.id: ndm}, + self.assertEqual(empty_deltas, calc_delta.execute(self.load_balancer_mock, {})) + # Test with one amp and one pool and one member + # Management network is defined in AZ metadata + # Base network AND member network/subnet already plugged + # Delta should be empty + mock_driver.reset_mock() + member_mock = mock.MagicMock() + member_mock.subnet_id = member_private_subnet.id + pool_mock.members = [member_mock] + + mgmt2_subnet_id = uuidutils.generate_uuid() + mgmt2_net_id = uuidutils.generate_uuid() + mgmt2_subnet = data_models.Subnet( + id=mgmt2_subnet_id, + network_id=mgmt2_net_id) + mgmt2_net = data_models.Network( + id=mgmt2_net_id, + subnets=[mgmt2_subnet.id] + ) + mgmt2_interface = data_models.Interface( + network_id=mgmt2_net_id, + fixed_ips=[ + data_models.FixedIP( + subnet=mgmt2_subnet, + subnet_id=mgmt2_subnet_id, + ) + ]) + mock_driver.get_network.return_value = mgmt2_net + az = { + constants.MANAGEMENT_NETWORK: mgmt2_net_id, + } + mock_driver.get_subnet.return_value = member_private_subnet + mock_driver.get_plugged_networks.return_value = [ + mgmt2_interface, + data_models.Interface( + network_id=vrrp_subnet.network_id, + fixed_ips=vrrp_port.fixed_ips), + data_models.Interface( + network_id=member_private_subnet.network_id, + fixed_ips=member_private_subnet_port.fixed_ips)] + + self.assertEqual(empty_deltas, + calc_delta.execute(self.load_balancer_mock, az)) + + # Test with one amp and one pool and one member, wrong network plugged + # Delta should be one network/subnet to add and one to remove + mock_driver.reset_mock() + mock_driver.get_network.return_value = mgmt_net + member_mock = mock.MagicMock() + member_mock.subnet_id = member_private_subnet.id + pool_mock.members = [member_mock] + az = { + constants.COMPUTE_ZONE: 'foo' + } + mock_driver.get_subnet.return_value = member_private_subnet + mock_driver.get_plugged_networks.return_value = [ + mgmt_interface, + data_models.Interface( + network_id=vrrp_subnet.network_id, + fixed_ips=vrrp_port.fixed_ips), + data_models.Interface( + network_id='bad_net', + fixed_ips=[data_models.FixedIP(subnet_id='bad_subnet')])] + + ndm = data_models.Delta( + amphora_id=self.amphora_mock.id, + compute_id=self.amphora_mock.compute_id, + add_nics=[data_models.Interface( + network_id=member_private_net.id, + fixed_ips=[data_models.FixedIP( + subnet_id=member_private_subnet.id)])], + delete_nics=[data_models.Interface(network_id='bad_net')], + add_subnets=[{ + 'subnet_id': member_private_subnet.id, + 'network_id': member_private_net.id, + 'port_id': None + }], + delete_subnets=[{ + 'subnet_id': 'bad_subnet', + 'network_id': 'bad_net', + 'port_id': None + }]) + self.assertEqual({self.amphora_mock.id: ndm}, + calc_delta.execute(self.load_balancer_mock, az)) + # Test with one amp and one pool and no members, one network plugged # Delta should be one network to remove mock_driver.reset_mock() pool_mock.members = [] + mock_driver.get_subnet.side_effect = [ + vrrp_subnet] mock_driver.get_plugged_networks.return_value = [ - data_models.Interface(network_id=2)] + mgmt_interface, + data_models.Interface( + network_id=vrrp_subnet.network_id, + fixed_ips=vrrp_port.fixed_ips), + data_models.Interface( + network_id='bad_net', + fixed_ips=[data_models.FixedIP(subnet_id='bad_subnet')])] - ndm = data_models.Delta(amphora_id=self.amphora_mock.id, - compute_id=self.amphora_mock.compute_id, - add_nics=[], - delete_nics=[ - data_models.Interface(network_id=2)]) + ndm = data_models.Delta( + amphora_id=self.amphora_mock.id, + compute_id=self.amphora_mock.compute_id, + add_nics=[], + delete_nics=[data_models.Interface(network_id='bad_net')], + add_subnets=[], + delete_subnets=[{ + 'subnet_id': 'bad_subnet', + 'network_id': 'bad_net', + 'port_id': None + }]) self.assertEqual({self.amphora_mock.id: ndm}, calc_delta.execute(self.load_balancer_mock, {})) + # Add a new member on a new subnet, an interface with another subnet of + # the same network is already plugged + # Delta should be one new subnet + mock_driver.reset_mock() + pool_mock.members = [member_mock, member2_mock] + mock_driver.get_subnet.side_effect = [ + vrrp_subnet, + member_private_subnet, + member_private_subnet2] + mock_driver.get_plugged_networks.return_value = [ + mgmt_interface, + data_models.Interface( + network_id=vrrp_subnet.network_id, + fixed_ips=vrrp_port.fixed_ips), + data_models.Interface( + network_id=member_private_net_id, + port_id=member_private_subnet_port.id, + fixed_ips=member_private_subnet_port.fixed_ips)] + + ndm = data_models.Delta( + amphora_id=self.amphora_mock.id, + compute_id=self.amphora_mock.compute_id, + add_nics=[], + delete_nics=[], + add_subnets=[{ + 'subnet_id': member_private_subnet2.id, + 'network_id': member_private_net_id, + 'port_id': member_private_subnet_port.id + }], + delete_subnets=[] + ) + self.assertEqual({self.amphora_mock.id: ndm}, + calc_delta.execute(self.load_balancer_mock, {})) + + # a new member on a new subnet on an existing network, a delete member2 + # on another subnet of the same network + # Delta should be one new subnet, one deleted subnet, no interface + # change + mock_driver.reset_mock() + pool_mock.members = [member_mock] + mock_driver.get_subnet.return_value = member_private_subnet + mock_driver.get_plugged_networks.return_value = [ + mgmt_interface, + data_models.Interface( + network_id=vrrp_subnet.network_id, + fixed_ips=vrrp_port.fixed_ips), + data_models.Interface( + network_id=member_private_net_id, + port_id=member_private_subnet2_port.id, + fixed_ips=member_private_subnet2_port.fixed_ips)] + + ndm = data_models.Delta( + amphora_id=self.amphora_mock.id, + compute_id=self.amphora_mock.compute_id, + add_nics=[], + delete_nics=[], + add_subnets=[{ + 'subnet_id': member_private_subnet.id, + 'network_id': member_private_net_id, + 'port_id': member_private_subnet2_port.id}], + delete_subnets=[{ + 'subnet_id': member_private_subnet2.id, + 'network_id': member_private_net_id, + 'port_id': member_private_subnet2_port.id}] + ) + self.assertEqual({self.amphora_mock.id: ndm}, + calc_delta.execute(self.load_balancer_mock, {})) + + # member on subnet on the same network as the vip subnet + mock_driver.reset_mock() + member_mock.subnet_id = member_vip_subnet.id + pool_mock.members = [member_mock] + mock_driver.get_subnet.side_effect = [ + vrrp_subnet, + member_vip_subnet] + mock_driver.get_plugged_networks.return_value = [ + mgmt_interface, + data_models.Interface( + network_id=vrrp_subnet.network_id, + port_id=vrrp_port.id, + fixed_ips=vrrp_port.fixed_ips), + data_models.Interface( + network_id=member_private_net_id, + port_id=member_private_subnet_port.id, + fixed_ips=member_private_subnet_port.fixed_ips)] + + ndm = data_models.Delta( + amphora_id=self.amphora_mock.id, + compute_id=self.amphora_mock.compute_id, + add_nics=[], + delete_nics=[ + data_models.Interface( + network_id=member_private_net_id, + port_id=member_private_subnet_port.id)], + add_subnets=[{ + 'subnet_id': member_vip_subnet.id, + 'network_id': vip_net.id, + 'port_id': vrrp_port.id}], + delete_subnets=[{ + 'subnet_id': member_private_subnet.id, + 'network_id': member_private_net_id, + 'port_id': member_private_subnet_port.id}] + ) + self.assertEqual({self.amphora_mock.id: ndm}, + calc_delta.execute(self.load_balancer_mock, {})) + + def test_calculate_delta_ipv6_ipv4_subnets(self, mock_get_net_driver): + mock_driver = mock.MagicMock() + mock_get_net_driver.return_value = mock_driver + + # Pool mock should be configured explicitly for each test + pool_mock = mock.MagicMock() + self.load_balancer_mock.pools = [pool_mock] + self.amphora_mock.load_balancer = self.load_balancer_mock + self.load_balancer_mock.amphorae = [self.amphora_mock] + + # Test with one amp and one pool and one new member (in the VIP net) + # Delta should be one additional subnet to plug to the existing port + vrrp_subnet = data_models.Subnet( + id=self.vip_mock.subnet_id, network_id=self.vip_mock.network_id) + member_subnet = data_models.Subnet( + id=uuidutils.generate_uuid(), network_id=self.vip_mock.network_id) + flat_network = data_models.Network( + id=self.vip_mock.network_id, subnets=[member_subnet, vrrp_subnet]) + vrrp_port = data_models.Port( + id=uuidutils.generate_uuid(), + network_id=flat_network.id, network=flat_network, + fixed_ips=[ + data_models.FixedIP( + subnet=vrrp_subnet, subnet_id=vrrp_subnet.id, + ip_address=t_constants.MOCK_IP_ADDRESS) + ]) + mock_driver.get_subnet.return_value = member_subnet + member_mock = mock.MagicMock() + member_mock.subnet_id = member_subnet.id + pool_mock.members = [member_mock] + # Pretend the VIP is on the member network, so already plugged + mock_driver.get_plugged_networks.return_value = [ + data_models.Interface( + network_id=flat_network.id, port_id=vrrp_port.id, + fixed_ips=vrrp_port.fixed_ips)] + mock_driver.get_port.return_value = vrrp_port + + calc_delta = network_tasks.CalculateDelta() + deltas = calc_delta.execute(self.load_balancer_mock, {}) + + expected_delta = {self.amphora_mock.id: data_models.Delta( + amphora_id=self.amphora_mock.id, + compute_id=self.amphora_mock.compute_id, + add_nics=[], + delete_nics=[], + add_subnets=[member_subnet.id], + delete_subnets=[])} + self.assertEqual(expected_delta, deltas) + def test_get_plumbed_networks(self, mock_get_net_driver): mock_driver = mock.MagicMock() mock_get_net_driver.return_value = mock_driver @@ -401,8 +782,12 @@ class TestNetworkTasks(base.TestCase): mock_get_net_driver.return_value = mock_net_driver nic1 = mock.MagicMock() + nic1.fixed_ips = [data_models.FixedIP( + subnet_id=uuidutils.generate_uuid())] nic1.network_id = uuidutils.generate_uuid() nic2 = mock.MagicMock() + nic2.fixed_ips = [data_models.FixedIP( + subnet_id=uuidutils.generate_uuid())] nic2.network_id = uuidutils.generate_uuid() interface1 = mock.MagicMock() interface1.port_id = uuidutils.generate_uuid() @@ -417,7 +802,9 @@ class TestNetworkTasks(base.TestCase): delta = data_models.Delta(amphora_id=self.amphora_mock.id, compute_id=self.amphora_mock.compute_id, add_nics=[nic1], - delete_nics=[nic2, nic2, nic2]) + delete_nics=[nic2, nic2, nic2], + add_subnets=[], + delete_subnets=[]) mock_net_driver.plug_network.return_value = interface1 mock_net_driver.get_port.return_value = port1 @@ -463,82 +850,313 @@ class TestNetworkTasks(base.TestCase): mock_driver = mock.MagicMock() mock_get_net_driver.return_value = mock_driver - def _interface(network_id): - return [data_models.Interface(network_id=network_id)] + self.load_balancer_mock.amphorae = [self.amphora_mock] + + subnet1 = uuidutils.generate_uuid() + network1 = uuidutils.generate_uuid() + port1 = uuidutils.generate_uuid() + subnet2 = uuidutils.generate_uuid() + + def _interface(network_id, port_id=None, subnet_id=None): + return data_models.Interface( + network_id=network_id, + port_id=port_id, + fixed_ips=[ + data_models.FixedIP( + subnet_id=subnet_id)]) net = network_tasks.HandleNetworkDeltas() - net.execute({}) + net.execute({}, self.load_balancer_mock) self.assertFalse(mock_driver.plug_network.called) delta = data_models.Delta(amphora_id=self.amphora_mock.id, compute_id=self.amphora_mock.compute_id, add_nics=[], - delete_nics=[]) - net.execute({self.amphora_mock.id: delta}) + delete_nics=[], + add_subnets=[], + delete_subnets=[]) + net.execute({self.amphora_mock.id: delta}, self.load_balancer_mock) self.assertFalse(mock_driver.plug_network.called) + # Adding a subnet on a new network + port = data_models.Port( + id=port1, + network_id=network1, + fixed_ips=[ + data_models.FixedIP(subnet_id=subnet1)]) + mock_driver.get_port.return_value = port + mock_driver.plug_fixed_ip.return_value = port + mock_driver.get_network.return_value = data_models.Network( + id=network1) + mock_driver.get_subnet.return_value = data_models.Subnet( + id=subnet1, + network_id=network1) + add_nics = [_interface(network1, subnet_id=subnet1)] + add_subnets = [{ + 'subnet_id': subnet1, + 'network_id': network1, + 'port_id': None}] + delta = data_models.Delta(amphora_id=self.amphora_mock.id, compute_id=self.amphora_mock.compute_id, - add_nics=_interface(1), - delete_nics=[]) - net.execute({self.amphora_mock.id: delta}) - mock_driver.plug_network.assert_called_once_with(COMPUTE_ID, 1) + add_nics=add_nics, + delete_nics=[], + add_subnets=add_subnets, + delete_subnets=[]) + updated_ports = net.execute({self.amphora_mock.id: delta}, + self.load_balancer_mock) + mock_driver.plug_network.assert_called_once_with( + self.amphora_mock.compute_id, network1) + mock_driver.unplug_network.assert_not_called() + + self.assertEqual(1, len(updated_ports)) + + updated_port = updated_ports[self.amphora_mock.id][0] + self.assertEqual(port1, updated_port.id) + self.assertEqual(network1, updated_port.network_id) + self.assertEqual(1, len(updated_port.fixed_ips)) + self.assertEqual(subnet1, updated_port.fixed_ips[0].subnet_id) # revert - net.execute({self.amphora_mock.id: delta}) - self.assertFalse(mock_driver.unplug_network.called) + net.revert(None, {self.amphora_mock.id: delta}, + self.load_balancer_mock) + mock_driver.unplug_network.assert_called_once_with( + self.amphora_mock.compute_id, network1) + + # Adding a subnet on an existing network/port + mock_driver.reset_mock() + port = data_models.Port( + id=port1, + network_id=network1, + fixed_ips=[ + data_models.FixedIP(subnet_id=subnet2), + data_models.FixedIP(subnet_id=subnet1)]) + mock_driver.plug_fixed_ip.return_value = port + mock_driver.get_network.return_value = data_models.Network( + id=network1) + mock_driver.get_subnet.side_effect = [ + data_models.Subnet( + id=subnet2, + network_id=network1), + data_models.Subnet( + id=subnet1, + network_id=network1)] + add_nics = [_interface(network1)] + add_subnets = [{ + 'subnet_id': subnet1, + 'network_id': network1, + 'port_id': port1}] delta = data_models.Delta(amphora_id=self.amphora_mock.id, compute_id=self.amphora_mock.compute_id, add_nics=[], - delete_nics=[]) - net.execute({self.amphora_mock.id: delta}) - self.assertFalse(mock_driver.unplug_network.called) + delete_nics=[], + add_subnets=add_subnets, + delete_subnets=[]) + updated_ports = net.execute({self.amphora_mock.id: delta}, + self.load_balancer_mock) + mock_driver.plug_network.assert_not_called() + mock_driver.unplug_network.assert_not_called() + mock_driver.get_port.assert_not_called() + mock_driver.plug_fixed_ip.assert_called_once_with(port_id=port1, + subnet_id=subnet1) + self.assertEqual(1, len(updated_ports)) + + updated_port = updated_ports[self.amphora_mock.id][0] + self.assertEqual(port1, updated_port.id) + self.assertEqual(network1, updated_port.network_id) + self.assertEqual(2, len(updated_port.fixed_ips)) + self.assertEqual(subnet2, updated_port.fixed_ips[0].subnet_id) + self.assertEqual(subnet1, updated_port.fixed_ips[1].subnet_id) + + # Deleting a subnet + mock_driver.reset_mock() + delete_subnets = [{ + 'subnet_id': subnet1, + 'network_id': network1, + 'port_id': port1}] + mock_driver.get_subnet.side_effect = [ + data_models.Subnet( + id=subnet2, + network_id=network1)] + mock_driver.unplug_fixed_ip.return_value = data_models.Port( + id=port1, + network_id=network1, + fixed_ips=[ + data_models.FixedIP(subnet_id=subnet2)]) delta = data_models.Delta(amphora_id=self.amphora_mock.id, compute_id=self.amphora_mock.compute_id, - add_nics=_interface(1), - delete_nics=[]) + add_nics=[], + delete_nics=[], + add_subnets=[], + delete_subnets=delete_subnets) + updated_ports = net.execute({self.amphora_mock.id: delta}, + self.load_balancer_mock) + mock_driver.delete_port.assert_not_called() + mock_driver.plug_network.assert_not_called() + mock_driver.plug_fixed_ip.assert_not_called() + self.assertEqual(1, len(updated_ports)) + self.assertEqual(1, len(updated_ports[self.amphora_mock.id])) + updated_port = updated_ports[self.amphora_mock.id][0] + self.assertEqual(port1, updated_port.id) + self.assertEqual(network1, updated_port.network_id) + self.assertEqual(1, len(updated_port.fixed_ips)) + self.assertEqual(subnet2, updated_port.fixed_ips[0].subnet_id) + + # Deleting a subnet, but neutron doesn't unplug it + # Delta are empty because there's nothing to update mock_driver.reset_mock() - mock_driver.unplug_network.side_effect = net_base.NetworkNotFound + delete_subnets = [{ + 'subnet_id': subnet1, + 'network_id': network1, + 'port_id': port1}] + mock_driver.get_subnet.side_effect = [ + data_models.Subnet( + id=subnet2, + network_id=network1), + data_models.Subnet( + id=subnet2, + network_id=network1)] + mock_driver.unplug_fixed_ip.return_value = data_models.Port( + id=port1, + network_id=network1, + fixed_ips=[ + data_models.FixedIP(subnet_id=subnet1), + data_models.FixedIP(subnet_id=subnet2)]) + delta = data_models.Delta(amphora_id=self.amphora_mock.id, + compute_id=self.amphora_mock.compute_id, + add_nics=[], + delete_nics=[], + add_subnets=[], + delete_subnets=[]) + net.execute({self.amphora_mock.id: delta}, + self.load_balancer_mock) + mock_driver.delete_port.assert_not_called() + mock_driver.plug_network.assert_not_called() + mock_driver.plug_fixed_ip.assert_not_called() + + # Deleting a subnet and a network + mock_driver.reset_mock() + mock_driver.get_subnet.side_effect = [ + data_models.Subnet( + id=subnet2, + network_id=network1), + data_models.Subnet( + id=subnet1, + network_id=network1)] + delete_nics = [_interface(network1, port_id=port1)] + delete_subnets = [{ + 'subnet_id': subnet1, + 'network_id': network1, + 'port_id': port1}] + + delta = data_models.Delta(amphora_id=self.amphora_mock.id, + compute_id=self.amphora_mock.compute_id, + add_nics=[], + delete_nics=delete_nics, + add_subnets=[], + delete_subnets=delete_subnets) + updated_ports = net.execute({self.amphora_mock.id: delta}, + self.load_balancer_mock) + mock_driver.delete_port.assert_called_once_with(port1) + mock_driver.plug_network.assert_not_called() + mock_driver.plug_fixed_ip.assert_not_called() + self.assertEqual(1, len(updated_ports)) + self.assertEqual(0, len(updated_ports[self.amphora_mock.id])) + + # No delta, no actions + mock_driver.reset_mock() + delta = data_models.Delta(amphora_id=self.amphora_mock.id, + compute_id=self.amphora_mock.compute_id, + add_nics=[], + delete_nics=[], + add_subnets=[], + delete_subnets=[]) + net.execute({self.amphora_mock.id: delta}, self.load_balancer_mock) + mock_driver.plug_network.assert_not_called() + mock_driver.plug_fixed_ip.assert_not_called() + mock_driver.unplug_network.assert_not_called() + + delta = data_models.Delta(amphora_id=self.amphora_mock.id, + compute_id=self.amphora_mock.compute_id, + add_nics=[_interface(1, port_id=12)], + delete_nics=[], + add_subnets=[], + delete_subnets=[]) mock_driver.reset_mock() mock_driver.unplug_network.side_effect = TestException('test') - self.assertRaises(TestException, net.revert, mock.ANY, - {self.amphora_mock.id: delta}) - mock_driver.unplug_network.assert_called_once_with(COMPUTE_ID, 1) + net.revert(None, {self.amphora_mock.id: delta}, + self.load_balancer_mock) + mock_driver.unplug_network.assert_called_once_with( + self.amphora_mock.compute_id, 1) mock_driver.reset_mock() - net.execute({}) + mock_driver.delete_port.side_effect = TestException('test') + net.revert(None, {self.amphora_mock.id: delta}, + self.load_balancer_mock) + mock_driver.unplug_network.assert_called_once_with( + self.amphora_mock.compute_id, 1) + mock_driver.delete_port.assert_called_once_with(12) + + mock_driver.reset_mock() + net.execute({}, self.load_balancer_mock) self.assertFalse(mock_driver.unplug_network.called) delta = data_models.Delta(amphora_id=self.amphora_mock.id, compute_id=self.amphora_mock.compute_id, add_nics=[], - delete_nics=[]) - net.execute({self.amphora_mock.id: delta}) + delete_nics=[], + add_subnets=[], + delete_subnets=[]) + net.execute({self.amphora_mock.id: delta}, self.load_balancer_mock) self.assertFalse(mock_driver.unplug_network.called) delta = data_models.Delta(amphora_id=self.amphora_mock.id, compute_id=self.amphora_mock.compute_id, add_nics=[], - delete_nics=_interface(1)) - net.execute({self.amphora_mock.id: delta}) + delete_nics=[_interface(1)], + add_subnets=[], + delete_subnets=[]) + net.execute({self.amphora_mock.id: delta}, self.load_balancer_mock) mock_driver.unplug_network.assert_called_once_with(COMPUTE_ID, 1) mock_driver.reset_mock() mock_driver.unplug_network.side_effect = net_base.NetworkNotFound - net.execute({self.amphora_mock.id: delta}) + net.execute({self.amphora_mock.id: delta}, self.load_balancer_mock) mock_driver.unplug_network.assert_called_once_with(COMPUTE_ID, 1) # Do a test with a general exception in case behavior changes mock_driver.reset_mock() mock_driver.unplug_network.side_effect = Exception() - net.execute({self.amphora_mock.id: delta}) + net.execute({self.amphora_mock.id: delta}, self.load_balancer_mock) mock_driver.unplug_network.assert_called_once_with(COMPUTE_ID, 1) + # Do a test with a general exception in case behavior changes + delta = data_models.Delta(amphora_id=self.amphora_mock.id, + compute_id=self.amphora_mock.compute_id, + add_nics=[], + delete_nics=[_interface(1, port_id=12)], + add_subnets=[], + delete_subnets=[]) + mock_driver.reset_mock() + mock_driver.delete_port.side_effect = Exception() + net.execute({self.amphora_mock.id: delta}, self.load_balancer_mock) + mock_driver.unplug_network.assert_called_once_with(COMPUTE_ID, 1) + mock_driver.delete_port.assert_called_once_with(12) + + mock_driver.unplug_network.reset_mock() + net.revert( + failure.Failure.from_exception(Exception('boom')), None, None) + mock_driver.unplug_network.assert_not_called() + + mock_driver.unplug_network.reset_mock() + net.revert(None, None, None) + mock_driver.unplug_network.assert_not_called() + def test_plug_vip(self, mock_get_net_driver): mock_driver = mock.MagicMock() mock_get_net_driver.return_value = mock_driver diff --git a/octavia/tests/unit/controller/worker/v1/test_controller_worker.py b/octavia/tests/unit/controller/worker/v1/test_controller_worker.py index fb0882682e..32b5286eae 100644 --- a/octavia/tests/unit/controller/worker/v1/test_controller_worker.py +++ b/octavia/tests/unit/controller/worker/v1/test_controller_worker.py @@ -731,6 +731,7 @@ class TestControllerWorker(base.TestCase): store={constants.MEMBER: _member_mock, constants.LISTENERS: [_listener_mock], constants.LOADBALANCER: _load_balancer_mock, + constants.LOADBALANCER_ID: _load_balancer_mock.id, constants.POOL: _pool_mock, constants.AVAILABILITY_ZONE: {}})) @@ -769,6 +770,8 @@ class TestControllerWorker(base.TestCase): [_listener_mock], constants.LOADBALANCER: _load_balancer_mock, + constants.LOADBALANCER_ID: + _load_balancer_mock.id, constants.POOL: _pool_mock, constants.AVAILABILITY_ZONE: {}})) diff --git a/octavia/tests/unit/controller/worker/v2/flows/test_amphora_flows.py b/octavia/tests/unit/controller/worker/v2/flows/test_amphora_flows.py index fdddaeb696..9484afee65 100644 --- a/octavia/tests/unit/controller/worker/v2/flows/test_amphora_flows.py +++ b/octavia/tests/unit/controller/worker/v2/flows/test_amphora_flows.py @@ -281,7 +281,7 @@ class TestAmphoraFlows(base.TestCase): self.assertIn(constants.LOADBALANCER_ID, amp_flow.requires) self.assertIn(constants.VIP, amp_flow.requires) - self.assertIn(constants.ADDED_PORTS, amp_flow.provides) + self.assertIn(constants.UPDATED_PORTS, amp_flow.provides) self.assertIn(constants.AMP_VRRP_INT, amp_flow.provides) self.assertIn(constants.AMPHORA, amp_flow.provides) self.assertIn(constants.AMPHORA_ID, amp_flow.provides) @@ -316,7 +316,7 @@ class TestAmphoraFlows(base.TestCase): self.assertIn(constants.LOADBALANCER_ID, amp_flow.requires) self.assertIn(constants.VIP, amp_flow.requires) - self.assertIn(constants.ADDED_PORTS, amp_flow.provides) + self.assertIn(constants.UPDATED_PORTS, amp_flow.provides) self.assertIn(constants.AMPHORA, amp_flow.provides) self.assertIn(constants.AMPHORA_ID, amp_flow.provides) self.assertIn(constants.AMPHORAE, amp_flow.provides) diff --git a/octavia/tests/unit/controller/worker/v2/flows/test_load_balancer_flows.py b/octavia/tests/unit/controller/worker/v2/flows/test_load_balancer_flows.py index 16b2d96a68..2504cabebf 100644 --- a/octavia/tests/unit/controller/worker/v2/flows/test_load_balancer_flows.py +++ b/octavia/tests/unit/controller/worker/v2/flows/test_load_balancer_flows.py @@ -210,8 +210,14 @@ class TestLoadBalancerFlows(base.TestCase): self.assertIsInstance(create_flow, flow.Flow) self.assertIn(constants.LOADBALANCER_ID, create_flow.requires) self.assertIn(constants.UPDATE_DICT, create_flow.requires) + self.assertIn(constants.BUILD_TYPE_PRIORITY, create_flow.requires) + self.assertIn(constants.FLAVOR, create_flow.requires) + self.assertIn(constants.AMPHORAE_NETWORK_CONFIG, create_flow.requires) + self.assertIn(constants.AVAILABILITY_ZONE, create_flow.requires) + self.assertIn(constants.SERVER_GROUP_ID, create_flow.requires) self.assertIn(constants.LISTENERS, create_flow.provides) + self.assertIn(constants.SUBNET, create_flow.provides) self.assertIn(constants.AMPHORA, create_flow.provides) self.assertIn(constants.AMPHORA_ID, create_flow.provides) self.assertIn(constants.AMPHORA_NETWORK_CONFIG, create_flow.provides) @@ -220,12 +226,11 @@ class TestLoadBalancerFlows(base.TestCase): self.assertIn(constants.COMPUTE_OBJ, create_flow.provides) self.assertIn(constants.LOADBALANCER, create_flow.provides) self.assertIn(constants.DELTAS, create_flow.provides) - self.assertIn(constants.ADDED_PORTS, create_flow.provides) + self.assertIn(constants.UPDATED_PORTS, create_flow.provides) self.assertIn(constants.SERVER_PEM, create_flow.provides) - self.assertIn(constants.SUBNET, create_flow.provides) self.assertIn(constants.VIP, create_flow.provides) - self.assertEqual(6, len(create_flow.requires)) + self.assertEqual(7, len(create_flow.requires)) self.assertEqual(13, len(create_flow.provides), create_flow.provides) @@ -244,7 +249,7 @@ class TestLoadBalancerFlows(base.TestCase): self.assertIn(constants.SERVER_GROUP_ID, create_flow.requires) self.assertIn(constants.UPDATE_DICT, create_flow.requires) - self.assertIn(constants.ADDED_PORTS, create_flow.provides) + self.assertIn(constants.UPDATED_PORTS, create_flow.provides) self.assertIn(constants.AMP_DATA, create_flow.provides) self.assertIn(constants.AMP_VRRP_INT, create_flow.provides) self.assertIn(constants.AMPHORA, create_flow.provides) @@ -280,7 +285,7 @@ class TestLoadBalancerFlows(base.TestCase): self.assertIn(constants.LOADBALANCER, failover_flow.requires) self.assertIn(constants.LOADBALANCER_ID, failover_flow.requires) - self.assertIn(constants.ADDED_PORTS, failover_flow.provides) + self.assertIn(constants.UPDATED_PORTS, failover_flow.provides) self.assertIn(constants.AMPHORA, failover_flow.provides) self.assertIn(constants.AMPHORA_ID, failover_flow.provides) self.assertIn(constants.AMPHORAE_NETWORK_CONFIG, @@ -343,7 +348,7 @@ class TestLoadBalancerFlows(base.TestCase): self.assertIn(constants.LOADBALANCER, failover_flow.requires) self.assertIn(constants.LOADBALANCER_ID, failover_flow.requires) - self.assertIn(constants.ADDED_PORTS, failover_flow.provides) + self.assertIn(constants.UPDATED_PORTS, failover_flow.provides) self.assertIn(constants.AMPHORA, failover_flow.provides) self.assertIn(constants.AMPHORA_ID, failover_flow.provides) self.assertIn(constants.AMPHORAE_NETWORK_CONFIG, diff --git a/octavia/tests/unit/controller/worker/v2/flows/test_member_flows.py b/octavia/tests/unit/controller/worker/v2/flows/test_member_flows.py index 2ffab476de..1cab682504 100644 --- a/octavia/tests/unit/controller/worker/v2/flows/test_member_flows.py +++ b/octavia/tests/unit/controller/worker/v2/flows/test_member_flows.py @@ -37,18 +37,19 @@ class TestMemberFlows(base.TestCase): self.assertIsInstance(member_flow, flow.Flow) + self.assertIn(constants.MEMBER, member_flow.requires) self.assertIn(constants.LISTENERS, member_flow.requires) self.assertIn(constants.LOADBALANCER, member_flow.requires) self.assertIn(constants.LOADBALANCER_ID, member_flow.requires) self.assertIn(constants.POOL_ID, member_flow.requires) - self.assertIn(constants.MEMBER, member_flow.requires) self.assertIn(constants.AVAILABILITY_ZONE, member_flow.requires) self.assertIn(constants.DELTAS, member_flow.provides) - self.assertIn(constants.ADDED_PORTS, member_flow.provides) + self.assertIn(constants.UPDATED_PORTS, member_flow.provides) + self.assertIn(constants.AMPHORAE_NETWORK_CONFIG, member_flow.provides) self.assertEqual(6, len(member_flow.requires)) - self.assertEqual(2, len(member_flow.provides)) + self.assertEqual(3, len(member_flow.provides)) def test_get_delete_member_flow(self, mock_get_net_driver): @@ -62,9 +63,14 @@ class TestMemberFlows(base.TestCase): self.assertIn(constants.LOADBALANCER_ID, member_flow.requires) self.assertIn(constants.POOL_ID, member_flow.requires) self.assertIn(constants.PROJECT_ID, member_flow.requires) + self.assertIn(constants.AVAILABILITY_ZONE, member_flow.requires) - self.assertEqual(6, len(member_flow.requires)) - self.assertEqual(0, len(member_flow.provides)) + self.assertIn(constants.DELTAS, member_flow.provides) + self.assertIn(constants.UPDATED_PORTS, member_flow.provides) + self.assertIn(constants.AMPHORAE_NETWORK_CONFIG, member_flow.provides) + + self.assertEqual(7, len(member_flow.requires)) + self.assertEqual(3, len(member_flow.provides)) def test_get_update_member_flow(self, mock_get_net_driver): @@ -96,7 +102,8 @@ class TestMemberFlows(base.TestCase): self.assertIn(constants.AVAILABILITY_ZONE, member_flow.requires) self.assertIn(constants.DELTAS, member_flow.provides) - self.assertIn(constants.ADDED_PORTS, member_flow.provides) + self.assertIn(constants.UPDATED_PORTS, member_flow.provides) + self.assertIn(constants.AMPHORAE_NETWORK_CONFIG, member_flow.provides) self.assertEqual(5, len(member_flow.requires)) - self.assertEqual(2, len(member_flow.provides)) + self.assertEqual(3, len(member_flow.provides)) diff --git a/octavia/tests/unit/controller/worker/v2/tasks/test_amphora_driver_tasks.py b/octavia/tests/unit/controller/worker/v2/tasks/test_amphora_driver_tasks.py index 28de5af50e..2afdde9bbd 100644 --- a/octavia/tests/unit/controller/worker/v2/tasks/test_amphora_driver_tasks.py +++ b/octavia/tests/unit/controller/worker/v2/tasks/test_amphora_driver_tasks.py @@ -58,6 +58,9 @@ _LB_mock = { constants.LOADBALANCER_ID: LB_ID, } _amphorae_mock = [_db_amphora_mock] +_amphora_network_config_mock = mock.MagicMock() +_amphorae_network_config_mock = { + _amphora_mock[constants.ID]: _amphora_network_config_mock} _network_mock = mock.MagicMock() _session_mock = mock.MagicMock() @@ -68,7 +71,7 @@ _session_mock = mock.MagicMock() @mock.patch('octavia.db.repositories.ListenerRepository.get', return_value=_listener_mock) @mock.patch('octavia.db.api.get_session', return_value=_session_mock) -@mock.patch('octavia.controller.worker.v2.tasks.amphora_driver_tasks.LOG') +@mock.patch('octavia.controller.worker.v1.tasks.amphora_driver_tasks.LOG') @mock.patch('oslo_utils.uuidutils.generate_uuid', return_value=AMP_ID) @mock.patch('stevedore.driver.DriverManager.driver') class TestAmphoraDriverTasks(base.TestCase): @@ -385,11 +388,13 @@ class TestAmphoraDriverTasks(base.TestCase): port_mock = {constants.NETWORK: mock.MagicMock(), constants.FIXED_IPS: fixed_ips, constants.ID: uuidutils.generate_uuid()} - amphora_post_network_plug_obj.execute(_amphora_mock, [port_mock]) + amphora_post_network_plug_obj.execute(_amphora_mock, [port_mock], + _amphora_network_config_mock) (mock_driver.post_network_plug. assert_called_once_with)(_db_amphora_mock, - network_data_models.Port(**port_mock)) + network_data_models.Port(**port_mock), + _amphora_network_config_mock) # Test revert amp = amphora_post_network_plug_obj.revert(None, _amphora_mock) @@ -434,11 +439,13 @@ class TestAmphoraDriverTasks(base.TestCase): port_mock = {constants.NETWORK: mock.MagicMock(), constants.FIXED_IPS: fixed_ips, constants.ID: uuidutils.generate_uuid()} - amphora_post_network_plug_obj.execute(_amphora_mock, [port_mock]) + amphora_post_network_plug_obj.execute(_amphora_mock, [port_mock], + _amphora_network_config_mock) (mock_driver.post_network_plug. assert_called_once_with)(_db_amphora_mock, - network_data_models.Port(**port_mock)) + network_data_models.Port(**port_mock), + _amphora_network_config_mock) call_args = mock_driver.post_network_plug.call_args[0] port_arg = call_args[1] @@ -472,18 +479,21 @@ class TestAmphoraDriverTasks(base.TestCase): constants.ID: uuidutils.generate_uuid()} _deltas_mock = {_db_amphora_mock.id: [port_mock]} - amphora_post_network_plug_obj.execute(_LB_mock, _deltas_mock) + amphora_post_network_plug_obj.execute(_LB_mock, _deltas_mock, + _amphorae_network_config_mock) (mock_driver.post_network_plug. assert_called_once_with(_db_amphora_mock, - network_data_models.Port(**port_mock))) + network_data_models.Port(**port_mock), + _amphora_network_config_mock)) # Test with no ports to plug mock_driver.post_network_plug.reset_mock() _deltas_mock = {'0': [port_mock]} - amphora_post_network_plug_obj.execute(_LB_mock, _deltas_mock) + amphora_post_network_plug_obj.execute(_LB_mock, _deltas_mock, + _amphora_network_config_mock) mock_driver.post_network_plug.assert_not_called() # Test revert diff --git a/octavia/tests/unit/controller/worker/v2/tasks/test_network_tasks.py b/octavia/tests/unit/controller/worker/v2/tasks/test_network_tasks.py index b2e0eb609d..c4ec6fc7fe 100644 --- a/octavia/tests/unit/controller/worker/v2/tasks/test_network_tasks.py +++ b/octavia/tests/unit/controller/worker/v2/tasks/test_network_tasks.py @@ -36,6 +36,8 @@ COMPUTE_ID = uuidutils.generate_uuid() PORT_ID = uuidutils.generate_uuid() SUBNET_ID = uuidutils.generate_uuid() NETWORK_ID = uuidutils.generate_uuid() +MGMT_NETWORK_ID = uuidutils.generate_uuid() +MGMT_SUBNET_ID = uuidutils.generate_uuid() SG_ID = uuidutils.generate_uuid() IP_ADDRESS = "172.24.41.1" VIP = o_data_models.Vip(port_id=t_constants.MOCK_PORT_ID, @@ -79,15 +81,17 @@ class TestNetworkTasks(base.TestCase): self.db_load_balancer_mock = mock.MagicMock() self.vip_mock = mock.MagicMock() self.vip_mock.subnet_id = SUBNET_ID + self.vip_mock.network_id = NETWORK_ID self.db_load_balancer_mock.vip = self.vip_mock self.db_load_balancer_mock.amphorae = [] self.db_amphora_mock.id = AMPHORA_ID self.db_amphora_mock.compute_id = COMPUTE_ID self.db_amphora_mock.status = constants.AMPHORA_ALLOCATED - self.boot_net_id = NETWORK_ID + self.mgmt_net_id = MGMT_NETWORK_ID + self.mgmt_subnet_id = MGMT_SUBNET_ID conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) conf.config(group="controller_worker", - amp_boot_network_list=[self.boot_net_id]) + amp_boot_network_list=[self.mgmt_net_id]) conf.config(group="networking", max_retries=1) self.amphora_mock = {constants.ID: AMPHORA_ID, constants.COMPUTE_ID: COMPUTE_ID, @@ -95,7 +99,8 @@ class TestNetworkTasks(base.TestCase): } self.load_balancer_mock = { constants.LOADBALANCER_ID: uuidutils.generate_uuid(), - constants.VIP_SUBNET_ID: VIP.subnet_id, + constants.VIP_SUBNET_ID: SUBNET_ID, + constants.VIP_NETWORK_ID: NETWORK_ID, constants.VIP_PORT_ID: VIP.port_id, constants.VIP_ADDRESS: VIP.ip_address, constants.VIP_QOS_POLICY_ID: t_constants.MOCK_QOS_POLICY_ID1 @@ -103,7 +108,7 @@ class TestNetworkTasks(base.TestCase): conf = oslo_fixture.Config(cfg.CONF) conf.config(group="controller_worker", - amp_boot_network_list=[self.boot_net_id]) + amp_boot_network_list=[self.mgmt_net_id]) super().setUp() @@ -112,10 +117,12 @@ class TestNetworkTasks(base.TestCase): def test_calculate_amphora_delta(self, mock_get_session, mock_lb_repo_get, mock_get_net_driver): LB_ID = uuidutils.generate_uuid() + VRRP_PORT_ID = uuidutils.generate_uuid() + VIP_NETWORK_ID = uuidutils.generate_uuid() + VIP_SUBNET_ID = uuidutils.generate_uuid() DELETE_NETWORK_ID = uuidutils.generate_uuid() MEMBER_NETWORK_ID = uuidutils.generate_uuid() MEMBER_SUBNET_ID = uuidutils.generate_uuid() - VRRP_PORT_ID = uuidutils.generate_uuid() mock_driver = mock.MagicMock() mock_get_net_driver.return_value = mock_driver member_mock = mock.MagicMock() @@ -124,25 +131,60 @@ class TestNetworkTasks(base.TestCase): pool_mock.members = [member_mock] lb_mock = mock.MagicMock() lb_mock.pools = [pool_mock] - lb_dict = {constants.LOADBALANCER_ID: LB_ID} + lb_mock.vip.subnet.network_id = MEMBER_NETWORK_ID + lb_dict = { + constants.LOADBALANCER_ID: LB_ID, + constants.VIP_SUBNET_ID: VIP_SUBNET_ID, + constants.VIP_NETWORK_ID: VIP_NETWORK_ID + } + amphora_dict = {constants.ID: AMPHORA_ID, constants.COMPUTE_ID: COMPUTE_ID, constants.VRRP_PORT_ID: VRRP_PORT_ID} - vrrp_port_mock = mock.MagicMock() - vrrp_port_mock.network_id = self.boot_net_id - vrrp_port_dict = {constants.NETWORK_ID: self.boot_net_id} - mock_subnet = mock.MagicMock() - mock_subnet.network_id = MEMBER_NETWORK_ID - nic1_delete_mock = mock.MagicMock() - nic1_delete_mock.network_id = DELETE_NETWORK_ID - nic2_keep_mock = mock.MagicMock() - nic2_keep_mock.network_id = self.boot_net_id + + mgmt_subnet = data_models.Subnet( + id=MGMT_SUBNET_ID, + network_id=MGMT_NETWORK_ID) + mgmt_net = data_models.Network( + id=MGMT_NETWORK_ID, + subnets=[mgmt_subnet.id]) + mgmt_interface = data_models.Interface( + network_id=mgmt_net.id, + fixed_ips=[ + data_models.FixedIP( + subnet_id=mgmt_subnet.id)]) + + vrrp_subnet = data_models.Subnet( + id=VIP_SUBNET_ID, + network_id=VIP_NETWORK_ID) + vrrp_port = data_models.Port( + id=VRRP_PORT_ID, + network_id=VIP_NETWORK_ID, + fixed_ips=[ + data_models.FixedIP( + subnet=vrrp_subnet, + subnet_id=vrrp_subnet.id)]) + vrrp_interface = data_models.Interface( + network_id=VIP_NETWORK_ID, + fixed_ips=vrrp_port.fixed_ips) + vrrp_port_dict = vrrp_port.to_dict(recurse=True) + + member_subnet = data_models.Subnet( + id=MEMBER_SUBNET_ID, + network_id=MEMBER_NETWORK_ID) + + to_be_deleted_interface = data_models.Interface( + id=mock.Mock(), + network_id=DELETE_NETWORK_ID) mock_lb_repo_get.return_value = lb_mock - mock_driver.get_port.return_value = vrrp_port_mock - mock_driver.get_subnet.return_value = mock_subnet - mock_driver.get_plugged_networks.return_value = [nic1_delete_mock, - nic2_keep_mock] + mock_driver.get_port.return_value = vrrp_port + mock_driver.get_subnet.return_value = member_subnet + mock_driver.get_network.return_value = mgmt_net + mock_driver.get_plugged_networks.return_value = [ + mgmt_interface, + vrrp_interface, + to_be_deleted_interface] calc_amp_delta = network_tasks.CalculateAmphoraDelta() @@ -158,12 +200,19 @@ class TestNetworkTasks(base.TestCase): self.assertEqual( DELETE_NETWORK_ID, result[constants.DELETE_NICS][0][constants.NETWORK_ID]) - mock_driver.get_port.assert_called_once_with(VRRP_PORT_ID) - mock_driver.get_subnet.assert_called_once_with(MEMBER_SUBNET_ID) + mock_driver.get_subnet.assert_called_once_with( + MEMBER_SUBNET_ID) mock_driver.get_plugged_networks.assert_called_once_with(COMPUTE_ID) # Test with vrrp_port_id mock_driver.reset_mock() + mock_driver.get_port.return_value = vrrp_port + mock_driver.get_subnet.return_value = member_subnet + mock_driver.get_network.return_value = mgmt_net + mock_driver.get_plugged_networks.return_value = [ + mgmt_interface, + vrrp_interface, + to_be_deleted_interface] result = calc_amp_delta.execute(lb_dict, amphora_dict, {}, vrrp_port=vrrp_port_dict) @@ -177,7 +226,6 @@ class TestNetworkTasks(base.TestCase): self.assertEqual( DELETE_NETWORK_ID, result[constants.DELETE_NICS][0][constants.NETWORK_ID]) - mock_driver.get_port.assert_not_called() mock_driver.get_subnet.assert_called_once_with(MEMBER_SUBNET_ID) mock_driver.get_plugged_networks.assert_called_once_with(COMPUTE_ID) @@ -192,23 +240,100 @@ class TestNetworkTasks(base.TestCase): constants.ID: AMPHORA_ID, constants.COMPUTE_ID: COMPUTE_ID, constants.VRRP_PORT_ID: PORT_ID} mock_get_net_driver.return_value = mock_driver - mock_driver.get_plugged_networks.return_value = [ - data_models.Interface(network_id=self.boot_net_id)] - mock_driver.get_port.return_value = data_models.Port( - network_id=self.boot_net_id) EMPTY = {} empty_deltas = {self.db_amphora_mock.id: data_models.Delta( amphora_id=AMPHORA_ID, compute_id=COMPUTE_ID, add_nics=[], - delete_nics=[]).to_dict(recurse=True)} + delete_nics=[], + add_subnets=[], + delete_subnets=[], + ).to_dict()} + + mgmt_subnet = data_models.Subnet( + id=self.mgmt_subnet_id, network_id=self.mgmt_net_id) + mgmt_net = data_models.Network( + id=self.mgmt_net_id, + subnets=[mgmt_subnet.id]) + mgmt_ip_address = mock.MagicMock() + mgmt_interface = data_models.Interface( + network_id=self.mgmt_net_id, + fixed_ips=[ + data_models.FixedIP( + subnet=mgmt_subnet, + subnet_id=self.mgmt_subnet_id, + ip_address=mgmt_ip_address + ) + ]) + vrrp_subnet = data_models.Subnet( + id=self.vip_mock.subnet_id, network_id=self.vip_mock.network_id, + name='vrrp_subnet') + member_vip_subnet = data_models.Subnet( + id=uuidutils.generate_uuid(), network_id=self.vip_mock.network_id, + name='member_vip_subnet') + vip_net = data_models.Network( + id=self.vip_mock.network_id, + subnets=[member_vip_subnet, vrrp_subnet], + name='flat_network') + vrrp_port = data_models.Port( + id=uuidutils.generate_uuid(), + network_id=vip_net.id, network=vip_net, + fixed_ips=[ + data_models.FixedIP( + subnet=vrrp_subnet, subnet_id=vrrp_subnet.id, + ip_address=t_constants.MOCK_IP_ADDRESS) + ], + name='vrrp_port') + + member_private_net_id = uuidutils.generate_uuid() + member_private_subnet = data_models.Subnet( + id=uuidutils.generate_uuid(), network_id=member_private_net_id, + name='member_private_subnet') + member_private_subnet2 = data_models.Subnet( + id=uuidutils.generate_uuid(), network_id=member_private_net_id, + name='member_private_subnet2') + member_private_net = data_models.Network( + id=member_private_subnet.network_id, + subnets=[member_private_subnet, member_private_subnet2], + name='member_private_net') + member_private_subnet_port = data_models.Port( + id=uuidutils.generate_uuid(), + network_id=member_private_net.id, network=member_private_net, + fixed_ips=[ + data_models.FixedIP( + subnet=member_private_subnet, + subnet_id=member_private_subnet.id, + ip_address=t_constants.MOCK_IP_ADDRESS2) + ], + name='member_private_net_port') + member_private_subnet2_port = data_models.Port( + id=uuidutils.generate_uuid(), + network_id=member_private_net.id, network=member_private_net, + fixed_ips=[ + data_models.FixedIP( + subnet=member_private_subnet2, + subnet_id=member_private_subnet2.id, + ip_address=t_constants.MOCK_IP_ADDRESS2) + ], + name='member_private_net_port') + + # Pretend the VIP is on the member network, so already plugged + mock_driver.get_plugged_networks.return_value = [ + mgmt_interface, + data_models.Interface( + network_id=vip_net.id, port_id=vrrp_port.id, + fixed_ips=vrrp_port.fixed_ips)] + mock_driver.get_port.return_value = vrrp_port + mock_driver.get_subnet.return_value = vrrp_subnet + mock_driver.get_network.return_value = mgmt_net calc_delta = network_tasks.CalculateDelta() - self.assertEqual(EMPTY, - calc_delta.execute(self.load_balancer_mock, {})) + # Test with no amps or anything at all + self.assertEqual(EMPTY, calc_delta.execute( + self.load_balancer_mock, {})) - # Test with one amp and no pools, nothing plugged + # Test with one amp and no pools, only the base network plugged # Delta should be empty mock_driver.reset_mock() @@ -219,93 +344,292 @@ class TestNetworkTasks(base.TestCase): calc_delta.execute(self.load_balancer_mock, {})) mock_driver.get_plugged_networks.assert_called_once_with(COMPUTE_ID) - # Pool mock should be configured explicitly for each test - pool_mock = mock.MagicMock() - self.db_load_balancer_mock.pools = [pool_mock] - # Test with one amp and one pool but no members, nothing plugged # Delta should be empty + mock_driver.reset_mock() + pool_mock = mock.MagicMock() pool_mock.members = [] + self.db_load_balancer_mock.pools = [pool_mock] self.assertEqual(empty_deltas, calc_delta.execute(self.load_balancer_mock, {})) - # Test with one amp and one pool and one member, nothing plugged + # Test with one amp/pool and one member (on a distinct member subnet) # Dummy AZ is provided - # Delta should be one additional subnet to plug + # Only the base network is already plugged + # Delta should be one additional network/subnet to plug mock_driver.reset_mock() member_mock = mock.MagicMock() - member_mock.subnet_id = 1 + member_mock.subnet_id = member_private_subnet.id + member2_mock = mock.MagicMock() + member2_mock.subnet_id = member_private_subnet2.id pool_mock.members = [member_mock] az = { constants.COMPUTE_ZONE: 'foo' } - mock_driver.get_subnet.return_value = data_models.Subnet(id=2, - network_id=3) + mock_driver.get_subnet.return_value = data_models.Subnet( + id=2, network_id=3) - ndm = data_models.Delta(amphora_id=self.db_amphora_mock.id, - compute_id=self.db_amphora_mock.compute_id, - add_nics=[ - data_models.Interface(network_id=3)], - delete_nics=[]).to_dict(recurse=True) + ndm = data_models.Delta( + amphora_id=self.db_amphora_mock.id, + compute_id=self.db_amphora_mock.compute_id, + add_nics=[ + data_models.Interface( + network_id=3, + fixed_ips=[ + data_models.FixedIP( + subnet_id=member_private_subnet.id)])], + delete_nics=[], + add_subnets=[{ + 'subnet_id': member_private_subnet.id, + 'network_id': 3, + 'port_id': None}], + delete_subnets=[]).to_dict(recurse=True) self.assertEqual({self.db_amphora_mock.id: ndm}, calc_delta.execute(self.load_balancer_mock, az)) - vrrp_port_call = mock.call(PORT_ID) - mock_driver.get_port.assert_has_calls([vrrp_port_call]) - self.assertEqual(1, mock_driver.get_port.call_count) + mock_driver.get_subnet.assert_called_once_with( + member_mock.subnet_id) - member_subnet_call = mock.call(member_mock.subnet_id) - mock_driver.get_subnet.assert_has_calls([member_subnet_call]) - self.assertEqual(1, mock_driver.get_subnet.call_count) - - # Test with one amp and one pool and one member, already plugged + # Test with one amp/pool and one member (not plugged) that is being + # deleted + # Only the base network is already plugged # Delta should be empty mock_driver.reset_mock() member_mock = mock.MagicMock() - member_mock.subnet_id = 1 + member_mock.subnet_id = member_private_subnet.id + member_mock.provisioning_status = constants.PENDING_DELETE pool_mock.members = [member_mock] - mock_driver.get_plugged_networks.return_value = [ - data_models.Interface(network_id=3), - data_models.Interface(network_id=self.boot_net_id)] self.assertEqual(empty_deltas, calc_delta.execute(self.load_balancer_mock, {})) - # Test with one amp and one pool and one member, wrong network plugged - # Delta should be one network to add and one to remove + # Test with one amp/pool and one member (without any subnets) + # Only the base network is already plugged + # No delta mock_driver.reset_mock() member_mock = mock.MagicMock() - member_mock.subnet_id = 1 + member_mock.subnet_id = None pool_mock.members = [member_mock] - mock_driver.get_plugged_networks.return_value = [ - data_models.Interface(network_id=2), - data_models.Interface(network_id=self.boot_net_id)] - ndm = data_models.Delta(amphora_id=self.db_amphora_mock.id, - compute_id=self.db_amphora_mock.compute_id, - add_nics=[ - data_models.Interface(network_id=3)], - delete_nics=[ - data_models.Interface(network_id=2)] - ).to_dict(recurse=True) - self.assertEqual({self.db_amphora_mock.id: ndm}, + self.assertEqual(empty_deltas, calc_delta.execute(self.load_balancer_mock, {})) + # Test with one amp and one pool and one member + # Management network is defined in AZ metadata + # Base network AND member network/subnet already plugged + # Delta should be empty + mock_driver.reset_mock() + member_mock = mock.MagicMock() + member_mock.subnet_id = member_private_subnet.id + pool_mock.members = [member_mock] + + mgmt2_subnet_id = uuidutils.generate_uuid() + mgmt2_net_id = uuidutils.generate_uuid() + mgmt2_subnet = data_models.Subnet( + id=mgmt2_subnet_id, + network_id=mgmt2_net_id) + mgmt2_net = data_models.Network( + id=mgmt2_net_id, + subnets=[mgmt2_subnet.id] + ) + mgmt2_interface = data_models.Interface( + network_id=mgmt2_net_id, + fixed_ips=[ + data_models.FixedIP( + subnet=mgmt2_subnet, + subnet_id=mgmt2_subnet_id, + ) + ]) + mock_driver.get_network.return_value = mgmt2_net + az = { + constants.MANAGEMENT_NETWORK: mgmt2_net_id, + } + mock_driver.get_subnet.return_value = member_private_subnet + mock_driver.get_plugged_networks.return_value = [ + mgmt2_interface, + data_models.Interface( + network_id=vrrp_subnet.network_id, + fixed_ips=vrrp_port.fixed_ips), + data_models.Interface( + network_id=member_private_subnet.network_id, + fixed_ips=member_private_subnet_port.fixed_ips)] + + self.assertEqual(empty_deltas, + calc_delta.execute(self.load_balancer_mock, az)) + + # Test with one amp and one pool and one member, wrong network plugged + # Delta should be one network/subnet to add and one to remove + mock_driver.reset_mock() + mock_driver.get_network.return_value = mgmt_net + member_mock = mock.MagicMock() + member_mock.subnet_id = member_private_subnet.id + pool_mock.members = [member_mock] + az = { + constants.COMPUTE_ZONE: 'foo' + } + mock_driver.get_subnet.return_value = member_private_subnet + mock_driver.get_plugged_networks.return_value = [ + mgmt_interface, + data_models.Interface( + network_id=vrrp_subnet.network_id, + fixed_ips=vrrp_port.fixed_ips), + data_models.Interface( + network_id='bad_net', + fixed_ips=[data_models.FixedIP(subnet_id='bad_subnet')])] + + ndm = data_models.Delta( + amphora_id=self.db_amphora_mock.id, + compute_id=self.db_amphora_mock.compute_id, + add_nics=[data_models.Interface( + network_id=member_private_net.id, + fixed_ips=[data_models.FixedIP( + subnet_id=member_private_subnet.id)])], + delete_nics=[data_models.Interface(network_id='bad_net')], + add_subnets=[{ + 'subnet_id': member_private_subnet.id, + 'network_id': member_private_net.id, + 'port_id': None + }], + delete_subnets=[{ + 'subnet_id': 'bad_subnet', + 'network_id': 'bad_net', + 'port_id': None + }]).to_dict(recurse=True) + self.assertEqual({self.db_amphora_mock.id: ndm}, + calc_delta.execute(self.load_balancer_mock, az)) + # Test with one amp and one pool and no members, one network plugged # Delta should be one network to remove mock_driver.reset_mock() pool_mock.members = [] + mock_driver.get_subnet.side_effect = [ + vrrp_subnet] mock_driver.get_plugged_networks.return_value = [ - data_models.Interface(network_id=2), - data_models.Interface(network_id=self.boot_net_id) - ] + mgmt_interface, + data_models.Interface( + network_id=vrrp_subnet.network_id, + fixed_ips=vrrp_port.fixed_ips), + data_models.Interface( + network_id='bad_net', + fixed_ips=[data_models.FixedIP(subnet_id='bad_subnet')])] - ndm = data_models.Delta(amphora_id=self.db_amphora_mock.id, - compute_id=self.db_amphora_mock.compute_id, - add_nics=[], - delete_nics=[ - data_models.Interface(network_id=2)] - ).to_dict(recurse=True) + ndm = data_models.Delta( + amphora_id=self.db_amphora_mock.id, + compute_id=self.db_amphora_mock.compute_id, + add_nics=[], + delete_nics=[data_models.Interface(network_id='bad_net')], + add_subnets=[], + delete_subnets=[{ + 'subnet_id': 'bad_subnet', + 'network_id': 'bad_net', + 'port_id': None + }]).to_dict(recurse=True) + self.assertEqual({self.db_amphora_mock.id: ndm}, + calc_delta.execute(self.load_balancer_mock, {})) + + # Add a new member on a new subnet, an interface with another subnet of + # the same network is already plugged + # Delta should be one new subnet + mock_driver.reset_mock() + pool_mock.members = [member_mock, member2_mock] + mock_driver.get_subnet.side_effect = [ + vrrp_subnet, + member_private_subnet, + member_private_subnet2] + mock_driver.get_plugged_networks.return_value = [ + mgmt_interface, + data_models.Interface( + network_id=vrrp_subnet.network_id, + fixed_ips=vrrp_port.fixed_ips), + data_models.Interface( + network_id=member_private_net_id, + port_id=member_private_subnet_port.id, + fixed_ips=member_private_subnet_port.fixed_ips)] + + ndm = data_models.Delta( + amphora_id=self.db_amphora_mock.id, + compute_id=self.db_amphora_mock.compute_id, + add_nics=[], + delete_nics=[], + add_subnets=[{ + 'subnet_id': member_private_subnet2.id, + 'network_id': member_private_net_id, + 'port_id': member_private_subnet_port.id + }], + delete_subnets=[] + ).to_dict(recurse=True) + self.assertEqual({self.db_amphora_mock.id: ndm}, + calc_delta.execute(self.load_balancer_mock, {})) + + # a new member on a new subnet on an existing network, a delete member2 + # on another subnet of the same network + # Delta should be one new subnet, one deleted subnet, no interface + # change + mock_driver.reset_mock() + pool_mock.members = [member_mock] + mock_driver.get_subnet.return_value = member_private_subnet + mock_driver.get_plugged_networks.return_value = [ + mgmt_interface, + data_models.Interface( + network_id=vrrp_subnet.network_id, + fixed_ips=vrrp_port.fixed_ips), + data_models.Interface( + network_id=member_private_net_id, + port_id=member_private_subnet2_port.id, + fixed_ips=member_private_subnet2_port.fixed_ips)] + + ndm = data_models.Delta( + amphora_id=self.db_amphora_mock.id, + compute_id=self.db_amphora_mock.compute_id, + add_nics=[], + delete_nics=[], + add_subnets=[{ + 'subnet_id': member_private_subnet.id, + 'network_id': member_private_net_id, + 'port_id': member_private_subnet2_port.id}], + delete_subnets=[{ + 'subnet_id': member_private_subnet2.id, + 'network_id': member_private_net_id, + 'port_id': member_private_subnet2_port.id}] + ).to_dict(recurse=True) + self.assertEqual({self.db_amphora_mock.id: ndm}, + calc_delta.execute(self.load_balancer_mock, {})) + + # member on subnet on the same network as the vip subnet + mock_driver.reset_mock() + member_mock.subnet_id = member_vip_subnet.id + pool_mock.members = [member_mock] + mock_driver.get_subnet.side_effect = [ + vrrp_subnet, + member_vip_subnet] + mock_driver.get_plugged_networks.return_value = [ + mgmt_interface, + data_models.Interface( + network_id=vrrp_subnet.network_id, + port_id=vrrp_port.id, + fixed_ips=vrrp_port.fixed_ips), + data_models.Interface( + network_id=member_private_net_id, + port_id=member_private_subnet_port.id, + fixed_ips=member_private_subnet_port.fixed_ips)] + + ndm = data_models.Delta( + amphora_id=self.db_amphora_mock.id, + compute_id=self.db_amphora_mock.compute_id, + add_nics=[], + delete_nics=[ + data_models.Interface( + network_id=member_private_net_id, + port_id=member_private_subnet_port.id)], + add_subnets=[{ + 'subnet_id': member_vip_subnet.id, + 'network_id': vip_net.id, + 'port_id': vrrp_port.id}], + delete_subnets=[{ + 'subnet_id': member_private_subnet.id, + 'network_id': member_private_net_id, + 'port_id': member_private_subnet_port.id}] + ).to_dict(recurse=True) self.assertEqual({self.db_amphora_mock.id: ndm}, calc_delta.execute(self.load_balancer_mock, {})) @@ -449,15 +773,23 @@ class TestNetworkTasks(base.TestCase): mock_driver.get_subnet.assert_called_once_with(1) self.assertEqual([port_mock], ports) - def test_handle_network_delta(self, mock_get_net_driver): + @mock.patch('octavia.db.repositories.AmphoraRepository.get') + @mock.patch('octavia.db.api.get_session', return_value=_session_mock) + def test_handle_network_delta(self, mock_session, mock_amp_get, + mock_get_net_driver): mock_net_driver = mock.MagicMock() self.db_amphora_mock.to_dict.return_value = { constants.ID: AMPHORA_ID, constants.COMPUTE_ID: COMPUTE_ID} mock_get_net_driver.return_value = mock_net_driver + mock_amp_get.return_value = self.db_amphora_mock nic1 = data_models.Interface() + nic1.fixed_ips = [data_models.FixedIP( + subnet_id=uuidutils.generate_uuid())] nic1.network_id = uuidutils.generate_uuid() nic2 = data_models.Interface() + nic2.fixed_ips = [data_models.FixedIP( + subnet_id=uuidutils.generate_uuid())] nic2.network_id = uuidutils.generate_uuid() interface1 = mock.MagicMock() interface1.port_id = uuidutils.generate_uuid() @@ -472,7 +804,9 @@ class TestNetworkTasks(base.TestCase): delta = data_models.Delta(amphora_id=self.db_amphora_mock.id, compute_id=self.db_amphora_mock.compute_id, add_nics=[nic1], - delete_nics=[nic2, nic2, nic2] + delete_nics=[nic2, nic2, nic2], + add_subnets=[], + delete_subnets=[] ).to_dict(recurse=True) mock_net_driver.plug_network.return_value = interface1 @@ -517,89 +851,331 @@ class TestNetworkTasks(base.TestCase): mock_net_driver.unplug_network.reset_mock() handle_net_delta_obj.revert(None, None, delta2) - def test_handle_network_deltas(self, mock_get_net_driver): + mock_net_driver.unplug_network.reset_mock() + mock_net_driver.delete_port.side_effect = Exception('boom') + handle_net_delta_obj.revert(None, None, delta2) + + @mock.patch('octavia.db.repositories.AmphoraRepository.get') + @mock.patch('octavia.db.repositories.LoadBalancerRepository.get') + @mock.patch('octavia.db.api.get_session', return_value=_session_mock) + def test_handle_network_deltas(self, mock_get_session, + mock_get_lb, mock_get_amp, + mock_get_net_driver): mock_driver = mock.MagicMock() + self.db_load_balancer_mock.amphorae = [self.db_amphora_mock] self.db_amphora_mock.to_dict.return_value = { constants.ID: AMPHORA_ID, constants.COMPUTE_ID: COMPUTE_ID} mock_get_net_driver.return_value = mock_driver + mock_get_lb.return_value = self.db_load_balancer_mock + mock_get_amp.return_value = self.db_amphora_mock - def _interface(network_id): - return [data_models.Interface(network_id=network_id)] + subnet1 = uuidutils.generate_uuid() + network1 = uuidutils.generate_uuid() + port1 = uuidutils.generate_uuid() + subnet2 = uuidutils.generate_uuid() + + def _interface(network_id, port_id=None, subnet_id=None): + return data_models.Interface( + network_id=network_id, + port_id=port_id, + fixed_ips=[ + data_models.FixedIP( + subnet_id=subnet_id)]) net = network_tasks.HandleNetworkDeltas() - net.execute({}) + net.execute({}, self.load_balancer_mock) self.assertFalse(mock_driver.plug_network.called) delta = data_models.Delta(amphora_id=self.db_amphora_mock.id, compute_id=self.db_amphora_mock.compute_id, add_nics=[], - delete_nics=[]).to_dict(recurse=True) - net.execute({self.db_amphora_mock.id: delta}) + delete_nics=[], + add_subnets=[], + delete_subnets=[]).to_dict(recurse=True) + net.execute({self.db_amphora_mock.id: delta}, self.load_balancer_mock) self.assertFalse(mock_driver.plug_network.called) + # Adding a subnet on a new network + port = data_models.Port( + id=port1, + network_id=network1, + fixed_ips=[ + data_models.FixedIP(subnet_id=subnet1)]) + mock_driver.get_port.return_value = port + mock_driver.plug_fixed_ip.return_value = port + mock_driver.get_network.return_value = data_models.Network( + id=network1) + mock_driver.get_subnet.return_value = data_models.Subnet( + id=subnet1, + network_id=network1) + add_nics = [_interface(network1, subnet_id=subnet1)] + add_subnets = [{ + 'subnet_id': subnet1, + 'network_id': network1, + 'port_id': None}] + delta = data_models.Delta(amphora_id=self.db_amphora_mock.id, compute_id=self.db_amphora_mock.compute_id, - add_nics=_interface(1), - delete_nics=[]).to_dict(recurse=True) - net.execute({self.db_amphora_mock.id: delta}) - mock_driver.plug_network.assert_called_once_with(COMPUTE_ID, 1) + add_nics=add_nics, + delete_nics=[], + add_subnets=add_subnets, + delete_subnets=[]).to_dict(recurse=True) + updated_ports = net.execute({self.db_amphora_mock.id: delta}, + self.load_balancer_mock) + mock_driver.plug_network.assert_called_once_with( + self.db_amphora_mock.compute_id, network1) + mock_driver.unplug_network.assert_not_called() + + self.assertEqual(1, len(updated_ports)) + + updated_port = updated_ports[self.db_amphora_mock.id][0] + self.assertEqual(port1, updated_port['id']) + self.assertEqual(network1, updated_port['network_id']) + self.assertEqual(1, len(updated_port['fixed_ips'])) + self.assertEqual(subnet1, updated_port['fixed_ips'][0]['subnet_id']) # revert - net.execute({self.db_amphora_mock.id: delta}) - self.assertFalse(mock_driver.unplug_network.called) + net.revert(None, {self.db_amphora_mock.id: delta}, + self.load_balancer_mock) + mock_driver.unplug_network.assert_called_once_with( + self.db_amphora_mock.compute_id, network1) + + # Adding a subnet on an existing network/port + mock_driver.reset_mock() + port = data_models.Port( + id=port1, + network_id=network1, + fixed_ips=[ + data_models.FixedIP(subnet_id=subnet2), + data_models.FixedIP(subnet_id=subnet1)]) + mock_driver.plug_fixed_ip.return_value = port + mock_driver.get_network.return_value = data_models.Network( + id=network1) + mock_driver.get_subnet.side_effect = [ + data_models.Subnet( + id=subnet2, + network_id=network1), + data_models.Subnet( + id=subnet1, + network_id=network1)] + add_nics = [_interface(network1)] + add_subnets = [{ + 'subnet_id': subnet1, + 'network_id': network1, + 'port_id': port1}] delta = data_models.Delta(amphora_id=self.db_amphora_mock.id, compute_id=self.db_amphora_mock.compute_id, add_nics=[], - delete_nics=[]).to_dict(recurse=True) - net.execute({self.db_amphora_mock.id: delta}) - self.assertFalse(mock_driver.unplug_network.called) + delete_nics=[], + add_subnets=add_subnets, + delete_subnets=[]).to_dict(recurse=True) + updated_ports = net.execute({self.db_amphora_mock.id: delta}, + self.load_balancer_mock) + mock_driver.plug_network.assert_not_called() + mock_driver.unplug_network.assert_not_called() + mock_driver.get_port.assert_not_called() + mock_driver.plug_fixed_ip.assert_called_once_with(port_id=port1, + subnet_id=subnet1) + self.assertEqual(1, len(updated_ports)) + + updated_port = updated_ports[self.db_amphora_mock.id][0] + self.assertEqual(port1, updated_port['id']) + self.assertEqual(network1, updated_port['network_id']) + self.assertEqual(2, len(updated_port['fixed_ips'])) + self.assertEqual(subnet2, updated_port['fixed_ips'][0]['subnet_id']) + self.assertEqual(subnet1, updated_port['fixed_ips'][1]['subnet_id']) + + # Deleting a subnet + mock_driver.reset_mock() + delete_subnets = [{ + 'subnet_id': subnet1, + 'network_id': network1, + 'port_id': port1}] + mock_driver.get_subnet.side_effect = [ + data_models.Subnet( + id=subnet2, + network_id=network1)] + mock_driver.unplug_fixed_ip.return_value = data_models.Port( + id=port1, + network_id=network1, + fixed_ips=[ + data_models.FixedIP(subnet_id=subnet2)]) delta = data_models.Delta(amphora_id=self.db_amphora_mock.id, compute_id=self.db_amphora_mock.compute_id, - add_nics=_interface(1), - delete_nics=[]).to_dict(recurse=True) + add_nics=[], + delete_nics=[], + add_subnets=[], + delete_subnets=delete_subnets).to_dict( + recurse=True) + updated_ports = net.execute({self.db_amphora_mock.id: delta}, + self.load_balancer_mock) + mock_driver.delete_port.assert_not_called() + mock_driver.plug_network.assert_not_called() + mock_driver.plug_fixed_ip.assert_not_called() + self.assertEqual(1, len(updated_ports)) + self.assertEqual(1, len(updated_ports[self.db_amphora_mock.id])) + updated_port = updated_ports[self.db_amphora_mock.id][0] + self.assertEqual(port1, updated_port['id']) + self.assertEqual(network1, updated_port['network_id']) + self.assertEqual(1, len(updated_port['fixed_ips'])) + self.assertEqual(subnet2, updated_port['fixed_ips'][0]['subnet_id']) + + # Deleting a subnet, but neutron doesn't unplug it + # Delta are empty because there's nothing to update mock_driver.reset_mock() - mock_driver.unplug_network.side_effect = net_base.NetworkNotFound + delete_subnets = [{ + 'subnet_id': subnet1, + 'network_id': network1, + 'port_id': port1}] + mock_driver.get_subnet.side_effect = [ + data_models.Subnet( + id=subnet2, + network_id=network1), + data_models.Subnet( + id=subnet2, + network_id=network1)] + mock_driver.unplug_fixed_ip.return_value = data_models.Port( + id=port1, + network_id=network1, + fixed_ips=[ + data_models.FixedIP(subnet_id=subnet1), + data_models.FixedIP(subnet_id=subnet2)]) + delta = data_models.Delta(amphora_id=self.db_amphora_mock.id, + compute_id=self.db_amphora_mock.compute_id, + add_nics=[], + delete_nics=[], + add_subnets=[], + delete_subnets=[]).to_dict(recurse=True) + net.execute({self.db_amphora_mock.id: delta}, + self.load_balancer_mock) + mock_driver.delete_port.assert_not_called() + mock_driver.plug_network.assert_not_called() + mock_driver.plug_fixed_ip.assert_not_called() + + # Deleting a subnet and a network + mock_driver.reset_mock() + mock_driver.get_subnet.side_effect = [ + data_models.Subnet( + id=subnet2, + network_id=network1), + data_models.Subnet( + id=subnet1, + network_id=network1)] + delete_nics = [_interface(network1, port_id=port1)] + delete_subnets = [{ + 'subnet_id': subnet1, + 'network_id': network1, + 'port_id': port1}] + + delta = data_models.Delta(amphora_id=self.db_amphora_mock.id, + compute_id=self.db_amphora_mock.compute_id, + add_nics=[], + delete_nics=delete_nics, + add_subnets=[], + delete_subnets=delete_subnets).to_dict( + recurse=True) + updated_ports = net.execute({self.db_amphora_mock.id: delta}, + self.load_balancer_mock) + mock_driver.delete_port.assert_called_once_with(port1) + mock_driver.plug_network.assert_not_called() + mock_driver.plug_fixed_ip.assert_not_called() + self.assertEqual(1, len(updated_ports)) + self.assertEqual(0, len(updated_ports[self.db_amphora_mock.id])) + + # No delta, no actions + mock_driver.reset_mock() + delta = data_models.Delta(amphora_id=self.db_amphora_mock.id, + compute_id=self.db_amphora_mock.compute_id, + add_nics=[], + delete_nics=[], + add_subnets=[], + delete_subnets=[]).to_dict(recurse=True) + net.execute({self.db_amphora_mock.id: delta}, self.load_balancer_mock) + mock_driver.plug_network.assert_not_called() + mock_driver.plug_fixed_ip.assert_not_called() + mock_driver.unplug_network.assert_not_called() + + delta = data_models.Delta(amphora_id=self.db_amphora_mock.id, + compute_id=self.db_amphora_mock.compute_id, + add_nics=[_interface(1, port_id=12)], + delete_nics=[], + add_subnets=[], + delete_subnets=[]).to_dict(recurse=True) mock_driver.reset_mock() mock_driver.unplug_network.side_effect = TestException('test') - self.assertRaises(TestException, net.revert, mock.ANY, - {self.db_amphora_mock.id: delta}) - mock_driver.unplug_network.assert_called_once_with(COMPUTE_ID, 1) + net.revert(None, {self.db_amphora_mock.id: delta}, + self.load_balancer_mock) + mock_driver.unplug_network.assert_called_once_with( + self.db_amphora_mock.compute_id, 1) mock_driver.reset_mock() - net.execute({}) + mock_driver.delete_port.side_effect = TestException('test') + net.revert(None, {self.db_amphora_mock.id: delta}, + self.load_balancer_mock) + mock_driver.unplug_network.assert_called_once_with( + self.db_amphora_mock.compute_id, 1) + mock_driver.delete_port.assert_called_once_with(12) + + mock_driver.reset_mock() + net.execute({}, self.load_balancer_mock) self.assertFalse(mock_driver.unplug_network.called) delta = data_models.Delta(amphora_id=self.db_amphora_mock.id, compute_id=self.db_amphora_mock.compute_id, add_nics=[], - delete_nics=[]).to_dict(recurse=True) - net.execute({self.db_amphora_mock.id: delta}) + delete_nics=[], + add_subnets=[], + delete_subnets=[]).to_dict(recurse=True) + net.execute({self.db_amphora_mock.id: delta}, self.load_balancer_mock) self.assertFalse(mock_driver.unplug_network.called) delta = data_models.Delta(amphora_id=self.db_amphora_mock.id, compute_id=self.db_amphora_mock.compute_id, add_nics=[], - delete_nics=_interface(1) - ).to_dict(recurse=True) - net.execute({self.db_amphora_mock.id: delta}) + delete_nics=[_interface(1)], + add_subnets=[], + delete_subnets=[]).to_dict(recurse=True) + net.execute({self.db_amphora_mock.id: delta}, self.load_balancer_mock) mock_driver.unplug_network.assert_called_once_with(COMPUTE_ID, 1) mock_driver.reset_mock() mock_driver.unplug_network.side_effect = net_base.NetworkNotFound - net.execute({self.db_amphora_mock.id: delta}) + net.execute({self.db_amphora_mock.id: delta}, self.load_balancer_mock) mock_driver.unplug_network.assert_called_once_with(COMPUTE_ID, 1) # Do a test with a general exception in case behavior changes mock_driver.reset_mock() mock_driver.unplug_network.side_effect = Exception() - net.execute({self.db_amphora_mock.id: delta}) + net.execute({self.db_amphora_mock.id: delta}, self.load_balancer_mock) mock_driver.unplug_network.assert_called_once_with(COMPUTE_ID, 1) + # Do a test with a general exception in case behavior changes + delta = data_models.Delta(amphora_id=self.db_amphora_mock.id, + compute_id=self.db_amphora_mock.compute_id, + add_nics=[], + delete_nics=[_interface(1, port_id=12)], + add_subnets=[], + delete_subnets=[]).to_dict(recurse=True) + mock_driver.reset_mock() + mock_driver.delete_port.side_effect = Exception() + net.execute({self.db_amphora_mock.id: delta}, self.load_balancer_mock) + mock_driver.unplug_network.assert_called_once_with(COMPUTE_ID, 1) + mock_driver.delete_port.assert_called_once_with(12) + + mock_driver.unplug_network.reset_mock() + net.revert( + failure.Failure.from_exception(Exception('boom')), None, None) + mock_driver.unplug_network.assert_not_called() + + mock_driver.unplug_network.reset_mock() + net.revert(None, None, None) + mock_driver.unplug_network.assert_not_called() + @mock.patch('octavia.db.repositories.LoadBalancerRepository.get') @mock.patch('octavia.db.api.get_session', return_value=_session_mock) def test_plug_vip(self, mock_get_session, mock_get_lb, @@ -1005,7 +1581,8 @@ class TestNetworkTasks(base.TestCase): net = network_tasks.GetSubnetFromVIP() net.execute(self.load_balancer_mock) - mock_driver.get_subnet.assert_called_once_with(LB.vip.subnet_id) + mock_driver.get_subnet.assert_called_once_with( + SUBNET_ID) @mock.patch('octavia.db.repositories.AmphoraRepository.get') @mock.patch('octavia.db.repositories.LoadBalancerRepository.get') diff --git a/octavia/tests/unit/network/drivers/neutron/test_base.py b/octavia/tests/unit/network/drivers/neutron/test_base.py index eabd7a4690..ee6d859071 100644 --- a/octavia/tests/unit/network/drivers/neutron/test_base.py +++ b/octavia/tests/unit/network/drivers/neutron/test_base.py @@ -563,3 +563,144 @@ class TestBaseNeutronNetworkDriver(base.TestCase): self.assertEqual(t_constants.MOCK_NETWORK_ID, ip_avail.network_id) self.assertEqual(t_constants.MOCK_SUBNET_IP_AVAILABILITY, ip_avail.subnet_ip_availability) + + def test_plug_fixed_ip(self): + show_port = self.driver.neutron_client.show_port + show_port.return_value = { + 'id': t_constants.MOCK_PORT_ID, + 'fixed_ips': [ + { + 'subnet_id': t_constants.MOCK_SUBNET_ID, + 'ip_address': t_constants.MOCK_IP_ADDRESS, + 'subnet': None + }] + } + + self.driver.plug_fixed_ip(t_constants.MOCK_PORT_ID, + t_constants.MOCK_SUBNET_ID2, + t_constants.MOCK_IP_ADDRESS2) + + expected_body = { + 'port': { + 'fixed_ips': [ + { + 'subnet_id': t_constants.MOCK_SUBNET_ID, + 'ip_address': t_constants.MOCK_IP_ADDRESS, + 'subnet': None + }, { + 'subnet_id': t_constants.MOCK_SUBNET_ID2, + 'ip_address': t_constants.MOCK_IP_ADDRESS2 + } + ] + } + + } + self.driver.neutron_client.update_port.assert_called_once_with( + t_constants.MOCK_PORT_ID, + expected_body) + + def test_plug_fixed_ip_no_ip_address(self): + show_port = self.driver.neutron_client.show_port + show_port.return_value = { + 'id': t_constants.MOCK_PORT_ID, + 'fixed_ips': [ + { + 'subnet_id': t_constants.MOCK_SUBNET_ID, + 'ip_address': t_constants.MOCK_IP_ADDRESS, + 'subnet': None + }] + } + + self.driver.plug_fixed_ip(t_constants.MOCK_PORT_ID, + t_constants.MOCK_SUBNET_ID2) + + expected_body = { + 'port': { + 'fixed_ips': [ + { + 'subnet_id': t_constants.MOCK_SUBNET_ID, + 'ip_address': t_constants.MOCK_IP_ADDRESS, + 'subnet': None + }, { + 'subnet_id': t_constants.MOCK_SUBNET_ID2, + } + ] + } + + } + self.driver.neutron_client.update_port.assert_called_once_with( + t_constants.MOCK_PORT_ID, + expected_body) + + def test_plug_fixed_ip_exception(self): + show_port = self.driver.neutron_client.show_port + show_port.return_value = { + 'id': t_constants.MOCK_PORT_ID, + 'fixed_ips': [ + { + 'subnet_id': t_constants.MOCK_SUBNET_ID, + 'ip_address': t_constants.MOCK_IP_ADDRESS, + 'subnet': None + }] + } + + self.driver.neutron_client.update_port.side_effect = Exception + + self.assertRaises(network_base.NetworkException, + self.driver.plug_fixed_ip, + t_constants.MOCK_PORT_ID, + t_constants.MOCK_SUBNET_ID2) + + def test_unplug_fixed_ip(self): + show_port = self.driver.neutron_client.show_port + show_port.return_value = { + 'id': t_constants.MOCK_PORT_ID, + 'fixed_ips': [ + { + 'subnet_id': t_constants.MOCK_SUBNET_ID, + 'ip_address': t_constants.MOCK_IP_ADDRESS, + 'subnet': None + }, { + 'subnet_id': t_constants.MOCK_SUBNET_ID2, + 'ip_address': t_constants.MOCK_IP_ADDRESS2, + 'subnet': None + }] + } + + self.driver.unplug_fixed_ip(t_constants.MOCK_PORT_ID, + t_constants.MOCK_SUBNET_ID) + + expected_body = { + 'port': { + 'fixed_ips': [ + { + 'subnet_id': t_constants.MOCK_SUBNET_ID2, + 'ip_address': t_constants.MOCK_IP_ADDRESS2, + 'subnet': None + } + ] + } + + } + self.driver.neutron_client.update_port.assert_called_once_with( + t_constants.MOCK_PORT_ID, + expected_body) + + def test_unplug_fixed_ip_exception(self): + show_port = self.driver.neutron_client.show_port + show_port.return_value = { + 'id': t_constants.MOCK_PORT_ID, + 'fixed_ips': [ + { + 'subnet_id': t_constants.MOCK_SUBNET_ID, + 'ip_address': t_constants.MOCK_IP_ADDRESS, + 'subnet': None + }] + } + + self.driver.neutron_client.update_port.side_effect = Exception + + self.assertRaises(network_base.NetworkException, + self.driver.unplug_fixed_ip, + t_constants.MOCK_PORT_ID, + t_constants.MOCK_SUBNET_ID) diff --git a/octavia/tests/unit/network/drivers/noop_driver/test_driver.py b/octavia/tests/unit/network/drivers/noop_driver/test_driver.py index fde3301af1..960ed46677 100644 --- a/octavia/tests/unit/network/drivers/noop_driver/test_driver.py +++ b/octavia/tests/unit/network/drivers/noop_driver/test_driver.py @@ -28,6 +28,7 @@ class TestNoopNetworkDriver(base.TestCase): FAKE_UUID_4 = uuidutils.generate_uuid() FAKE_UUID_5 = uuidutils.generate_uuid() FAKE_UUID_6 = uuidutils.generate_uuid() + FAKE_UUID_7 = uuidutils.generate_uuid() def setUp(self): super().setUp() @@ -49,6 +50,7 @@ class TestNoopNetworkDriver(base.TestCase): self.vip.port_id = uuidutils.generate_uuid() self.amphora_id = self.FAKE_UUID_1 self.compute_id = self.FAKE_UUID_2 + self.compute2_id = self.FAKE_UUID_2 self.subnet_id = self.FAKE_UUID_3 self.subnet_name = 'subnet1' self.qos_policy_id = self.FAKE_UUID_5 @@ -56,12 +58,14 @@ class TestNoopNetworkDriver(base.TestCase): self.amphora1 = models.Amphora() self.amphora1.id = uuidutils.generate_uuid() + self.amphora1.compute_id = self.compute_id self.amphora1.vrrp_port_id = uuidutils.generate_uuid() self.amphora1.ha_port_id = uuidutils.generate_uuid() self.amphora1.vrrp_ip = '10.0.1.10' self.amphora1.ha_ip = '10.0.1.11' self.amphora2 = models.Amphora() self.amphora2.id = uuidutils.generate_uuid() + self.amphora2.compute_id = self.compute2_id self.amphora2.vrrp_port_id = uuidutils.generate_uuid() self.amphora2.ha_port_id = uuidutils.generate_uuid() self.amphora2.vrrp_ip = '10.0.2.10' @@ -69,6 +73,7 @@ class TestNoopNetworkDriver(base.TestCase): self.load_balancer.amphorae = [self.amphora1, self.amphora2] self.load_balancer.vip = self.vip self.subnet = mock.MagicMock() + self.subnet.id = self.subnet_id def test_allocate_vip(self): self.driver.allocate_vip(self.load_balancer) @@ -105,28 +110,54 @@ class TestNoopNetworkDriver(base.TestCase): self.load_balancer.id, self.vip.ip_address)]) def test_plug_network(self): - self.driver.plug_network(self.amphora_id, self.network_id, - self.ip_address) - self.assertEqual((self.amphora_id, self.network_id, self.ip_address, + self.driver.plug_network(self.compute_id, self.network_id, + self.subnet_id) + self.assertEqual((self.compute_id, self.network_id, self.subnet_id, 'plug_network'), self.driver.driver.networkconfigconfig[( - self.amphora_id, self.network_id, - self.ip_address)]) + self.compute_id, self.network_id)]) def test_unplug_network(self): - self.driver.unplug_network(self.amphora_id, self.network_id, - ip_address=self.ip_address) - self.assertEqual((self.amphora_id, self.network_id, self.ip_address, - 'unplug_network'), + self.driver.unplug_network(self.compute_id, self.network_id) + self.assertEqual((self.compute_id, self.network_id, 'unplug_network'), self.driver.driver.networkconfigconfig[( - self.amphora_id, self.network_id, - self.ip_address)]) + self.compute_id, self.network_id)]) def test_get_plugged_networks(self): - self.driver.get_plugged_networks(self.amphora_id) - self.assertEqual((self.amphora_id, 'get_plugged_networks'), + self.driver.get_plugged_networks(self.compute_id) + self.assertEqual((self.compute_id, 'get_plugged_networks'), self.driver.driver.networkconfigconfig[( - self.amphora_id)]) + self.compute_id)]) + + def test_plug_unplug_and_get_plugged_networks(self): + amphora = mock.MagicMock() + amphora.compute_id = uuidutils.generate_uuid() + network = self.driver.plug_network(amphora.compute_id, + self.network_id, + self.subnet_id) + self.assertEqual( + network, + network_models.Interface( + id=mock.ANY, + compute_id=amphora.compute_id, + network_id=self.network_id, + fixed_ips=[], + port_id=mock.ANY + )) + networks = self.driver.get_plugged_networks(amphora.compute_id) + self.assertEqual( + networks, + [network_models.Interface( + id=mock.ANY, + compute_id=amphora.compute_id, + network_id=self.network_id, + fixed_ips=[], + port_id=mock.ANY + )]) + self.driver.unplug_network(amphora.compute_id, + self.network_id) + networks = self.driver.get_plugged_networks(amphora.compute_id) + self.assertEqual([], networks) def test_update_vip(self): self.driver.update_vip(self.load_balancer) @@ -293,3 +324,12 @@ class TestNoopNetworkDriver(base.TestCase): self.assertEqual(SUBNET_ID, result.fixed_ips[1].subnet_id) self.assertEqual(QOS_POLICY_ID, result.qos_policy_id) self.assertFalse(result.admin_state_up) + + def test_plug_fixed_ip(self): + self.driver.plug_fixed_ip(self.port_id, self.subnet_id, + self.ip_address) + self.assertEqual( + (self.port_id, self.subnet_id, self.ip_address, 'plug_fixed_ip'), + self.driver.driver.networkconfigconfig[ + self.port_id, self.subnet_id] + ) diff --git a/releasenotes/notes/fix-plugging-member-subnets-8560cd9403ff79a7.yaml b/releasenotes/notes/fix-plugging-member-subnets-8560cd9403ff79a7.yaml new file mode 100644 index 0000000000..fb5647a523 --- /dev/null +++ b/releasenotes/notes/fix-plugging-member-subnets-8560cd9403ff79a7.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fix a bug when adding a member on a subnet that belongs to a network with + multiple subnets, an incorrect subnet may have been plugged in the amphora. + - | + Fix a bug when deleting the last member plugged on a network, the port that + was no longer used was not deleted.