46a734
From 6e79106a09a0d142915da1fb48640575bb4bfe08 Mon Sep 17 00:00:00 2001
46a734
From: Anh Vo <anhvo@microsoft.com>
46a734
Date: Tue, 13 Apr 2021 17:39:39 -0400
46a734
Subject: [PATCH 3/7] azure: Removing ability to invoke walinuxagent (#799)
46a734
46a734
RH-Author: Eduardo Otubo <otubo@redhat.com>
46a734
RH-MergeRequest: 45: Add support for userdata on Azure from IMDS
46a734
RH-Commit: [3/7] f5e98665bf2093edeeccfcd95b47df2e44a40536
46a734
RH-Bugzilla: 2023940
46a734
RH-Acked-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
46a734
RH-Acked-by: Mohamed Gamal Morsy <mmorsy@redhat.com>
46a734
46a734
Invoking walinuxagent from within cloud-init is no longer
46a734
supported/necessary
46a734
---
46a734
 cloudinit/sources/DataSourceAzure.py          | 137 ++++--------------
46a734
 doc/rtd/topics/datasources/azure.rst          |  62 ++------
46a734
 tests/unittests/test_datasource/test_azure.py |  97 -------------
46a734
 3 files changed, 35 insertions(+), 261 deletions(-)
46a734
46a734
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
46a734
index de1452ce..020b7006 100755
46a734
--- a/cloudinit/sources/DataSourceAzure.py
46a734
+++ b/cloudinit/sources/DataSourceAzure.py
46a734
@@ -381,53 +381,6 @@ class DataSourceAzure(sources.DataSource):
46a734
                     util.logexc(LOG, "handling set_hostname failed")
46a734
         return False
46a734
 
46a734
-    @azure_ds_telemetry_reporter
46a734
-    def get_metadata_from_agent(self):
46a734
-        temp_hostname = self.metadata.get('local-hostname')
46a734
-        agent_cmd = self.ds_cfg['agent_command']
46a734
-        LOG.debug("Getting metadata via agent.  hostname=%s cmd=%s",
46a734
-                  temp_hostname, agent_cmd)
46a734
-
46a734
-        self.bounce_network_with_azure_hostname()
46a734
-
46a734
-        try:
46a734
-            invoke_agent(agent_cmd)
46a734
-        except subp.ProcessExecutionError:
46a734
-            # claim the datasource even if the command failed
46a734
-            util.logexc(LOG, "agent command '%s' failed.",
46a734
-                        self.ds_cfg['agent_command'])
46a734
-
46a734
-        ddir = self.ds_cfg['data_dir']
46a734
-
46a734
-        fp_files = []
46a734
-        key_value = None
46a734
-        for pk in self.cfg.get('_pubkeys', []):
46a734
-            if pk.get('value', None):
46a734
-                key_value = pk['value']
46a734
-                LOG.debug("SSH authentication: using value from fabric")
46a734
-            else:
46a734
-                bname = str(pk['fingerprint'] + ".crt")
46a734
-                fp_files += [os.path.join(ddir, bname)]
46a734
-                LOG.debug("SSH authentication: "
46a734
-                          "using fingerprint from fabric")
46a734
-
46a734
-        with events.ReportEventStack(
46a734
-                name="waiting-for-ssh-public-key",
46a734
-                description="wait for agents to retrieve SSH keys",
46a734
-                parent=azure_ds_reporter):
46a734
-            # wait very long for public SSH keys to arrive
46a734
-            # https://bugs.launchpad.net/cloud-init/+bug/1717611
46a734
-            missing = util.log_time(logfunc=LOG.debug,
46a734
-                                    msg="waiting for SSH public key files",
46a734
-                                    func=util.wait_for_files,
46a734
-                                    args=(fp_files, 900))
46a734
-            if len(missing):
46a734
-                LOG.warning("Did not find files, but going on: %s", missing)
46a734
-
46a734
-        metadata = {}
46a734
-        metadata['public-keys'] = key_value or pubkeys_from_crt_files(fp_files)
46a734
-        return metadata
46a734
-
46a734
     def _get_subplatform(self):
46a734
         """Return the subplatform metadata source details."""
46a734
         if self.seed.startswith('/dev'):
46a734
@@ -1354,35 +1307,32 @@ class DataSourceAzure(sources.DataSource):
46a734
            On failure, returns False.
46a734
         """
46a734
 
46a734
-        if self.ds_cfg['agent_command'] == AGENT_START_BUILTIN:
46a734
-            self.bounce_network_with_azure_hostname()
46a734
+        self.bounce_network_with_azure_hostname()
46a734
 
46a734
-            pubkey_info = None
46a734
-            try:
46a734
-                raise KeyError(
46a734
-                    "Not using public SSH keys from IMDS"
46a734
-                )
46a734
-                # pylint:disable=unreachable
46a734
-                public_keys = self.metadata['imds']['compute']['publicKeys']
46a734
-                LOG.debug(
46a734
-                    'Successfully retrieved %s key(s) from IMDS',
46a734
-                    len(public_keys)
46a734
-                    if public_keys is not None
46a734
-                    else 0
46a734
-                )
46a734
-            except KeyError:
46a734
-                LOG.debug(
46a734
-                    'Unable to retrieve SSH keys from IMDS during '
46a734
-                    'negotiation, falling back to OVF'
46a734
-                )
46a734
-                pubkey_info = self.cfg.get('_pubkeys', None)
46a734
-
46a734
-            metadata_func = partial(get_metadata_from_fabric,
46a734
-                                    fallback_lease_file=self.
46a734
-                                    dhclient_lease_file,
46a734
-                                    pubkey_info=pubkey_info)
46a734
-        else:
46a734
-            metadata_func = self.get_metadata_from_agent
46a734
+        pubkey_info = None
46a734
+        try:
46a734
+            raise KeyError(
46a734
+                "Not using public SSH keys from IMDS"
46a734
+            )
46a734
+            # pylint:disable=unreachable
46a734
+            public_keys = self.metadata['imds']['compute']['publicKeys']
46a734
+            LOG.debug(
46a734
+                'Successfully retrieved %s key(s) from IMDS',
46a734
+                len(public_keys)
46a734
+                if public_keys is not None
46a734
+                else 0
46a734
+            )
46a734
+        except KeyError:
46a734
+            LOG.debug(
46a734
+                'Unable to retrieve SSH keys from IMDS during '
46a734
+                'negotiation, falling back to OVF'
46a734
+            )
46a734
+            pubkey_info = self.cfg.get('_pubkeys', None)
46a734
+
46a734
+        metadata_func = partial(get_metadata_from_fabric,
46a734
+                                fallback_lease_file=self.
46a734
+                                dhclient_lease_file,
46a734
+                                pubkey_info=pubkey_info)
46a734
 
46a734
         LOG.debug("negotiating with fabric via agent command %s",
46a734
                   self.ds_cfg['agent_command'])
46a734
@@ -1617,33 +1567,6 @@ def perform_hostname_bounce(hostname, cfg, prev_hostname):
46a734
     return True
46a734
 
46a734
 
46a734
-@azure_ds_telemetry_reporter
46a734
-def crtfile_to_pubkey(fname, data=None):
46a734
-    pipeline = ('openssl x509 -noout -pubkey < "$0" |'
46a734
-                'ssh-keygen -i -m PKCS8 -f /dev/stdin')
46a734
-    (out, _err) = subp.subp(['sh', '-c', pipeline, fname],
46a734
-                            capture=True, data=data)
46a734
-    return out.rstrip()
46a734
-
46a734
-
46a734
-@azure_ds_telemetry_reporter
46a734
-def pubkeys_from_crt_files(flist):
46a734
-    pubkeys = []
46a734
-    errors = []
46a734
-    for fname in flist:
46a734
-        try:
46a734
-            pubkeys.append(crtfile_to_pubkey(fname))
46a734
-        except subp.ProcessExecutionError:
46a734
-            errors.append(fname)
46a734
-
46a734
-    if errors:
46a734
-        report_diagnostic_event(
46a734
-            "failed to convert the crt files to pubkey: %s" % errors,
46a734
-            logger_func=LOG.warning)
46a734
-
46a734
-    return pubkeys
46a734
-
46a734
-
46a734
 @azure_ds_telemetry_reporter
46a734
 def write_files(datadir, files, dirmode=None):
46a734
 
46a734
@@ -1672,16 +1595,6 @@ def write_files(datadir, files, dirmode=None):
46a734
         util.write_file(filename=fname, content=content, mode=0o600)
46a734
 
46a734
 
46a734
-@azure_ds_telemetry_reporter
46a734
-def invoke_agent(cmd):
46a734
-    # this is a function itself to simplify patching it for test
46a734
-    if cmd:
46a734
-        LOG.debug("invoking agent: %s", cmd)
46a734
-        subp.subp(cmd, shell=(not isinstance(cmd, list)))
46a734
-    else:
46a734
-        LOG.debug("not invoking agent")
46a734
-
46a734
-
46a734
 def find_child(node, filter_func):
46a734
     ret = []
46a734
     if not node.hasChildNodes():
46a734
diff --git a/doc/rtd/topics/datasources/azure.rst b/doc/rtd/topics/datasources/azure.rst
46a734
index e04c3a33..ad9f2236 100644
46a734
--- a/doc/rtd/topics/datasources/azure.rst
46a734
+++ b/doc/rtd/topics/datasources/azure.rst
46a734
@@ -5,28 +5,6 @@ Azure
46a734
 
46a734
 This datasource finds metadata and user-data from the Azure cloud platform.
46a734
 
46a734
-walinuxagent
46a734
-------------
46a734
-walinuxagent has several functions within images.  For cloud-init
46a734
-specifically, the relevant functionality it performs is to register the
46a734
-instance with the Azure cloud platform at boot so networking will be
46a734
-permitted.  For more information about the other functionality of
46a734
-walinuxagent, see `Azure's documentation
46a734
-<https://github.com/Azure/WALinuxAgent#introduction>`_ for more details.
46a734
-(Note, however, that only one of walinuxagent's provisioning and cloud-init
46a734
-should be used to perform instance customisation.)
46a734
-
46a734
-If you are configuring walinuxagent yourself, you will want to ensure that you
46a734
-have `Provisioning.UseCloudInit
46a734
-<https://github.com/Azure/WALinuxAgent#provisioningusecloudinit>`_ set to
46a734
-``y``.
46a734
-
46a734
-
46a734
-Builtin Agent
46a734
--------------
46a734
-An alternative to using walinuxagent to register to the Azure cloud platform
46a734
-is to use the ``__builtin__`` agent command.  This section contains more
46a734
-background on what that code path does, and how to enable it.
46a734
 
46a734
 The Azure cloud platform provides initial data to an instance via an attached
46a734
 CD formatted in UDF.  That CD contains a 'ovf-env.xml' file that provides some
46a734
@@ -41,16 +19,6 @@ by calling a script in /etc/dhcp/dhclient-exit-hooks or a file in
46a734
 'dhclient_hook' of cloud-init itself. This sub-command will write the client
46a734
 information in json format to /run/cloud-init/dhclient.hook/<interface>.json.
46a734
 
46a734
-In order for cloud-init to leverage this method to find the endpoint, the
46a734
-cloud.cfg file must contain:
46a734
-
46a734
-.. sourcecode:: yaml
46a734
-
46a734
-  datasource:
46a734
-    Azure:
46a734
-      set_hostname: False
46a734
-      agent_command: __builtin__
46a734
-
46a734
 If those files are not available, the fallback is to check the leases file
46a734
 for the endpoint server (again option 245).
46a734
 
46a734
@@ -83,9 +51,6 @@ configuration (in ``/etc/cloud/cloud.cfg`` or ``/etc/cloud/cloud.cfg.d/``).
46a734
 
46a734
 The settings that may be configured are:
46a734
 
46a734
- * **agent_command**: Either __builtin__ (default) or a command to run to getcw
46a734
-   metadata. If __builtin__, get metadata from walinuxagent. Otherwise run the
46a734
-   provided command to obtain metadata.
46a734
  * **apply_network_config**: Boolean set to True to use network configuration
46a734
    described by Azure's IMDS endpoint instead of fallback network config of
46a734
    dhcp on eth0. Default is True. For Ubuntu 16.04 or earlier, default is
46a734
@@ -121,7 +86,6 @@ An example configuration with the default values is provided below:
46a734
 
46a734
   datasource:
46a734
     Azure:
46a734
-      agent_command: __builtin__
46a734
       apply_network_config: true
46a734
       data_dir: /var/lib/waagent
46a734
       dhclient_lease_file: /var/lib/dhcp/dhclient.eth0.leases
46a734
@@ -144,9 +108,7 @@ child of the ``LinuxProvisioningConfigurationSet`` (a sibling to ``UserName``)
46a734
 If both ``UserData`` and ``CustomData`` are provided behavior is undefined on
46a734
 which will be selected.
46a734
 
46a734
-In the example below, user-data provided is 'this is my userdata', and the
46a734
-datasource config provided is ``{"agent_command": ["start", "walinuxagent"]}``.
46a734
-That agent command will take affect as if it were specified in system config.
46a734
+In the example below, user-data provided is 'this is my userdata'
46a734
 
46a734
 Example:
46a734
 
46a734
@@ -184,20 +146,16 @@ The hostname is provided to the instance in the ovf-env.xml file as
46a734
 Whatever value the instance provides in its dhcp request will resolve in the
46a734
 domain returned in the 'search' request.
46a734
 
46a734
-The interesting issue is that a generic image will already have a hostname
46a734
-configured.  The ubuntu cloud images have 'ubuntu' as the hostname of the
46a734
-system, and the initial dhcp request on eth0 is not guaranteed to occur after
46a734
-the datasource code has been run.  So, on first boot, that initial value will
46a734
-be sent in the dhcp request and *that* value will resolve.
46a734
-
46a734
-In order to make the ``HostName`` provided in the ovf-env.xml resolve, a
46a734
-dhcp request must be made with the new value.  Walinuxagent (in its current
46a734
-version) handles this by polling the state of hostname and bouncing ('``ifdown
46a734
-eth0; ifup eth0``' the network interface if it sees that a change has been
46a734
-made.
46a734
+A generic image will already have a hostname configured.  The ubuntu
46a734
+cloud images have 'ubuntu' as the hostname of the system, and the
46a734
+initial dhcp request on eth0 is not guaranteed to occur after the
46a734
+datasource code has been run.  So, on first boot, that initial value
46a734
+will be sent in the dhcp request and *that* value will resolve.
46a734
 
46a734
-cloud-init handles this by setting the hostname in the DataSource's 'get_data'
46a734
-method via '``hostname $HostName``', and then bouncing the interface.  This
46a734
+In order to make the ``HostName`` provided in the ovf-env.xml resolve,
46a734
+a dhcp request must be made with the new value. cloud-init handles
46a734
+this by setting the hostname in the DataSource's 'get_data' method via
46a734
+'``hostname $HostName``', and then bouncing the interface.  This
46a734
 behavior can be configured or disabled in the datasource config.  See
46a734
 'Configuration' above.
46a734
 
46a734
diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/test_datasource/test_azure.py
46a734
index dedebeb1..320fa857 100644
46a734
--- a/tests/unittests/test_datasource/test_azure.py
46a734
+++ b/tests/unittests/test_datasource/test_azure.py
46a734
@@ -638,17 +638,10 @@ scbus-1 on xpt0 bus 0
46a734
         def dsdevs():
46a734
             return data.get('dsdevs', [])
46a734
 
46a734
-        def _invoke_agent(cmd):
46a734
-            data['agent_invoked'] = cmd
46a734
-
46a734
         def _wait_for_files(flist, _maxwait=None, _naplen=None):
46a734
             data['waited'] = flist
46a734
             return []
46a734
 
46a734
-        def _pubkeys_from_crt_files(flist):
46a734
-            data['pubkey_files'] = flist
46a734
-            return ["pubkey_from: %s" % f for f in flist]
46a734
-
46a734
         if data.get('ovfcontent') is not None:
46a734
             populate_dir(os.path.join(self.paths.seed_dir, "azure"),
46a734
                          {'ovf-env.xml': data['ovfcontent']})
46a734
@@ -675,8 +668,6 @@ scbus-1 on xpt0 bus 0
46a734
 
46a734
         self.apply_patches([
46a734
             (dsaz, 'list_possible_azure_ds_devs', dsdevs),
46a734
-            (dsaz, 'invoke_agent', _invoke_agent),
46a734
-            (dsaz, 'pubkeys_from_crt_files', _pubkeys_from_crt_files),
46a734
             (dsaz, 'perform_hostname_bounce', mock.MagicMock()),
46a734
             (dsaz, 'get_hostname', mock.MagicMock()),
46a734
             (dsaz, 'set_hostname', mock.MagicMock()),
46a734
@@ -765,7 +756,6 @@ scbus-1 on xpt0 bus 0
46a734
             ret = dsrc.get_data()
46a734
             self.m_is_platform_viable.assert_called_with(dsrc.seed_dir)
46a734
             self.assertFalse(ret)
46a734
-            self.assertNotIn('agent_invoked', data)
46a734
             # Assert that for non viable platforms,
46a734
             # there is no communication with the Azure datasource.
46a734
             self.assertEqual(
46a734
@@ -789,7 +779,6 @@ scbus-1 on xpt0 bus 0
46a734
             ret = dsrc.get_data()
46a734
             self.m_is_platform_viable.assert_called_with(dsrc.seed_dir)
46a734
             self.assertFalse(ret)
46a734
-            self.assertNotIn('agent_invoked', data)
46a734
             self.assertEqual(
46a734
                 1,
46a734
                 m_report_failure.call_count)
46a734
@@ -806,7 +795,6 @@ scbus-1 on xpt0 bus 0
46a734
                 1,
46a734
                 m_crawl_metadata.call_count)
46a734
             self.assertFalse(ret)
46a734
-            self.assertNotIn('agent_invoked', data)
46a734
 
46a734
     def test_crawl_metadata_exception_should_report_failure_with_msg(self):
46a734
         data = {}
46a734
@@ -1086,21 +1074,6 @@ scbus-1 on xpt0 bus 0
46a734
         self.assertTrue(os.path.isdir(self.waagent_d))
46a734
         self.assertEqual(stat.S_IMODE(os.stat(self.waagent_d).st_mode), 0o700)
46a734
 
46a734
-    def test_user_cfg_set_agent_command_plain(self):
46a734
-        # set dscfg in via plaintext
46a734
-        # we must have friendly-to-xml formatted plaintext in yaml_cfg
46a734
-        # not all plaintext is expected to work.
46a734
-        yaml_cfg = "{agent_command: my_command}\n"
46a734
-        cfg = yaml.safe_load(yaml_cfg)
46a734
-        odata = {'HostName': "myhost", 'UserName': "myuser",
46a734
-                 'dscfg': {'text': yaml_cfg, 'encoding': 'plain'}}
46a734
-        data = {'ovfcontent': construct_valid_ovf_env(data=odata)}
46a734
-
46a734
-        dsrc = self._get_ds(data)
46a734
-        ret = self._get_and_setup(dsrc)
46a734
-        self.assertTrue(ret)
46a734
-        self.assertEqual(data['agent_invoked'], cfg['agent_command'])
46a734
-
46a734
     @mock.patch('cloudinit.sources.DataSourceAzure.device_driver',
46a734
                 return_value=None)
46a734
     def test_network_config_set_from_imds(self, m_driver):
46a734
@@ -1205,29 +1178,6 @@ scbus-1 on xpt0 bus 0
46a734
         dsrc.get_data()
46a734
         self.assertEqual('eastus2', dsrc.region)
46a734
 
46a734
-    def test_user_cfg_set_agent_command(self):
46a734
-        # set dscfg in via base64 encoded yaml
46a734
-        cfg = {'agent_command': "my_command"}
46a734
-        odata = {'HostName': "myhost", 'UserName': "myuser",
46a734
-                 'dscfg': {'text': b64e(yaml.dump(cfg)),
46a734
-                           'encoding': 'base64'}}
46a734
-        data = {'ovfcontent': construct_valid_ovf_env(data=odata)}
46a734
-
46a734
-        dsrc = self._get_ds(data)
46a734
-        ret = self._get_and_setup(dsrc)
46a734
-        self.assertTrue(ret)
46a734
-        self.assertEqual(data['agent_invoked'], cfg['agent_command'])
46a734
-
46a734
-    def test_sys_cfg_set_agent_command(self):
46a734
-        sys_cfg = {'datasource': {'Azure': {'agent_command': '_COMMAND'}}}
46a734
-        data = {'ovfcontent': construct_valid_ovf_env(data={}),
46a734
-                'sys_cfg': sys_cfg}
46a734
-
46a734
-        dsrc = self._get_ds(data)
46a734
-        ret = self._get_and_setup(dsrc)
46a734
-        self.assertTrue(ret)
46a734
-        self.assertEqual(data['agent_invoked'], '_COMMAND')
46a734
-
46a734
     def test_sys_cfg_set_never_destroy_ntfs(self):
46a734
         sys_cfg = {'datasource': {'Azure': {
46a734
             'never_destroy_ntfs': 'user-supplied-value'}}}
46a734
@@ -1311,51 +1261,6 @@ scbus-1 on xpt0 bus 0
46a734
         self.assertTrue(ret)
46a734
         self.assertEqual(dsrc.userdata_raw, mydata.encode('utf-8'))
46a734
 
46a734
-    def test_cfg_has_pubkeys_fingerprint(self):
46a734
-        odata = {'HostName': "myhost", 'UserName': "myuser"}
46a734
-        mypklist = [{'fingerprint': 'fp1', 'path': 'path1', 'value': ''}]
46a734
-        pubkeys = [(x['fingerprint'], x['path'], x['value']) for x in mypklist]
46a734
-        data = {'ovfcontent': construct_valid_ovf_env(data=odata,
46a734
-                                                      pubkeys=pubkeys)}
46a734
-
46a734
-        dsrc = self._get_ds(data, agent_command=['not', '__builtin__'])
46a734
-        ret = self._get_and_setup(dsrc)
46a734
-        self.assertTrue(ret)
46a734
-        for mypk in mypklist:
46a734
-            self.assertIn(mypk, dsrc.cfg['_pubkeys'])
46a734
-            self.assertIn('pubkey_from', dsrc.metadata['public-keys'][-1])
46a734
-
46a734
-    def test_cfg_has_pubkeys_value(self):
46a734
-        # make sure that provided key is used over fingerprint
46a734
-        odata = {'HostName': "myhost", 'UserName': "myuser"}
46a734
-        mypklist = [{'fingerprint': 'fp1', 'path': 'path1', 'value': 'value1'}]
46a734
-        pubkeys = [(x['fingerprint'], x['path'], x['value']) for x in mypklist]
46a734
-        data = {'ovfcontent': construct_valid_ovf_env(data=odata,
46a734
-                                                      pubkeys=pubkeys)}
46a734
-
46a734
-        dsrc = self._get_ds(data, agent_command=['not', '__builtin__'])
46a734
-        ret = self._get_and_setup(dsrc)
46a734
-        self.assertTrue(ret)
46a734
-
46a734
-        for mypk in mypklist:
46a734
-            self.assertIn(mypk, dsrc.cfg['_pubkeys'])
46a734
-            self.assertIn(mypk['value'], dsrc.metadata['public-keys'])
46a734
-
46a734
-    def test_cfg_has_no_fingerprint_has_value(self):
46a734
-        # test value is used when fingerprint not provided
46a734
-        odata = {'HostName': "myhost", 'UserName': "myuser"}
46a734
-        mypklist = [{'fingerprint': None, 'path': 'path1', 'value': 'value1'}]
46a734
-        pubkeys = [(x['fingerprint'], x['path'], x['value']) for x in mypklist]
46a734
-        data = {'ovfcontent': construct_valid_ovf_env(data=odata,
46a734
-                                                      pubkeys=pubkeys)}
46a734
-
46a734
-        dsrc = self._get_ds(data, agent_command=['not', '__builtin__'])
46a734
-        ret = self._get_and_setup(dsrc)
46a734
-        self.assertTrue(ret)
46a734
-
46a734
-        for mypk in mypklist:
46a734
-            self.assertIn(mypk['value'], dsrc.metadata['public-keys'])
46a734
-
46a734
     def test_default_ephemeral_configs_ephemeral_exists(self):
46a734
         # make sure the ephemeral configs are correct if disk present
46a734
         odata = {}
46a734
@@ -1919,8 +1824,6 @@ class TestAzureBounce(CiTestCase):
46a734
     with_logs = True
46a734
 
46a734
     def mock_out_azure_moving_parts(self):
46a734
-        self.patches.enter_context(
46a734
-            mock.patch.object(dsaz, 'invoke_agent'))
46a734
         self.patches.enter_context(
46a734
             mock.patch.object(dsaz.util, 'wait_for_files'))
46a734
         self.patches.enter_context(
46a734
-- 
46a734
2.27.0
46a734