diff --git a/SOURCES/openvswitch-3.1.0.patch b/SOURCES/openvswitch-3.1.0.patch
index 57f7d84..5206c13 100644
--- a/SOURCES/openvswitch-3.1.0.patch
+++ b/SOURCES/openvswitch-3.1.0.patch
@@ -349,10 +349,33 @@ index 82675b973..80c449336 100644
      - name: update APT cache
        run:  sudo apt update || true
 diff --git a/Documentation/ref/ovs-actions.7.rst b/Documentation/ref/ovs-actions.7.rst
-index b59b7634f..d13895655 100644
+index b59b7634f..36adcc5db 100644
 --- a/Documentation/ref/ovs-actions.7.rst
 +++ b/Documentation/ref/ovs-actions.7.rst
-@@ -1380,7 +1380,7 @@ The ``delete_field`` action
+@@ -694,7 +694,8 @@ encapsulated in an OpenFlow ``packet-in`` message.  The supported options are:
+     Limit to *max_len* the number of bytes of the packet to send in the
+     ``packet-in.``  A *max_len* of 0 prevents any of the packet from being
+     sent (thus, only metadata is included).  By default, the entire packet is
+-    sent, equivalent to a *max_len* of 65535.
++    sent, equivalent to a *max_len* of 65535.  This option has no effect in
++    Open vSwith 2.7 and later: the entire packet will always be sent.
+ 
+   ``reason=``\ *reason*
+     Specify *reason* as the reason for sending the message in the
+@@ -733,6 +734,12 @@ encapsulated in an OpenFlow ``packet-in`` message.  The supported options are:
+   options require the Open vSwitch ``NXAST_CONTROLLER`` extension action added
+   in Open vSwitch 1.6.
+ 
++  Open vSwitch 2.7 and later is configured to not buffer packets for the
++  packet-in event.  As a result, the full packet is always sent to
++  controllers.  This means that the ``max_len`` option has no effect on the
++  ``controller`` action, and all values (even 0) are equivalent to the default
++  value of 65535.
++
+ 
+ The ``enqueue`` action
+ ----------------------
+@@ -1380,7 +1387,7 @@ The ``delete_field`` action
    | ``delete_field:``\ *field*
  
  The ``delete_field`` action deletes a *field* in the syntax described under
@@ -362,7 +385,7 @@ index b59b7634f..d13895655 100644
  
  This action was added in Open vSwitch 2.14.
 diff --git a/Makefile.am b/Makefile.am
-index e605187b8..49d98d6b2 100644
+index e605187b8..eddb981ae 100644
 --- a/Makefile.am
 +++ b/Makefile.am
 @@ -75,6 +75,8 @@ EXTRA_DIST = \
@@ -383,6 +406,15 @@ index e605187b8..49d98d6b2 100644
  	  if grep warning: $@.tmp; then error=:; fi; \
  	  rm -f $@.tmp; \
  	done; \
+@@ -412,7 +414,7 @@ endif
+ CLEANFILES += flake8-check
+ 
+ -include manpages.mk
+-manpages.mk: $(MAN_ROOTS) build-aux/sodepends.py python/build/soutil.py
++manpages.mk: $(MAN_ROOTS) build-aux/sodepends.py python/ovs_build_helpers/soutil.py
+ 	@PYTHONPATH=$$PYTHONPATH$(psep)$(srcdir)/python $(PYTHON3) $(srcdir)/build-aux/sodepends.py -I. -I$(srcdir) $(MAN_ROOTS) >$(@F).tmp
+ 	@if cmp -s $(@F).tmp $@; then \
+ 	  touch $@; \
 diff --git a/NEWS b/NEWS
 index 37a01dea5..8fbf7219d 100644
 --- a/NEWS
@@ -405,9 +437,21 @@ index 37a01dea5..8fbf7219d 100644
  --------------------
     - ovs-vswitchd now detects changes in CPU affinity and adjusts the number
 diff --git a/build-aux/extract-ofp-fields b/build-aux/extract-ofp-fields
-index efec59c25..6366bd446 100755
+index efec59c25..05d3e1df3 100755
 --- a/build-aux/extract-ofp-fields
 +++ b/build-aux/extract-ofp-fields
+@@ -4,9 +4,9 @@ import getopt
+ import sys
+ import os.path
+ import xml.dom.minidom
+-import build.nroff
+ 
+-from build.extract_ofp_fields import (
++from ovs_build_helpers import nroff
++from ovs_build_helpers.extract_ofp_fields import (
+     extract_ofp_fields,
+     PREREQS,
+     OXM_CLASSES,
 @@ -216,7 +216,7 @@ def field_to_xml(field_node, f, body, summary):
          """.PP
  \\fB%s Field\\fR
@@ -417,8 +461,26 @@ index efec59c25..6366bd446 100755
  l lx.
  """
          % title
-@@ -317,7 +317,7 @@ def group_xml_to_nroff(group_node, fields):
-         '.SH "%s"\n' % build.nroff.text_to_nroff(title.upper() + " FIELDS"),
+@@ -297,7 +297,7 @@ l lx.
+     body += [".TE\n"]
+ 
+     body += [".PP\n"]
+-    body += [build.nroff.block_xml_to_nroff(field_node.childNodes)]
++    body += [nroff.block_xml_to_nroff(field_node.childNodes)]
+ 
+ 
+ def group_xml_to_nroff(group_node, fields):
+@@ -310,14 +310,14 @@ def group_xml_to_nroff(group_node, fields):
+             id_ = node.attributes["id"].nodeValue
+             field_to_xml(node, fields[id_], body, summary)
+         else:
+-            body += [build.nroff.block_xml_to_nroff([node])]
++            body += [nroff.block_xml_to_nroff([node])]
+ 
+     content = [
+         ".bp\n",
+-        '.SH "%s"\n' % build.nroff.text_to_nroff(title.upper() + " FIELDS"),
++        '.SH "%s"\n' % nroff.text_to_nroff(title.upper() + " FIELDS"),
          '.SS "Summary:"\n',
          ".TS\n",
 -        "tab(;);\n",
@@ -435,6 +497,105 @@ index efec59c25..6366bd446 100755
  l l l.
  Prefix;Vendor;Class
  \_;\_;\_
+@@ -422,7 +422,7 @@ ovs\-fields \- protocol header fields in OpenFlow and Open vSwitch
+         elif node.nodeType == node.COMMENT_NODE:
+             pass
+         else:
+-            s += build.nroff.block_xml_to_nroff([node])
++            s += nroff.block_xml_to_nroff([node])
+ 
+     for f in fields:
+         if "used" not in f:
+diff --git a/build-aux/gen_ofp_field_decoders b/build-aux/gen_ofp_field_decoders
+index 0b797ee8c..0cb6108c2 100755
+--- a/build-aux/gen_ofp_field_decoders
++++ b/build-aux/gen_ofp_field_decoders
+@@ -2,7 +2,7 @@
+ 
+ import argparse
+ 
+-import build.extract_ofp_fields as extract_fields
++from ovs_build_helpers.extract_ofp_fields import extract_ofp_fields
+ 
+ 
+ def main():
+@@ -19,7 +19,7 @@ def main():
+ 
+     args = parser.parse_args()
+ 
+-    fields = extract_fields.extract_ofp_fields(args.metaflow)
++    fields = extract_ofp_fields(args.metaflow)
+ 
+     field_decoders = {}
+     aliases = {}
+diff --git a/build-aux/sodepends.py b/build-aux/sodepends.py
+index 45812bcbd..ac8dd61a4 100755
+--- a/build-aux/sodepends.py
++++ b/build-aux/sodepends.py
+@@ -14,9 +14,10 @@
+ # See the License for the specific language governing permissions and
+ # limitations under the License.
+ 
+-from build import soutil
+ import sys
+ 
++from ovs_build_helpers import soutil
++
+ 
+ def sodepends(include_dirs, filenames, dst):
+     ok = True
+diff --git a/build-aux/soexpand.py b/build-aux/soexpand.py
+index 00adcf47a..7d4dc0486 100755
+--- a/build-aux/soexpand.py
++++ b/build-aux/soexpand.py
+@@ -14,9 +14,10 @@
+ # See the License for the specific language governing permissions and
+ # limitations under the License.
+ 
+-from build import soutil
+ import sys
+ 
++from ovs_build_helpers import soutil
++
+ 
+ def soexpand(include_dirs, src, dst):
+     ok = True
+diff --git a/build-aux/xml2nroff b/build-aux/xml2nroff
+index ee5553f45..3e937910b 100755
+--- a/build-aux/xml2nroff
++++ b/build-aux/xml2nroff
+@@ -18,7 +18,7 @@ import getopt
+ import sys
+ import xml.dom.minidom
+ 
+-import build.nroff
++from ovs_build_helpers import nroff
+ 
+ argv0 = sys.argv[0]
+ 
+@@ -90,10 +90,10 @@ def manpage_to_nroff(xml_file, subst, include_path, version=None):
+ .  I "\\$1"
+ .  RE
+ ..
+-''' % (build.nroff.text_to_nroff(program), build.nroff.text_to_nroff(section),
+-       build.nroff.text_to_nroff(title), build.nroff.text_to_nroff(version))
++''' % (nroff.text_to_nroff(program), nroff.text_to_nroff(section),
++       nroff.text_to_nroff(title), nroff.text_to_nroff(version))
+ 
+-    s += build.nroff.block_xml_to_nroff(doc.childNodes) + "\n"
++    s += nroff.block_xml_to_nroff(doc.childNodes) + "\n"
+ 
+     return s
+ 
+@@ -139,7 +139,7 @@ if __name__ == "__main__":
+ 
+     try:
+         s = manpage_to_nroff(args[0], subst, include_path, version)
+-    except build.nroff.error.Error as e:
++    except nroff.error.Error as e:
+         sys.stderr.write("%s: %s\n" % (argv0, e.msg))
+         sys.exit(1)
+     for line in s.splitlines():
 diff --git a/configure.ac b/configure.ac
 index 9bf896c01..48169c8fd 100644
 --- a/configure.ac
@@ -775,10 +936,26 @@ index d12d9b8a5..41b23d8ae 100644
      free(argcopy);
      return 0;
 diff --git a/lib/dpif-netdev.c b/lib/dpif-netdev.c
-index c9f7179c3..a0878086e 100644
+index c9f7179c3..10371359c 100644
 --- a/lib/dpif-netdev.c
 +++ b/lib/dpif-netdev.c
-@@ -4191,7 +4191,7 @@ flow_put_on_pmd(struct dp_netdev_pmd_thread *pmd,
+@@ -3365,14 +3365,13 @@ static inline void
+ netdev_flow_key_init(struct netdev_flow_key *key,
+                      const struct flow *flow)
+ {
+-    uint64_t *dst = miniflow_values(&key->mf);
+     uint32_t hash = 0;
+     uint64_t value;
+ 
+     miniflow_map_init(&key->mf, flow);
+     miniflow_init(&key->mf, flow);
+ 
+-    size_t n = dst - miniflow_get_values(&key->mf);
++    size_t n = miniflow_n_values(&key->mf);
+ 
+     FLOW_FOR_EACH_IN_MAPS (value, flow, key->mf.map) {
+         hash = hash_add64(hash, value);
+@@ -4191,7 +4190,7 @@ flow_put_on_pmd(struct dp_netdev_pmd_thread *pmd,
                  const struct dpif_flow_put *put,
                  struct dpif_flow_stats *stats)
  {
@@ -787,7 +964,7 @@ index c9f7179c3..a0878086e 100644
      int error = 0;
  
      if (stats) {
-@@ -4199,16 +4199,35 @@ flow_put_on_pmd(struct dp_netdev_pmd_thread *pmd,
+@@ -4199,16 +4198,35 @@ flow_put_on_pmd(struct dp_netdev_pmd_thread *pmd,
      }
  
      ovs_mutex_lock(&pmd->flow_mutex);
@@ -831,7 +1008,7 @@ index c9f7179c3..a0878086e 100644
              struct dp_netdev_actions *new_actions;
              struct dp_netdev_actions *old_actions;
  
-@@ -4239,15 +4258,11 @@ flow_put_on_pmd(struct dp_netdev_pmd_thread *pmd,
+@@ -4239,15 +4257,11 @@ flow_put_on_pmd(struct dp_netdev_pmd_thread *pmd,
                   *   counter, and subtracting it before outputting the stats */
                  error = EOPNOTSUPP;
              }
@@ -849,7 +1026,7 @@ index c9f7179c3..a0878086e 100644
      ovs_mutex_unlock(&pmd->flow_mutex);
      return error;
  }
-@@ -9616,6 +9631,7 @@ dpif_netdev_bond_stats_get(struct dpif *dpif, uint32_t bond_id,
+@@ -9616,6 +9630,7 @@ dpif_netdev_bond_stats_get(struct dpif *dpif, uint32_t bond_id,
  const struct dpif_class dpif_netdev_class = {
      "netdev",
      true,                       /* cleanup_required */
@@ -4322,6 +4499,19 @@ index 191befcae..bf5d083cc 100644
          } else if (mcs == dbmon->new_change_set) {
              dbmon->new_change_set = NULL;
          }
+diff --git a/ovsdb/ovsdb-doc b/ovsdb/ovsdb-doc
+index 10d0c0c13..099770d25 100755
+--- a/ovsdb/ovsdb-doc
++++ b/ovsdb/ovsdb-doc
+@@ -24,7 +24,7 @@ import ovs.json
+ from ovs.db import error
+ import ovs.db.schema
+ 
+-from build.nroff import *
++from ovs_build_helpers.nroff import *
+ 
+ argv0 = sys.argv[0]
+ 
 diff --git a/ovsdb/ovsdb-server.c b/ovsdb/ovsdb-server.c
 index 33ca4910d..b7b4d1559 100644
 --- a/ovsdb/ovsdb-server.c
@@ -4801,6 +4991,58 @@ index 01bb80e28..3c93ae580 100644
              json_destroy(txn_json);
              t->reply = jsonrpc_create_reply(json_object_create(),
                                              t->request->id);
+diff --git a/python/automake.mk b/python/automake.mk
+index d00911828..b0f444169 100644
+--- a/python/automake.mk
++++ b/python/automake.mk
+@@ -64,10 +64,10 @@ ovs_pytests = \
+ # These python files are used at build time but not runtime,
+ # so they are not installed.
+ EXTRA_DIST += \
+-	python/build/__init__.py \
+-	python/build/extract_ofp_fields.py \
+-	python/build/nroff.py \
+-	python/build/soutil.py
++	python/ovs_build_helpers/__init__.py \
++	python/ovs_build_helpers/extract_ofp_fields.py \
++	python/ovs_build_helpers/nroff.py \
++	python/ovs_build_helpers/soutil.py
+ 
+ # PyPI support.
+ EXTRA_DIST += \
+@@ -86,10 +86,10 @@ PYCOV_CLEAN_FILES += $(PYFILES:.py=.py,cover)
+ 
+ FLAKE8_PYFILES += \
+ 	$(filter-out python/ovs/compat/% python/ovs/dirs.py,$(PYFILES)) \
+-	python/build/__init__.py \
+-	python/build/extract_ofp_fields.py \
+-	python/build/nroff.py \
+-	python/build/soutil.py \
++	python/ovs_build_helpers/__init__.py \
++	python/ovs_build_helpers/extract_ofp_fields.py \
++	python/ovs_build_helpers/nroff.py \
++	python/ovs_build_helpers/soutil.py \
+ 	python/ovs/dirs.py.template \
+ 	python/setup.py
+ 
+@@ -110,11 +110,14 @@ ovs-install-data-local:
+ 	$(INSTALL_DATA) python/ovs/dirs.py.tmp $(DESTDIR)$(pkgdatadir)/python/ovs/dirs.py
+ 	rm python/ovs/dirs.py.tmp
+ 
++.PHONY: python-sdist
+ python-sdist: $(srcdir)/python/ovs/version.py $(ovs_pyfiles) python/ovs/dirs.py
+-	(cd python/ && $(PYTHON3) setup.py sdist)
++	cd python/ && $(PYTHON3) -m build --sdist
++
++.PHONY: pypi-upload
++pypi-upload: python-sdist
++	twine upload python/dist/ovs-$(VERSION).tar.gz
+ 
+-pypi-upload: $(srcdir)/python/ovs/version.py $(ovs_pyfiles) python/ovs/dirs.py
+-	(cd python/ && $(PYTHON3) setup.py sdist upload)
+ install-data-local: ovs-install-data-local
+ 
+ UNINSTALL_LOCAL += ovs-uninstall-local
 diff --git a/python/ovs/stream.py b/python/ovs/stream.py
 index ac5b0fd0c..b32341076 100644
 --- a/python/ovs/stream.py
@@ -4815,6 +5057,22 @@ index ac5b0fd0c..b32341076 100644
              return ovs.socket_util.get_exception_errno(e)
  
          return 0
+diff --git a/python/build/__init__.py b/python/ovs_build_helpers/__init__.py
+similarity index 100%
+rename from python/build/__init__.py
+rename to python/ovs_build_helpers/__init__.py
+diff --git a/python/build/extract_ofp_fields.py b/python/ovs_build_helpers/extract_ofp_fields.py
+similarity index 100%
+rename from python/build/extract_ofp_fields.py
+rename to python/ovs_build_helpers/extract_ofp_fields.py
+diff --git a/python/build/nroff.py b/python/ovs_build_helpers/nroff.py
+similarity index 100%
+rename from python/build/nroff.py
+rename to python/ovs_build_helpers/nroff.py
+diff --git a/python/build/soutil.py b/python/ovs_build_helpers/soutil.py
+similarity index 100%
+rename from python/build/soutil.py
+rename to python/ovs_build_helpers/soutil.py
 diff --git a/tests/.gitignore b/tests/.gitignore
 index 83b1cb3b4..3a8c45975 100644
 --- a/tests/.gitignore
diff --git a/SPECS/openvswitch3.1.spec b/SPECS/openvswitch3.1.spec
index a1ff67e..c3047a6 100644
--- a/SPECS/openvswitch3.1.spec
+++ b/SPECS/openvswitch3.1.spec
@@ -57,7 +57,7 @@ Summary: Open vSwitch
 Group: System Environment/Daemons daemon/database/utilities
 URL: http://www.openvswitch.org/
 Version: 3.1.0
-Release: 48%{?dist}
+Release: 49%{?dist}
 
 # Nearly all of openvswitch is ASL 2.0.  The bugtool is LGPLv2+, and the
 # lib/sflow*.[ch] files are SISSL
@@ -754,6 +754,16 @@ exit 0
 %endif
 
 %changelog
+* Fri Aug 25 2023 Open vSwitch CI <ovs-ci@redhat.com> - 3.1.0-49
+- Merging upstream branch-3.1 [RH git: b0b5197296]
+    Commit list:
+    ed1b5f0c6b python: Use build to generate PEP517 compatible archives.
+    4d7a3d1621 python: Use twine to upload sdist package to pypi.org.
+    8cf0163595 python: Rename build related code to ovs_build_helpers.
+    2a0b280558 dpif-netdev: Fix length calculation of netdet_flow_key.
+    0ba4e07c8c doc: Fix description of max_len for controller action.
+
+
 * Fri Aug 25 2023 Open vSwitch CI <ovs-ci@redhat.com> - 3.1.0-48
 - Merging upstream branch-3.1 [RH git: cbe033d0b2]
     Commit list: