From f09dc6431a624e45aa2ac4d00cfe400f49b3331c Mon Sep 17 00:00:00 2001
From: Madalin <mbivolan@cloudbasesolutions.com>
Date: Mon, 2 Dec 2019 16:16:20 +0200
Subject: [PATCH] write_files: Added option to append the file with given
 content

If the option is set to true, the code will append the file with given content.

Cloud-config example:
write_files:
  -
    content: "append"
    path: 'C:\\path.txt'
    append: true

Fixes: https://github.com/cloudbase/cloudbase-init/issues/25

Change-Id: Ifd3d21842042dd34f7fde38a992e7fd74c1a595d
---
 .../cloudconfigplugins/write_files.py         |  8 ++-
 .../cloudconfigplugins/test_write_files.py    | 49 +++++++++++++++++++
 2 files changed, 56 insertions(+), 1 deletion(-)

diff --git a/cloudbaseinit/plugins/common/userdataplugins/cloudconfigplugins/write_files.py b/cloudbaseinit/plugins/common/userdataplugins/cloudconfigplugins/write_files.py
index c966442c..1ea3cced 100644
--- a/cloudbaseinit/plugins/common/userdataplugins/cloudconfigplugins/write_files.py
+++ b/cloudbaseinit/plugins/common/userdataplugins/cloudconfigplugins/write_files.py
@@ -132,6 +132,7 @@ class WriteFilesPlugin(base.BaseCloudConfigPlugin):
         permissions: The octal permissions set that should be given for
         this file.
         encoding: An optional encoding specification for the file.
+        append: An optional flag to append the content
 
     The only required keys in this dictionary are `path` and `content`.
     """
@@ -146,7 +147,12 @@ class WriteFilesPlugin(base.BaseCloudConfigPlugin):
         content = _process_content(item['content'],
                                    item.get('encoding'))
         permissions = _convert_permissions(item.get('permissions'))
-        _write_file(path, content, permissions)
+
+        open_mode = "wb"
+        if item.get('append', False):
+            open_mode = "ab"
+
+        _write_file(path, content, permissions, open_mode)
 
     def process(self, data):
         """Process the given data received from the cloud-config userdata.
diff --git a/cloudbaseinit/tests/plugins/common/userdataplugins/cloudconfigplugins/test_write_files.py b/cloudbaseinit/tests/plugins/common/userdataplugins/cloudconfigplugins/test_write_files.py
index 73707b33..c769e5d4 100644
--- a/cloudbaseinit/tests/plugins/common/userdataplugins/cloudconfigplugins/test_write_files.py
+++ b/cloudbaseinit/tests/plugins/common/userdataplugins/cloudconfigplugins/test_write_files.py
@@ -266,3 +266,52 @@ class WriteFilesPluginTests(unittest.TestCase):
 
         expected = "Can't process the type of data %r" % type(1)
         self.assertEqual(expected, str(cm.exception))
+
+    def test_process_item_fail(self):
+        fake_data = {}
+
+        with testutils.LogSnatcher('cloudbaseinit.plugins.common.'
+                                   'userdataplugins.cloudconfigplugins.'
+                                   'write_files') as snatcher:
+            write_files.WriteFilesPlugin()._process_item(fake_data)
+
+        self.assertEqual(['Missing required keys from file information {}'],
+                         snatcher.output)
+
+    @mock.patch('cloudbaseinit.plugins.common.userdataplugins.'
+                'cloudconfigplugins.write_files._process_content')
+    @mock.patch('cloudbaseinit.plugins.common.userdataplugins.'
+                'cloudconfigplugins.write_files._write_file')
+    @mock.patch('os.path.abspath')
+    def _test_process_item(self, fake_data,
+                           mock_os_path,
+                           mock_write_file,
+                           mock_process_content):
+        fake_path = mock.MagicMock()
+        mock_os_path.return_value = fake_path
+
+        fake_content = mock.MagicMock()
+        mock_process_content.return_value = fake_content
+
+        with testutils.LogSnatcher('cloudbaseinit.plugins.common.'
+                                   'userdataplugins.cloudconfigplugins.'
+                                   'write_files') as snatcher:
+            write_files.WriteFilesPlugin()._process_item(fake_data)
+
+        self.assertEqual(['Fail to process permissions None, assuming 420'],
+                         snatcher.output)
+
+        open_mode = 'wb'
+        if fake_data.get('append', False) is True:
+            open_mode = 'ab'
+
+        mock_write_file.assert_called_with(fake_path, fake_content, 420,
+                                           open_mode)
+
+    def test_process_item_write(self):
+        self._test_process_item(
+            {'path': 'fake_path', 'content': 'fake_content', 'append': False})
+
+    def test_process_item_append(self):
+        self._test_process_item(
+            {'path': 'fake_path', 'content': 'fake_content', 'append': True})