From dc7699bc0e421a0dfa6619ba951cbb8c31c3b6ee Mon Sep 17 00:00:00 2001 From: Alessandro Pilotti Date: Wed, 1 Feb 2017 11:43:42 +0200 Subject: [PATCH] Implemented automatic updates plugin Adds a plugin to configure Windows automatic updates. The automatic updates configuration is retrieved from the metadata service, or, if not set in the metadata, from the config option "enable_automatic_updates". The possible values for the config option are: - Not set (None): no updates policy is configured - True: updates are automatically installed - False: updates are disabled on the system Implements: blueprint windows-automated-updates-plugin Co-Authored-By: Stefan Caraiman Co-Authored-By: Adrian Vladu Change-Id: Ida07a3a279321fe434a82a5123338679ec4506df Signed-off-by: Stefan Caraiman --- cloudbaseinit/conf/default.py | 4 ++ cloudbaseinit/metadata/services/base.py | 4 ++ cloudbaseinit/plugins/windows/updates.py | 38 +++++++++++ .../tests/plugins/windows/test_updates.py | 66 +++++++++++++++++++ .../tests/utils/windows/test_updates.py | 63 ++++++++++++++++++ cloudbaseinit/utils/windows/updates.py | 43 ++++++++++++ 6 files changed, 218 insertions(+) create mode 100644 cloudbaseinit/plugins/windows/updates.py create mode 100644 cloudbaseinit/tests/plugins/windows/test_updates.py create mode 100644 cloudbaseinit/tests/utils/windows/test_updates.py create mode 100644 cloudbaseinit/utils/windows/updates.py diff --git a/cloudbaseinit/conf/default.py b/cloudbaseinit/conf/default.py index bd75c026..3c1d0750 100644 --- a/cloudbaseinit/conf/default.py +++ b/cloudbaseinit/conf/default.py @@ -267,6 +267,10 @@ class GlobalOptions(conf_base.Options): help='Copies the userdata to the given file path. The path ' 'can include environment variables that will be expanded,' ' e.g. "%%SYSTEMDRIVE%%\\CloudbaseInit\\UserData.bin"'), + cfg.BoolOpt( + 'enable_automatic_updates', default=None, + help='If set, enables or disables automatic operating ' + 'system updates.'), ] self._cli_options = [ diff --git a/cloudbaseinit/metadata/services/base.py b/cloudbaseinit/metadata/services/base.py index 861acff7..f43eda94 100644 --- a/cloudbaseinit/metadata/services/base.py +++ b/cloudbaseinit/metadata/services/base.py @@ -202,6 +202,10 @@ class BaseMetadataService(object): def get_use_avma_licensing(self): pass + def get_enable_automatic_updates(self): + """Check if the metadata provider enforces automatic updates.""" + pass + class BaseHTTPMetadataService(BaseMetadataService): diff --git a/cloudbaseinit/plugins/windows/updates.py b/cloudbaseinit/plugins/windows/updates.py new file mode 100644 index 00000000..80395d6d --- /dev/null +++ b/cloudbaseinit/plugins/windows/updates.py @@ -0,0 +1,38 @@ +# Copyright (c) 2017 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as oslo_logging + +from cloudbaseinit import conf as cloudbaseinit_conf +from cloudbaseinit.plugins.common import base +from cloudbaseinit.utils.windows import updates + +CONF = cloudbaseinit_conf.CONF +LOG = oslo_logging.getLogger(__name__) + + +class WindowsAutoUpdatesPlugin(base.BasePlugin): + def execute(self, service, shared_data): + enable_updates = service.get_enable_automatic_updates() + + if enable_updates is None: + enable_updates = CONF.enable_automatic_updates + if enable_updates is not None: + LOG.info("Configuring automatic updates: %s", enable_updates) + updates.set_automatic_updates(enable_updates) + + return base.PLUGIN_EXECUTION_DONE, False + + def get_os_requirements(self): + return 'win32', (5, 2) diff --git a/cloudbaseinit/tests/plugins/windows/test_updates.py b/cloudbaseinit/tests/plugins/windows/test_updates.py new file mode 100644 index 00000000..0668be14 --- /dev/null +++ b/cloudbaseinit/tests/plugins/windows/test_updates.py @@ -0,0 +1,66 @@ +# Copyright (c) 2017 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import importlib +import unittest + +try: + import unittest.mock as mock +except ImportError: + import mock + +from cloudbaseinit import conf as cloudbaseinit_conf +from cloudbaseinit.plugins.common import base +from cloudbaseinit.tests import testutils + +CONF = cloudbaseinit_conf.CONF +MODPATH = "cloudbaseinit.plugins.windows.updates" + + +class WindowsAutoUpdatesPluginTest(unittest.TestCase): + + def setUp(self): + self.mock_win32com = mock.MagicMock() + patcher = mock.patch.dict( + "sys.modules", + { + "win32com": self.mock_win32com + } + ) + patcher.start() + self.addCleanup(patcher.stop) + updates = importlib.import_module(MODPATH) + self._updates_plugin = updates.WindowsAutoUpdatesPlugin() + self.snatcher = testutils.LogSnatcher(MODPATH) + + @testutils.ConfPatcher("enable_automatic_updates", True) + @mock.patch("cloudbaseinit.utils.windows.updates.set_automatic_updates") + def test_execute(self, mock_set_updates): + mock_service = mock.Mock() + mock_shared_data = mock.Mock() + mock_service.get_enable_automatic_updates.return_value = True + + expected_res = (base.PLUGIN_EXECUTION_DONE, False) + expected_logs = ["Configuring automatic updates: %s" % True] + with self.snatcher: + res = self._updates_plugin.execute(mock_service, mock_shared_data) + self.assertEqual(res, expected_res) + self.assertEqual(self.snatcher.output, expected_logs) + mock_service.get_enable_automatic_updates.assert_called_once_with() + mock_set_updates.assert_called_once_with(True) + + def test_get_os_requirements(self): + expected_res = ('win32', (5, 2)) + requirements_res = self._updates_plugin.get_os_requirements() + self.assertEqual(requirements_res, expected_res) diff --git a/cloudbaseinit/tests/utils/windows/test_updates.py b/cloudbaseinit/tests/utils/windows/test_updates.py new file mode 100644 index 00000000..42d59b2f --- /dev/null +++ b/cloudbaseinit/tests/utils/windows/test_updates.py @@ -0,0 +1,63 @@ +# Copyright (c) 2017 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import importlib +import unittest + +try: + import unittest.mock as mock +except ImportError: + import mock + + +MODPATH = "cloudbaseinit.utils.windows.updates" + + +class UpdatesUtilTest(unittest.TestCase): + + def setUp(self): + self._win32_com = mock.MagicMock() + self._module_patcher = mock.patch.dict( + 'sys.modules', { + 'win32com': self._win32_com}) + self._win32_com_client = self._win32_com.client + self._module_patcher.start() + self._updates = importlib.import_module(MODPATH) + + def tearDown(self): + self._module_patcher.stop() + + @mock.patch("cloudbaseinit.osutils.factory.get_os_utils") + def _test_set_automatic_updates(self, mock_get_os_utils, enabled=True): + mock_osutils = mock.Mock() + mock_updates = mock.Mock() + mock_get_os_utils.return_value = mock_osutils + mock_osutils.check_os_version.return_value = False + self._win32_com_client.Dispatch.return_value = mock_updates + if not enabled: + self._updates.set_automatic_updates(enabled) + self.assertEqual(mock_updates.Settings.NotificationLevel, 1) + mock_updates.Settings.Save.assert_called_once_with() + else: + self._updates.set_automatic_updates(enabled) + mock_get_os_utils.assert_called_once_with() + self.assertIsNotNone( + mock_updates.SettingsScheduledInstallationTime) + mock_updates.Settings.Save.assert_called_once_with() + + def test_set_automatic_no_updates(self): + self._test_set_automatic_updates(enabled=False) + + def test_set_automatic_updates(self): + self._test_set_automatic_updates() diff --git a/cloudbaseinit/utils/windows/updates.py b/cloudbaseinit/utils/windows/updates.py new file mode 100644 index 00000000..ed22bb4c --- /dev/null +++ b/cloudbaseinit/utils/windows/updates.py @@ -0,0 +1,43 @@ +# Copyright (c) 2017 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import random + +from win32com import client + +from cloudbaseinit.osutils import factory as osutils_factory + +AU_DISABLED = 1 +AU_SCHEDULED_INSTALLATION = 4 + +MIN_INSTALL_HOUR = 1 +MAX_INSTALL_HOUR = 5 + + +def set_automatic_updates(enabled): + # TODO(alexpilotti): the following settings are ignored on + # Windows 10 / Windows Server 2016 build 14393 + auto_update = client.Dispatch("Microsoft.Update.AutoUpdate") + if enabled: + auto_update.Settings.NotificationLevel = AU_SCHEDULED_INSTALLATION + osutils = osutils_factory.get_os_utils() + if not osutils.check_os_version(6, 2): + # NOTE(alexpilotti): this setting is not supported starting + # with Windows 8 / Windows Server 2012 + hour = random.randint(MIN_INSTALL_HOUR, MAX_INSTALL_HOUR) + auto_update.SettingsScheduledInstallationTime = hour + else: + auto_update.Settings.NotificationLevel = AU_DISABLED + + auto_update.Settings.Save()