Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-ovsdbapp for openSUSE:Factory 
checked in at 2024-01-05 21:41:38
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-ovsdbapp (Old)
 and      /work/SRC/openSUSE:Factory/.python-ovsdbapp.new.28375 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-ovsdbapp"

Fri Jan  5 21:41:38 2024 rev:15 rq:1136724 version:2.5.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-ovsdbapp/python-ovsdbapp.changes  
2023-06-22 23:25:13.929623435 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-ovsdbapp.new.28375/python-ovsdbapp.changes   
    2024-01-05 21:42:46.933283270 +0100
@@ -1,0 +2,15 @@
+Thu Jan  4 09:07:26 UTC 2024 - [email protected]
+
+- update to version 2.5.0
+  - Update master for stable/2023.2
+  - TAAS: Add commands for creating Mirrors
+  - Add support for ecmp routes
+  - Ensure LrpAddCommand may_exits works without peers
+  - Add some basic usage documentation
+  - Handle events with conditions and match_fn
+  - nb: provide 'route_table' in lr-route-{add,del,list}
+  - nb: allow to add route even if learned route exists
+  - venv: unit tests don't work for 'hardware_vtep'
+  - nb: add 'nexthop' argument to 'lr_route_del'
+
+-------------------------------------------------------------------

Old:
----
  ovsdbapp-2.3.0.tar.gz

New:
----
  ovsdbapp-2.5.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-ovsdbapp.spec ++++++
--- /var/tmp/diff_new_pack.09bHqS/_old  2024-01-05 21:42:47.473303016 +0100
+++ /var/tmp/diff_new_pack.09bHqS/_new  2024-01-05 21:42:47.473303016 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-ovsdbapp
 #
-# Copyright (c) 2023 SUSE LLC
+# Copyright (c) 2024 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,13 +18,13 @@
 
 %define with_tests 1
 Name:           python-ovsdbapp
-Version:        2.3.0
+Version:        2.5.0
 Release:        0
 Summary:        A library for creating OVSDB applications
 License:        Apache-2.0
 Group:          Development/Languages/Python
 URL:            https://docs.openstack.org/ovsdbapp
-Source0:        
https://files.pythonhosted.org/packages/source/o/ovsdbapp/ovsdbapp-2.3.0.tar.gz
+Source0:        
https://files.pythonhosted.org/packages/source/o/ovsdbapp/ovsdbapp-2.5.0.tar.gz
 BuildRequires:  openstack-macros
 BuildRequires:  python3-fixtures >= 3.0.0
 BuildRequires:  python3-netaddr >= 0.7.18
@@ -70,7 +70,7 @@
 %{py3_build}
 
 # generate html docs
-PBR_VERSION=2.3.0 PYTHONPATH=. \
+PBR_VERSION=2.5.0 PYTHONPATH=. \
     %sphinx_build -b html doc/source doc/build/html
 rm -rf doc/build/html/.{doctrees,buildinfo}
 

++++++ _service ++++++
--- /var/tmp/diff_new_pack.09bHqS/_old  2024-01-05 21:42:47.501304040 +0100
+++ /var/tmp/diff_new_pack.09bHqS/_new  2024-01-05 21:42:47.501304040 +0100
@@ -1,13 +1,13 @@
 <services>
-  <service mode="disabled" name="renderspec">
+  <service mode="manual" name="renderspec">
     <param 
name="input-template">https://opendev.org/openstack/rpm-packaging/raw/master/openstack/ovsdbapp/ovsdbapp.spec.j2</param>
     <param name="output-name">python-ovsdbapp.spec</param>
     <param 
name="requirements">https://opendev.org/openstack/ovsdbapp/raw/master/requirements.txt</param>
     <param name="changelog-email">[email protected]</param>
     <param name="changelog-provider">gh,openstack,ovsdbapp</param>
   </service>
-  <service mode="disabled" name="download_files">
+  <service mode="manual" name="download_files">
   </service>
-  <service name="format_spec_file" mode="disabled"/>
+  <service name="format_spec_file" mode="manual"/>
 </services>
 

++++++ ovsdbapp-2.3.0.tar.gz -> ovsdbapp-2.5.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ovsdbapp-2.3.0/AUTHORS new/ovsdbapp-2.5.0/AUTHORS
--- old/ovsdbapp-2.3.0/AUTHORS  2023-04-27 17:14:39.000000000 +0200
+++ new/ovsdbapp-2.5.0/AUTHORS  2023-11-21 10:22:19.000000000 +0100
@@ -71,6 +71,7 @@
 Yunxiang Tao <[email protected]>
 caoyuan <[email protected]>
 chenxing <[email protected]>
+elajkat <[email protected]>
 gengchc2 <[email protected]>
 hgangwx <[email protected]>
 huang.zhiping <[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ovsdbapp-2.3.0/ChangeLog new/ovsdbapp-2.5.0/ChangeLog
--- old/ovsdbapp-2.3.0/ChangeLog        2023-04-27 17:14:39.000000000 +0200
+++ new/ovsdbapp-2.5.0/ChangeLog        2023-11-21 10:22:19.000000000 +0100
@@ -1,6 +1,28 @@
 CHANGES
 =======
 
+2.5.0
+-----
+
+* nb: allow to add route even if learned route exists
+* TAAS: Add commands for creating Mirrors
+* nb: provide 'route\_table' in lr-route-{add,del,list}
+* nb: add 'nexthop' argument to 'lr\_route\_del'
+* Handle events with conditions and match\_fn
+* venv: unit tests don't work for 'hardware\_vtep'
+* Update master for stable/2023.2
+
+2.4.1
+-----
+
+* Add some basic usage documentation
+
+2.4.0
+-----
+
+* Ensure LrpAddCommand may\_exits works without peers
+* Add support for ecmp routes
+
 2.3.0
 -----
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ovsdbapp-2.3.0/PKG-INFO new/ovsdbapp-2.5.0/PKG-INFO
--- old/ovsdbapp-2.3.0/PKG-INFO 2023-04-27 17:14:39.963996200 +0200
+++ new/ovsdbapp-2.5.0/PKG-INFO 2023-11-21 10:22:19.864071600 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: ovsdbapp
-Version: 2.3.0
+Version: 2.5.0
 Summary: A library for creating OVSDB applications
 Home-page: https://pypi.org/project/ovsdbapp/
 Author: OpenStack
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ovsdbapp-2.3.0/doc/source/user/index.rst 
new/ovsdbapp-2.5.0/doc/source/user/index.rst
--- old/ovsdbapp-2.3.0/doc/source/user/index.rst        2023-04-27 
17:14:09.000000000 +0200
+++ new/ovsdbapp-2.5.0/doc/source/user/index.rst        2023-11-21 
10:21:52.000000000 +0100
@@ -1,7 +1,20 @@
-=====
-Usage
-=====
+==================
+Library User Guide
+==================
 
-To use ovsdbapp in a project::
+This document describes OVSDBapp concepts and best practices to enable
+writing effective OVSDBapp-based applications.
 
-    import ovsdbapp
+Overview
+--------
+.. toctree::
+   :maxdepth: 2
+
+   overview
+
+Tutorial
+--------
+.. toctree::
+   :maxdepth: 2
+
+   tutorial
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ovsdbapp-2.3.0/doc/source/user/overview.rst 
new/ovsdbapp-2.5.0/doc/source/user/overview.rst
--- old/ovsdbapp-2.3.0/doc/source/user/overview.rst     1970-01-01 
01:00:00.000000000 +0100
+++ new/ovsdbapp-2.5.0/doc/source/user/overview.rst     2023-11-21 
10:21:52.000000000 +0100
@@ -0,0 +1,48 @@
+========
+Overview
+========
+
+OVSDBapp is a library to make it easier to write applications that interact
+with an Open vSwitch database server. It allows the user to separate support
+for a particular OVSDB schema and the backend method of communication with the
+OVSDB server.
+
+OVSDBapp Concepts
+-----------------
+
+API
+  The interface that an application will use for reading or modifying entries
+  in the OVS database. Whatever backend communication method is used, as long
+  as user code only accesses methods in this API, no user code should need to
+  be changed when swapping between backends.
+Backend
+  The Backend handles the communication with Open vSwitch. Originally, there
+  were two OVSDBapp backends: 1) one that used the ovs-vsctl CLI utility to
+  interact with the OVS database and 2) one that maintains a persistent
+  connection to an OVSDB server using the python-ovs library. Currently, only
+  the python-ovs backend is being maintained.
+Command
+  OVSDBapp uses the `Command Pattern`_ to isolate individual units of work
+  that will be run as part of an OVSDB transaction.
+Event
+  OVSDB provides the ability to monitor database changes as they happen.
+  OVSDBapp backends each implement the :code:`RowEvent` and
+  :code:`RowEventHandler` to handle delivering these events to user code.
+API Implementations:
+  The backend-specific implementation of an OVSDBapp API. Only this code
+  should need to be implemented to support a new backend. All other user
+  code should be backend-agnostic.
+Schema
+  The OVSDB database schema for which the API is implemented. In current
+  ovsdbapp code, the schema and API are intrinsically linked in a
+  1:1 manner, but ultimately they are independent. User code could easily
+  define an API specific to their application that encompasses multiple
+  OVSDB schemas as long as the Backend supported it.
+Transaction
+  An OVSDB transaction consisting of one or more Commands.
+Virtual Environment
+  OVSDBapp supports running OVS and OVN services in a virtual environment.
+  This is primarily used for testing.
+
+
+  .. _Command Pattern: https://en.wikipedia.org/wiki/Command_pattern
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ovsdbapp-2.3.0/doc/source/user/tutorial.rst 
new/ovsdbapp-2.5.0/doc/source/user/tutorial.rst
--- old/ovsdbapp-2.3.0/doc/source/user/tutorial.rst     1970-01-01 
01:00:00.000000000 +0100
+++ new/ovsdbapp-2.5.0/doc/source/user/tutorial.rst     2023-11-21 
10:21:52.000000000 +0100
@@ -0,0 +1,127 @@
+========
+Tutorial
+========
+
+
+Open vSwitch Environment Setup
+------------------------------
+This tutorial will use the Open vSwitch sandbox environment from the OVS
+source tree. For the sake of simplicity, we will build OVS without SSL support.
+You will need git, C development tools, automake, autoconf, and libtool. See
+the `Installing Open vSwitch`_ instructions for build requirements
+and more detailed build instructions.
+
+.. code-block:: shell
+
+   git clone https://github.com/openvswitch/ovs
+   cd ovs
+   ./boot.sh
+   ./configure --disable-ssl --enable-shared
+   export OVS_SRCDIR=`pwd`
+   make -j $(nproc) sandbox
+
+Backend Setup
+-------------
+While the original ovs-vsctl -based backend required no setup, other backends
+may. For example, the python-ovs IDL backend maintains a constant connection
+to ovsdb-server and requires an IDL class to be instantiated and passed to
+an OVSDBapp IDL backend Connection object.
+
+
+.. code-block:: python
+
+   import os
+   from ovs.db import idl as ovs_idl
+   from ovsdbapp.backend.ovs_idl import connection
+   from ovsdbapp.schema.open_vswitch import impl_idl
+
+   src_dir = os.getenv("OVS_SRCDIR")
+   run_dir = os.getenv("OVS_RUNDIR", "/var/run/openvswitch")
+   schema_file = os.path.join(src_dir, "vswitchd", "vswitch.ovsschema")
+   db_sock = os.path.join(run_dir, "db.sock")
+   remote = f"unix:{db_sock}"
+
+   schema_helper = ovs_idl.SchemaHelper(schema_file)
+   schema_helper.register_all()
+   idl = ovs_idl.Idl(remote, schema_helper)
+   conn = connection.Connection(idl=idl, timeout=60)
+
+   api = impl_idl.OvsdbIdl(conn)
+
+
+Using the API
+-------------
+Each API definition varies based on the schemas it supports and what the
+app requires. There is built-in support for many common OVS and OVN-related
+schemas, but it is possible that the APIs defined for these are not optimized
+for a given app's use cases. It may often make sense for apps to define APIs
+separate from those that are in ovsdbapp.
+
+With that said, any api that inherits from ovsdbapp.api.API will at least
+have methods defined for the standard generic OVSDB DB operations found
+described in the `ovs-vsctl manpage`_ under Database Commands.
+
+* list
+* find
+* get
+* set
+* add
+* remove
+* clear
+* create
+* destroy
+
+They are all prefixed with db\_ (e.g. list becomes db_list) and have an
+interface similar to that used by ovs-vsctl, ovn-nbctl, ovn-sbctl, etc.
+db_list() and db_find() return results as lists of dicts with each dict
+representing a row, with keys as the column names. Later versions added
+db_list_rows() and db_find_rows() to return lists of RowView objects.
+
+API commands that interact with the OVSDB server typically return an instance
+of a subclass of ovsdbapp.api.Command. These objects hold the state of a
+request that will be sent to an OVSDB server as part of a transaction. They
+can be thought of as the equivalent of queries in SQL.
+
+For a Command to be sent to the OVSDB server, it must be attached to a
+transaction, and committed. For single commands, this can be done with
+execute():
+
+.. code-block:: python
+
+   results = api.db_list("Open_vSwitch").execute(check_error=True)
+
+This implicitly creates a transaction, adds the Command returned by db_list()
+to that transaction, calls commit() on the transaction, and returns the result
+that is stored on the Command object. It is the equivalent of:
+
+.. code-block:: python
+
+   txn = api.create_transaction(check_error=True)
+   list_cmd = api.db_list("Open_vSwitch")
+   txn.add(list_cmd)
+   txn.commit()
+   results = list_cmd.result
+
+That API also defines transaction(), a context manager, that makes
+multi-command transactions easier.
+
+.. code-block:: python
+
+   with api.transaction(check_error=True) as txn:
+       br_cmd = txn.add(api.db_create("Bridge", name="test-br"))
+       txn.add(api.db_add("Open_vSwitch", ".", "bridges", br_cmd))
+
+There are some things to note with the above code. First, is that
+Transaction.add() returns the Command object that is passed to it. In the case
+of the db_create() command, the row it will create can be referenced in other
+commands in the same transaction. Second, if a table is defined as having at
+most one row, like the Open_vSwitch table, instead of passing its UUID, "."
+can be passed. Lastly, note that we are creating a Bridge row and adding it to
+the Open_vSwitch row's "bridges" field. The Bridge table is not set as a "root"
+table in the Open_vSwitch schema. What this means is that if no row in a root
+table references this Bridge, ovsdb-server will automatically clean up this
+row. The Open_vSwitch table is a root table, so referencing the bridge in that
+row prevents the bridge that was just created from being immediately removed.
+
+.. _Installing Open vSwitch: 
https://docs.openvswitch.org/en/latest/intro/install/
+.. _ovs-vsctl manpage: 
http://www.openvswitch.org/support/dist-docs/ovs-vsctl.8.html
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ovsdbapp-2.3.0/ovsdbapp/backend/ovs_idl/event.py 
new/ovsdbapp-2.5.0/ovsdbapp/backend/ovs_idl/event.py
--- old/ovsdbapp-2.3.0/ovsdbapp/backend/ovs_idl/event.py        2023-04-27 
17:14:09.000000000 +0200
+++ new/ovsdbapp-2.5.0/ovsdbapp/backend/ovs_idl/event.py        2023-11-21 
10:21:52.000000000 +0100
@@ -20,6 +20,16 @@
 
 class RowEvent(ovsdb_event.RowEvent):  # pylint: disable=abstract-method
     def match_fn(self, event, row, old):
+        """User-overridable custom matching function
+
+        This method takes the same arguments as the RowEvent API call
+        `matches` and allows for more complex matching criteria. This
+        method will apply additional checks to those specified in the
+        creation of the RowEvent
+        """
+        return True
+
+    def base_match(self, event, row, old):
         if self.conditions and not idlutils.row_match(row, self.conditions):
             return False
         if self.old_conditions:
@@ -38,6 +48,8 @@
             return False
         if row._table.name != self.table:
             return False
+        if not self.base_match(event, row, old):
+            return False
         if not self.match_fn(event, row, old):
             return False
         LOG.debug("Matched %s: %r to row=%s old=%s", event.upper(), self,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ovsdbapp-2.3.0/ovsdbapp/constants.py 
new/ovsdbapp-2.5.0/ovsdbapp/constants.py
--- old/ovsdbapp-2.3.0/ovsdbapp/constants.py    2023-04-27 17:14:09.000000000 
+0200
+++ new/ovsdbapp-2.5.0/ovsdbapp/constants.py    2023-11-21 10:21:52.000000000 
+0100
@@ -39,5 +39,6 @@
 PROTO_UDP = 'udp'
 
 ROUTE_DISCARD = "discard"
+MAIN_ROUTE_TABLE = ""
 
 LOCALNET = 'localnet'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ovsdbapp-2.3.0/ovsdbapp/schema/ovn_northbound/api.py 
new/ovsdbapp-2.5.0/ovsdbapp/schema/ovn_northbound/api.py
--- old/ovsdbapp-2.3.0/ovsdbapp/schema/ovn_northbound/api.py    2023-04-27 
17:14:09.000000000 +0200
+++ new/ovsdbapp-2.5.0/ovsdbapp/schema/ovn_northbound/api.py    2023-11-21 
10:21:52.000000000 +0100
@@ -735,7 +735,8 @@
 
     @abc.abstractmethod
     def lr_route_add(self, router, prefix, nexthop, port=None,
-                     policy='dst-ip', may_exist=False):
+                     policy='dst-ip', may_exist=False, ecmp=False,
+                     route_table=const.MAIN_ROUTE_TABLE):
         """Add a route to 'router'
 
         :param router:    The name or uuid of the router
@@ -754,11 +755,18 @@
         :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
+        :param ecmp:      Enable ECMP support. If True adding routes with
+                          same IP prefix is allowed as long as the nexthop is
+                          different
+        :type ecmp:       boolean
+        :param route_table: The name of route table
+        :type route_table:  str
         returns:          :class:`Command` with RowView result
         """
 
     @abc.abstractmethod
-    def lr_route_del(self, router, prefix=None, if_exists=False):
+    def lr_route_del(self, router, prefix=None, if_exists=False, nexthop=None,
+                     route_table=const.MAIN_ROUTE_TABLE):
         """Remove routes from 'router'
 
         :param router:    The name or uuid of the router
@@ -767,16 +775,25 @@
         :type prefix:     type string
         :param if_exists: If True, don't fail if the port doesn't exist
         :type if_exists:  boolean
+        :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 route_table: The name of route table
+        :type route_table:  str
         :returns:        :class:`Command` with no result
         """
 
     @abc.abstractmethod
-    def lr_route_list(self, router):
+    def lr_route_list(self, router, route_table=None):
         """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
+        :param router:      The name or uuid of the router
+        :type router:       string or uuid.UUID
+        :param route_table: The name of route table. Pass "" to get routes of
+                            global route table only
+        :type route_table:  str
+        :returns:           :class:`Command` with RowView list result
         """
 
     @abc.abstractmethod
@@ -1466,3 +1483,71 @@
         :type uuid:    uuid.UUID
         :returns:      :class:`Command` with RowView result
         """
+
+    @abc.abstractmethod
+    def mirror_get(self, uuid):
+        """Get the Mirror entry"""
+
+    @abc.abstractmethod
+    def mirror_del(self, mirror):
+        """Delete a Mirror"""
+
+    @abc.abstractmethod
+    def mirror_add(self, name, mirror_type, index, direction_filter, dest,
+                   external_ids=None,
+                   may_exist=False):
+        """Create a Mirror entry
+
+        :param name:    Name of the Mirror to create.
+        :type name:     str
+        :param mirror_type:    The type of the mirroring can be gre or erspan.
+        :type mirror_type:     str
+        :param index:   The index filed will be used for the Index field in
+                        ERSPAN header as decimal, and as hexadecimal value in
+                        the SpanID field, for GRE mirrors it will be the Key
+                        field.
+        :type index:    int
+        :param direction_filter:  The direction of the traffic to be mirrored,
+                                  can be from-lport and to-lprt.
+        :type direction_filter:   str
+        :param dest:    The destination IP address of the mirroring.
+        :type dest:     str
+
+
+        :param external_ids: Values to be added as external_id pairs.
+        :type external_ids:  Optional[Dict[str,str]]
+        :param may_exist:    If True, update any existing Mirror entry if it
+                             already exists.  Default is False which will raise
+                             an error if a Mirror entry with same logical_port,
+                             sink pair already exists.
+        :type may_exist:     Optional[bool]
+        :returns:            :class:`Command` with RowView result
+        """
+
+    @abc.abstractmethod
+    def lsp_attach_mirror(self, port, mirror, may_exist=False):
+        """Attaches an lsp to the given mirror
+
+        :param port: the id of the lsp
+        :type port: str
+        :param mirror: the name or ID of the mirror.
+        :type mirror: str
+        :param may_exist:    If True, don't fail if the mirror_rule already
+                             exists.
+        :type may_exist:     Optional[bool]
+        :returns: :class:`Command` with RowView result
+        """
+
+    @abc.abstractmethod
+    def lsp_detach_mirror(self, port, mirror, if_exist=False):
+        """Detaches an lsp from the given mirror
+
+        :param port: the id of the lsp
+        :type port: str
+        :param mirror: the name or ID of the mirror
+        :type mirror: str
+        :param if_exist:    If True, don't fail if the mirror_rules entry
+                            doesn't exist.
+        :type if_exist:     Optional[bool]
+        :returns: :class:`Command` with RowView result
+        """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ovsdbapp-2.3.0/ovsdbapp/schema/ovn_northbound/commands.py 
new/ovsdbapp-2.5.0/ovsdbapp/schema/ovn_northbound/commands.py
--- old/ovsdbapp-2.3.0/ovsdbapp/schema/ovn_northbound/commands.py       
2023-04-27 17:14:09.000000000 +0200
+++ new/ovsdbapp-2.5.0/ovsdbapp/schema/ovn_northbound/commands.py       
2023-11-21 10:21:52.000000000 +0100
@@ -853,7 +853,7 @@
         self.networks = [str(netaddr.IPNetwork(net)) for net in networks]
         self.router = router
         self.port = port
-        self.peer = peer
+        self.peer = peer if peer else []
         self.may_exist = may_exist
         self.columns = columns
         super().__init__(api)
@@ -1137,9 +1137,117 @@
     table = 'BFD'
 
 
+class MirrorAddCommand(cmd.AddCommand):
+    # cmd.AddCommand uses self.table_name, other base commands use self.table
+    table_name = 'Mirror'
+
+    def __init__(self, api, name, mirror_type, index, direction_filter, dest,
+                 external_ids=None, may_exist=False):
+        self.name = name
+        self.direction_filter = direction_filter
+        self.mirror_type = mirror_type
+        if mirror_type != 'local':
+            self.dest = str(netaddr.IPAddress(dest))
+        else:
+            self.dest = dest
+        self.index = index
+
+        super().__init__(api)
+
+        self.columns = {
+            'name': name,
+            'filter': direction_filter,
+            'sink': dest,
+            'type': mirror_type,
+            'index': index,
+            'external_ids': external_ids or {},
+        }
+        self.may_exist = may_exist
+
+    def run_idl(self, txn):
+        try:
+            mirror_result = self.api.lookup(self.table_name, self.name)
+            self.result = rowview.RowView(mirror_result)
+            if self.may_exist:
+                self.set_columns(mirror_result, **self.columns)
+                self.result = rowview.RowView(mirror_result)
+                return
+            raise RuntimeError("Mirror %s already exists" % self.name)
+        except idlutils.RowNotFound:
+            pass
+
+        mirror = txn.insert(self.api.tables[self.table_name])
+        mirror.name = self.name
+        mirror.type = self.mirror_type
+        mirror.filter = self.direction_filter
+        mirror.sink = self.dest
+        mirror.index = self.index
+        self.set_columns(mirror, **self.columns)
+        # Setting the result to something other than a :class:`rowview.RowView`
+        # or :class:`ovs.db.idl.Row` typed value will make the parent
+        # `post_commit` method retrieve the newly insterted row from IDL and
+        # return that to the caller.
+        self.result = mirror.uuid
+
+
+class MirrorDelCommand(cmd.DbDestroyCommand):
+    table = 'Mirror'
+
+    def __init__(self, api, record):
+        super().__init__(api, self.table, record)
+
+
+class MirrorGetCommand(cmd.BaseGetRowCommand):
+    table = 'Mirror'
+
+
+class LspAttachMirror(cmd.BaseCommand):
+    def __init__(self, api, port, mirror, may_exist=False):
+        super().__init__(api)
+        self.port = port
+        self.mirror = mirror
+        self.may_exist = may_exist
+
+    def run_idl(self, txn):
+        try:
+            lsp = self.api.lookup('Logical_Switch_Port', self.port)
+            mirror = self.api.lookup('Mirror', self.mirror)
+            if mirror in lsp.mirror_rules and not self.may_exist:
+                msg = "Mirror Rule %s is already set on LSP %s" % (self.mirror,
+                                                                   self.port)
+                raise RuntimeError(msg)
+            lsp.addvalue('mirror_rules', self.mirror)
+        except idlutils.RowNotFound as e:
+            raise RuntimeError("LSP %s not found" % self.port) from e
+
+
+class LspDetachMirror(cmd.BaseCommand):
+    def __init__(self, api, port, mirror, if_exist=False):
+        super().__init__(api)
+        self.port = port
+        self.mirror = mirror
+        self.if_exist = if_exist
+
+    def run_idl(self, txn):
+        try:
+            lsp = self.api.lookup('Logical_Switch_Port', self.port)
+            mirror = self.api.lookup('Mirror', self.mirror)
+            if mirror not in lsp.mirror_rules and not self.if_exist:
+                msg = "Mirror Rule %s doesn't exist on LSP %s" % (self.mirror,
+                                                                  self.port)
+                raise RuntimeError(msg)
+            lsp.delvalue('mirror_rules', self.mirror)
+        except idlutils.RowNotFound as e:
+            if self.if_exists:
+                return
+            msg = "LSP %s doesn't exist" % self.port
+            raise RuntimeError(msg) from e
+
+
 class LrRouteAddCommand(cmd.BaseCommand):
     def __init__(self, api, router, prefix, nexthop, port=None,
-                 policy='dst-ip', may_exist=False):
+                 policy='dst-ip', may_exist=False, ecmp=False,
+                 route_table=const.MAIN_ROUTE_TABLE):
         prefix = str(netaddr.IPNetwork(prefix))
         if nexthop != const.ROUTE_DISCARD:
             nexthop = str(netaddr.IPAddress(nexthop))
@@ -1149,12 +1257,20 @@
         self.nexthop = nexthop
         self.port = port
         self.policy = policy
+        self.ecmp = ecmp
+        self.route_table = route_table
         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 (
+                self.prefix == route.ip_prefix and
+                self.route_table == route.route_table and
+                "ic-learned-route" not in route.external_ids
+            ):
+                if self.ecmp and self.nexthop != route.nexthop:
+                    continue
                 if not self.may_exist:
                     msg = "Route %s already exists on router %s" % (
                         self.prefix, self.router)
@@ -1169,6 +1285,7 @@
         route.ip_prefix = self.prefix
         route.nexthop = self.nexthop
         route.policy = self.policy
+        route.route_table = self.route_table
         if self.port:
             route.output_port = self.port
         lr.addvalue('static_routes', route)
@@ -1183,12 +1300,15 @@
 
 
 class LrRouteDelCommand(cmd.BaseCommand):
-    def __init__(self, api, router, prefix=None, if_exists=False):
+    def __init__(self, api, router, prefix=None, if_exists=False,
+                 nexthop=None, route_table=const.MAIN_ROUTE_TABLE):
         if prefix is not None:
             prefix = str(netaddr.IPNetwork(prefix))
         super().__init__(api)
         self.router = router
         self.prefix = prefix
+        self.nexthop = nexthop
+        self.route_table = route_table
         self.if_exists = if_exists
 
     def run_idl(self, txn):
@@ -1197,9 +1317,14 @@
             lr.static_routes = []
             return
         for route in lr.static_routes:
-            if self.prefix == route.ip_prefix:
+            if (
+                self.prefix == route.ip_prefix and
+                self.route_table == route.route_table
+            ):
+                if self.nexthop and route.nexthop != self.nexthop:
+                    continue
+
                 lr.delvalue('static_routes', route)
-                # There should only be one possible match
                 return
 
         if not self.if_exists:
@@ -1209,13 +1334,19 @@
 
 
 class LrRouteListCommand(cmd.ReadOnlyCommand):
-    def __init__(self, api, router):
+    def __init__(self, api, router, route_table=None):
         super().__init__(api)
         self.router = router
+        self.route_table = route_table
 
     def run_idl(self, txn):
         lr = self.api.lookup('Logical_Router', self.router)
-        self.result = [rowview.RowView(r) for r in lr.static_routes]
+        if self.route_table is not None:
+            self.result = [rowview.RowView(r)
+                           for r in lr.static_routes
+                           if r.route_table == self.route_table]
+        else:
+            self.result = [rowview.RowView(r) for r in lr.static_routes]
 
 
 class LrNatAddCommand(cmd.BaseCommand):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ovsdbapp-2.3.0/ovsdbapp/schema/ovn_northbound/impl_idl.py 
new/ovsdbapp-2.5.0/ovsdbapp/schema/ovn_northbound/impl_idl.py
--- old/ovsdbapp-2.3.0/ovsdbapp/schema/ovn_northbound/impl_idl.py       
2023-04-27 17:14:09.000000000 +0200
+++ new/ovsdbapp-2.5.0/ovsdbapp/schema/ovn_northbound/impl_idl.py       
2023-11-21 10:21:52.000000000 +0100
@@ -236,15 +236,18 @@
         return cmd.LrpDelNetworksCommand(self, port, networks, if_exists)
 
     def lr_route_add(self, router, prefix, nexthop, port=None,
-                     policy='dst-ip', may_exist=False):
+                     policy='dst-ip', may_exist=False, ecmp=False,
+                     route_table=const.MAIN_ROUTE_TABLE):
         return cmd.LrRouteAddCommand(self, router, prefix, nexthop, port,
-                                     policy, may_exist)
+                                     policy, may_exist, ecmp, route_table)
 
-    def lr_route_del(self, router, prefix=None, if_exists=False):
-        return cmd.LrRouteDelCommand(self, router, prefix, if_exists)
+    def lr_route_del(self, router, prefix=None, if_exists=False, nexthop=None,
+                     route_table=const.MAIN_ROUTE_TABLE):
+        return cmd.LrRouteDelCommand(self, router, prefix, if_exists, nexthop,
+                                     route_table)
 
-    def lr_route_list(self, router):
-        return cmd.LrRouteListCommand(self, router)
+    def lr_route_list(self, router, route_table=None):
+        return cmd.LrRouteListCommand(self, router, route_table)
 
     def lr_nat_add(self, router, nat_type, external_ip, logical_ip,
                    logical_port=None, external_mac=None, may_exist=False):
@@ -432,3 +435,25 @@
 
     def bfd_get(self, uuid):
         return cmd.BFDGetCommand(self, uuid)
+
+    def mirror_get(self, uuid):
+        return cmd.MirrorGetCommand(self, uuid)
+
+    def mirror_del(self, mirror):
+        return cmd.MirrorDelCommand(self, mirror)
+
+    def mirror_add(self, name, mirror_type, index, direction_filter,
+                   dest, external_ids=None, may_exist=False):
+        return cmd.MirrorAddCommand(self, name=name,
+                                    mirror_type=mirror_type,
+                                    index=index,
+                                    direction_filter=direction_filter,
+                                    dest=dest,
+                                    external_ids=external_ids,
+                                    may_exist=may_exist)
+
+    def lsp_attach_mirror(self, port, mirror, may_exist=False):
+        return cmd.LspAttachMirror(self, port, mirror, may_exist)
+
+    def lsp_detach_mirror(self, port, mirror, if_exist=False):
+        return cmd.LspDetachMirror(self, port, mirror, if_exist)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ovsdbapp-2.3.0/ovsdbapp/tests/functional/schema/hardware_vtep/test_impl_idl.py
 
new/ovsdbapp-2.5.0/ovsdbapp/tests/functional/schema/hardware_vtep/test_impl_idl.py
--- 
old/ovsdbapp-2.3.0/ovsdbapp/tests/functional/schema/hardware_vtep/test_impl_idl.py
  2023-04-27 17:14:09.000000000 +0200
+++ 
new/ovsdbapp-2.5.0/ovsdbapp/tests/functional/schema/hardware_vtep/test_impl_idl.py
  2023-11-21 10:21:52.000000000 +0100
@@ -13,6 +13,8 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import time
+
 from ovsdbapp.backend.ovs_idl import idlutils
 from ovsdbapp.schema.hardware_vtep.commands import get_global_record
 from ovsdbapp.tests.functional import base
@@ -352,6 +354,16 @@
         ]:
             self.addCleanup(self.ovsvenv.call, args)
 
+    def _wait_db_rows(self, table):
+        """Wait for rows in specified table. Raises RuntimeError otherwise."""
+
+        for _ in range(4):
+            if table.rows:
+                return
+            time.sleep(0.5)
+
+        raise RuntimeError("Table '%s' is empty" % table.name)
+
     def test_list_local_macs(self):
         local_macs = self.api.list_local_macs(
             self.ls.name).execute(check_error=True)
@@ -370,6 +382,7 @@
         ucast_table = self.api.tables['Ucast_Macs_Local']
         mcast_table = self.api.tables['Mcast_Macs_Local']
         for table in [ucast_table, mcast_table]:
+            self._wait_db_rows(table)
             self.assertEqual(len(table.rows), 1)
 
         self.api.clear_local_macs(self.ls.name).execute(check_error=True)
@@ -380,6 +393,7 @@
         ucast_table = self.api.tables['Ucast_Macs_Remote']
         mcast_table = self.api.tables['Mcast_Macs_Remote']
         for table in [ucast_table, mcast_table]:
+            self._wait_db_rows(table)
             self.assertEqual(len(table.rows), 1)
 
         self.api.clear_remote_macs(self.ls.name).execute(check_error=True)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ovsdbapp-2.3.0/ovsdbapp/tests/functional/schema/ovn_northbound/test_impl_idl.py
 
new/ovsdbapp-2.5.0/ovsdbapp/tests/functional/schema/ovn_northbound/test_impl_idl.py
--- 
old/ovsdbapp-2.3.0/ovsdbapp/tests/functional/schema/ovn_northbound/test_impl_idl.py
 2023-04-27 17:14:09.000000000 +0200
+++ 
new/ovsdbapp-2.5.0/ovsdbapp/tests/functional/schema/ovn_northbound/test_impl_idl.py
 2023-11-21 10:21:52.000000000 +0100
@@ -956,14 +956,18 @@
         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,
+                      ecmp=False, route_table=const.MAIN_ROUTE_TABLE,
                       **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'
         port = port or "port_name"
-        sr = self.api.lr_route_add(lr.uuid, prefix, nexthop, port,
-                                   **kwargs).execute(check_error=True)
+        sr = self.api.lr_route_add(
+            lr.uuid, prefix, nexthop, port,
+            ecmp=ecmp, route_table=route_table,
+            **kwargs
+        ).execute(check_error=True)
         self.assertIn(sr, lr.static_routes)
         self.assertEqual(prefix, sr.ip_prefix)
         self.assertEqual(nexthop, sr.nexthop)
@@ -992,11 +996,45 @@
         self._lr_add_route(router_name)
         self._lr_add_route(router_name, may_exist=True)
 
+    def test_lr_route_add_exists_ecmp(self):
+        router_name = utils.get_rand_device_name()
+        self._lr_add_route(router_name)
+        self._lr_add_route(router=router_name, nexthop='192.0.3.254',
+                           ecmp=True)
+
     def test_lr_route_add_discard(self):
         self._lr_add_route(nexthop=const.ROUTE_DISCARD)
         self.assertRaises(netaddr.AddrFormatError, self._lr_add_route,
                           prefix='not-discard')
 
+    def test_lr_route_add_route_table(self):
+        lr = self._lr_add()
+        route_table = "route-table"
+
+        # add route to 'main' route table
+        route = self._lr_add_route(lr.name)
+        self.assertEqual(route.route_table, const.MAIN_ROUTE_TABLE)
+
+        route = self._lr_add_route(lr.name, route_table=route_table)
+        self.assertEqual(route.route_table, route_table)
+
+        self.assertEqual(
+            len(self.api.tables['Logical_Router_Static_Route'].rows), 2)
+
+    def test_lr_route_add_learned_route_exist(self):
+        router_name = utils.get_rand_device_name()
+
+        learned_route = self._lr_add_route(router_name)
+        self.api.db_set(
+            'Logical_Router_Static_Route', learned_route.uuid,
+            ('external_ids', {'ic-learned-route': str(uuid.uuid4())})).execute(
+                check_error=True)
+
+        route = self._lr_add_route(router_name)
+
+        self.assertNotEqual(learned_route.uuid, route.uuid)
+        self.assertNotIn("ic-learned-route", route.external_ids)
+
     def test_lr_route_del(self):
         prefix = "192.0.2.0/25"
         route = self._lr_add_route(prefix=prefix)
@@ -1025,6 +1063,55 @@
         self.api.lr_route_del(lr.uuid, '192.0.2.0/25', if_exists=True).execute(
             check_error=True)
 
+    def test_lr_route_del_ecmp(self):
+        prefix = "10.0.0.0/24"
+        nexthop1 = "1.1.1.1"
+        nexthop2 = "2.2.2.2"
+        lr = self._lr_add()
+
+        self._lr_add_route(lr.uuid, prefix=prefix, nexthop=nexthop1)
+        ecmp_route = self._lr_add_route(lr.uuid, prefix=prefix,
+                                        nexthop=nexthop2, ecmp=True)
+        self.assertEqual(
+            len(self.api.tables['Logical_Router_Static_Route'].rows), 2)
+
+        self.api.lr_route_del(lr.uuid, prefix, nexthop=nexthop2).execute(
+            check_error=True)
+
+        self.assertNotIn(
+            ecmp_route.uuid,
+            len(self.api.tables['Logical_Router_Static_Route'].rows),
+        )
+
+        cmd = self.api.lr_route_del(lr.uuid, prefix, nexthop=nexthop2)
+        self.assertRaises(RuntimeError, cmd.execute, check_error=True)
+
+        self.assertEqual(
+            len(self.api.tables['Logical_Router_Static_Route'].rows), 1)
+
+    def test_lr_route_del_route_table(self):
+        lr = self._lr_add()
+        route_table = "route-table"
+
+        route_in_main = self._lr_add_route(lr.uuid, prefix="10.0.0.0/24")
+        route = self._lr_add_route(
+            lr.uuid, prefix="10.0.1.0/24", route_table=route_table)
+
+        self.assertEqual(len(lr.static_routes), 2)
+
+        # try to delete from the 'main' table implicitly
+        cmd = self.api.lr_route_del(lr.uuid, route.ip_prefix)
+        self.assertRaises(RuntimeError, cmd.execute, check_error=True)
+
+        self.api.lr_route_del(
+            lr.uuid, prefix=route.ip_prefix, route_table=route_table
+        ).execute(check_error=True)
+        self.assertEqual(len(lr.static_routes), 1)
+
+        self.api.lr_route_del(
+            lr.uuid, route_in_main.ip_prefix).execute(check_error=True)
+        self.assertEqual(len(lr.static_routes), 0)
+
     def test_lr_route_list(self):
         lr = self._lr_add()
         routes = {self._lr_add_route(lr.uuid, prefix="192.0.%s.0/25" % p)
@@ -1033,6 +1120,29 @@
             check_error=True))
         self.assertTrue(routes.issubset(route_set))
 
+    def test_lr_route_list_route_table(self):
+        lr = self._lr_add()
+        route_table = "route-table"
+
+        prefix1 = "10.0.0.0/24"
+        prefix2 = "10.0.1.0/24"
+
+        self._lr_add_route(lr.uuid, prefix=prefix1)
+        self._lr_add_route(lr.uuid, prefix=prefix2, route_table=route_table)
+
+        routes = self.api.lr_route_list(lr.uuid).execute(check_error=True)
+        self.assertEqual(len(routes), 2)  # all routes in logical router
+
+        for route_table, prefix in zip(
+            [const.MAIN_ROUTE_TABLE, route_table],
+            [prefix1, prefix2]
+        ):
+            routes = self.api.lr_route_list(
+                lr.uuid, route_table=route_table).execute(check_error=True)
+            self.assertEqual(len(routes), 1)
+            self.assertEqual(routes[0].ip_prefix, prefix)
+            self.assertEqual(routes[0].route_table, route_table)
+
     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(
@@ -1378,7 +1488,7 @@
     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):
+    def test_lrp_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)
@@ -1409,7 +1519,7 @@
         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)
+        self._lrp_add(*args, may_exist=True)
 
     def test_lrp_add_may_exist_different_router(self):
         name = utils.get_rand_device_name()
@@ -2526,3 +2636,137 @@
         b1 = self.api.bfd_add(name, name).execute(check_error=True)
         b2 = self.api.bfd_get(b1.uuid).execute(check_error=True)
         self.assertEqual(b1, b2)
+
+
+class TestMirrorOps(OvnNorthboundTest):
+
+    def setUp(self):
+        super(TestMirrorOps, self).setUp()
+        self.table = self.api.tables['Mirror']
+        self.switch = self.useFixture(
+            fixtures.LogicalSwitchFixture(self.api)).obj
+        lsp_add_cmd = self.api.lsp_add(self.switch.uuid, 'testport')
+        with self.api.transaction(check_error=True) as txn:
+            txn.add(lsp_add_cmd)
+
+        self.port_uuid = lsp_add_cmd.result.uuid
+
+    def _mirror_add(self, name=None, direction_filter='to-lport',
+                    dest='10.11.1.1', mirror_type='gre', index=42, **kwargs):
+        if not name:
+            name = utils.get_rand_name()
+        cmd = self.api.mirror_add(name, mirror_type, index, direction_filter,
+                                  dest, **kwargs)
+        row = cmd.execute(check_error=True)
+        self.assertEqual(cmd.name, row.name)
+        self.assertEqual(cmd.direction_filter, row.filter)
+        self.assertEqual(cmd.dest, row.sink)
+        self.assertEqual(cmd.mirror_type, row.type)
+        self.assertEqual(cmd.index, row.index)
+        return idlutils.frozen_row(row)
+
+    def test_mirror_addx(self):
+        self._mirror_add(dest='10.13.1.1')
+
+    def test_mirror_add_duplicate(self):
+        name = utils.get_rand_name()
+        cmd = self.api.mirror_add(name, 'gre', 100, 'from-lport',
+                                  '192.169.1.1')
+        cmd.execute(check_error=True)
+        self.assertRaises(RuntimeError, cmd.execute, check_error=True)
+
+    def test_mirror_add_may_exist_no_change(self):
+        name = utils.get_rand_name()
+        mirror1 = self._mirror_add(name=name, dest='10.18.1.1')
+        mirror2 = self._mirror_add(name=name, dest='10.18.1.1',
+                                   may_exist=True)
+        self.assertEqual(mirror1, mirror2)
+
+    def test_mirror_add_may_exist_change(self):
+        name = utils.get_rand_name()
+        mirror1 = self._mirror_add(name=name, dest='10.12.1.0')
+        mirror2 = self._mirror_add(
+            name=name, direction_filter='from-lport', dest='10.12.1.0',
+            mirror_type='gre', index=100, may_exist=True,
+        )
+        self.assertNotEqual(mirror1, mirror2)
+        self.assertEqual(mirror1.uuid, mirror2.uuid)
+
+    def test_mirror_del(self):
+        name = utils.get_rand_name()
+        mirror1 = self._mirror_add(name=name, dest='10.14.1.0')
+        self.assertIn(mirror1.uuid, self.table.rows)
+        self.api.mirror_del(mirror1.uuid).execute(check_error=True)
+        self.assertNotIn(mirror1.uuid, self.table.rows)
+
+    def test_mirror_get(self):
+        name = utils.get_rand_name()
+        mirror1 = self.api.mirror_add(name, 'gre', 100, 'from-lport',
+                                      '10.15.1.1').execute(check_error=True)
+        mirror2 = self.api.mirror_get(mirror1.uuid).execute(check_error=True)
+        self.assertEqual(mirror1, mirror2)
+
+    def test_lsp_attach_detach_mirror(self):
+        mirror = self._mirror_add(name='my_mirror')
+        self.api.lsp_attach_mirror(
+            self.port_uuid, mirror.uuid).execute(check_error=True)
+        port = self.api.lsp_get(self.port_uuid).execute(check_error=True)
+
+        self.assertEqual(1, len(port.mirror_rules))
+        mir_rule = self.api.lookup('Mirror', port.mirror_rules[0].uuid)
+        self.assertEqual(mirror.uuid, mir_rule.uuid)
+
+        self.api.lsp_detach_mirror(
+            self.port_uuid, mirror.uuid).execute(check_error=True)
+        port = self.api.lsp_get(self.port_uuid).execute(check_error=True)
+
+        self.assertEqual(0, len(port.mirror_rules))
+
+    def test_lsp_attach_detach_may_exist(self):
+        mirror1 = self._mirror_add(name='mirror1')
+        self.api.lsp_attach_mirror(
+            self.port_uuid, mirror1.uuid).execute(check_error=True)
+        mirror2 = self._mirror_add(name='mirror2', dest='10.17.1.0')
+
+        # Try to attach a mirror to a port which already has mirror_rule
+        # attached
+        failing_cmd = self.api.lsp_attach_mirror(
+            self.port_uuid, mirror1.uuid,
+            may_exist=False)
+        self.assertRaises(
+            RuntimeError,
+            failing_cmd.execute,
+            check_error=True
+        )
+
+        self.api.lsp_attach_mirror(
+            self.port_uuid, mirror2.uuid,
+            may_exist=True).execute(check_error=True)
+        check_res = self.api.lsp_get(self.port_uuid).execute(check_error=True)
+        rule_on_lsp = False
+        for m_rule in check_res.mirror_rules:
+            if mirror2.uuid == m_rule.uuid:
+                rule_on_lsp = True
+        self.assertTrue(rule_on_lsp)
+
+        self.api.lsp_detach_mirror(
+            self.port_uuid, mirror2.uuid).execute(check_error=True)
+        port = self.api.lsp_get(self.port_uuid).execute(check_error=True)
+        self.assertEqual(1, len(port.mirror_rules))
+        self.assertEqual(mirror1.uuid, port.mirror_rules[0].uuid)
+
+        # Try to detach a rule that is already detached
+        failing_cmd = self.api.lsp_detach_mirror(
+            self.port_uuid, mirror2.uuid)
+        self.assertRaises(
+            RuntimeError,
+            failing_cmd.execute,
+            check_error=True
+        )
+
+        # detach with if_exist=True, and check the result, to be as previously
+        self.api.lsp_detach_mirror(
+            self.port_uuid, mirror2.uuid,
+            if_exist=True).execute(check_error=True)
+        self.assertEqual(1, len(port.mirror_rules))
+        self.assertEqual(mirror1.uuid, port.mirror_rules[0].uuid)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ovsdbapp-2.3.0/ovsdbapp/tests/functional/schema/ovn_southbound/event.py 
new/ovsdbapp-2.5.0/ovsdbapp/tests/functional/schema/ovn_southbound/event.py
--- old/ovsdbapp-2.3.0/ovsdbapp/tests/functional/schema/ovn_southbound/event.py 
2023-04-27 17:14:09.000000000 +0200
+++ new/ovsdbapp-2.5.0/ovsdbapp/tests/functional/schema/ovn_southbound/event.py 
2023-11-21 10:21:52.000000000 +0100
@@ -28,3 +28,14 @@
 
     def match_fn(self, event, row, old):
         raise Exception()
+
+
+class MatchFnConditionsEvent(event.WaitEvent):
+    def __init__(self, *args, timeout=1, **kwargs):
+        super().__init__(*args, timeout=timeout, **kwargs)
+
+    def match_fn(self, event, row, old):
+        # This should only be called if we pass all other conditions
+        # so make sure wait() returns False if we see any other events. This
+        # ensures that adding a match_fn() doesn't skip the conditions checks.
+        return True
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/ovsdbapp-2.3.0/ovsdbapp/tests/functional/schema/ovn_southbound/test_impl_idl.py
 
new/ovsdbapp-2.5.0/ovsdbapp/tests/functional/schema/ovn_southbound/test_impl_idl.py
--- 
old/ovsdbapp-2.3.0/ovsdbapp/tests/functional/schema/ovn_southbound/test_impl_idl.py
 2023-04-27 17:14:09.000000000 +0200
+++ 
new/ovsdbapp-2.5.0/ovsdbapp/tests/functional/schema/ovn_southbound/test_impl_idl.py
 2023-11-21 10:21:52.000000000 +0100
@@ -138,3 +138,19 @@
     def test_lsp_unbind_if_exists(self):
         pname = utils.get_rand_device_name()
         self.api.lsp_unbind(pname, if_exists=True).execute(check_error=True)
+
+    def test_event_with_match_fn_and_conditions(self):
+        cond_event = event.MatchFnConditionsEvent(
+            events=(event.MatchFnConditionsEvent.ROW_UPDATE,),
+            table="SB_Global",
+            conditions=(("external_ids", "=", {"foo": "bar"}),))
+        self.handler.watch_event(cond_event)
+        # Test that we match on condition with an Event that has a match_fn()
+        cmd = self.api.db_set("SB_Global", ".", external_ids={"foo": "bar"})
+        cmd.execute(check_error=True)
+        self.assertTrue(cond_event.wait())
+        cond_event.event.clear()
+        # Test that we don't ignore the condition when match_fn() returns True
+        cmd = self.api.db_set("SB_Global", ".", external_ids={"bar": "bar"})
+        cmd.execute(check_error=True)
+        self.assertFalse(cond_event.wait())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ovsdbapp-2.3.0/ovsdbapp/venv.py 
new/ovsdbapp-2.5.0/ovsdbapp/venv.py
--- old/ovsdbapp-2.3.0/ovsdbapp/venv.py 2023-04-27 17:14:09.000000000 +0200
+++ new/ovsdbapp-2.5.0/ovsdbapp/venv.py 2023-11-21 10:21:52.000000000 +0100
@@ -282,10 +282,12 @@
 class OvsVtepVenvFixture(OvsOvnVenvFixture):
     VTEP_SCHEMA = 'vtep.ovsschema'
 
-    def __init__(self, venv, vtepdir=None, **kwargs):
+    def __init__(self, venv, **kwargs):
+        vtepdir = os.getenv('VTEP_SRCDIR')
         if vtepdir and os.path.isdir(vtepdir):
             self.PATH_VAR_TEMPLATE += ":{0}".format(vtepdir)
-        self.vtepdir = self._share_path(self.OVS_PATHS, vtepdir)
+        self.vtepdir = self._share_path(self.OVS_PATHS, vtepdir,
+                                        [self.VTEP_SCHEMA])
         super().__init__(venv, **kwargs)
 
     def _setUp(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ovsdbapp-2.3.0/ovsdbapp.egg-info/PKG-INFO 
new/ovsdbapp-2.5.0/ovsdbapp.egg-info/PKG-INFO
--- old/ovsdbapp-2.3.0/ovsdbapp.egg-info/PKG-INFO       2023-04-27 
17:14:39.000000000 +0200
+++ new/ovsdbapp-2.5.0/ovsdbapp.egg-info/PKG-INFO       2023-11-21 
10:22:19.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: ovsdbapp
-Version: 2.3.0
+Version: 2.5.0
 Summary: A library for creating OVSDB applications
 Home-page: https://pypi.org/project/ovsdbapp/
 Author: OpenStack
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ovsdbapp-2.3.0/ovsdbapp.egg-info/SOURCES.txt 
new/ovsdbapp-2.5.0/ovsdbapp.egg-info/SOURCES.txt
--- old/ovsdbapp-2.3.0/ovsdbapp.egg-info/SOURCES.txt    2023-04-27 
17:14:39.000000000 +0200
+++ new/ovsdbapp-2.5.0/ovsdbapp.egg-info/SOURCES.txt    2023-11-21 
10:22:19.000000000 +0100
@@ -19,6 +19,8 @@
 doc/source/contributor/index.rst
 doc/source/install/index.rst
 doc/source/user/index.rst
+doc/source/user/overview.rst
+doc/source/user/tutorial.rst
 ovsdbapp/CHANGES
 ovsdbapp/__init__.py
 ovsdbapp/api.py
@@ -129,6 +131,7 @@
 releasenotes/notes/provide-lrp-get-method-a33a99a7f86b827e.yaml
 releasenotes/notes/provide-lrp-networks-modifying-1af13589064c12c6.yaml
 releasenotes/source/2023.1.rst
+releasenotes/source/2023.2.rst
 releasenotes/source/conf.py
 releasenotes/source/index.rst
 releasenotes/source/pike.rst
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ovsdbapp-2.3.0/ovsdbapp.egg-info/pbr.json 
new/ovsdbapp-2.5.0/ovsdbapp.egg-info/pbr.json
--- old/ovsdbapp-2.3.0/ovsdbapp.egg-info/pbr.json       2023-04-27 
17:14:39.000000000 +0200
+++ new/ovsdbapp-2.5.0/ovsdbapp.egg-info/pbr.json       2023-11-21 
10:22:19.000000000 +0100
@@ -1 +1 @@
-{"git_version": "770f77d", "is_release": true}
\ No newline at end of file
+{"git_version": "8e55b0b", "is_release": true}
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ovsdbapp-2.3.0/releasenotes/source/2023.2.rst 
new/ovsdbapp-2.5.0/releasenotes/source/2023.2.rst
--- old/ovsdbapp-2.3.0/releasenotes/source/2023.2.rst   1970-01-01 
01:00:00.000000000 +0100
+++ new/ovsdbapp-2.5.0/releasenotes/source/2023.2.rst   2023-11-21 
10:21:52.000000000 +0100
@@ -0,0 +1,6 @@
+===========================
+2023.2 Series Release Notes
+===========================
+
+.. release-notes::
+   :branch: stable/2023.2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ovsdbapp-2.3.0/releasenotes/source/index.rst 
new/ovsdbapp-2.5.0/releasenotes/source/index.rst
--- old/ovsdbapp-2.3.0/releasenotes/source/index.rst    2023-04-27 
17:14:09.000000000 +0200
+++ new/ovsdbapp-2.5.0/releasenotes/source/index.rst    2023-11-21 
10:21:52.000000000 +0100
@@ -6,6 +6,7 @@
    :maxdepth: 1
 
    unreleased
+   2023.2
    2023.1
    zed
    yoga
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ovsdbapp-2.3.0/tox.ini new/ovsdbapp-2.5.0/tox.ini
--- old/ovsdbapp-2.3.0/tox.ini  2023-04-27 17:14:09.000000000 +0200
+++ new/ovsdbapp-2.5.0/tox.ini  2023-11-21 10:21:52.000000000 +0100
@@ -65,7 +65,7 @@
          OS_TEST_PATH=./ovsdbapp/tests/functional
          OVN_SRCDIR={envdir}/src/ovn
          OVS_SRCDIR={envdir}/src/ovn/ovs
-         VTEP_SRCDIR={envdir}src/ovn/ovs/vtep
+         VTEP_SRCDIR={envdir}/src/ovn/ovs/vtep
          OVN_BRANCH={env:OVN_BRANCH:}
 passenv = KEEP_VENV
 commands =

Reply via email to