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