From 8f044e69d8abb4a88c180c1905fd493ad0b94688 Mon Sep 17 00:00:00 2001 From: Terry Wilson Date: Fri, 9 Jun 2017 10:28:45 -0500 Subject: [PATCH] Add OVN_Northbound API LR, LRP, and LB commands This adds the remaining ovn-nbctl functionality. Change-Id: I8fcced2da16af6503348a87c69f3df990a52fc0c --- ovsdbapp/backend/ovs_idl/__init__.py | 3 + ovsdbapp/constants.py | 9 + ovsdbapp/schema/ovn_northbound/api.py | 299 +++++++++ ovsdbapp/schema/ovn_northbound/commands.py | 538 ++++++++++++++++- ovsdbapp/schema/ovn_northbound/impl_idl.py | 82 +++ .../schema/ovn_northbound/fixtures.py | 6 + .../schema/ovn_northbound/test_impl_idl.py | 569 ++++++++++++++++++ ovsdbapp/tests/unit/test_utils.py | 53 ++ ovsdbapp/utils.py | 43 ++ 9 files changed, 1587 insertions(+), 15 deletions(-) create mode 100644 ovsdbapp/tests/unit/test_utils.py create mode 100644 ovsdbapp/utils.py diff --git a/ovsdbapp/backend/ovs_idl/__init__.py b/ovsdbapp/backend/ovs_idl/__init__.py index 2c13f337..4bf7af67 100644 --- a/ovsdbapp/backend/ovs_idl/__init__.py +++ b/ovsdbapp/backend/ovs_idl/__init__.py @@ -70,6 +70,9 @@ class Backend(object): raise def _lookup(self, table, record): + if record == "": + raise TypeError("Cannot look up record by empty string") + t = self.tables[table] try: if isinstance(record, uuid.UUID): diff --git a/ovsdbapp/constants.py b/ovsdbapp/constants.py index 82c0df54..f3396f2c 100644 --- a/ovsdbapp/constants.py +++ b/ovsdbapp/constants.py @@ -17,4 +17,13 @@ DEFAULT_OVNNB_CONNECTION = 'tcp:127.0.0.1:6641' DEFAULT_TIMEOUT = 5 DEVICE_NAME_MAX_LEN = 14 + ACL_PRIORITY_MAX = 32767 + +NAT_SNAT = 'snat' +NAT_DNAT = 'dnat' +NAT_BOTH = 'dnat_and_snat' +NAT_TYPES = (NAT_SNAT, NAT_DNAT, NAT_BOTH) + +PROTO_TCP = 'tcp' +PROTO_UDP = 'udp' diff --git a/ovsdbapp/schema/ovn_northbound/api.py b/ovsdbapp/schema/ovn_northbound/api.py index 863728e7..2eac60f8 100644 --- a/ovsdbapp/schema/ovn_northbound/api.py +++ b/ovsdbapp/schema/ovn_northbound/api.py @@ -15,6 +15,7 @@ import abc import six from ovsdbapp import api +from ovsdbapp import constants as const @six.add_metaclass(abc.ABCMeta) @@ -290,6 +291,304 @@ class API(api.API): :returns: :class:`Command` with no result """ + @abc.abstractmethod + def lr_add(self, router=None, may_exist=False, **columns): + """Create a logical router named `router` + + :param router: The optional name or uuid of the router + :type router: string or uuid.UUID + :param may_exist: If True, don't fail if the router already exists + :type may_exist: boolean + :param **columns: Additional columns to directly set on the router + :returns: :class:`Command` with RowView result + """ + + @abc.abstractmethod + def lr_del(self, router, if_exists=False): + """Delete 'router' and all its ports + + :param router: The name or uuid of the router + :type router: string or uuid.UUID + :param if_exists: If True, don't fail if the router doesn't exist + :type if_exists: boolean + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def lr_list(self): + """Get the UUIDs of all logical routers + + :returns: :class:`Command` with RowView list result + """ + + @abc.abstractmethod + def lrp_add(self, router, port, mac, networks, peer=None, may_exist=False, + **columns): + """Add logical port 'port' on 'router' + + :param router: The name or uuid of the router to attach the port + :type router: string or uuid.UUID + :param mac: The MAC address of the port + :type mac: string + :param networks: One or more IP address/netmask to assign to the port + :type networks: list of strings + :param peer: Optional logical router port connected to this one + :param may_exist: If True, don't fail if the port already exists + :type may_exist: boolean + :param **columns: Additional column values to directly set on the port + :returns: :class:`Command` with RowView result + """ + + @abc.abstractmethod + def lrp_del(self, port, router=None, if_exists=None): + """Delete 'port' from its attached router + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :param router: Only delete router if attached to `router` + :type router: string or uuiwhd.UUID + :param if_exists: If True, don't fail if the port doesn't exist + :type if_exists: boolean + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def lrp_list(self, router): + """Get the UUIDs of all ports on 'router' + + :param router: The name or uuid of the router + :type router: string or uuid.UUID + :returns: :class:`Command` with RowView list result + """ + + @abc.abstractmethod + def lrp_set_enabled(self, port, is_enabled): + """Set administrative state of 'port' + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :param is_enabled: True for enabled, False for disabled + :type is_enabled: boolean + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def lrp_get_enabled(self, port): + """Get administrative state of 'port' + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :returns: + """ + + @abc.abstractmethod + def lr_route_add(self, router, prefix, nexthop, port=None, + policy='dst-ip', may_exist=False): + """Add a route to 'router' + + :param router: The name or uuid of the router + :type router: string or uuid.UUID + :param prefix: an IPv4/6 prefix for this route, e.g. 192.168.1.0/24 + :type prefix: type string + :parm nexthop: The gateway to use for this route, which should be + the IP address of one of `router`'s logical router + ports or the IP address of a logical port + :type nexthop: string + :param port: If specified, packets that match this route will be + sent out this port. Otherwise OVN infers the output + port based on nexthop. + :type port: string + :param policy: the policy used to make routing decisions + :type policy: string, 'dst-ip' or 'src-ip' + :param may_exist: If True, don't fail if the route already exists + :type may_exist: boolean + returns: :class:`Command` with RowView result + """ + + @abc.abstractmethod + def lr_route_del(self, router, prefix=None, if_exists=False): + """Remove routes from 'router' + + :param router: The name or uuid of the router + :type router: string or uuid.UUID + :param prefix: an IPv4/6 prefix to match, e.g. 192.168.1.0/24 + :type prefix: type string + :param if_exists: If True, don't fail if the port doesn't exist + :type if_exists: boolean + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def lr_route_list(self, router): + """Get the UUIDs of static logical routes from 'router' + + :param router: The name or uuid of the router + :type router: string or uuid.UUID + :returns: :class:`Command` with RowView list result + """ + + @abc.abstractmethod + def lr_nat_add(self, router, nat_type, external_ip, logical_ip, + logical_port=None, external_mac=None, may_exist=False): + """Add a NAT to 'router' + + :param router: The name or uuid of the router + :type router: string or uuid.UUID + :param nat_type: The type of NAT to be done + :type nat_type: NAT_SNAT, NAT_DNAT, or NAT_BOTH + :param external_ip: Externally visible Ipv4 address + :type external_ip: string + :param logical_ip: The logical IPv4 network or address with which + `external_ip` is NATted + :type logical_ip: string + :param logical_port: The name of an existing logical switch port where + the logical_ip resides + :type logical_port: string + :param external_mac: ARP replies for the external_ip return the value + of `external_mac`. Packets transmitted with + source IP address equal to `external_ip` will be + sent using `external_mac`. + :type external_mac: string + :param may_exist: If True, don't fail if the route already exists + and if `logical_port` and `external_mac` are + specified, they will be updated + :type may_exist: boolean + :returns: :class:`Command` with RowView result + """ + + @abc.abstractmethod + def lr_nat_del(self, router, nat_type=None, match_ip=None, if_exists=None): + """Remove NATs from 'router' + + :param router: The name or uuid of the router + :type router: string or uuid.UUID + :param nat_type: The type of NAT to match + :type nat_type: NAT_SNAT, NAT_DNAT, or NAT_BOTH + :param match_ip: The IPv4 address to match on. If + `nat_type` is specified and is NAT_SNAT, the IP + should be the logical ip, otherwise the IP should + be the external IP. + :type match_ip: string + :param if_exists: If True, don't fail if the port doesn't exist + :type if_exists: boolean + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def lr_nat_list(self, router): + """Get the NATs on 'router' + + :param router: The name or uuid of the router + :type router: string or uuid.UUID + :returns: :class:`Command` with RowView list result + """ + + @abc.abstractmethod + def lb_add(self, vip, ips, protocol=const.PROTO_TCP, may_exist=False): + """Create a load-balancer or add a VIP to an existing load balancer + + :param lb: The name or uuid of the load-balancer + :type lb: string or uuid.UUID + :param vip: A virtual IP in the format IP[:PORT] + :type vip: string + :param ips: A list of ips in the form IP[:PORT] + :type ips: string + :param protocol: The IP protocol for load balancing + :type protocol: PROTO_TCP or PROTO_UDP + :param may_exist: If True, don't fail if a LB w/ `vip` exists, and + instead, replace the vips on the LB + :type may_exist: boolean + :returns: :class:`Command` with RowView result + """ + + @abc.abstractmethod + def lb_del(self, lb, vip=None, if_exists=False): + """Remove a load balancer or just the VIP from a load balancer + + :param lb: The name or uuid of a load balancer + :type lb: string or uuid.UUID + :param vip: The VIP on the load balancer to match + :type: string + :param if_exists: If True, don't fail if the port doesn't exist + :type if_exists: boolean + """ + + @abc.abstractmethod + def lb_list(self): + """Get the UUIDs of all load balanacers""" + + @abc.abstractmethod + def lr_lb_add(self, router, lb, may_exist=False): + """Add a load-balancer to 'router' + + :param router: The name or uuid of the router + :type router: string or uuid.UUID + :param lb: The name or uuid of the load balancer + :type lb: string or uuid.UUID + :param may_exist: If True, don't fail if lb already assigned to lr + :type may_exist: boolean + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def lr_lb_del(self, router, lb=None, if_exists=False): + """Remove load-balancers from 'router' + + :param router: The name or uuid of the router + :type router: string or uuid.UUID + :param lb: The name or uuid of the load balancer to remove. None + to remove all load balancers from the router + :type lb: string or uuid.UUID + :type if_exists: If True, don't fail if the switch doesn't exist + :type if_exists: boolean + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def lr_lb_list(self, router): + """Get UUIDs of load-balancers on 'router' + + :param router: The name or uuid of the router + :type router: string or uuid.UUID + :returns: :class:`Command` with RowView list result + """ + + @abc.abstractmethod + def ls_lb_add(self, switch, lb, may_exist=False): + """Add a load-balancer to 'switch' + + :param switch: The name or uuid of the switch + :type switch: string or uuid.UUID + :param lb: The name or uuid of the load balancer + :type lb: string or uuid.UUID + :param may_exist: If True, don't fail if lb already assigned to lr + :type may_exist: boolean + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def ls_lb_del(self, switch, lb=None, if_exists=False): + """Remove load-balancers from 'switch' + + :param switch: The name or uuid of the switch + :type switch: string or uuid.UUID + :param lb: The name or uuid of the load balancer to remove. None + to remove all load balancers from the switch + :type lb: string or uuid.UUID + :type if_exists: If True, don't fail if the switch doesn't exist + :type if_exists: boolean + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def ls_lb_list(self, switch): + """Get UUIDs of load-balancers on 'switch' + + :param switch: The name or uuid of the switch + :type switch: string or uuid.UUID + :returns: :class:`Command` with RowView list result + """ + @abc.abstractmethod def dhcp_options_add(self, cidr, **external_ids): """Create a DHCP options row with CIDR diff --git a/ovsdbapp/schema/ovn_northbound/commands.py b/ovsdbapp/schema/ovn_northbound/commands.py index 6abce976..2aef8aba 100644 --- a/ovsdbapp/schema/ovn_northbound/commands.py +++ b/ovsdbapp/schema/ovn_northbound/commands.py @@ -17,6 +17,7 @@ from ovsdbapp.backend import ovs_idl from ovsdbapp.backend.ovs_idl import command as cmd from ovsdbapp.backend.ovs_idl import idlutils from ovsdbapp import constants as const +from ovsdbapp import utils class AddCommand(cmd.BaseCommand): @@ -226,33 +227,43 @@ class LspAddCommand(AddCommand): self.result = lsp.uuid -class LspDelCommand(cmd.BaseCommand): - def __init__(self, api, port, switch=None, if_exists=False): - super(LspDelCommand, self).__init__(api) +class PortDelCommand(cmd.BaseCommand): + def __init__(self, api, table, port, parent_table, parent=None, + if_exists=False): + super(PortDelCommand, self).__init__(api) + self.table = table self.port = port - self.switch = switch + self.parent_table = parent_table + self.parent = parent self.if_exists = if_exists def run_idl(self, txn): try: - lsp = self.api.lookup('Logical_Switch_Port', self.port) + row = self.api.lookup(self.table, self.port) except idlutils.RowNotFound: if self.if_exists: return raise RuntimeError("%s does not exist" % self.port) - # We need to delete the port from its switch - if self.switch: - sw = self.api.lookup('Logical_Switch', self.switch) + # We need to delete the port from its parent + if self.parent: + parent = self.api.lookup(self.parent_table, self.parent) else: - sw = next(iter( - s for s in self.api.tables['Logical_Switch'].rows.values() - if lsp in s.ports), None) - if not (sw and lsp in sw.ports): + parent = next(iter( + p for p in self.api.tables[self.parent_table].rows.values() + if row in p.ports), None) + if not (parent and row in parent.ports): raise RuntimeError("%s does not exist in %s" % ( - self.port, self.switch)) - sw.delvalue('ports', lsp) - lsp.delete() + self.port, self.parent)) + parent.delvalue('ports', row) + row.delete() + + +class LspDelCommand(PortDelCommand): + def __init__(self, api, port, switch=None, if_exists=False): + super(LspDelCommand, self).__init__( + api, 'Logical_Switch_Port', port, 'Logical_Switch', switch, + if_exists) class LspListCommand(cmd.BaseCommand): @@ -484,3 +495,500 @@ class DhcpOptionsGetOptionsCommand(cmd.BaseCommand): def run_idl(self, txn): dhcpopt = self.api.lookup('DHCP_Options', self.dhcpopt_uuid) self.result = dhcpopt.options + + +class LrAddCommand(cmd.BaseCommand): + def __init__(self, api, router=None, may_exist=False, **columns): + super(LrAddCommand, self).__init__(api) + self.router = router + self.may_exist = may_exist + self.columns = columns + + def run_idl(self, txn): + if self.router: + try: + lr = self.api.lookup('Logical_Router', self.router) + if self.may_exist: + self.result = ovs_idl.RowView(lr) + return + except idlutils.RowNotFound: + pass + lr = txn.insert(self.api.tables['Logical_Router']) + lr.name = self.router if self.router else "" + for col, value in self.columns.items(): + setattr(lr, col, value) + self.result = lr.uuid + + def post_commit(self, txn): + real_uuid = txn.get_insert_uuid(self.result) + if real_uuid: + row = self.api.tables['Logical_Router'].rows[real_uuid] + self.result = ovs_idl.RowView(row) + + +class LrDelCommand(cmd.BaseCommand): + def __init__(self, api, router, if_exists=False): + super(LrDelCommand, self).__init__(api) + self.router = router + self.if_exists = if_exists + + def run_idl(self, txn): + try: + lr = self.api.lookup('Logical_Router', self.router) + lr.delete() + except idlutils.RowNotFound: + if self.if_exists: + return + msg = "Logical Router %s does not exist" % self.router + raise RuntimeError(msg) + + +class LrListCommand(cmd.BaseCommand): + def run_idl(self, txn): + self.result = [ovs_idl.RowView(r) for + r in self.api.tables['Logical_Router'].rows.values()] + + +class LrpAddCommand(cmd.BaseCommand): + def __init__(self, api, router, port, mac, networks, + peer=None, may_exist=False, **columns): + self.mac = str(netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded)) + self.networks = [str(netaddr.IPNetwork(net)) for net in networks] + self.router = router + self.port = port + self.peer = peer + self.may_exist = may_exist + self.columns = columns + super(LrpAddCommand, self).__init__(api) + + def run_idl(self, txn): + lr = self.api.lookup('Logical_Router', self.router) + try: + lrp = self.api.lookup('Logical_Router_Port', self.port) + if self.may_exist: + msg = None + if lrp not in lr.ports: + msg = "Port %s exists, but is not in router %s" % ( + self.port, self.router) + elif netaddr.EUI(lrp.mac) != netaddr.EUI(self.mac): + msg = "Port %s exists with different mac" % (self.port) + elif set(self.networks) != set(lrp.networks): + msg = "Port %s exists with different networks" % ( + self.port) + elif (not self.peer) != (not lrp.peer) or ( + self.peer != lrp.peer): + msg = "Port %s exists with different peer" % (self.port) + if msg: + raise RuntimeError(msg) + self.result = ovs_idl.RowView(lrp) + return + except idlutils.RowNotFound: + pass + lrp = txn.insert(self.api.tables['Logical_Router_Port']) + # This is what ovn-nbctl does, though the lookup is by uuid or name + lrp.name = self.port + lrp.mac = self.mac + lrp.networks = self.networks + if self.peer: + lrp.peer = self.peer + lr.addvalue('ports', lrp) + for col, value in self.columns.items(): + setattr(lrp, col, value) + self.result = lrp.uuid + + def post_commit(self, txn): + real_uuid = txn.get_insert_uuid(self.result) + if real_uuid: + row = self.api.tables['Logical_Router_Port'].rows[real_uuid] + self.result = ovs_idl.RowView(row) + + +class LrpDelCommand(PortDelCommand): + def __init__(self, api, port, router=None, if_exists=False): + super(LrpDelCommand, self).__init__( + api, 'Logical_Router_Port', port, 'Logical_Router', router, + if_exists) + + +class LrpListCommand(cmd.BaseCommand): + def __init__(self, api, router): + super(LrpListCommand, self).__init__(api) + self.router = router + + def run_idl(self, txn): + router = self.api.lookup('Logical_Router', self.router) + self.result = [ovs_idl.RowView(r) for r in router.ports] + + +class LrpSetEnabledCommand(cmd.BaseCommand): + def __init__(self, api, port, is_enabled): + super(LrpSetEnabledCommand, self).__init__(api) + self.port = port + self.is_enabled = is_enabled + + def run_idl(self, txn): + lrp = self.api.lookup('Logical_Router_Port', self.port) + lrp.enabled = self.is_enabled + + +class LrpGetEnabledCommand(cmd.BaseCommand): + def __init__(self, api, port): + super(LrpGetEnabledCommand, self).__init__(api) + self.port = port + + def run_idl(self, txn): + lrp = self.api.lookup('Logical_Router_Port', self.port) + # enabled is optional, but if not disabled then enabled + self.result = next(iter(lrp.enabled), True) + + +class LrRouteAddCommand(cmd.BaseCommand): + def __init__(self, api, router, prefix, nexthop, port=None, + policy='dst-ip', may_exist=False): + prefix = str(netaddr.IPNetwork(prefix)) + nexthop = str(netaddr.IPAddress(nexthop)) + super(LrRouteAddCommand, self).__init__(api) + self.router = router + self.prefix = prefix + self.nexthop = nexthop + self.port = port + self.policy = policy + self.may_exist = may_exist + + def run_idl(self, txn): + lr = self.api.lookup('Logical_Router', self.router) + for route in lr.static_routes: + if self.prefix == route.ip_prefix: + if not self.may_exist: + msg = "Route %s already exists on router %s" % ( + self.prefix, self.router) + raise RuntimeError(msg) + route.nexthop = self.nexthop + route.policy = self.policy + if self.port: + route.port = self.port + self.result = ovs_idl.RowView(route) + return + route = txn.insert(self.api.tables['Logical_Router_Static_Route']) + route.ip_prefix = self.prefix + route.nexthop = self.nexthop + route.policy = self.policy + if self.port: + route.port = self.port + lr.addvalue('static_routes', route) + self.result = route.uuid + + def post_commit(self, txn): + real_uuid = txn.get_insert_uuid(self.result) + if real_uuid: + table = self.api.tables['Logical_Router_Static_Route'] + row = table.rows[real_uuid] + self.result = ovs_idl.RowView(row) + + +class LrRouteDelCommand(cmd.BaseCommand): + def __init__(self, api, router, prefix=None, if_exists=False): + if prefix is not None: + prefix = str(netaddr.IPNetwork(prefix)) + super(LrRouteDelCommand, self).__init__(api) + self.router = router + self.prefix = prefix + self.if_exists = if_exists + + def run_idl(self, txn): + lr = self.api.lookup('Logical_Router', self.router) + if not self.prefix: + lr.static_routes = [] + return + for route in lr.static_routes: + if self.prefix == route.ip_prefix: + lr.delvalue('static_routes', route) + # There should only be one possible match + return + + if not self.if_exists: + msg = "Route for %s in router %s does not exist" % ( + self.prefix, self.router) + raise RuntimeError(msg) + + +class LrRouteListCommand(cmd.BaseCommand): + def __init__(self, api, router): + super(LrRouteListCommand, self).__init__(api) + self.router = router + + def run_idl(self, txn): + lr = self.api.lookup('Logical_Router', self.router) + self.result = [ovs_idl.RowView(r) for r in lr.static_routes] + + +class LrNatAddCommand(cmd.BaseCommand): + def __init__(self, api, router, nat_type, external_ip, logical_ip, + logical_port=None, external_mac=None, may_exist=False): + if nat_type not in const.NAT_TYPES: + raise TypeError("nat_type not in %s" % str(const.NAT_TYPES)) + external_ip = str(netaddr.IPAddress(external_ip)) + if nat_type == const.NAT_DNAT: + logical_ip = str(netaddr.IPAddress(logical_ip)) + else: + net = netaddr.IPNetwork(logical_ip) + logical_ip = str(net.ip if net.prefixlen == 32 else net) + if (logical_port is None) != (external_mac is None): + msg = "logical_port and external_mac must be passed together" + raise TypeError(msg) + if logical_port and nat_type != const.NAT_BOTH: + msg = "logical_port/external_mac only valid for %s" % ( + const.NAT_BOTH,) + raise TypeError(msg) + if external_mac: + external_mac = str( + netaddr.EUI(external_mac, dialect=netaddr.mac_unix_expanded)) + super(LrNatAddCommand, self).__init__(api) + self.router = router + self.nat_type = nat_type + self.external_ip = external_ip + self.logical_ip = logical_ip + self.logical_port = logical_port or [] + self.external_mac = external_mac or [] + self.may_exist = may_exist + + def run_idl(self, txn): + lr = self.api.lookup('Logical_Router', self.router) + if self.logical_port: + lp = self.api.lookup('Logical_Switch_Port', self.logical_port) + for nat in lr.nat: + if ((self.nat_type, self.external_ip, self.logical_ip) == + (nat.type, nat.external_ip, nat.logical_ip)): + if self.may_exist: + nat.logical_port = self.logical_port + nat.external_mac = self.external_mac + self.result = ovs_idl.RowView(nat) + return + raise RuntimeError("NAT already exists") + nat = txn.insert(self.api.tables['NAT']) + nat.type = self.nat_type + nat.external_ip = self.external_ip + nat.logical_ip = self.logical_ip + if self.logical_port: + # It seems kind of weird that ovn uses a name string instead of + # a ref to a LSP, especially when ovn-nbctl looks the value up by + # either name or uuid (and discards the result and store the name). + nat.logical_port = lp.name + nat.external_mac = self.external_mac + lr.addvalue('nat', nat) + self.result = nat.uuid + + def post_commit(self, txn): + real_uuid = txn.get_insert_uuid(self.result) + if real_uuid: + row = self.api.tables['NAT'].rows[real_uuid] + self.result = ovs_idl.RowView(row) + + +class LrNatDelCommand(cmd.BaseCommand): + def __init__(self, api, router, nat_type=None, match_ip=None, + if_exists=False): + super(LrNatDelCommand, self).__init__(api) + self.conditions = [] + if nat_type: + if nat_type not in const.NAT_TYPES: + raise TypeError("nat_type not in %s" % str(const.NAT_TYPES)) + self.conditions += [('type', '=', nat_type)] + if match_ip: + match_ip = str(netaddr.IPAddress(match_ip)) + self.col = ('logical_ip' if nat_type == const.NAT_SNAT + else 'external_ip') + self.conditions += [(self.col, '=', match_ip)] + elif match_ip: + raise TypeError("must specify nat_type with match_ip") + self.router = router + self.nat_type = nat_type + self.match_ip = match_ip + self.if_exists = if_exists + + def run_idl(self, txn): + lr = self.api.lookup('Logical_Router', self.router) + found = False + for nat in [r for r in lr.nat + if idlutils.row_match(r, self.conditions)]: + found = True + lr.delvalue('nat', nat) + nat.delete() + if self.match_ip: + break + if self.match_ip and not (found or self.if_exists): + raise idlutils.RowNotFound(table='NAT', col=self.col, + match=self.match_ip) + + +class LrNatListCommand(cmd.BaseCommand): + def __init__(self, api, router): + super(LrNatListCommand, self).__init__(api) + self.router = router + + def run_idl(self, txn): + lr = self.api.lookup('Logical_Router', self.router) + self.result = [ovs_idl.RowView(r) for r in lr.nat] + + +class LbAddCommand(cmd.BaseCommand): + def __init__(self, api, lb, vip, ips, protocol=const.PROTO_TCP, + may_exist=False, **columns): + super(LbAddCommand, self).__init__(api) + self.lb = lb + self.vip = utils.normalize_ip_port(vip) + self.ips = ",".join(utils.normalize_ip(ip) for ip in ips) + self.protocol = protocol + self.may_exist = may_exist + self.columns = columns + + def run_idl(self, txn): + try: + lb = self.api.lookup('Load_Balancer', self.lb) + if lb.vips.get(self.vip): + if not self.may_exist: + raise RuntimeError("Load Balancer %s exists" % lb.name) + # Update load balancer vip + lb.setkey('vips', self.vip, self.ips) + lb.protocol = self.protocol + except idlutils.RowNotFound: + # New load balancer + lb = txn.insert(self.api.tables['Load_Balancer']) + lb.name = self.lb + lb.protocol = self.protocol + lb.vips = {self.vip: self.ips} + for col, val in self.columns.items(): + setattr(lb, col, val) + + self.result = lb.uuid + + def post_commit(self, txn): + real_uuid = txn.get_insert_uuid(self.result) or self.result + row = self.api.tables['Load_Balancer'].rows[real_uuid] + self.result = ovs_idl.RowView(row) + + +class LbDelCommand(cmd.BaseCommand): + def __init__(self, api, lb, vip=None, if_exists=False): + super(LbDelCommand, self).__init__(api) + self.lb = lb + self.vip = utils.normalize_ip_port(vip) if vip else vip + self.if_exists = if_exists + + def run_idl(self, txn): + try: + lb = self.api.lookup('Load_Balancer', self.lb) + if self.vip: + if self.vip in lb.vips: + if self.if_exists: + return + lb.delkey('vips', self.vip) + else: + lb.delete() + except idlutils.RowNotFound: + if not self.if_exists: + raise + + +class LbListCommand(cmd.BaseCommand): + def run_idl(self, txn): + self.result = [ovs_idl.RowView(r) + for r in self.api.tables['Load_Balancer'].rows.values()] + + +class LrLbAddCommand(cmd.BaseCommand): + def __init__(self, api, router, lb, may_exist=False): + super(LrLbAddCommand, self).__init__(api) + self.router = router + self.lb = lb + self.may_exist = may_exist + + def run_idl(self, txn): + lr = self.api.lookup('Logical_Router', self.router) + lb = self.api.lookup('Load_Balancer', self.lb) + if lb in lr.load_balancer: + if self.may_exist: + return + raise RuntimeError("LB %s already exist in router %s" % ( + lb.uuid, lr.uuid)) + lr.addvalue('load_balancer', lb) + + +class LrLbDelCommand(cmd.BaseCommand): + def __init__(self, api, router, lb=None, if_exists=False): + super(LrLbDelCommand, self).__init__(api) + self.router = router + self.lb = lb + self.if_exists = if_exists + + def run_idl(self, txn): + lr = self.api.lookup('Logical_Router', self.router) + if not self.lb: + lr.load_balancer = [] + return + try: + lb = self.api.lookup('Load_Balancer', self.lb) + lr.delvalue('load_balancer', lb) + except idlutils.RowNotFound: + if self.if_exists: + return + raise + + +class LrLbListCommand(cmd.BaseCommand): + def __init__(self, api, router): + super(LrLbListCommand, self).__init__(api) + self.router = router + + def run_idl(self, txn): + lr = self.api.lookup('Logical_Router', self.router) + self.result = [ovs_idl.RowView(r) for r in lr.load_balancer] + + +class LsLbAddCommand(cmd.BaseCommand): + def __init__(self, api, switch, lb, may_exist=False): + super(LsLbAddCommand, self).__init__(api) + self.switch = switch + self.lb = lb + self.may_exist = may_exist + + def run_idl(self, txn): + ls = self.api.lookup('Logical_Switch', self.switch) + lb = self.api.lookup('Load_Balancer', self.lb) + if lb in ls.load_balancer: + if self.may_exist: + return + raise RuntimeError("LB %s alseady exist in switch %s" % ( + lb.uuid, ls.uuid)) + ls.addvalue('load_balancer', lb) + + +class LsLbDelCommand(cmd.BaseCommand): + def __init__(self, api, switch, lb=None, if_exists=False): + super(LsLbDelCommand, self).__init__(api) + self.switch = switch + self.lb = lb + self.if_exists = if_exists + + def run_idl(self, txn): + ls = self.api.lookup('Logical_Switch', self.switch) + if not self.lb: + ls.load_balancer = [] + return + try: + lb = self.api.lookup('Load_Balancer', self.lb) + ls.delvalue('load_balancer', lb) + except idlutils.RowNotFound: + if self.if_exists: + return + raise + + +class LsLbListCommand(cmd.BaseCommand): + def __init__(self, api, switch): + super(LsLbListCommand, self).__init__(api) + self.switch = switch + + def run_idl(self, txn): + ls = self.api.lookup('Logical_Switch', self.switch) + self.result = [ovs_idl.RowView(r) for r in ls.load_balancer] diff --git a/ovsdbapp/schema/ovn_northbound/impl_idl.py b/ovsdbapp/schema/ovn_northbound/impl_idl.py index 48d6b547..f18deda9 100644 --- a/ovsdbapp/schema/ovn_northbound/impl_idl.py +++ b/ovsdbapp/schema/ovn_northbound/impl_idl.py @@ -15,6 +15,7 @@ import logging from ovsdbapp.backend import ovs_idl from ovsdbapp.backend.ovs_idl import idlutils from ovsdbapp.backend.ovs_idl import transaction +from ovsdbapp import constants as const from ovsdbapp import exceptions from ovsdbapp.schema.ovn_northbound import api from ovsdbapp.schema.ovn_northbound import commands as cmd @@ -27,6 +28,8 @@ class OvnNbApiIdlImpl(ovs_idl.Backend, api.API): ovsdb_connection = None lookup_table = { 'Logical_Switch': idlutils.RowLookup('Logical_Switch', 'name', None), + 'Logical_Router': idlutils.RowLookup('Logical_Router', 'name', None), + 'Load_Balancer': idlutils.RowLookup('Load_Balancer', 'name', None), } def __init__(self, connection): @@ -136,6 +139,85 @@ class OvnNbApiIdlImpl(ovs_idl.Backend, api.API): def lsp_get_dhcpv4_options(self, port): return cmd.LspGetDhcpV4OptionsCommand(self, port) + def lr_add(self, router=None, may_exist=False, **columns): + return cmd.LrAddCommand(self, router, may_exist, **columns) + + def lr_del(self, router, if_exists=False): + return cmd.LrDelCommand(self, router, if_exists) + + def lr_list(self): + return cmd.LrListCommand(self) + + def lrp_add(self, router, port, mac, networks, peer=None, may_exist=False, + **columns): + return cmd.LrpAddCommand(self, router, port, mac, networks, + peer, may_exist, **columns) + + def lrp_del(self, port, router=None, if_exists=False): + return cmd.LrpDelCommand(self, port, router, if_exists) + + def lrp_list(self, router): + return cmd.LrpListCommand(self, router) + + def lrp_set_enabled(self, port, is_enabled): + return cmd.LrpSetEnabledCommand(self, port, is_enabled) + + def lrp_get_enabled(self, port): + return cmd.LrpGetEnabledCommand(self, port) + + def lr_route_add(self, router, prefix, nexthop, port=None, + policy='dst-ip', may_exist=False): + return cmd.LrRouteAddCommand(self, router, prefix, nexthop, port, + policy, may_exist) + + def lr_route_del(self, router, prefix=None, if_exists=False): + return cmd.LrRouteDelCommand(self, router, prefix, if_exists) + + def lr_route_list(self, router): + return cmd.LrRouteListCommand(self, router) + + def lr_nat_add(self, router, nat_type, external_ip, logical_ip, + logical_port=None, external_mac=None, may_exist=False): + return cmd.LrNatAddCommand( + self, router, nat_type, external_ip, logical_ip, logical_port, + external_mac, may_exist) + + def lr_nat_del(self, router, nat_type=None, match_ip=None, + if_exists=False): + return cmd.LrNatDelCommand(self, router, nat_type, match_ip, if_exists) + + def lr_nat_list(self, router): + return cmd.LrNatListCommand(self, router) + + def lb_add(self, lb, vip, ips, protocol=const.PROTO_TCP, may_exist=False, + **columns): + return cmd.LbAddCommand(self, lb, vip, ips, protocol, may_exist, + **columns) + + def lb_del(self, lb, vip=None, if_exists=False): + return cmd.LbDelCommand(self, lb, vip, if_exists) + + def lb_list(self): + return cmd.LbListCommand(self) + + def lr_lb_add(self, router, lb, may_exist=False): + return cmd.LrLbAddCommand(self, router, lb, may_exist) + + def lr_lb_del(self, router, lb=None, if_exists=False): + return cmd.LrLbDelCommand(self, router, lb, if_exists) + + def lr_lb_list(self, router): + return cmd.LrLbListCommand(self, router) + + def ls_lb_add(self, switch, lb, may_exist=False): + return cmd.LsLbAddCommand(self, switch, lb, may_exist) + + def ls_lb_del(self, switch, lb=None, if_exists=False): + return cmd.LsLbDelCommand(self, switch, lb, if_exists) + + def ls_lb_list(self, switch): + return cmd.LsLbListCommand(self, switch) + def dhcp_options_add(self, cidr, **external_ids): return cmd.DhcpOptionsAddCommand(self, cidr, **external_ids) diff --git a/ovsdbapp/tests/functional/schema/ovn_northbound/fixtures.py b/ovsdbapp/tests/functional/schema/ovn_northbound/fixtures.py index 65987bb0..37423f2e 100644 --- a/ovsdbapp/tests/functional/schema/ovn_northbound/fixtures.py +++ b/ovsdbapp/tests/functional/schema/ovn_northbound/fixtures.py @@ -53,3 +53,9 @@ class LogicalRouterFixture(ImplIdlFixture): api = impl_idl.OvnNbApiIdlImpl create = 'lr_add' delete = 'lr_del' + + +class LoadBalancerFixture(ImplIdlFixture): + api = impl_idl.OvnNbApiIdlImpl + create = 'lb_add' + delete = 'lb_del' diff --git a/ovsdbapp/tests/functional/schema/ovn_northbound/test_impl_idl.py b/ovsdbapp/tests/functional/schema/ovn_northbound/test_impl_idl.py index 138c7dd0..1a82b987 100644 --- a/ovsdbapp/tests/functional/schema/ovn_northbound/test_impl_idl.py +++ b/ovsdbapp/tests/functional/schema/ovn_northbound/test_impl_idl.py @@ -11,11 +11,15 @@ # under the License. import netaddr +import testscenarios +from ovsdbapp.backend.ovs_idl import idlutils +from ovsdbapp import constants as const from ovsdbapp.schema.ovn_northbound import impl_idl from ovsdbapp.tests.functional import base from ovsdbapp.tests.functional.schema.ovn_northbound import fixtures from ovsdbapp.tests import utils +from ovsdbapp import utils as ovsdb_utils class OvnNorthboundTest(base.FunctionalTestCase): @@ -415,3 +419,568 @@ class TestDhcpOptionsOps(OvnNorthboundTest): dhcpopt.uuid, **options).execute(check_error=True) cmd = self.api.dhcp_options_get_options(dhcpopt.uuid) self.assertEqual(options, cmd.execute(check_error=True)) + + +class TestLogicalRouterOps(OvnNorthboundTest): + def _lr_add(self, *args, **kwargs): + lr = self.useFixture( + fixtures.LogicalRouterFixture(*args, **kwargs)).obj + self.assertIn(lr.uuid, self.api.tables['Logical_Router'].rows) + return lr + + def test_lr_add(self): + self._lr_add() + + def test_lr_add_name(self): + name = utils.get_rand_device_name() + lr = self._lr_add(name) + self.assertEqual(name, lr.name) + + def test_lr_add_columns(self): + external_ids = {'mykey': 'myvalue', 'yourkey': 'yourvalue'} + lr = self._lr_add(external_ids=external_ids) + self.assertEqual(external_ids, lr.external_ids) + + def test_lr_del(self): + lr = self._lr_add() + self.api.lr_del(lr.uuid).execute(check_error=True) + self.assertNotIn(lr.uuid, + self.api.tables['Logical_Router'].rows.keys()) + + def test_lr_del_name(self): + lr = self._lr_add(utils.get_rand_device_name()) + self.api.lr_del(lr.name).execute(check_error=True) + self.assertNotIn(lr.uuid, + self.api.tables['Logical_Router'].rows.keys()) + + def test_lr_list(self): + lrs = {self._lr_add() for _ in range(3)} + lr_set = set(self.api.lr_list().execute(check_error=True)) + self.assertTrue(lrs.issubset(lr_set), "%s vs %s" % (lrs, lr_set)) + + def _lr_add_route(self, router=None, prefix=None, nexthop=None, port=None, + **kwargs): + lr = self._lr_add(router or utils.get_rand_device_name(), + may_exist=True) + prefix = prefix or '192.0.2.0/25' + nexthop = nexthop or '192.0.2.254' + sr = self.api.lr_route_add(lr.uuid, prefix, nexthop, port, + **kwargs).execute(check_error=True) + self.assertIn(sr, lr.static_routes) + self.assertEqual(prefix, sr.ip_prefix) + self.assertEqual(nexthop, sr.nexthop) + sr.router = lr + return sr + + def test_lr_route_add(self): + self._lr_add_route() + + def test_lr_route_add_invalid_prefix(self): + self.assertRaises(netaddr.AddrFormatError, self._lr_add_route, + prefix='192.168.1.1/40') + + def test_lr_route_add_invalid_nexthop(self): + self.assertRaises(netaddr.AddrFormatError, self._lr_add_route, + nexthop='256.0.1.3') + + def test_lr_route_add_exist(self): + router_name = utils.get_rand_device_name() + self._lr_add_route(router_name) + self.assertRaises(RuntimeError, self._lr_add_route, router=router_name) + + def test_lr_route_add_may_exist(self): + router_name = utils.get_rand_device_name() + self._lr_add_route(router_name) + self._lr_add_route(router_name, may_exist=True) + + def test_lr_route_del(self): + prefix = "192.0.2.0/25" + route = self._lr_add_route(prefix=prefix) + self.api.lr_route_del(route.router.uuid, prefix).execute( + check_error=True) + self.assertNotIn(route, route.router.static_routes) + + def test_lr_route_del_all(self): + router = self._lr_add() + for p in range(3): + self._lr_add_route(router.uuid, prefix="192.0.%s.0/24" % p) + self.api.lr_route_del(router.uuid).execute(check_error=True) + self.assertEqual([], router.static_routes) + + def test_lr_route_del_no_router(self): + cmd = self.api.lr_route_del("fake_router", '192.0.2.0/25') + self.assertRaises(RuntimeError, cmd.execute, check_error=True) + + def test_lr_route_del_no_exist(self): + lr = self._lr_add() + cmd = self.api.lr_route_del(lr.uuid, '192.0.2.0/25') + self.assertRaises(RuntimeError, cmd.execute, check_error=True) + + def test_lr_route_del_if_exist(self): + lr = self._lr_add() + self.api.lr_route_del(lr.uuid, '192.0.2.0/25', if_exists=True).execute( + check_error=True) + + def test_lr_route_list(self): + lr = self._lr_add() + routes = {self._lr_add_route(lr.uuid, prefix="192.0.%s.0/25" % p) + for p in range(3)} + route_set = set(self.api.lr_route_list(lr.uuid).execute( + check_error=True)) + self.assertTrue(routes.issubset(route_set)) + + def _lr_nat_add(self, *args, **kwargs): + lr = kwargs.pop('router', self._lr_add(utils.get_rand_device_name())) + nat = self.api.lr_nat_add( + lr.uuid, *args, **kwargs).execute( + check_error=True) + self.assertIn(nat, lr.nat) + nat.router = lr + return nat + + def test_lr_nat_add_dnat(self): + ext, log = ('10.172.4.1', '192.0.2.1') + nat = self._lr_nat_add(const.NAT_DNAT, ext, log) + self.assertEqual(ext, nat.external_ip) + self.assertEqual(log, nat.logical_ip) + + def test_lr_nat_add_snat(self): + ext, log = ('10.172.4.1', '192.0.2.0/24') + nat = self._lr_nat_add(const.NAT_SNAT, ext, log) + self.assertEqual(ext, nat.external_ip) + self.assertEqual(log, nat.logical_ip) + + def test_lr_nat_add_port(self): + sw = self.useFixture( + fixtures.LogicalSwitchFixture()).obj + lsp = self.api.lsp_add(sw.uuid, utils.get_rand_device_name()).execute( + check_error=True) + lport, mac = (lsp.name, 'de:ad:be:ef:4d:ad') + nat = self._lr_nat_add(const.NAT_BOTH, '10.172.4.1', '192.0.2.1', + lport, mac) + self.assertIn(lport, nat.logical_port) # because optional + self.assertIn(mac, nat.external_mac) + + def test_lr_nat_add_port_no_mac(self): + # yes, this and other TypeError tests are technically unit tests + self.assertRaises(TypeError, self.api.lr_nat_add, 'faker', + const.NAT_DNAT, '10.17.4.1', '192.0.2.1', 'fake') + + def test_lr_nat_add_port_wrong_type(self): + for nat_type in (const.NAT_DNAT, const.NAT_SNAT): + self.assertRaises( + TypeError, self.api.lr_nat_add, 'faker', nat_type, + '10.17.4.1', '192.0.2.1', 'fake', 'de:ad:be:ef:4d:ad') + + def test_lr_nat_add_exists(self): + args = (const.NAT_SNAT, '10.17.4.1', '192.0.2.0/24') + nat1 = self._lr_nat_add(*args) + cmd = self.api.lr_nat_add(nat1.router.uuid, *args) + self.assertRaises(RuntimeError, cmd.execute, check_error=True) + + def test_lr_nat_add_may_exist(self): + sw = self.useFixture( + fixtures.LogicalSwitchFixture()).obj + lsp = self.api.lsp_add(sw.uuid, utils.get_rand_device_name()).execute( + check_error=True) + args = (const.NAT_BOTH, '10.17.4.1', '192.0.2.1') + nat1 = self._lr_nat_add(*args) + lp, mac = (lsp.name, 'de:ad:be:ef:4d:ad') + nat2 = self.api.lr_nat_add( + nat1.router.uuid, *args, logical_port=lp, + external_mac=mac, may_exist=True).execute(check_error=True) + self.assertEqual(nat1, nat2) + self.assertIn(lp, nat2.logical_port) # because optional + self.assertIn(mac, nat2.external_mac) + + def test_lr_nat_add_may_exist_remove_port(self): + sw = self.useFixture( + fixtures.LogicalSwitchFixture()).obj + lsp = self.api.lsp_add(sw.uuid, utils.get_rand_device_name()).execute( + check_error=True) + args = (const.NAT_BOTH, '10.17.4.1', '192.0.2.1') + lp, mac = (lsp.name, 'de:ad:be:ef:4d:ad') + nat1 = self._lr_nat_add(*args, logical_port=lp, external_mac=mac) + nat2 = self.api.lr_nat_add( + nat1.router.uuid, *args, may_exist=True).execute(check_error=True) + self.assertEqual(nat1, nat2) + self.assertEqual([], nat2.logical_port) # because optional + self.assertEqual([], nat2.external_mac) + + def _three_nats(self): + lr = self._lr_add(utils.get_rand_device_name()) + for n, nat_type in enumerate((const.NAT_DNAT, const.NAT_SNAT, + const.NAT_BOTH)): + nat_kwargs = {'router': lr, 'nat_type': nat_type, + 'logical_ip': '10.17.4.%s' % (n + 1), + 'external_ip': '192.0.2.%s' % (n + 1)} + self._lr_nat_add(**nat_kwargs) + return lr + + def _lr_nat_del(self, *args, **kwargs): + lr = self._three_nats() + self.api.lr_nat_del(lr.name, *args, **kwargs).execute(check_error=True) + return lr + + def test_lr_nat_del_all(self): + lr = self._lr_nat_del() + self.assertEqual([], lr.nat) + + def test_lr_nat_del_type(self): + lr = self._lr_nat_del(nat_type=const.NAT_SNAT) + types = tuple(nat.type for nat in lr.nat) + self.assertNotIn(const.NAT_SNAT, types) + self.assertEqual(len(types), len(const.NAT_TYPES) - 1) + + def test_lr_nat_del_specific_dnat(self): + lr = self._lr_nat_del(nat_type=const.NAT_DNAT, match_ip='192.0.2.1') + self.assertEqual(len(lr.nat), len(const.NAT_TYPES) - 1) + for nat in lr.nat: + self.assertNotEqual('192.0.2.1', nat.external_ip) + self.assertNotEqual(const.NAT_DNAT, nat.type) + + def test_lr_nat_del_specific_snat(self): + lr = self._lr_nat_del(nat_type=const.NAT_SNAT, match_ip='10.17.4.2') + self.assertEqual(len(lr.nat), len(const.NAT_TYPES) - 1) + for nat in lr.nat: + self.assertNotEqual('10.17.4.2', nat.external_ip) + self.assertNotEqual(const.NAT_SNAT, nat.type) + + def test_lr_nat_del_specific_both(self): + lr = self._lr_nat_del(nat_type=const.NAT_BOTH, match_ip='192.0.2.3') + self.assertEqual(len(lr.nat), len(const.NAT_TYPES) - 1) + for nat in lr.nat: + self.assertNotEqual('192.0.2.3', nat.external_ip) + self.assertNotEqual(const.NAT_BOTH, nat.type) + + def test_lr_nat_del_specific_not_found(self): + self.assertRaises(idlutils.RowNotFound, self._lr_nat_del, + nat_type=const.NAT_BOTH, match_ip='10.17.4.2') + + def test_lr_nat_del_specific_if_exists(self): + lr = self._lr_nat_del(nat_type=const.NAT_BOTH, match_ip='10.17.4.2', + if_exists=True) + self.assertEqual(len(lr.nat), len(const.NAT_TYPES)) + + def test_lr_nat_list(self): + lr = self._three_nats() + nats = self.api.lr_nat_list(lr.uuid).execute(check_error=True) + self.assertEqual(lr.nat, nats) + + +class TestLogicalRouterPortOps(OvnNorthboundTest): + def setUp(self): + super(TestLogicalRouterPortOps, self).setUp() + self.lr = self.useFixture(fixtures.LogicalRouterFixture()).obj + + def _lrp_add(self, port, mac='de:ad:be:ef:4d:ad', + networks=None, *args, **kwargs): + if port is None: + port = utils.get_rand_device_name() + if networks is None: + networks = ['192.0.2.0/24'] + lrp = self.api.lrp_add(self.lr.uuid, port, mac, networks, + *args, **kwargs).execute(check_error=True) + self.assertIn(lrp, self.lr.ports) + self.assertEqual(mac, lrp.mac) + self.assertEqual(set(networks), set(lrp.networks)) + return lrp + + def test_lrp_add(self): + self._lrp_add(None, 'de:ad:be:ef:4d:ad', ['192.0.2.0/24']) + + def test_lpr_add_peer(self): + lrp = self._lrp_add(None, 'de:ad:be:ef:4d:ad', ['192.0.2.0/24'], + peer='fake_peer') + self.assertIn('fake_peer', lrp.peer) + + def test_lpr_add_multiple_networks(self): + networks = ['192.0.2.0/24', '192.2.1.0/24'] + self._lrp_add(None, 'de:ad:be:ef:4d:ad', networks) + + def test_lrp_add_invalid_mac(self): + self.assertRaises( + netaddr.AddrFormatError, + self.api.lrp_add, "fake", "fake", "000:11:22:33:44:55", + ['192.0.2.0/24']) + + def test_lrp_add_invalid_network(self): + self.assertRaises( + netaddr.AddrFormatError, + self.api.lrp_add, "fake", "fake", "01:02:03:04:05:06", + ['256.2.0.1/24']) + + def test_lrp_add_exists(self): + name = utils.get_rand_device_name() + args = (name, 'de:ad:be:ef:4d:ad', ['192.0.2.0/24']) + self._lrp_add(*args) + self.assertRaises(RuntimeError, self._lrp_add, *args) + + def test_lrp_add_may_exist(self): + name = utils.get_rand_device_name() + args = (name, 'de:ad:be:ef:4d:ad', ['192.0.2.0/24']) + self._lrp_add(*args) + self.assertRaises(RuntimeError, self._lrp_add, *args, may_exist=True) + + def test_lrp_add_may_exist_different_router(self): + name = utils.get_rand_device_name() + args = (name, 'de:ad:be:ef:4d:ad', ['192.0.2.0/24']) + lr2 = self.useFixture(fixtures.LogicalRouterFixture()).obj + self._lrp_add(*args) + cmd = self.api.lrp_add(lr2.uuid, *args, may_exist=True) + self.assertRaises(RuntimeError, cmd.execute, check_error=True) + + def test_lrp_add_may_exist_different_mac(self): + name = utils.get_rand_device_name() + args = {'port': name, 'mac': 'de:ad:be:ef:4d:ad', + 'networks': ['192.0.2.0/24']} + self._lrp_add(**args) + args['mac'] = 'da:d4:de:ad:be:ef' + self.assertRaises(RuntimeError, self._lrp_add, may_exist=True, **args) + + def test_lrp_add_may_exist_different_networks(self): + name = utils.get_rand_device_name() + args = (name, 'de:ad:be:ef:4d:ad') + self._lrp_add(*args, networks=['192.0.2.0/24']) + self.assertRaises(RuntimeError, self._lrp_add, *args, + networks=['192.2.1.0/24'], may_exist=True) + + def test_lrp_add_may_exist_different_peer(self): + name = utils.get_rand_device_name() + args = (name, 'de:ad:be:ef:4d:ad', ['192.0.2.0/24']) + self._lrp_add(*args) + self.assertRaises(RuntimeError, self._lrp_add, *args, + peer='fake', may_exist=True) + + def test_lrp_add_columns(self): + options = {'myside': 'yourside'} + external_ids = {'myside': 'yourside'} + lrp = self._lrp_add(None, options=options, external_ids=external_ids) + self.assertEqual(options, lrp.options) + self.assertEqual(external_ids, lrp.external_ids) + + def test_lrp_del_uuid(self): + lrp = self._lrp_add(None) + self.api.lrp_del(lrp.uuid).execute(check_error=True) + self.assertNotIn(lrp, self.lr.ports) + + def test_lrp_del_name(self): + lrp = self._lrp_add(None) + self.api.lrp_del(lrp.name).execute(check_error=True) + self.assertNotIn(lrp, self.lr.ports) + + def test_lrp_del_router(self): + lrp = self._lrp_add(None) + self.api.lrp_del(lrp.uuid, self.lr.uuid).execute(check_error=True) + self.assertNotIn(lrp, self.lr.ports) + + def test_lrp_del_router_name(self): + lrp = self._lrp_add(None) + self.api.lrp_del(lrp.uuid, + self.lr.name).execute(check_error=True) + self.assertNotIn(lrp, self.lr.ports) + + def test_lrp_del_wrong_router(self): + lrp = self._lrp_add(None) + sw_id = self.useFixture(fixtures.LogicalSwitchFixture()).obj + cmd = self.api.lrp_del(lrp.uuid, sw_id) + self.assertRaises(RuntimeError, cmd.execute, check_error=True) + + def test_lrp_del_router_no_exist(self): + lrp = self._lrp_add(None) + cmd = self.api.lrp_del(lrp.uuid, utils.get_rand_device_name()) + self.assertRaises(RuntimeError, cmd.execute, check_error=True) + + def test_lrp_del_no_exist(self): + cmd = self.api.lrp_del("fake_port") + self.assertRaises(RuntimeError, cmd.execute, check_error=True) + + def test_lrp_del_if_exist(self): + self.api.lrp_del("fake_port", if_exists=True).execute(check_error=True) + + def test_lrp_list(self): + ports = {self._lrp_add(None) for _ in range(3)} + port_set = set(self.api.lrp_list(self.lr.uuid).execute( + check_error=True)) + self.assertTrue(ports.issubset(port_set)) + + def test_lrp_get_set_enabled(self): + lrp = self._lrp_add(None) + # default is True + self.assertTrue(self.api.lrp_get_enabled(lrp.name).execute( + check_error=True)) + self.api.lrp_set_enabled(lrp.name, False).execute(check_error=True) + self.assertFalse(self.api.lrp_get_enabled(lrp.name).execute( + check_error=True)) + self.api.lrp_set_enabled(lrp.name, True).execute(check_error=True) + self.assertTrue(self.api.lrp_get_enabled(lrp.name).execute( + check_error=True)) + + +class TestLoadBalancerOps(OvnNorthboundTest): + + def _lb_add(self, lb, vip, ips, protocol=const.PROTO_TCP, may_exist=False, + **columns): + lbal = self.useFixture(fixtures.LoadBalancerFixture( + lb, vip, ips, protocol, may_exist, **columns)).obj + self.assertEqual(lb, lbal.name) + norm_vip = ovsdb_utils.normalize_ip_port(vip) + self.assertIn(norm_vip, lbal.vips) + self.assertEqual(",".join(ovsdb_utils.normalize_ip(ip) for ip in ips), + lbal.vips[norm_vip]) + self.assertIn(protocol, lbal.protocol) # because optional + return lbal + + def test_lb_add(self): + vip = '192.0.2.1' + ips = ['10.0.0.1', '10.0.0.2', '10.0.0.3'] + self._lb_add(utils.get_rand_device_name(), vip, ips) + + def test_lb_add_port(self): + vip = '192.0.2.1:80' + ips = ['10.0.0.1', '10.0.0.2', '10.0.0.3'] + self._lb_add(utils.get_rand_device_name(), vip, ips) + + def test_lb_add_protocol(self): + vip = '192.0.2.1' + ips = ['10.0.0.1', '10.0.0.2', '10.0.0.3'] + self._lb_add(utils.get_rand_device_name(), vip, ips, const.PROTO_UDP) + + def test_lb_add_new_vip(self): + name = utils.get_rand_device_name() + lb1 = self._lb_add(name, '192.0.2.1', ['10.0.0.1', '10.0.0.2']) + lb2 = self._lb_add(name, '192.0.2.2', ['10.1.0.1', '10.1.0.2']) + self.assertEqual(lb1, lb2) + self.assertEqual(2, len(lb1.vips)) + + def test_lb_add_exists(self): + name = utils.get_rand_device_name() + vip = '192.0.2.1' + ips = ['10.0.0.1', '10.0.0.2', '10.0.0.3'] + self._lb_add(name, vip, ips) + cmd = self.api.lb_add(name, vip, ips) + self.assertRaises(RuntimeError, cmd.execute, check_error=True) + + def test_lb_add_may_exist(self): + name = utils.get_rand_device_name() + vip = '192.0.2.1' + ips = ['10.0.0.1', '10.0.0.2', '10.0.0.3'] + lb1 = self._lb_add(name, vip, ips) + ips += ['10.0.0.4'] + lb2 = self.api.lb_add(name, vip, ips, may_exist=True).execute( + check_error=True) + self.assertEqual(lb1, lb2) + self.assertEqual(",".join(ips), lb1.vips[vip]) + + def test_lb_add_columns(self): + ext_ids = {'one': 'two'} + name = utils.get_rand_device_name() + lb = self._lb_add(name, '192.0.2.1', ['10.0.0.1', '10.0.0.2'], + external_ids=ext_ids) + self.assertEqual(ext_ids, lb.external_ids) + + def test_lb_del(self): + name = utils.get_rand_device_name() + lb = self._lb_add(name, '192.0.2.1', ['10.0.0.1', '10.0.0.2']).uuid + self.api.lb_del(lb).execute(check_error=True) + self.assertNotIn(lb, self.api.tables['Load_Balancer'].rows) + + def test_lb_del_vip(self): + name = utils.get_rand_device_name() + lb1 = self._lb_add(name, '192.0.2.1', ['10.0.0.1', '10.0.0.2']) + lb2 = self._lb_add(name, '192.0.2.2', ['10.1.0.1', '10.1.0.2']) + self.assertEqual(lb1, lb2) + self.api.lb_del(lb1.name, '192.0.2.1').execute(check_error=True) + self.assertNotIn('192.0.2.1', lb1.vips) + self.assertIn('192.0.2.2', lb1.vips) + + def test_lb_del_no_exist(self): + cmd = self.api.lb_del(utils.get_rand_device_name()) + self.assertRaises(idlutils.RowNotFound, cmd.execute, check_error=True) + + def test_lb_del_if_exists(self): + self.api.lb_del(utils.get_rand_device_name(), if_exists=True).execute( + check_error=True) + + def test_lb_list(self): + lbs = {self._lb_add(utils.get_rand_device_name(), '192.0.2.1', + ['10.0.0.1', '10.0.0.2']) for _ in range(3)} + lbset = self.api.lb_list().execute(check_error=True) + self.assertTrue(lbs.issubset(lbset)) + + +class TestObLbOps(testscenarios.TestWithScenarios, OvnNorthboundTest): + scenarios = [ + ('LrLbOps', dict(fixture=fixtures.LogicalRouterFixture, + _add_fn='lr_lb_add', _del_fn='lr_lb_del', + _list_fn='lr_lb_list')), + ('LsLbOps', dict(fixture=fixtures.LogicalSwitchFixture, + _add_fn='ls_lb_add', _del_fn='ls_lb_del', + _list_fn='ls_lb_list')), + ] + + def setUp(self): + super(TestObLbOps, self).setUp() + self.add_fn = getattr(self.api, self._add_fn) + self.del_fn = getattr(self.api, self._del_fn) + self.list_fn = getattr(self.api, self._list_fn) + # They must be in this order because the load balancer + # can't be deleted when there is a reference in the router + self.lb = self.useFixture(fixtures.LoadBalancerFixture( + utils.get_rand_device_name(), '192.0.2.1', + ['10.0.0.1', '10.0.0.2'])).obj + self.lb2 = self.useFixture(fixtures.LoadBalancerFixture( + utils.get_rand_device_name(), '192.0.2.2', + ['10.1.0.1', '10.1.0.2'])).obj + self.lr = self.useFixture(self.fixture( + utils.get_rand_device_name())).obj + + def test_ob_lb_add(self): + self.add_fn(self.lr.name, self.lb.name).execute( + check_error=True) + self.assertIn(self.lb, self.lr.load_balancer) + + def test_ob_lb_add_exists(self): + cmd = self.add_fn(self.lr.name, self.lb.name) + cmd.execute(check_error=True) + self.assertRaises(RuntimeError, cmd.execute, check_error=True) + + def test_ob_lb_add_may_exist(self): + cmd = self.add_fn(self.lr.name, self.lb.name, may_exist=True) + lb1 = cmd.execute(check_error=True) + lb2 = cmd.execute(check_error=True) + self.assertEqual(lb1, lb2) + + def test_ob_lb_del(self): + self.add_fn(self.lr.name, self.lb.name).execute( + check_error=True) + self.assertIn(self.lb, self.lr.load_balancer) + self.del_fn(self.lr.name).execute(check_error=True) + self.assertEqual(0, len(self.lr.load_balancer)) + + def test_ob_lb_del_lb(self): + self.add_fn(self.lr.name, self.lb.name).execute( + check_error=True) + self.add_fn(self.lr.name, self.lb2.name).execute( + check_error=True) + self.del_fn(self.lr.name, self.lb2.name).execute( + check_error=True) + self.assertNotIn(self.lb2, self.lr.load_balancer) + self.assertIn(self.lb, self.lr.load_balancer) + + def test_ob_lb_del_no_exist(self): + cmd = self.del_fn(self.lr.name, 'fake') + self.assertRaises(idlutils.RowNotFound, cmd.execute, check_error=True) + + def test_ob_lb_del_if_exists(self): + self.del_fn(self.lr.name, 'fake', if_exists=True).execute( + check_error=True) + + def test_ob_lb_list(self): + self.add_fn(self.lr.name, self.lb.name).execute( + check_error=True) + self.add_fn(self.lr.name, self.lb2.name).execute( + check_error=True) + rows = self.list_fn(self.lr.name).execute(check_error=True) + self.assertIn(self.lb, rows) + self.assertIn(self.lb2, rows) diff --git a/ovsdbapp/tests/unit/test_utils.py b/ovsdbapp/tests/unit/test_utils.py new file mode 100644 index 00000000..52187375 --- /dev/null +++ b/ovsdbapp/tests/unit/test_utils.py @@ -0,0 +1,53 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import netaddr + +from ovsdbapp.tests import base +from ovsdbapp import utils + + +class TestUtils(base.TestCase): + + def test_normalize_ip(self): + good = [ + ('4.4.4.4', '4.4.4.4'), + ('10.0.0.0', '10.0.0.0'), + ('123', '0.0.0.123'), + ('2001:0db8:85a3:0000:0000:8a2e:0370:7334', + '2001:db8:85a3::8a2e:370:7334') + ] + bad = ('256.1.3.2', 'bad', '192.168.1.1:80') + for before, after in good: + norm = utils.normalize_ip(before) + self.assertEqual(after, norm, + "%s does not match %s" % (after, norm)) + for val in bad: + self.assertRaises(netaddr.AddrFormatError, utils.normalize_ip, val) + + def test_normalize_ip_port(self): + good = [ + ('4.4.4.4:53', '4.4.4.4:53'), + ('10.0.0.0:7', '10.0.0.0:7'), + ('123:12', '0.0.0.123:12'), + ('[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:80', + '[2001:db8:85a3::8a2e:370:7334]:80') + ] + bad = ('1.2.3.4:0', '1.2.3.4:99000', + '2001:0db8:85a3:0000:0000:8a2e:0370:7334:80') + for before, after in good: + norm = utils.normalize_ip_port(before) + self.assertEqual(after, norm, + "%s does not match %s" % (after, norm)) + for val in bad: + self.assertRaises(netaddr.AddrFormatError, + utils.normalize_ip_port, val) diff --git a/ovsdbapp/utils.py b/ovsdbapp/utils.py new file mode 100644 index 00000000..7bc26ba5 --- /dev/null +++ b/ovsdbapp/utils.py @@ -0,0 +1,43 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import netaddr + +# NOTE(twilson) Clearly these are silly, but they are good enough for now +# I'm happy for someone to replace them with better parsing + + +def normalize_ip(ip): + return str(netaddr.IPAddress(ip)) + + +def normalize_ip_port(ipport): + try: + return normalize_ip(ipport) + except netaddr.AddrFormatError: + # maybe we have a port + if ipport[0] == '[': + # Should be an IPv6 w/ port + try: + ip, port = ipport[1:].split(']:') + except ValueError: + raise netaddr.AddrFormatError("Invalid Port") + ip = "[%s]" % normalize_ip(ip) + else: + try: + ip, port = ipport.split(':') + except ValueError: + raise netaddr.AddrFormatError("Invalid Port") + ip = normalize_ip(ip) + if int(port) <= 0 or int(port) > 65535: + raise netaddr.AddrFormatError("Invalid port") + return "%s:%s" % (ip, port)