From cca1dd3ba1795ef795ff9325a5c1d10b1b82542a Mon Sep 17 00:00:00 2001 From: Lucendio Date: Tue, 8 Jun 2021 01:37:32 +0200 Subject: [PATCH 01/16] Change module order: run 'users-groups' before 'write-files' The module 'write-files' allows to configure ownership. If the configured user (and/or group) does not exist at the point when the 'write-files' module is applied, the whole stage fails. Moving 'users-groups' solves the issue. The reasoning in full length can be found here: https://bugs.launchpad.net/cloud-init/+bug/1486113 --- config/cloud.cfg.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl index de1d75e5040..ca64f73dacd 100644 --- a/config/cloud.cfg.tmpl +++ b/config/cloud.cfg.tmpl @@ -73,6 +73,7 @@ cloud_init_modules: - seed_random {% endif %} - bootcmd + - users-groups - write-files {% if variant not in ["netbsd", "openbsd"] %} - growpart @@ -94,7 +95,6 @@ cloud_init_modules: {% endif %} - rsyslog {% endif %} - - users-groups - ssh # The modules that run in the 'config' stage From c0bd6cba204c93c7bf2ce630a68e310be0b7f1c4 Mon Sep 17 00:00:00 2001 From: Lucendio Date: Tue, 20 Jul 2021 22:42:04 +0200 Subject: [PATCH 02/16] Revert "Change module order: run 'users-groups' before 'write-files'" During a follow-up discussion it was desiced to extend the user interface instead of shuffling modules around. So, this commit reverts the initial approach of the PR https://github.com/canonical/cloud-init/pull/916 before starting to implement the solution being discused. This reverts commit 1888f4f42ee6b25b99b67118c52a3fde134287bb. --- config/cloud.cfg.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl index ca64f73dacd..de1d75e5040 100644 --- a/config/cloud.cfg.tmpl +++ b/config/cloud.cfg.tmpl @@ -73,7 +73,6 @@ cloud_init_modules: - seed_random {% endif %} - bootcmd - - users-groups - write-files {% if variant not in ["netbsd", "openbsd"] %} - growpart @@ -95,6 +94,7 @@ cloud_init_modules: {% endif %} - rsyslog {% endif %} + - users-groups - ssh # The modules that run in the 'config' stage From a8c86bee92cd019b62863806c3f133f9612b521c Mon Sep 17 00:00:00 2001 From: Lucendio Date: Sun, 1 Aug 2021 19:42:53 +0200 Subject: [PATCH 03/16] Add module to allow deferring file writing THe main idea is to introduce a second module that takes care of writing files, but in the 'final' stage. While the introduction of a second module would allow for choosing the appropriate place withing the order of modules (and stages), there is no addition top-level directive being added to the cloud configuration schema. Instead, existing schemes are being extended, like the 'users' directive. The new module 'write-deferred-files' tries to reuse as much as possible of the 'write-files' functionality, including the scheme or the 'write_files' function. further details on how it would be used, are shown in the scheme definition of the module. --- cloudinit/config/cc_users_groups.py | 10 ++ cloudinit/config/cc_write_deferred_files.py | 134 ++++++++++++++++++++ config/cloud.cfg.tmpl | 1 + 3 files changed, 145 insertions(+) create mode 100644 cloudinit/config/cc_write_deferred_files.py diff --git a/cloudinit/config/cc_users_groups.py b/cloudinit/config/cc_users_groups.py index ac4a44100d8..5562b1a18ab 100644 --- a/cloudinit/config/cc_users_groups.py +++ b/cloudinit/config/cc_users_groups.py @@ -28,6 +28,11 @@ - ``name``: The user's login name - ``expiredate``: Optional. Date on which the user's account will be disabled. Default: none + - ``files``: Optional. List of files following the same scheme of + ``write_files`` items, except that ``owner`` is inferred (note that the + user part is always ignored, group may be recognized if value stats with + a ``:``), that ``permissions`` defaults to ``'0600'``, and that these + configurations are being applied only after users & groups have been created - ``gecos``: Optional. Comment about the user, usually a comma-separated string of real name and contact information. Default: none - ``groups``: Optional. Additional groups to add the user to. Default: none @@ -104,6 +109,7 @@ sudo: false - name: expiredate: '' + files: gecos: groups: homedir: @@ -169,6 +175,10 @@ def handle(name, cfg, cloud, _log, _args): else: config['ssh_redirect_user'] = default_user config['cloud_public_ssh_keys'] = cloud_keys + + # Remove configuration handled by module 'write-deferred-files' + config.pop('files', None) + cloud.distro.create_user(user, **config) # vi: ts=4 expandtab diff --git a/cloudinit/config/cc_write_deferred_files.py b/cloudinit/config/cc_write_deferred_files.py new file mode 100644 index 00000000000..95756fe3db2 --- /dev/null +++ b/cloudinit/config/cc_write_deferred_files.py @@ -0,0 +1,134 @@ +# Copyright (C) 2021 TODO +# +# Author: TODO +# +# This file is part of cloud-init. See LICENSE file for license information. + +"""Defer writing certain files like files defined in the Users & Groups module""" + +from textwrap import dedent + +from cloudinit.config.schema import validate_cloudconfig_schema +from cloudinit import log as logging +from cloudinit.settings import PER_INSTANCE +from cloudinit import util +from cloudinit.config.cc_write_files import ( schema as write_files_schema, write_files ) +from cloudinit.distros import ug_util + + +frequency = PER_INSTANCE + +DEFAULT_OWNER = ":" +DEFAULT_PERMS = '0600' +UNKNOWN_ENC = 'text/plain' + +LOG = logging.getLogger(__name__) + +distros = ['all'] + +schema = { + 'id': 'cc_write_deferred_files', + 'name': 'Write Deferred Files', + 'title': 'write certain files, whose creation as been deferred, during final stage', + 'description': dedent("""\ + This module is heavily based on `'Write Files' `__, but + the list of files being created, is gathered from other parts of the + configuration. Therefore, the same options are available, but, depending + on where a file is defined, certain attributes may be inferred. + + See the respective module documentation for details: + + - `Users and Groups`_ + `"""), + 'distros': distros, + 'examples': [ + dedent("""\ + # Extend ~/.profile after user (and probably the file itself) has been created + users: + - name: 'alice' + files: + - path: '/home/alice/.profile' + content: | + PATH="/usr/local/opt/python37/bin:$PATH" + append: true + """) + ], + 'frequency': frequency, + 'type': 'object', + 'properties': { + 'users': { + 'type': ['array'], + 'items': { + 'type': ['string', 'object'], + 'additionalProperties': True, + 'properties': { + 'name': { + 'type': 'string', + 'description': 'name of the user' + }, + 'files': util.mergemanydict([ + { + 'type': ['array'], + 'items': { + 'type': 'object', + 'properties': { + 'owner': { + 'type': 'string', + 'default': DEFAULT_OWNER, + 'description': dedent("""\ + Optional group to chown on the file (user is ignored). Default: + **user:user** + """), + }, + 'permissions': { + 'type': 'string', + 'default': DEFAULT_PERMS, + 'description': dedent("""\ + Optional file permissions to set on ``path`` + represented as an octal string '0###'. Default: + **'{perms}'** + """.format(perms=DEFAULT_PERMS)), + } + } + } + }, + write_files_schema.get('properties').get('write_files') + ]) + } + } + } + } +} + +# Not exposed, because related modules should document this behaviour +__doc__ = None + + +def handle(name, cfg, cloud, log, _args): + validate_cloudconfig_schema(cfg, schema) + file_list = extract_deferred_files(cfg, cloud) + if len(file_list) <= 0: + log.debug(("Skipping module named %s," + " no deferred file writing in configuration"), name) + return + write_files(name, file_list) + + +def extract_deferred_files(cfg, cloud): + deferred_files = [] + + (users, _) = ug_util.normalize_users_groups(cfg, cloud.distro) + for (user, config) in users.items(): + user_files = config.get('files', []) + for file in user_files: + (_, file_owner_group) = util.extract_usergroup(file.get('owner', DEFAULT_OWNER)) + if file_owner_group is None: + file_owner_group = user + file['owner'] = "{u}:{g}".format(u=user, g=file_owner_group) + file['permissions'] = config.get('permissions', DEFAULT_PERMS) + deferred_files.append(file) + + return deferred_files + + +# vi: ts=4 expandtab diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl index de1d75e5040..234b2ca53c4 100644 --- a/config/cloud.cfg.tmpl +++ b/config/cloud.cfg.tmpl @@ -151,6 +151,7 @@ cloud_final_modules: {% if variant in ["ubuntu", "unknown"] %} - ubuntu-drivers {% endif %} + - write-deferred-files - puppet - chef - mcollective From f502cfcf1a3551daea1953521e88ffbf9b5c0727 Mon Sep 17 00:00:00 2001 From: Lucendio Date: Sun, 1 Aug 2021 19:58:46 +0200 Subject: [PATCH 04/16] Add tests for the module 'write-deferred-files' This is an initial small chuck of unittests, which verifie that (a) a broken scheme ('path' is required) will log - similar to the scheme testing in test_handler_write_files.py; it even reuses some of the test data (b) when gathering file configurations from the 'users' directive, the defaults indicated by the docs are being set As this module is build upon the 'write-files' module, a larger part is already covered by unit tests. Still, more tests - especially integration tests - are going to be added before getting this branch into mainline. --- .../test_handler_write_deferred_files.py | 72 +++++++++++++++++++ tests/unittests/test_handler/test_schema.py | 1 + 2 files changed, 73 insertions(+) create mode 100644 tests/unittests/test_handler/test_handler_write_deferred_files.py diff --git a/tests/unittests/test_handler/test_handler_write_deferred_files.py b/tests/unittests/test_handler/test_handler_write_deferred_files.py new file mode 100644 index 00000000000..0fbc55f0828 --- /dev/null +++ b/tests/unittests/test_handler/test_handler_write_deferred_files.py @@ -0,0 +1,72 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit.config.cc_write_deferred_files import ( + handle, extract_deferred_files) +from .test_handler_write_files import (VALID_SCHEMA, INVALID_SCHEMA) +from cloudinit import log as logging +from cloudinit import util + +from cloudinit.tests.helpers import ( + CiTestCase, FilesystemMockingTestCase, mock, skipUnlessJsonSchema) + +LOG = logging.getLogger(__name__) + +YAML_TEXT = """ +users: + - name: 'bar' + files: + - path: '/home/bar/my-file.txt' + content: | + hi mom line 1 + hi mom line 2 +""" + + +@skipUnlessJsonSchema() +@mock.patch('cloudinit.config.cc_write_deferred_files.write_files') +class TestWriteDeferredFilesSchema(CiTestCase): + + with_logs = True + + def test_schema_validation_warns_missing_path(self, m_write_deferred_files): + """The only required file item property is 'path'.""" + + valid_config = { + 'users': [ + {'name': 'jeff', + 'files': [ + *VALID_SCHEMA.get('write_files'), + {'content': 'foo', 'path': '/bar'} + ]} + ] + } + + invalid_config = { # Dropped required path key + 'users': [ + {'name': 'jeff', + 'files': INVALID_SCHEMA.get('write_files')} + ] + } + + cc = self.tmp_cloud('ubuntu') + handle('cc_write_deferred_files', valid_config, cc, LOG, []) + self.assertNotIn('Invalid config:', self.logs.getvalue()) + handle('cc_write_deferred_files', invalid_config, cc, LOG, []) + self.assertIn('Invalid config:', self.logs.getvalue()) + self.assertIn("'path' is a required property", self.logs.getvalue()) + + +class TestWriteDeferredFiles(FilesystemMockingTestCase): + + with_logs = True + + def test_extracting_file_list_and_default_values(self): + cfg = util.load_yaml(YAML_TEXT) + cloud = self.tmp_cloud('ubuntu') + file_list = extract_deferred_files(cfg, cloud) + self.assertEqual(len(file_list), 1) + self.assertEqual(file_list[0].get('owner'), 'bar:bar') + self.assertEqual(file_list[0].get('permissions'), '0600') + + +# vi: ts=4 expandtab diff --git a/tests/unittests/test_handler/test_schema.py b/tests/unittests/test_handler/test_schema.py index 6f37ceb7427..1de3c5b732b 100644 --- a/tests/unittests/test_handler/test_schema.py +++ b/tests/unittests/test_handler/test_schema.py @@ -26,6 +26,7 @@ def test_get_schema_coalesces_known_schema(self): 'cc_apk_configure', 'cc_apt_configure', 'cc_bootcmd', + 'cc_write_deferred_files', 'cc_locale', 'cc_ntp', 'cc_resizefs', From 073785f9540318be90382e18228c859ea567a5b5 Mon Sep 17 00:00:00 2001 From: Lucendio Date: Sun, 5 Sep 2021 15:15:25 +0200 Subject: [PATCH 05/16] Remove users.files in favour of the new write_files[*].defer field This change set implements the feedback from recent discussions [1] on how to best support writing files at some later point during execution, so that, for example, a user who is configured as file owner would exist when the file is being created. THe general idea here is to keep the structure of the handlers of both modules (write_files[_deferred]) as similar as possible, since they do pretty much the same. The main difference is the lambda function used to filter deferred or not deferred files. For now, there is just some unit testing for the module handler. Integration tests should be added after the implementation has been reviewed. Specific details worth mentioning: * 'validate_cloudconfig_schema' has been moved to the top of the handler functions, because a) even if the 'write_files' top-level directive is configured and not empty, it might still be empty for one of the two write-file modules b) the schema validation is a formal check and has no awareness of any logical semantics, so it makes sense to first verify the former before moving forward * the new module handling deferred files has been renamed * write_files.handle has been slightly restructured to adhere to the changes in write_files_deferred.handle * revert all changes made to cc_users_groups.py in this branche while working on the deferred files feature [1] https://github.com/canonical/cloud-init/pull/916 --- cloudinit/config/cc_users_groups.py | 10 -- cloudinit/config/cc_write_deferred_files.py | 134 ------------------ cloudinit/config/cc_write_files.py | 37 ++++- cloudinit/config/cc_write_files_deferred.py | 50 +++++++ config/cloud.cfg.tmpl | 2 +- .../test_handler_write_deferred_files.py | 72 ---------- .../test_handler_write_files_deferred.py | 70 +++++++++ tests/unittests/test_handler/test_schema.py | 2 +- 8 files changed, 155 insertions(+), 222 deletions(-) delete mode 100644 cloudinit/config/cc_write_deferred_files.py create mode 100644 cloudinit/config/cc_write_files_deferred.py delete mode 100644 tests/unittests/test_handler/test_handler_write_deferred_files.py create mode 100644 tests/unittests/test_handler/test_handler_write_files_deferred.py diff --git a/cloudinit/config/cc_users_groups.py b/cloudinit/config/cc_users_groups.py index 5562b1a18ab..ac4a44100d8 100644 --- a/cloudinit/config/cc_users_groups.py +++ b/cloudinit/config/cc_users_groups.py @@ -28,11 +28,6 @@ - ``name``: The user's login name - ``expiredate``: Optional. Date on which the user's account will be disabled. Default: none - - ``files``: Optional. List of files following the same scheme of - ``write_files`` items, except that ``owner`` is inferred (note that the - user part is always ignored, group may be recognized if value stats with - a ``:``), that ``permissions`` defaults to ``'0600'``, and that these - configurations are being applied only after users & groups have been created - ``gecos``: Optional. Comment about the user, usually a comma-separated string of real name and contact information. Default: none - ``groups``: Optional. Additional groups to add the user to. Default: none @@ -109,7 +104,6 @@ sudo: false - name: expiredate: '' - files: gecos: groups: homedir: @@ -175,10 +169,6 @@ def handle(name, cfg, cloud, _log, _args): else: config['ssh_redirect_user'] = default_user config['cloud_public_ssh_keys'] = cloud_keys - - # Remove configuration handled by module 'write-deferred-files' - config.pop('files', None) - cloud.distro.create_user(user, **config) # vi: ts=4 expandtab diff --git a/cloudinit/config/cc_write_deferred_files.py b/cloudinit/config/cc_write_deferred_files.py deleted file mode 100644 index 95756fe3db2..00000000000 --- a/cloudinit/config/cc_write_deferred_files.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright (C) 2021 TODO -# -# Author: TODO -# -# This file is part of cloud-init. See LICENSE file for license information. - -"""Defer writing certain files like files defined in the Users & Groups module""" - -from textwrap import dedent - -from cloudinit.config.schema import validate_cloudconfig_schema -from cloudinit import log as logging -from cloudinit.settings import PER_INSTANCE -from cloudinit import util -from cloudinit.config.cc_write_files import ( schema as write_files_schema, write_files ) -from cloudinit.distros import ug_util - - -frequency = PER_INSTANCE - -DEFAULT_OWNER = ":" -DEFAULT_PERMS = '0600' -UNKNOWN_ENC = 'text/plain' - -LOG = logging.getLogger(__name__) - -distros = ['all'] - -schema = { - 'id': 'cc_write_deferred_files', - 'name': 'Write Deferred Files', - 'title': 'write certain files, whose creation as been deferred, during final stage', - 'description': dedent("""\ - This module is heavily based on `'Write Files' `__, but - the list of files being created, is gathered from other parts of the - configuration. Therefore, the same options are available, but, depending - on where a file is defined, certain attributes may be inferred. - - See the respective module documentation for details: - - - `Users and Groups`_ - `"""), - 'distros': distros, - 'examples': [ - dedent("""\ - # Extend ~/.profile after user (and probably the file itself) has been created - users: - - name: 'alice' - files: - - path: '/home/alice/.profile' - content: | - PATH="/usr/local/opt/python37/bin:$PATH" - append: true - """) - ], - 'frequency': frequency, - 'type': 'object', - 'properties': { - 'users': { - 'type': ['array'], - 'items': { - 'type': ['string', 'object'], - 'additionalProperties': True, - 'properties': { - 'name': { - 'type': 'string', - 'description': 'name of the user' - }, - 'files': util.mergemanydict([ - { - 'type': ['array'], - 'items': { - 'type': 'object', - 'properties': { - 'owner': { - 'type': 'string', - 'default': DEFAULT_OWNER, - 'description': dedent("""\ - Optional group to chown on the file (user is ignored). Default: - **user:user** - """), - }, - 'permissions': { - 'type': 'string', - 'default': DEFAULT_PERMS, - 'description': dedent("""\ - Optional file permissions to set on ``path`` - represented as an octal string '0###'. Default: - **'{perms}'** - """.format(perms=DEFAULT_PERMS)), - } - } - } - }, - write_files_schema.get('properties').get('write_files') - ]) - } - } - } - } -} - -# Not exposed, because related modules should document this behaviour -__doc__ = None - - -def handle(name, cfg, cloud, log, _args): - validate_cloudconfig_schema(cfg, schema) - file_list = extract_deferred_files(cfg, cloud) - if len(file_list) <= 0: - log.debug(("Skipping module named %s," - " no deferred file writing in configuration"), name) - return - write_files(name, file_list) - - -def extract_deferred_files(cfg, cloud): - deferred_files = [] - - (users, _) = ug_util.normalize_users_groups(cfg, cloud.distro) - for (user, config) in users.items(): - user_files = config.get('files', []) - for file in user_files: - (_, file_owner_group) = util.extract_usergroup(file.get('owner', DEFAULT_OWNER)) - if file_owner_group is None: - file_owner_group = user - file['owner'] = "{u}:{g}".format(u=user, g=file_owner_group) - file['permissions'] = config.get('permissions', DEFAULT_PERMS) - deferred_files.append(file) - - return deferred_files - - -# vi: ts=4 expandtab diff --git a/cloudinit/config/cc_write_files.py b/cloudinit/config/cc_write_files.py index 8601e70799f..389afe5027b 100644 --- a/cloudinit/config/cc_write_files.py +++ b/cloudinit/config/cc_write_files.py @@ -21,6 +21,7 @@ DEFAULT_OWNER = "root:root" DEFAULT_PERMS = 0o644 +DEFAULT_DEFER = False UNKNOWN_ENC = 'text/plain' LOG = logging.getLogger(__name__) @@ -90,6 +91,24 @@ # Create an empty file on the system write_files: - path: /root/CLOUD_INIT_WAS_HERE + """), + dedent("""\ + # Defer writing the file until after the package (Nginx) is + # installed and its user is created alongside + write_files: + - path: /etc/nginx/conf.d/example.com.conf + content: | + server { + server_name example.com; + listen 80; + root /var/www; + location / { + try_files $uri $uri/ $uri.html =404; + } + } + owner: 'nginx:nginx' + permissions: '0640' + defer: true """)], 'frequency': frequency, 'type': 'object', @@ -151,6 +170,14 @@ ``path`` exists. Default: **false**. """), }, + 'defer': { + 'type': 'boolean', + 'default': DEFAULT_DEFER, + 'description': dedent("""\ + Defer writing the file until 'final' stage, after users + were created, and packages were installed. Default: **{defer}**. + """.format(defer=DEFAULT_DEFER)), + }, }, 'required': ['path'], 'additionalProperties': False @@ -163,13 +190,15 @@ def handle(name, cfg, _cloud, log, _args): - files = cfg.get('write_files') - if not files: + validate_cloudconfig_schema(cfg, schema) + file_list = cfg.get('write_files', []) + is_not_deferred_file = lambda f: f.get('defer', DEFAULT_DEFER) == False + filtered_files = list(filter(is_not_deferred_file, file_list)) + if not filtered_files: log.debug(("Skipping module named %s," " no/empty 'write_files' key in configuration"), name) return - validate_cloudconfig_schema(cfg, schema) - write_files(name, files) + write_files(name, filtered_files) def canonicalize_extraction(encoding_type): diff --git a/cloudinit/config/cc_write_files_deferred.py b/cloudinit/config/cc_write_files_deferred.py new file mode 100644 index 00000000000..5625de88f64 --- /dev/null +++ b/cloudinit/config/cc_write_files_deferred.py @@ -0,0 +1,50 @@ +# Copyright (C) 2021 TODO +# +# Author: TODO +# +# This file is part of cloud-init. See LICENSE file for license information. + +"""Defer writing certain files""" + +from textwrap import dedent + +from cloudinit.config.schema import validate_cloudconfig_schema +from cloudinit import util +from cloudinit.config.cc_write_files import ( + schema as write_files_schema, write_files, DEFAULT_DEFER) + + +schema = util.mergemanydict([ + { + 'id': 'cc_write_files_deferred', + 'name': 'Write Deferred Files', + 'title': 'write certain files, whose creation as been deferred, during final stage', + 'description': dedent("""\ + This module is based on `'Write Files' `__, and will handle all + files from the write_files list, that have been marked as deferred and thus are + not being processed by the write-files module. + + *Please note that his module is not exposed to the user through its own dedicated + top-level directive.* + `""") + }, + write_files_schema +]) + +# Not exposed, because related modules should document this behaviour +__doc__ = None + + +def handle(name, cfg, _cloud, log, _args): + validate_cloudconfig_schema(cfg, schema) + file_list = cfg.get('write_files', []) + is_deferred_file = lambda f: f.get('defer', DEFAULT_DEFER) == True + filtered_files = list(filter(is_deferred_file, file_list)) + if not filtered_files: + log.debug(("Skipping module named %s," + " no deferred file defined in configuration"), name) + return + write_files(name, filtered_files) + + +# vi: ts=4 expandtab diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl index 234b2ca53c4..66c48fd5406 100644 --- a/config/cloud.cfg.tmpl +++ b/config/cloud.cfg.tmpl @@ -151,7 +151,7 @@ cloud_final_modules: {% if variant in ["ubuntu", "unknown"] %} - ubuntu-drivers {% endif %} - - write-deferred-files + - write-files-deferred - puppet - chef - mcollective diff --git a/tests/unittests/test_handler/test_handler_write_deferred_files.py b/tests/unittests/test_handler/test_handler_write_deferred_files.py deleted file mode 100644 index 0fbc55f0828..00000000000 --- a/tests/unittests/test_handler/test_handler_write_deferred_files.py +++ /dev/null @@ -1,72 +0,0 @@ -# This file is part of cloud-init. See LICENSE file for license information. - -from cloudinit.config.cc_write_deferred_files import ( - handle, extract_deferred_files) -from .test_handler_write_files import (VALID_SCHEMA, INVALID_SCHEMA) -from cloudinit import log as logging -from cloudinit import util - -from cloudinit.tests.helpers import ( - CiTestCase, FilesystemMockingTestCase, mock, skipUnlessJsonSchema) - -LOG = logging.getLogger(__name__) - -YAML_TEXT = """ -users: - - name: 'bar' - files: - - path: '/home/bar/my-file.txt' - content: | - hi mom line 1 - hi mom line 2 -""" - - -@skipUnlessJsonSchema() -@mock.patch('cloudinit.config.cc_write_deferred_files.write_files') -class TestWriteDeferredFilesSchema(CiTestCase): - - with_logs = True - - def test_schema_validation_warns_missing_path(self, m_write_deferred_files): - """The only required file item property is 'path'.""" - - valid_config = { - 'users': [ - {'name': 'jeff', - 'files': [ - *VALID_SCHEMA.get('write_files'), - {'content': 'foo', 'path': '/bar'} - ]} - ] - } - - invalid_config = { # Dropped required path key - 'users': [ - {'name': 'jeff', - 'files': INVALID_SCHEMA.get('write_files')} - ] - } - - cc = self.tmp_cloud('ubuntu') - handle('cc_write_deferred_files', valid_config, cc, LOG, []) - self.assertNotIn('Invalid config:', self.logs.getvalue()) - handle('cc_write_deferred_files', invalid_config, cc, LOG, []) - self.assertIn('Invalid config:', self.logs.getvalue()) - self.assertIn("'path' is a required property", self.logs.getvalue()) - - -class TestWriteDeferredFiles(FilesystemMockingTestCase): - - with_logs = True - - def test_extracting_file_list_and_default_values(self): - cfg = util.load_yaml(YAML_TEXT) - cloud = self.tmp_cloud('ubuntu') - file_list = extract_deferred_files(cfg, cloud) - self.assertEqual(len(file_list), 1) - self.assertEqual(file_list[0].get('owner'), 'bar:bar') - self.assertEqual(file_list[0].get('permissions'), '0600') - - -# vi: ts=4 expandtab diff --git a/tests/unittests/test_handler/test_handler_write_files_deferred.py b/tests/unittests/test_handler/test_handler_write_files_deferred.py new file mode 100644 index 00000000000..cc0a1a90d33 --- /dev/null +++ b/tests/unittests/test_handler/test_handler_write_files_deferred.py @@ -0,0 +1,70 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +import tempfile +import shutil + +from cloudinit.config.cc_write_files_deferred import (handle) +from .test_handler_write_files import (VALID_SCHEMA) +from cloudinit import log as logging +from cloudinit import util + +from cloudinit.tests.helpers import ( + CiTestCase, FilesystemMockingTestCase, mock, skipUnlessJsonSchema) + +LOG = logging.getLogger(__name__) + + +@skipUnlessJsonSchema() +@mock.patch('cloudinit.config.cc_write_files_deferred.write_files') +class TestWriteFilesDeferredSchema(CiTestCase): + + with_logs = True + + def test_schema_validation_warns_invalid_value(self, m_write_files_deferred): + """If 'defer' is defined, it must be of type 'bool'.""" + + valid_config = { + 'write_files': [ + { **VALID_SCHEMA.get('write_files')[0], 'defer': True } + ] + } + + invalid_config = { + 'write_files': [ + { **VALID_SCHEMA.get('write_files')[0], 'defer': str('no') } + ] + } + + cc = self.tmp_cloud('ubuntu') + handle('cc_write_files_deferred', valid_config, cc, LOG, []) + self.assertNotIn('Invalid config:', self.logs.getvalue()) + handle('cc_write_files_deferred', invalid_config, cc, LOG, []) + self.assertIn('Invalid config:', self.logs.getvalue()) + self.assertIn("defer: 'no' is not of type 'boolean'", self.logs.getvalue()) + + +class TestWriteFilesDeferred(FilesystemMockingTestCase): + + with_logs = True + + def setUp(self): + super(TestWriteFilesDeferred, self).setUp() + self.tmp = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.tmp) + + def test_filtering_deferred_files(self): + self.patchUtils(self.tmp) + expected = "hello world\n" + config = { + 'write_files': [ + { 'path': '/tmp/deferred.file', 'defer': True, 'content': expected }, + { 'path': '/tmp/not_deferred.file' } + ] + } + cc = self.tmp_cloud('ubuntu') + handle('cc_write_files_deferred', config, cc, LOG, []) + self.assertEqual(util.load_file('/tmp/deferred.file'), expected) + self.assertRaises(FileNotFoundError, util.load_file, '/tmp/not_deferred.file') + + +# vi: ts=4 expandtab diff --git a/tests/unittests/test_handler/test_schema.py b/tests/unittests/test_handler/test_schema.py index 1de3c5b732b..59f58f7c682 100644 --- a/tests/unittests/test_handler/test_schema.py +++ b/tests/unittests/test_handler/test_schema.py @@ -26,7 +26,6 @@ def test_get_schema_coalesces_known_schema(self): 'cc_apk_configure', 'cc_apt_configure', 'cc_bootcmd', - 'cc_write_deferred_files', 'cc_locale', 'cc_ntp', 'cc_resizefs', @@ -35,6 +34,7 @@ def test_get_schema_coalesces_known_schema(self): 'cc_ubuntu_advantage', 'cc_ubuntu_drivers', 'cc_write_files', + 'cc_write_files_deferred', 'cc_zypper_add_repo', 'cc_chef' ], From 6bfbdfc25de742595de411a658d7d28a29c868bb Mon Sep 17 00:00:00 2001 From: Lucendio Date: Thu, 16 Sep 2021 22:32:33 +0200 Subject: [PATCH 06/16] Integrate first couple of review feedback (the simple ones) --- cloudinit/config/cc_write_files_deferred.py | 4 +--- .../test_handler/test_handler_write_files_deferred.py | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cloudinit/config/cc_write_files_deferred.py b/cloudinit/config/cc_write_files_deferred.py index 5625de88f64..3cf26fb1f69 100644 --- a/cloudinit/config/cc_write_files_deferred.py +++ b/cloudinit/config/cc_write_files_deferred.py @@ -1,6 +1,4 @@ -# Copyright (C) 2021 TODO -# -# Author: TODO +# Copyright (C) 2021 Canonical Ltd. # # This file is part of cloud-init. See LICENSE file for license information. diff --git a/tests/unittests/test_handler/test_handler_write_files_deferred.py b/tests/unittests/test_handler/test_handler_write_files_deferred.py index cc0a1a90d33..53f0ed036e7 100644 --- a/tests/unittests/test_handler/test_handler_write_files_deferred.py +++ b/tests/unittests/test_handler/test_handler_write_files_deferred.py @@ -64,7 +64,8 @@ def test_filtering_deferred_files(self): cc = self.tmp_cloud('ubuntu') handle('cc_write_files_deferred', config, cc, LOG, []) self.assertEqual(util.load_file('/tmp/deferred.file'), expected) - self.assertRaises(FileNotFoundError, util.load_file, '/tmp/not_deferred.file') + with self.assertRaises(FileNotFoundError): + util.load_file('/tmp/not_deferred.file') # vi: ts=4 expandtab From f0b2e1a1d43dca16b1b263da4c91b014ccc19b4b Mon Sep 17 00:00:00 2001 From: Lucendio Date: Thu, 16 Sep 2021 22:53:41 +0200 Subject: [PATCH 07/16] Adhere to linter rules * mostly line length or too many spaces --- cloudinit/config/cc_write_files.py | 5 +++-- cloudinit/config/cc_write_files_deferred.py | 20 +++++++++++-------- .../test_handler_write_files_deferred.py | 20 ++++++++++++------- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/cloudinit/config/cc_write_files.py b/cloudinit/config/cc_write_files.py index 389afe5027b..8958542e032 100644 --- a/cloudinit/config/cc_write_files.py +++ b/cloudinit/config/cc_write_files.py @@ -174,8 +174,9 @@ 'type': 'boolean', 'default': DEFAULT_DEFER, 'description': dedent("""\ - Defer writing the file until 'final' stage, after users - were created, and packages were installed. Default: **{defer}**. + Defer writing the file until 'final' stage, after + users were created, and packages were installed. + Default: **{defer}**. """.format(defer=DEFAULT_DEFER)), }, }, diff --git a/cloudinit/config/cc_write_files_deferred.py b/cloudinit/config/cc_write_files_deferred.py index 3cf26fb1f69..d3a59287b09 100644 --- a/cloudinit/config/cc_write_files_deferred.py +++ b/cloudinit/config/cc_write_files_deferred.py @@ -16,15 +16,19 @@ { 'id': 'cc_write_files_deferred', 'name': 'Write Deferred Files', - 'title': 'write certain files, whose creation as been deferred, during final stage', + 'title': dedent("""\ + write certain files, whose creation as been deferred, during + final stage + """), 'description': dedent("""\ - This module is based on `'Write Files' `__, and will handle all - files from the write_files list, that have been marked as deferred and thus are - not being processed by the write-files module. - - *Please note that his module is not exposed to the user through its own dedicated - top-level directive.* - `""") + This module is based on `'Write Files' `__, and + will handle all files from the write_files list, that have been + marked as deferred and thus are not being processed by the + write-files module. + + *Please note that his module is not exposed to the user through + its own dedicated top-level directive.* + """) }, write_files_schema ]) diff --git a/tests/unittests/test_handler/test_handler_write_files_deferred.py b/tests/unittests/test_handler/test_handler_write_files_deferred.py index 53f0ed036e7..57b6934a083 100644 --- a/tests/unittests/test_handler/test_handler_write_files_deferred.py +++ b/tests/unittests/test_handler/test_handler_write_files_deferred.py @@ -20,18 +20,19 @@ class TestWriteFilesDeferredSchema(CiTestCase): with_logs = True - def test_schema_validation_warns_invalid_value(self, m_write_files_deferred): + def test_schema_validation_warns_invalid_value(self, + m_write_files_deferred): """If 'defer' is defined, it must be of type 'bool'.""" - valid_config = { + valid_config = { 'write_files': [ - { **VALID_SCHEMA.get('write_files')[0], 'defer': True } + {**VALID_SCHEMA.get('write_files')[0], 'defer': True} ] } invalid_config = { 'write_files': [ - { **VALID_SCHEMA.get('write_files')[0], 'defer': str('no') } + {**VALID_SCHEMA.get('write_files')[0], 'defer': str('no')} ] } @@ -40,7 +41,8 @@ def test_schema_validation_warns_invalid_value(self, m_write_files_deferred): self.assertNotIn('Invalid config:', self.logs.getvalue()) handle('cc_write_files_deferred', invalid_config, cc, LOG, []) self.assertIn('Invalid config:', self.logs.getvalue()) - self.assertIn("defer: 'no' is not of type 'boolean'", self.logs.getvalue()) + self.assertIn("defer: 'no' is not of type 'boolean'", + self.logs.getvalue()) class TestWriteFilesDeferred(FilesystemMockingTestCase): @@ -57,8 +59,12 @@ def test_filtering_deferred_files(self): expected = "hello world\n" config = { 'write_files': [ - { 'path': '/tmp/deferred.file', 'defer': True, 'content': expected }, - { 'path': '/tmp/not_deferred.file' } + { + 'path': '/tmp/deferred.file', + 'defer': True, + 'content': expected + }, + {'path': '/tmp/not_deferred.file'} ] } cc = self.tmp_cloud('ubuntu') From f85690dc71a9fc15428a297d28304d5ee8f6b9be Mon Sep 17 00:00:00 2001 From: Lucendio Date: Thu, 16 Sep 2021 22:56:25 +0200 Subject: [PATCH 08/16] Move from functional filtering to list comprehension This also fixes the last flake8 complaints. --- cloudinit/config/cc_write_files.py | 5 +++-- cloudinit/config/cc_write_files_deferred.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cloudinit/config/cc_write_files.py b/cloudinit/config/cc_write_files.py index 8958542e032..12166f57633 100644 --- a/cloudinit/config/cc_write_files.py +++ b/cloudinit/config/cc_write_files.py @@ -193,8 +193,9 @@ def handle(name, cfg, _cloud, log, _args): validate_cloudconfig_schema(cfg, schema) file_list = cfg.get('write_files', []) - is_not_deferred_file = lambda f: f.get('defer', DEFAULT_DEFER) == False - filtered_files = list(filter(is_not_deferred_file, file_list)) + filtered_files = [ + f for f in file_list if not f.get('defer', DEFAULT_DEFER) + ] if not filtered_files: log.debug(("Skipping module named %s," " no/empty 'write_files' key in configuration"), name) diff --git a/cloudinit/config/cc_write_files_deferred.py b/cloudinit/config/cc_write_files_deferred.py index d3a59287b09..582e2b74535 100644 --- a/cloudinit/config/cc_write_files_deferred.py +++ b/cloudinit/config/cc_write_files_deferred.py @@ -40,8 +40,9 @@ def handle(name, cfg, _cloud, log, _args): validate_cloudconfig_schema(cfg, schema) file_list = cfg.get('write_files', []) - is_deferred_file = lambda f: f.get('defer', DEFAULT_DEFER) == True - filtered_files = list(filter(is_deferred_file, file_list)) + filtered_files = [ + f for f in file_list if f.get('defer', DEFAULT_DEFER) + ] if not filtered_files: log.debug(("Skipping module named %s," " no deferred file defined in configuration"), name) From 6f6042ab22eaa99909dcb46570720815be11f7e9 Mon Sep 17 00:00:00 2001 From: Lucendio Date: Sat, 18 Sep 2021 18:33:15 +0200 Subject: [PATCH 09/16] Make processing the input more robust If scheme validation fails, it will just warn but not stop the execution. As @TheRealFalcon put it: it's better to log a warning and do what the user intended rather than log a warning and do the wrong thing. --- cloudinit/config/cc_write_files.py | 4 +++- cloudinit/config/cc_write_files_deferred.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cloudinit/config/cc_write_files.py b/cloudinit/config/cc_write_files.py index 12166f57633..41c75fa2de0 100644 --- a/cloudinit/config/cc_write_files.py +++ b/cloudinit/config/cc_write_files.py @@ -194,7 +194,9 @@ def handle(name, cfg, _cloud, log, _args): validate_cloudconfig_schema(cfg, schema) file_list = cfg.get('write_files', []) filtered_files = [ - f for f in file_list if not f.get('defer', DEFAULT_DEFER) + f for f in file_list if not util.get_cfg_option_bool(f, + 'defer', + DEFAULT_DEFER) ] if not filtered_files: log.debug(("Skipping module named %s," diff --git a/cloudinit/config/cc_write_files_deferred.py b/cloudinit/config/cc_write_files_deferred.py index 582e2b74535..0c75aa22890 100644 --- a/cloudinit/config/cc_write_files_deferred.py +++ b/cloudinit/config/cc_write_files_deferred.py @@ -41,7 +41,9 @@ def handle(name, cfg, _cloud, log, _args): validate_cloudconfig_schema(cfg, schema) file_list = cfg.get('write_files', []) filtered_files = [ - f for f in file_list if f.get('defer', DEFAULT_DEFER) + f for f in file_list if util.get_cfg_option_bool(f, + 'defer', + DEFAULT_DEFER) ] if not filtered_files: log.debug(("Skipping module named %s," From 0b062c013aab589915a9d964d658a1cdb2047a1d Mon Sep 17 00:00:00 2001 From: Lucendio Date: Sat, 23 Oct 2021 01:55:54 +0200 Subject: [PATCH 10/16] Add unit test to write_files THis test is supposed to verify that files flagged as deferred are not being created by the 'write_files' module. --- .../test_handler/test_handler_write_files.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/unittests/test_handler/test_handler_write_files.py b/tests/unittests/test_handler/test_handler_write_files.py index 727681d3c96..0af92805798 100644 --- a/tests/unittests/test_handler/test_handler_write_files.py +++ b/tests/unittests/test_handler/test_handler_write_files.py @@ -189,6 +189,19 @@ def test_all_decodings(self): len(gz_aliases + gz_b64_aliases + b64_aliases) * len(datum)) self.assertEqual(len(expected), flen_expected) + def test_deferred(self): + self.patchUtils(self.tmp) + file_path = '/tmp/deferred.file' + config = { + 'write_files': [ + {'path': file_path, 'defer': True} + ] + } + cc = self.tmp_cloud('ubuntu') + handle('cc_write_file', config, cc, LOG, []) + with self.assertRaises(FileNotFoundError): + util.load_file(file_path) + class TestDecodePerms(CiTestCase): From 96a13386e5712c3621ab7477f900547ebeda84e3 Mon Sep 17 00:00:00 2001 From: Lucendio Date: Sat, 23 Oct 2021 02:20:59 +0200 Subject: [PATCH 11/16] Add integration test for write_files_deferred module The test is supposed to verify that: * deferred file writings are indeed happening at a later stage (compared to the default behaviour of write_files) * deferred files are created after users & groups have been created * thus deferred files can be owned by such users --- .../modules/test_write_files_deferred.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/integration_tests/modules/test_write_files_deferred.py diff --git a/tests/integration_tests/modules/test_write_files_deferred.py b/tests/integration_tests/modules/test_write_files_deferred.py new file mode 100644 index 00000000000..8ceead8fab4 --- /dev/null +++ b/tests/integration_tests/modules/test_write_files_deferred.py @@ -0,0 +1,44 @@ +"""Integration test for the write_files deferred module. + +This test aims to verify that a deferred file can be created +and owned by a user that is created during the same cloud-init +run. +""" + +import pytest + + +TEST_USER_NAME = 'testuser' +TEST_USER_FILE_PATH = '/home/testuser/.profile' + +USER_DATA = """\ +#cloud-config +users: + - name: '{user}' +write_files: + - path: '/var/lib/test/bin/test-cmd' + content: | + #!/usr/bin/env bash + echo 'hello world' + permissions: '0111' + - path: '{file}' + content: | + export PATH="/var/lib/test/bin:$PATH" + append: true + defer: true + owner: '{user}' +""".format(user=TEST_USER_NAME, file=TEST_USER_FILE_PATH) + + +@pytest.mark.ci +@pytest.mark.user_data(USER_DATA) +class TestWriteFilesDeferred: + + @pytest.mark.parametrize( + "cmd,expected_out", ( + ("ls -l {}".format(TEST_USER_FILE_PATH), TEST_USER_NAME), + ) + ) + def test_write_files_deferred(self, cmd, expected_out, class_client): + out = class_client.execute(cmd) + assert expected_out in out From 3a56b39bc187dcb6d2cadf1ec482f3be83d51a01 Mon Sep 17 00:00:00 2001 From: Lucendio Date: Sat, 23 Oct 2021 02:22:16 +0200 Subject: [PATCH 12/16] Add author's username to CLA signers --- tools/.github-cla-signers | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers index 99f7d99c297..fac3fceca0d 100644 --- a/tools/.github-cla-signers +++ b/tools/.github-cla-signers @@ -42,6 +42,7 @@ jshen28 klausenbusk landon912 lucasmoura +lucendio lungj mal mamercad From 32ac09ad17fa1bd8b841ea235a17144b82e0fd95 Mon Sep 17 00:00:00 2001 From: Lucendio Date: Sat, 23 Oct 2021 19:55:16 +0200 Subject: [PATCH 13/16] Just a test - To be reverted Should cause a failure during integration testing --- tests/integration_tests/modules/test_write_files_deferred.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_tests/modules/test_write_files_deferred.py b/tests/integration_tests/modules/test_write_files_deferred.py index 8ceead8fab4..1159f798ae4 100644 --- a/tests/integration_tests/modules/test_write_files_deferred.py +++ b/tests/integration_tests/modules/test_write_files_deferred.py @@ -25,7 +25,7 @@ content: | export PATH="/var/lib/test/bin:$PATH" append: true - defer: true + defer: false owner: '{user}' """.format(user=TEST_USER_NAME, file=TEST_USER_FILE_PATH) From db1d8dae41a2780af36042c862db1a7f652147aa Mon Sep 17 00:00:00 2001 From: Lucendio Date: Sun, 24 Oct 2021 02:52:11 +0200 Subject: [PATCH 14/16] Revert "Just a test - To be reverted" This reverts commit a401f726ea1d9b8f390867ec801981d292ce091f. --- tests/integration_tests/modules/test_write_files_deferred.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_tests/modules/test_write_files_deferred.py b/tests/integration_tests/modules/test_write_files_deferred.py index 1159f798ae4..8ceead8fab4 100644 --- a/tests/integration_tests/modules/test_write_files_deferred.py +++ b/tests/integration_tests/modules/test_write_files_deferred.py @@ -25,7 +25,7 @@ content: | export PATH="/var/lib/test/bin:$PATH" append: true - defer: false + defer: true owner: '{user}' """.format(user=TEST_USER_NAME, file=TEST_USER_FILE_PATH) From d1e2f2baf5aa1cfb084574b7b688463ac1c60237 Mon Sep 17 00:00:00 2001 From: Lucendio Date: Sun, 24 Oct 2021 02:52:51 +0200 Subject: [PATCH 15/16] Revert "Add integration test for write_files_deferred module" This reverts commit e71848553070d5a25a31652dc1c9bd0a976d89c7. --- .../modules/test_write_files_deferred.py | 44 ------------------- 1 file changed, 44 deletions(-) delete mode 100644 tests/integration_tests/modules/test_write_files_deferred.py diff --git a/tests/integration_tests/modules/test_write_files_deferred.py b/tests/integration_tests/modules/test_write_files_deferred.py deleted file mode 100644 index 8ceead8fab4..00000000000 --- a/tests/integration_tests/modules/test_write_files_deferred.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Integration test for the write_files deferred module. - -This test aims to verify that a deferred file can be created -and owned by a user that is created during the same cloud-init -run. -""" - -import pytest - - -TEST_USER_NAME = 'testuser' -TEST_USER_FILE_PATH = '/home/testuser/.profile' - -USER_DATA = """\ -#cloud-config -users: - - name: '{user}' -write_files: - - path: '/var/lib/test/bin/test-cmd' - content: | - #!/usr/bin/env bash - echo 'hello world' - permissions: '0111' - - path: '{file}' - content: | - export PATH="/var/lib/test/bin:$PATH" - append: true - defer: true - owner: '{user}' -""".format(user=TEST_USER_NAME, file=TEST_USER_FILE_PATH) - - -@pytest.mark.ci -@pytest.mark.user_data(USER_DATA) -class TestWriteFilesDeferred: - - @pytest.mark.parametrize( - "cmd,expected_out", ( - ("ls -l {}".format(TEST_USER_FILE_PATH), TEST_USER_NAME), - ) - ) - def test_write_files_deferred(self, cmd, expected_out, class_client): - out = class_client.execute(cmd) - assert expected_out in out From 28b4bb340ed3a546e852637da45e3095b15129c8 Mon Sep 17 00:00:00 2001 From: Lucendio Date: Sun, 24 Oct 2021 03:08:30 +0200 Subject: [PATCH 16/16] Extend write_files integration test to verify 'defer' behaviour The test is supposed to verify that: * deferred file writings are indeed happening at a later stage (compared to the default behaviour of write_files) * deferred files are created after users & groups have been created * thus deferred files can be configured with and owned by users or groups created during the same cloud-init run Co-authored-by: James Falcon --- .../modules/test_write_files.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/integration_tests/modules/test_write_files.py b/tests/integration_tests/modules/test_write_files.py index 15832ae3caf..1d532facfd3 100644 --- a/tests/integration_tests/modules/test_write_files.py +++ b/tests/integration_tests/modules/test_write_files.py @@ -21,6 +21,9 @@ # USER_DATA = """\ #cloud-config +users: +- default +- name: myuser write_files: - encoding: b64 content: {} @@ -41,6 +44,12 @@ H4sIAIDb/U8C/1NW1E/KzNMvzuBKTc7IV8hIzcnJVyjPL8pJ4QIA6N+MVxsAAAA= path: /root/file_gzip permissions: '0755' +- path: '/home/testuser/my-file' + content: | + echo 'hello world!' + defer: true + owner: 'myuser' + permissions: '0644' """.format(B64_CONTENT.decode("ascii")) @@ -64,3 +73,15 @@ class TestWriteFiles: def test_write_files(self, cmd, expected_out, class_client): out = class_client.execute(cmd) assert expected_out in out + + def test_write_files_deferred(self, class_client): + """Test that write files deferred works as expected. + + Users get created after write_files module runs, so ensure that + with `defer: true`, the file gets written with correct ownership. + """ + out = class_client.read_from_file("/home/testuser/my-file") + assert "echo 'hello world!'" == out + assert class_client.execute( + 'stat -c "%U %a" /home/testuser/my-file' + ) == 'myuser 644'