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