Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 37 additions & 4 deletions cloudinit/config/cc_write_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

DEFAULT_OWNER = "root:root"
DEFAULT_PERMS = 0o644
DEFAULT_DEFER = False
Comment thread
TheRealFalcon marked this conversation as resolved.
UNKNOWN_ENC = 'text/plain'

LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -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
Comment thread
lucendio marked this conversation as resolved.
# 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',
Expand Down Expand Up @@ -151,6 +170,15 @@
``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
Expand All @@ -163,13 +191,18 @@


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', [])
filtered_files = [
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,"
" 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):
Expand Down
55 changes: 55 additions & 0 deletions cloudinit/config/cc_write_files_deferred.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright (C) 2021 Canonical Ltd.
#
# 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)
Comment thread
TheRealFalcon marked this conversation as resolved.


schema = util.mergemanydict([
{
'id': 'cc_write_files_deferred',
'name': 'Write Deferred Files',
'title': dedent("""\
write certain files, whose creation as been deferred, during
final stage
"""),
'description': dedent("""\
This module is based on `'Write Files' <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', [])
filtered_files = [
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,"
" no deferred file defined in configuration"), name)
return
write_files(name, filtered_files)


# vi: ts=4 expandtab
1 change: 1 addition & 0 deletions config/cloud.cfg.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ cloud_final_modules:
{% if variant in ["ubuntu", "unknown"] %}
- ubuntu-drivers
{% endif %}
- write-files-deferred
- puppet
- chef
- mcollective
Expand Down
21 changes: 21 additions & 0 deletions tests/integration_tests/modules/test_write_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
#
USER_DATA = """\
#cloud-config
users:
- default
- name: myuser
write_files:
- encoding: b64
content: {}
Expand All @@ -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"))


Expand All @@ -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'
13 changes: 13 additions & 0 deletions tests/unittests/test_handler/test_handler_write_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
77 changes: 77 additions & 0 deletions tests/unittests/test_handler/test_handler_write_files_deferred.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# 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)
with self.assertRaises(FileNotFoundError):
util.load_file('/tmp/not_deferred.file')


# vi: ts=4 expandtab
1 change: 1 addition & 0 deletions tests/unittests/test_handler/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,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'
],
Expand Down
1 change: 1 addition & 0 deletions tools/.github-cla-signers
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ jshen28
klausenbusk
landon912
lucasmoura
lucendio
lungj
mal
mamercad
Expand Down