Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-certbot for openSUSE:Factory 
checked in at 2026-03-16 15:49:10
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-certbot (Old)
 and      /work/SRC/openSUSE:Factory/.python-certbot.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-certbot"

Mon Mar 16 15:49:10 2026 rev:65 rq:1339343 version:5.4.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-certbot/python-certbot.changes    
2026-02-24 18:31:22.936203306 +0100
+++ /work/SRC/openSUSE:Factory/.python-certbot.new.8177/python-certbot.changes  
2026-03-16 15:49:16.334153905 +0100
@@ -1,0 +2,7 @@
+Mon Mar 16 12:12:03 UTC 2026 - Markéta Machová <[email protected]>
+
+- Update to 5.4.0 
+  * The webroot plugin now supports IP address issuance.
+- Drop merged patch reset-mock-call-count.patch
+
+-------------------------------------------------------------------

Old:
----
  certbot-5.3.1.tar.gz
  reset-mock-call-count.patch

New:
----
  certbot-5.4.0.tar.gz

----------(Old B)----------
  Old:  * The webroot plugin now supports IP address issuance.
- Drop merged patch reset-mock-call-count.patch
----------(Old E)----------

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

Other differences:
------------------
++++++ python-certbot.spec ++++++
--- /var/tmp/diff_new_pack.CBKyJZ/_old  2026-03-16 15:49:17.174188659 +0100
+++ /var/tmp/diff_new_pack.CBKyJZ/_new  2026-03-16 15:49:17.178188824 +0100
@@ -23,14 +23,12 @@
 %endif
 %{?sle15_python_module_pythons}
 Name:           python-certbot
-Version:        5.3.1
+Version:        5.4.0
 Release:        0
 Summary:        ACME client
 License:        Apache-2.0
 URL:            https://github.com/certbot/certbot
 Source0:        
https://files.pythonhosted.org/packages/source/c/certbot/certbot-%{version}.tar.gz
-# https://github.com/certbot/certbot/pull/10576 Reset mock call count using 
reset_mock since new thread-safe implementation means it can no longer just be 
set to 0
-Patch0:         reset-mock-call-count.patch
 BuildRequires:  %{python_module acme >= %{version}}
 BuildRequires:  %{python_module configargparse >= 1.5.3}
 BuildRequires:  %{python_module configobj >= 5.0.6}

++++++ certbot-5.3.1.tar.gz -> certbot-5.4.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-5.3.1/CHANGELOG.md 
new/certbot-5.4.0/CHANGELOG.md
--- old/certbot-5.3.1/CHANGELOG.md      2026-02-09 22:19:25.000000000 +0100
+++ new/certbot-5.4.0/CHANGELOG.md      2026-03-10 18:46:56.000000000 +0100
@@ -4,6 +4,17 @@
 
 <!-- towncrier release notes start -->
 
+## 5.4.0 - 2026-03-10
+
+### Added
+
+- The webroot plugin now supports IP address issuance. 
([#10543](https://github.com/certbot/certbot/issues/10543))
+
+### Changed
+
+- certbot-nginx now requires pyparsing>=3.0.0. 
([#10560](https://github.com/certbot/certbot/issues/10560))
+
+
 ## 5.3.1 - 2026-02-09
 
 ### Fixed
@@ -15,7 +26,7 @@
 
 ### Added
 
-- A new command line flag, --ip-address, has been added. This requests 
certificates with IP address SANs when using the standalone or manual plugin.  
Note that for Let's Encrypt's implementation of IP address certificates, you'll 
also need to pass `--preferred-profile shortlived`. 
([#10465](https://github.com/certbot/certbot/issues/10465))
+- A new command line flag, --ip-address, has been added. This requests 
certificates with IP address SANs when using the standalone or manual plugin.  
Note that for Let's Encrypt's implementation of IP address certificates, you'll 
also need to pass `--preferred-profile shortlived`. 
([#10495](https://github.com/certbot/certbot/issues/10495), 
[#10544](https://github.com/certbot/certbot/pull/10544))
 
 ### Changed
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-5.3.1/PKG-INFO new/certbot-5.4.0/PKG-INFO
--- old/certbot-5.3.1/PKG-INFO  2026-02-09 22:19:26.456840500 +0100
+++ new/certbot-5.4.0/PKG-INFO  2026-03-10 18:46:57.393162700 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: certbot
-Version: 5.3.1
+Version: 5.4.0
 Summary: ACME client
 Author: Certbot Project
 License-Expression: Apache-2.0
@@ -25,7 +25,7 @@
 Requires-Python: >=3.10
 Description-Content-Type: text/x-rst
 License-File: LICENSE.txt
-Requires-Dist: acme>=5.3.1
+Requires-Dist: acme>=5.4.0
 Requires-Dist: ConfigArgParse>=1.5.3
 Requires-Dist: configobj>=5.0.6
 Requires-Dist: cryptography>=43.0.0
@@ -156,3 +156,10 @@
 * Configuration changes are logged and can be reverted.
 
 .. Do not modify this comment unless you know what you're doing. 
tag:features-end
+
+Thanks
+------
+
+We appreciate `Digital Ocean`_ for donating credits to help us test and 
develop Certbot.
+
+.. _Digital Ocean: https://www.digitalocean.com/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-5.3.1/README.rst new/certbot-5.4.0/README.rst
--- old/certbot-5.3.1/README.rst        2026-02-09 22:19:24.000000000 +0100
+++ new/certbot-5.4.0/README.rst        2026-03-10 18:46:56.000000000 +0100
@@ -85,3 +85,10 @@
 * Configuration changes are logged and can be reverted.
 
 .. Do not modify this comment unless you know what you're doing. 
tag:features-end
+
+Thanks
+------
+
+We appreciate `Digital Ocean`_ for donating credits to help us test and 
develop Certbot.
+
+.. _Digital Ocean: https://www.digitalocean.com/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-5.3.1/docs/cli-help.txt 
new/certbot-5.4.0/docs/cli-help.txt
--- old/certbot-5.3.1/docs/cli-help.txt 2026-02-09 22:19:24.000000000 +0100
+++ new/certbot-5.4.0/docs/cli-help.txt 2026-03-10 18:46:56.000000000 +0100
@@ -38,7 +38,7 @@
 
 options:
   -h, --help            show this help message and exit
-  -c, --config CONFIG_FILE
+  -c CONFIG_FILE, --config CONFIG_FILE
                         path to config file (default: /etc/letsencrypt/cli.ini
                         and ~/.config/letsencrypt/cli.ini)
   -v, --verbose         This flag can be used multiple times to incrementally
@@ -58,7 +58,7 @@
   --force-interactive   Force Certbot to be interactive even if it detects
                         it's not being run in a terminal. This flag cannot be
                         used with the renew subcommand. (default: False)
-  -d, --domains, --domain DOMAIN
+  -d DOMAIN, --domains DOMAIN, --domain DOMAIN
                         Domain names to include. For multiple domains you can
                         use multiple -d flags or enter a comma separated list
                         of domains as a parameter. All domains will be
@@ -147,7 +147,7 @@
                         case, and to know when to deprecate support for past
                         Python versions and flags. If you wish to hide this
                         information from the Let's Encrypt server, set this to
-                        "". (default: CertbotACMEClient/5.3.0 (certbot;
+                        "". (default: CertbotACMEClient/5.3.1 (certbot;
                         OS_NAME OS_VERSION) Authenticator/XXX Installer/YYY
                         (SUBCOMMAND; flags: FLAGS) Py/major.minor.patchlevel).
                         The flags encoded in the user agent are: --duplicate,
@@ -424,7 +424,8 @@
 register:
   Options for account registration
 
-  -m, --email EMAIL     Email used for registration and recovery contact. Use
+  -m EMAIL, --email EMAIL
+                        Email used for registration and recovery contact. Use
                         comma to register multiple emails, ex:
                         [email protected],[email protected]. (default: Ask).
   --eff-email           Share your e-mail address with EFF (default: Ask)
@@ -477,9 +478,9 @@
                         Name of the plugin that is both an authenticator and
                         an installer. Should not be used together with
                         --authenticator or --installer. (default: Ask)
-  -a, --authenticator AUTHENTICATOR
+  -a AUTHENTICATOR, --authenticator AUTHENTICATOR
                         Authenticator plugin name. (default: None)
-  -i, --installer INSTALLER
+  -i INSTALLER, --installer INSTALLER
                         Installer plugin name (also used to find domains).
                         (default: None)
   --apache              Obtain and install certificates using Apache (default:
@@ -756,7 +757,7 @@
   be running and serving files from the webroot path. HTTP challenge only
   (wildcards not supported).
 
-  --webroot-path, -w WEBROOT_PATH
+  --webroot-path WEBROOT_PATH, -w WEBROOT_PATH
                         public_html / webroot path. This can be specified
                         multiple times to handle different domains; each
                         domain will have the webroot path that preceded it.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-5.3.1/docs/using.rst 
new/certbot-5.4.0/docs/using.rst
--- old/certbot-5.3.1/docs/using.rst    2026-02-09 22:19:24.000000000 +0100
+++ new/certbot-5.4.0/docs/using.rst    2026-03-10 18:46:56.000000000 +0100
@@ -350,7 +350,7 @@
 .. _dns-clouddns: https://github.com/vshosting/certbot-dns-clouddns
 .. _dns-lightsail: https://github.com/noi/certbot-dns-lightsail
 .. _dns-inwx: https://github.com/oGGy990/certbot-dns-inwx/
-.. _dns-azure: https://github.com/binkhq/certbot-dns-azure
+.. _dns-azure: https://github.com/terricain/certbot-dns-azure
 .. _dns-godaddy: https://github.com/miigotu/certbot-dns-godaddy
 .. _dns-yandexcloud: https://github.com/PykupeJIbc/certbot-dns-yandexcloud
 .. _dns-bunny: https://github.com/mwt/certbot-dns-bunny
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-5.3.1/src/certbot/__init__.py 
new/certbot-5.4.0/src/certbot/__init__.py
--- old/certbot-5.3.1/src/certbot/__init__.py   2026-02-09 22:19:25.000000000 
+0100
+++ new/certbot-5.4.0/src/certbot/__init__.py   2026-03-10 18:46:56.000000000 
+0100
@@ -1,4 +1,4 @@
 """Certbot client."""
 
 # version number like 1.2.3a0, must have at least 2 parts, like 1.2
-__version__ = '5.3.1'
+__version__ = '5.4.0'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-5.3.1/src/certbot/_internal/cli/__init__.py 
new/certbot-5.4.0/src/certbot/_internal/cli/__init__.py
--- old/certbot-5.3.1/src/certbot/_internal/cli/__init__.py     2026-02-09 
22:19:24.000000000 +0100
+++ new/certbot-5.4.0/src/certbot/_internal/cli/__init__.py     2026-03-10 
18:46:56.000000000 +0100
@@ -25,12 +25,13 @@
 from certbot._internal.cli.cli_utils import _EncodeReasonAction
 from certbot._internal.cli.cli_utils import _PrefChallAction
 from certbot._internal.cli.cli_utils import _user_agent_comment_type
-from certbot._internal.cli.cli_utils import add_domains
+from certbot._internal.cli.cli_utils import add_dns_name
+from certbot._internal.cli.cli_utils import add_ip_address
 from certbot._internal.cli.cli_utils import CaseInsensitiveList
 from certbot._internal.cli.cli_utils import config_help
 from certbot._internal.cli.cli_utils import CustomHelpFormatter
 from certbot._internal.cli.cli_utils import DomainsAction
-from certbot._internal.cli.cli_utils import _IPAddressAction
+from certbot._internal.cli.cli_utils import IPAddressAction
 from certbot._internal.cli.cli_utils import flag_default
 from certbot._internal.cli.cli_utils import HelpfulArgumentGroup
 from certbot._internal.cli.cli_utils import nonnegative_int
@@ -125,7 +126,7 @@
     helpful.add(
         [None, "certonly", "certificates"],
         "--ip-address", dest="ip_addresses",
-        action=_IPAddressAction,
+        action=IPAddressAction,
         default=flag_default("ip_addresses"),
         help="IP addresses to include. For multiple IP addresses you can use 
multiple "
              "--ip-address flags. All IP addresses will be included as Subject 
Alternative Names "
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-5.3.1/src/certbot/_internal/cli/cli_utils.py 
new/certbot-5.4.0/src/certbot/_internal/cli/cli_utils.py
--- old/certbot-5.3.1/src/certbot/_internal/cli/cli_utils.py    2026-02-09 
22:19:24.000000000 +0100
+++ new/certbot-5.4.0/src/certbot/_internal/cli/cli_utils.py    2026-03-10 
18:46:56.000000000 +0100
@@ -98,45 +98,31 @@
     def __call__(self, parser: argparse.ArgumentParser, namespace: 
argparse.Namespace,
                  values: str | Sequence[Any] | None,
                  option_string: str | None = None) -> None:
-        """Just wrap add_domains in argparseese."""
         match values:
             case str():
-                add_domains(namespace, str(values))
+                for domain in values.split(","):
+                    add_dns_name(namespace, san.DNSName(domain.strip()))
             case _:
                 # https://docs.python.org/3/library/argparse.html#nargs
                 raise TypeError("shouldn't happen: non-str passed by argparse 
when nargs=None")
 
 
-def add_domains(args_or_config: Union[argparse.Namespace, 
configuration.NamespaceConfig],
-                domains: Optional[str]) -> list[san.DNSName]:
-    """Registers new domains to be used during the current client run.
+def add_dns_name(args_or_config: Union[argparse.Namespace, 
configuration.NamespaceConfig],
+                 dns_name: san.DNSName) -> None:
+    """Registers a new domain to be used during the current client run.
 
-    Domains are not added to the list of requested domains if they have
-    already been registered.
+    The domain is not added if it has already been registered.
 
     :param args_or_config: parsed command line arguments
     :type args_or_config: argparse.Namespace or
         configuration.NamespaceConfig
-    :param str domain: one or more comma separated domains
-
-    :returns: domains after they have been normalized and validated
-    :rtype: `list` of `str`
-
+    :param san.DNSName dns_name: a DNS name
     """
-    validated_domains: list[san.DNSName] = []
-    if not domains:
-        return validated_domains
-
-    for d in domains.split(","):
-        domain = san.DNSName(d.strip())
-        validated_domains.append(domain)
-        if domain not in args_or_config.domains:
-            args_or_config.domains.append(domain)
+    if dns_name not in args_or_config.domains:
+        args_or_config.domains.append(dns_name)
 
-    return validated_domains
 
-
-class _IPAddressAction(argparse.Action):
+class IPAddressAction(argparse.Action):
     """Action class for parsing IP addresses."""
 
     def __call__(self, parser: argparse.ArgumentParser, namespace: 
argparse.Namespace,
@@ -145,12 +131,26 @@
         match values:
             case str():
                 # This will throw an exception if the IP address doesn't parse.
-                namespace.ip_addresses.append(san.IPAddress(values))
+                add_ip_address(namespace, san.IPAddress(values))
             case _:
                 # https://docs.python.org/3/library/argparse.html#nargs
                 raise TypeError("shouldn't happen: non-str passed by argparse 
when nargs=None")
 
 
+def add_ip_address(args_or_config: Union[argparse.Namespace, 
configuration.NamespaceConfig],
+                   ip_address: san.IPAddress) -> None:
+    """Registers a new IP address to be used during the current client run.
+
+    The IP address is not added if it has already been registered.
+
+    :param args_or_config: parsed command line arguments
+    :type args_or_config: argparse.Namespace or
+        configuration.NamespaceConfig
+    :param san.IPAddress ip_address: an IP address
+    """
+    if ip_address not in args_or_config.ip_addresses:
+        args_or_config.ip_addresses.append(ip_address)
+
 class CaseInsensitiveList(list):
     """A list that will ignore case when searching.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot-5.3.1/src/certbot/_internal/plugins/webroot.py 
new/certbot-5.4.0/src/certbot/_internal/plugins/webroot.py
--- old/certbot-5.3.1/src/certbot/_internal/plugins/webroot.py  2026-02-09 
22:19:24.000000000 +0100
+++ new/certbot-5.4.0/src/certbot/_internal/plugins/webroot.py  2026-03-10 
18:46:56.000000000 +0100
@@ -15,6 +15,7 @@
 from certbot import errors
 from certbot import interfaces
 from certbot._internal import cli
+from certbot._internal import san
 from certbot.achallenges import AnnotatedChallenge
 from certbot.compat import filesystem
 from certbot.compat import os
@@ -69,14 +70,14 @@
     def add_parser_arguments(cls, add: Callable[..., None]) -> None:
         add("path", "-w", default=[], action=_WebrootPathAction,
             help="public_html / webroot path. This can be specified multiple "
-                 "times to handle different domains; each domain will have "
+                 "times to handle different identifiers; each identifier will 
have "
                  "the webroot path that preceded it.  For instance: `-w "
                  "/var/www/example -d example.com -d www.example.com -w "
                  "/var/www/thing -d thing.net -d m.thing.net` (default: Ask)")
         add("map", default={}, action=_WebrootMapAction,
-            help="JSON dictionary mapping domains to webroot paths; this "
-                 "implies -d for each entry. You may need to escape this from "
-                 "your shell. E.g.: --webroot-map "
+            help="JSON dictionary mapping identifiers to webroot paths; this "
+                 "implies -d or --ip-address for each entry. You may need to "
+                 " escape this from your shell. E.g.: --webroot-map "
                  '\'{"eg1.is,m.eg1.is":"/www/eg1/", "eg2.is":"/www/eg2"}\' '
                  "This option is merged with, but takes precedence over, -w / "
                  "-d entries. At present, if you put webroot-map in a config "
@@ -85,7 +86,7 @@
 
     def auth_hint(self, failed_achalls: list[AnnotatedChallenge]) -> str:  # 
pragma: no cover
         return ("The Certificate Authority failed to download the temporary 
challenge files "
-                "created by Certbot. Ensure that the listed domains serve 
their content from "
+                "created by Certbot. Ensure that the listed identifiers serve 
their content from "
                 "the provided --webroot-path/-w and that files created there 
can be downloaded "
                 "from the internet.")
 
@@ -106,9 +107,6 @@
         pass
 
     def perform(self, achalls: list[AnnotatedChallenge]) -> 
list[challenges.ChallengeResponse]:  # pylint: 
disable=missing-function-docstring
-        if any(achall.identifier.typ == messages.IDENTIFIER_IP for achall in 
achalls):
-            raise errors.ConfigurationError(
-                "webroot authenticator not supported for IP address 
certificates")
         self._set_webroots(achalls)
 
         self._create_challenge_dirs()
@@ -118,7 +116,7 @@
     def _set_webroots(self, achalls: Iterable[AnnotatedChallenge]) -> None:
         if self.conf("path"):
             webroot_path = self.conf("path")[-1]
-            logger.info("Using the webroot path %s for all unmatched domains.",
+            logger.info("Using the webroot path %s for all unmatched 
identifiers.",
                         webroot_path)
             for achall in achalls:
                 self.conf("map").setdefault(achall.identifier.value, 
webroot_path)
@@ -126,7 +124,7 @@
             known_webroots = list(set(self.conf("map").values()))
             for achall in achalls:
                 if achall.identifier.value not in self.conf("map"):
-                    new_webroot = 
self._prompt_for_webroot(achall.identifier.value,
+                    new_webroot = self._prompt_for_webroot(achall.identifier,
                                                            known_webroots)
                     # Put the most recently input
                     # webroot first for easy selection
@@ -137,46 +135,48 @@
                     known_webroots.insert(0, new_webroot)
                     self.conf("map")[achall.identifier.value] = new_webroot
 
-    def _prompt_for_webroot(self, domain: str, known_webroots: list[str]) -> 
Optional[str]:
+    def _prompt_for_webroot(self, identifier: messages.Identifier,
+                            known_webroots: list[str]) -> Optional[str]:
         webroot = None
 
         while webroot is None:
             if known_webroots:
                 # Only show the menu if we have options for it
-                webroot = self._prompt_with_webroot_list(domain, 
known_webroots)
+                webroot = self._prompt_with_webroot_list(identifier, 
known_webroots)
                 if webroot is None:
-                    webroot = self._prompt_for_new_webroot(domain)
+                    webroot = self._prompt_for_new_webroot(identifier)
             else:
                 # Allow prompt to raise PluginError instead of looping forever
-                webroot = self._prompt_for_new_webroot(domain, True)
+                webroot = self._prompt_for_new_webroot(identifier, True)
 
         return webroot
 
-    def _prompt_with_webroot_list(self, domain: str,
+    def _prompt_with_webroot_list(self, identifier: messages.Identifier,
                                   known_webroots: list[str]) -> Optional[str]:
         path_flag = "--" + self.option_name("path")
 
         while True:
             code, index = display_util.menu(
-                "Select the webroot for {0}:".format(domain),
+                "Select the webroot for {0}:".format(identifier.value),
                 ["Enter a new webroot"] + known_webroots,
                 cli_flag=path_flag, force_interactive=True)
             if code == display_util.CANCEL:
                 raise errors.PluginError(
-                    "Every requested domain must have a "
+                    "Every requested identifier must have a "
                     "webroot when using the webroot plugin.")
             return None if index == 0 else known_webroots[index - 1]  # code 
== display_util.OK
 
-    def _prompt_for_new_webroot(self, domain: str, allowraise: bool = False) 
-> Optional[str]:
+    def _prompt_for_new_webroot(self, identifier: messages.Identifier,
+                                allowraise: bool = False) -> Optional[str]:
         code, webroot = ops.validated_directory(
             _validate_webroot,
-            "Input the webroot for {0}:".format(domain),
+            "Input the webroot for {0}:".format(identifier.value),
             force_interactive=True)
         if code == display_util.CANCEL:
             if not allowraise:
                 return None
             raise errors.PluginError(
-                "Every requested domain must have a "
+                "Every requested identifier must have a "
                 "webroot when using the webroot plugin.")
         return _validate_webroot(webroot)  # code == display_util.OK
 
@@ -184,9 +184,10 @@
         path_map = self.conf("map")
         if not path_map:
             raise errors.PluginError(
-                "Missing parts of webroot configuration; please set either "
-                "--webroot-path and --domains, or --webroot-map. Run with "
-                " --help webroot for examples.")
+                "Missing parts of webroot configuration; please set "
+                "--webroot-path and --domains or --ip-address. "
+                "Alternatively you may set --webroot-map. "
+                "Run with --help webroot for examples.")
         for name, path in path_map.items():
             self.full_roots[name] = os.path.join(path, os.path.normcase(
                 challenges.HTTP01.URI_ROOT_PATH))
@@ -295,10 +296,16 @@
                  option_string: Optional[str] = None) -> None:
         if webroot_map is None:
             return
-        for domains, webroot_path in json.loads(str(webroot_map)).items():
+        for identlist, webroot_path in json.loads(str(webroot_map)).items():
             webroot_path = _validate_webroot(webroot_path)
-            namespace.webroot_map.update(
-                (d.dns_name, webroot_path) for d in cli.add_domains(namespace, 
domains))
+            for s in san.guess(identlist.split(",")):
+                match s:
+                    case san.IPAddress():
+                        cli.add_ip_address(namespace, s)
+                    case san.DNSName():
+                        cli.add_dns_name(namespace, s)
+
+                namespace.webroot_map[str(s)] = webroot_path
 
 
 class _WebrootPathAction(argparse.Action):
@@ -306,17 +313,17 @@
 
     def __init__(self, *args: Any, **kwargs: Any) -> None:
         super().__init__(*args, **kwargs)
-        self._domain_before_webroot = False
+        self._ident_before_webroot = False
 
     def __call__(self, parser: argparse.ArgumentParser, namespace: 
argparse.Namespace,
                  webroot_path: Union[str, Sequence[Any], None],
                  option_string: Optional[str] = None) -> None:
         if webroot_path is None:
             return
-        if self._domain_before_webroot:
+        if self._ident_before_webroot:
             raise errors.PluginError(
                 "If you specify multiple webroot paths, "
-                "one of them must precede all domain flags")
+                "one of them must precede all --domain and --ip-address flags")
 
         if namespace.webroot_path:
             # Apply previous webroot to all matched
@@ -324,8 +331,10 @@
             prev_webroot = namespace.webroot_path[-1]
             for domain in namespace.domains:
                 namespace.webroot_map.setdefault(domain.dns_name, prev_webroot)
-        elif namespace.domains:
-            self._domain_before_webroot = True
+            for ip_address in namespace.ip_addresses:
+                namespace.webroot_map.setdefault(str(ip_address), prev_webroot)
+        elif namespace.domains or namespace.ip_addresses:
+            self._ident_before_webroot = True
 
         namespace.webroot_path.append(_validate_webroot(str(webroot_path)))
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-5.3.1/src/certbot/_internal/renewal.py 
new/certbot-5.4.0/src/certbot/_internal/renewal.py
--- old/certbot-5.3.1/src/certbot/_internal/renewal.py  2026-02-09 
22:19:24.000000000 +0100
+++ new/certbot-5.4.0/src/certbot/_internal/renewal.py  2026-03-10 
18:46:56.000000000 +0100
@@ -632,16 +632,19 @@
 def handle_renewal_request(config: configuration.NamespaceConfig) -> None:
     """Examine each lineage; renew if due and report results"""
 
-    # This is trivially False if config.domains is empty
-    if any(domain.dns_name not in config.webroot_map for domain in 
config.domains):
-        # If more plugins start using cli.add_domains,
+    sans: list[san.SAN] = config.domains + config.ip_addresses
+
+    # This is trivially False if sans is empty
+    if any(str(san) not in config.webroot_map for san in sans):
+        # If more plugins start using cli.add_domain / cli.add_ip_address,
         # we may want to only log a warning here
         raise errors.Error("Currently, the renew verb is capable of either "
                            "renewing all installed certificates that are due "
                            "to be renewed or renewing a single certificate 
specified "
-                           "by its name. If you would like to renew specific "
-                           "certificates by their domains, use the certonly 
command "
-                           "instead. The renew verb may provide other options "
+                           "by its name using the --cert-name option (-d, 
--domain, and "
+                           "--ip-address are not valid options for the renew 
subcommand). If you "
+                           "would like to renew specific certificates by their 
identifiers, use "
+                           "the certonly command instead. The renew verb may 
provide other options "
                            "for selecting certificates to renew in the 
future.")
 
     if config.certname:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot-5.3.1/src/certbot/_internal/tests/plugins/webroot_test.py 
new/certbot-5.4.0/src/certbot/_internal/tests/plugins/webroot_test.py
--- old/certbot-5.3.1/src/certbot/_internal/tests/plugins/webroot_test.py       
2026-02-09 22:19:24.000000000 +0100
+++ new/certbot-5.4.0/src/certbot/_internal/tests/plugins/webroot_test.py       
2026-03-10 18:46:56.000000000 +0100
@@ -317,18 +317,33 @@
         identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, 
value="thing.com"),
         account_key=KEY)
 
+    ipchall = achallenges.KeyAuthorizationAnnotatedChallenge(
+        challb=acme_util.chall_to_challb(
+            challenges.HTTP01(token=((b'a' * 16))),
+            messages.STATUS_PENDING),
+        identifier=messages.Identifier(typ=messages.IDENTIFIER_IP, 
value="1.2.3.4"),
+        account_key=KEY)
+
     def setUp(self):
         from certbot._internal.plugins.webroot import Authenticator
         self.path = tempfile.mkdtemp()
         self.parser = argparse.ArgumentParser()
+        self.parser.ip_addresses = []
         self.parser.add_argument("-d", "--domains",
                                  action=cli_utils.DomainsAction, default=[])
+        self.parser.add_argument("--ip-address",
+                                 action=cli_utils.IPAddressAction,
+                                 dest="ip_addresses",
+                                 default=[])
         Authenticator.inject_parser_options(self.parser, "webroot")
 
     def test_webroot_map_action(self):
+        other_path = tempfile.mkdtemp()
         args = self.parser.parse_args(
-            ["--webroot-map", json.dumps({'thing.com': self.path})])
+            ["--webroot-map", json.dumps({'thing.com,thunk.com,9.8.7.6': 
self.path,'thunk.com': other_path})])
         assert args.webroot_map["thing.com"] == self.path
+        assert args.webroot_map["9.8.7.6"] == self.path
+        assert args.webroot_map["thunk.com"] == other_path
 
     def test_domain_before_webroot(self):
         args = self.parser.parse_args(
@@ -336,6 +351,15 @@
         config = self._get_config_after_perform(args)
         assert config.webroot_map[self.achall.identifier.value] == self.path
 
+    def test_multi_identifier(self):
+        args = self.parser.parse_args(
+            "-w {0} -d {1} --ip-address {2}".format(
+                self.path, self.achall.identifier.value, 
self.ipchall.identifier.value).split())
+
+        config = self._get_config_after_perform(args, challs=[self.achall, 
self.ipchall])
+        assert config.webroot_map[self.achall.identifier.value] == self.path
+        assert config.webroot_map[self.ipchall.identifier.value] == self.path
+
     def test_domain_before_webroot_error(self):
         with pytest.raises(errors.PluginError):
             self.parser.parse_args("-d foo -w bar -w baz".split())
@@ -343,11 +367,26 @@
             self.parser.parse_args("-d foo -w bar -d baz -w qux".split())
 
     def test_multiwebroot(self):
-        args = self.parser.parse_args("-w {0} -d {1} -w {2} -d bar".format(
-            self.path, self.achall.identifier.value, 
tempfile.mkdtemp()).split())
-        assert args.webroot_map[self.achall.identifier.value] == self.path
-        config = self._get_config_after_perform(args)
-        assert config.webroot_map[self.achall.identifier.value] == self.path
+        ip = self.ipchall.identifier.value
+        dns_name = self.achall.identifier.value
+
+        ip_path = tempfile.mkdtemp()
+        dns_path = tempfile.mkdtemp()
+        args = self.parser.parse_args(f"-w {dns_path} -d {dns_name} -w 
{ip_path} --ip-address {ip}".split())
+        config = self._get_config_after_perform(args, challs=[self.achall, 
self.ipchall])
+        assert config.webroot_map[dns_name] == dns_path
+        assert config.webroot_map[ip] == ip_path
+
+    def test_multiwebroot_ip_first(self):
+        ip = self.ipchall.identifier.value
+        dns_name = self.achall.identifier.value
+
+        ip_path = tempfile.mkdtemp()
+        dns_path = tempfile.mkdtemp()
+        args = self.parser.parse_args(f"-w {ip_path} --ip-address {ip} -w 
{dns_path} -d {dns_name}".split())
+        config = self._get_config_after_perform(args, challs=[self.achall, 
self.ipchall])
+        assert config.webroot_map[dns_name] == dns_path
+        assert config.webroot_map[ip] == ip_path
 
     def test_webroot_map_partial_without_perform(self):
         # This test acknowledges the fact that webroot_map content will be 
partial if webroot
@@ -362,10 +401,12 @@
         assert args.webroot_map == {self.achall.identifier.value: self.path}
         assert args.webroot_path == [self.path, other_webroot_path]
 
-    def _get_config_after_perform(self, config):
+    def _get_config_after_perform(self, config, challs=None):
+        if not challs:
+            challs = [self.achall]
         from certbot._internal.plugins.webroot import Authenticator
         auth = Authenticator(config, "webroot")
-        auth.perform([self.achall])
+        auth.perform(challs)
         return auth.config
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot-5.3.1/src/certbot/_internal/tests/reverter_test.py 
new/certbot-5.4.0/src/certbot/_internal/tests/reverter_test.py
--- old/certbot-5.3.1/src/certbot/_internal/tests/reverter_test.py      
2026-02-09 22:19:24.000000000 +0100
+++ new/certbot-5.4.0/src/certbot/_internal/tests/reverter_test.py      
2026-03-10 18:46:56.000000000 +0100
@@ -358,7 +358,7 @@
 
         # Test Generic warning
         self._setup_three_checkpoints()
-        mock_logger.warning.call_count = 0
+        mock_logger.warning.reset_mock()
         self.reverter.rollback_checkpoints(4)
         assert mock_logger.warning.call_count == 1
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-5.3.1/src/certbot.egg-info/PKG-INFO 
new/certbot-5.4.0/src/certbot.egg-info/PKG-INFO
--- old/certbot-5.3.1/src/certbot.egg-info/PKG-INFO     2026-02-09 
22:19:26.000000000 +0100
+++ new/certbot-5.4.0/src/certbot.egg-info/PKG-INFO     2026-03-10 
18:46:57.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: certbot
-Version: 5.3.1
+Version: 5.4.0
 Summary: ACME client
 Author: Certbot Project
 License-Expression: Apache-2.0
@@ -25,7 +25,7 @@
 Requires-Python: >=3.10
 Description-Content-Type: text/x-rst
 License-File: LICENSE.txt
-Requires-Dist: acme>=5.3.1
+Requires-Dist: acme>=5.4.0
 Requires-Dist: ConfigArgParse>=1.5.3
 Requires-Dist: configobj>=5.0.6
 Requires-Dist: cryptography>=43.0.0
@@ -156,3 +156,10 @@
 * Configuration changes are logged and can be reverted.
 
 .. Do not modify this comment unless you know what you're doing. 
tag:features-end
+
+Thanks
+------
+
+We appreciate `Digital Ocean`_ for donating credits to help us test and 
develop Certbot.
+
+.. _Digital Ocean: https://www.digitalocean.com/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-5.3.1/src/certbot.egg-info/requires.txt 
new/certbot-5.4.0/src/certbot.egg-info/requires.txt
--- old/certbot-5.3.1/src/certbot.egg-info/requires.txt 2026-02-09 
22:19:26.000000000 +0100
+++ new/certbot-5.4.0/src/certbot.egg-info/requires.txt 2026-03-10 
18:46:57.000000000 +0100
@@ -1,4 +1,4 @@
-acme>=5.3.1
+acme>=5.4.0
 ConfigArgParse>=1.5.3
 configobj>=5.0.6
 cryptography>=43.0.0

Reply via email to