From a9975e06b609b9826cf57168cf2f76039afaf269 Mon Sep 17 00:00:00 2001 From: Alessandro Pilotti Date: Mon, 6 Mar 2017 12:03:43 +0200 Subject: [PATCH] Adds Windows service CRUD methods Adds functionality to create and delete given services. Additionally the WMI calls for managing Windows services are replaced with win32 calls. Change-Id: I3576cb48510e4f7bb13dc48e1c68cddf76e1fb1c Co-Authored-By: Stefan Caraiman --- cloudbaseinit/osutils/base.py | 25 +++ cloudbaseinit/osutils/windows.py | 151 ++++++++++---- cloudbaseinit/tests/osutils/test_windows.py | 206 ++++++++++++-------- 3 files changed, 258 insertions(+), 124 deletions(-) diff --git a/cloudbaseinit/osutils/base.py b/cloudbaseinit/osutils/base.py index 404835a0..4942b290 100644 --- a/cloudbaseinit/osutils/base.py +++ b/cloudbaseinit/osutils/base.py @@ -138,6 +138,31 @@ class BaseOSUtils(object): """Set the username and password for a given service.""" raise NotImplementedError() + def create_service(self, service_name, display_name, path, start_mode, + username=None, password=None): + raise NotImplementedError() + + def delete_service(self, service_name): + raise NotImplementedError() + + def get_service_status(self, service_name): + raise NotImplementedError() + + def check_service_exists(self, service_name): + raise NotImplementedError() + + def get_service_start_mode(self, service_name): + raise NotImplementedError() + + def set_service_start_mode(self, service_name, start_mode): + raise NotImplementedError() + + def start_service(self, service_name): + raise NotImplementedError() + + def stop_service(self, service_name, wait=False): + raise NotImplementedError() + def get_service_username(self, service_name): """Retrieve the username under which a service runs.""" raise NotImplementedError() diff --git a/cloudbaseinit/osutils/windows.py b/cloudbaseinit/osutils/windows.py index cd6c617e..6e0f5547 100644 --- a/cloudbaseinit/osutils/windows.py +++ b/cloudbaseinit/osutils/windows.py @@ -367,6 +367,31 @@ class WindowsUtils(base.BaseOSUtils): SERVICE_START_MODE_MANUAL = "Manual" SERVICE_START_MODE_DISABLED = "Disabled" + _SERVICE_START_TYPE_MAP = { + SERVICE_START_MODE_AUTOMATIC: + win32service.SERVICE_AUTO_START, + SERVICE_START_MODE_MANUAL: + win32service.SERVICE_DEMAND_START, + SERVICE_START_MODE_DISABLED: + win32service.SERVICE_DISABLED} + + _SERVICE_STATUS_MAP = { + win32service.SERVICE_CONTINUE_PENDING: + SERVICE_STATUS_CONTINUE_PENDING, + win32service.SERVICE_PAUSE_PENDING: + SERVICE_STATUS_PAUSE_PENDING, + win32service.SERVICE_PAUSED: + SERVICE_STATUS_PAUSED, + win32service.SERVICE_RUNNING: + SERVICE_STATUS_RUNNING, + win32service.SERVICE_START_PENDING: + SERVICE_STATUS_START_PENDING, + win32service.SERVICE_STOP_PENDING: + SERVICE_STATUS_STOP_PENDING, + win32service.SERVICE_STOPPED: + SERVICE_STATUS_STOPPED, + } + ComputerNamePhysicalDnsHostname = 5 _config_key = 'SOFTWARE\\Cloudbase Solutions\\Cloudbase-Init\\' @@ -865,13 +890,8 @@ class WindowsUtils(base.BaseOSUtils): else: raise ex - def _get_service(self, service_name): - conn = wmi.WMI(moniker='//./root/cimv2') - service_list = conn.Win32_Service(Name=service_name) - if len(service_list): - return service_list[0] - def check_service_exists(self, service_name): + LOG.debug("Checking if service exists: %s", service_name) try: with self._get_service_handle(service_name): return True @@ -882,57 +902,110 @@ class WindowsUtils(base.BaseOSUtils): raise def get_service_status(self, service_name): - service = self._get_service(service_name) - return service.State + LOG.debug("Getting service status for: %s", service_name) + with self._get_service_handle( + service_name, win32service.SERVICE_QUERY_STATUS) as hs: + service_status = win32service.QueryServiceStatusEx(hs) + state = service_status['CurrentState'] + + return self._SERVICE_STATUS_MAP.get( + state, WindowsUtils.SERVICE_STATUS_UNKNOWN) def get_service_start_mode(self, service_name): - service = self._get_service(service_name) - return service.StartMode + LOG.debug("Getting service start mode for: %s", service_name) + with self._get_service_handle( + service_name, win32service.SERVICE_QUERY_CONFIG) as hs: + service_config = win32service.QueryServiceConfig(hs) + + start_type = service_config[1] + return [k for k, v in self._SERVICE_START_TYPE_MAP.items() + if v == start_type][0] def set_service_start_mode(self, service_name, start_mode): # TODO(alexpilotti): Handle the "Delayed Start" case - service = self._get_service(service_name) - (ret_val,) = service.ChangeStartMode(start_mode) - if ret_val != 0: - raise exception.CloudbaseInitException( - 'Setting service %(service_name)s start mode failed with ' - 'return value: %(ret_val)d' % {'service_name': service_name, - 'ret_val': ret_val}) + LOG.debug("Setting service start mode for: %s", service_name) + start_type = self._get_win32_start_type(start_mode) + + with self._get_service_handle( + service_name, win32service.SERVICE_CHANGE_CONFIG) as hs: + win32service.ChangeServiceConfig( + hs, win32service.SERVICE_NO_CHANGE, + start_type, win32service.SERVICE_NO_CHANGE, + None, None, False, None, None, None, None) def start_service(self, service_name): LOG.debug('Starting service %s', service_name) - service = self._get_service(service_name) - (ret_val,) = service.StartService() - if ret_val != 0: - raise exception.CloudbaseInitException( - 'Starting service %(service_name)s failed with return value: ' - '%(ret_val)d' % {'service_name': service_name, - 'ret_val': ret_val}) + with self._get_service_handle( + service_name, win32service.SERVICE_START) as hs: + win32service.StartService(hs, service_name) - def stop_service(self, service_name): + def stop_service(self, service_name, wait=False): LOG.debug('Stopping service %s', service_name) - service = self._get_service(service_name) - (ret_val,) = service.StopService() - if ret_val != 0: - raise exception.CloudbaseInitException( - 'Stopping service %(service_name)s failed with return value:' - ' %(ret_val)d' % {'service_name': service_name, - 'ret_val': ret_val}) + with self._get_service_handle( + service_name, + win32service.SERVICE_STOP | + win32service.SERVICE_QUERY_STATUS) as hs: + win32service.ControlService(hs, win32service.SERVICE_CONTROL_STOP) + if wait: + while True: + service_status = win32service.QueryServiceStatusEx(hs) + state = service_status['CurrentState'] + if state == win32service.SERVICE_STOPPED: + return + time.sleep(.1) + + @staticmethod + @contextlib.contextmanager + def _get_service_control_manager( + scm_access=win32service.SC_MANAGER_CONNECT): + hscm = win32service.OpenSCManager(None, None, scm_access) + try: + yield hscm + finally: + win32service.CloseServiceHandle(hscm) @staticmethod @contextlib.contextmanager def _get_service_handle(service_name, service_access=win32service.SERVICE_QUERY_CONFIG, scm_access=win32service.SC_MANAGER_CONNECT): - hscm = win32service.OpenSCManager(None, None, scm_access) - hs = None - try: + with WindowsUtils._get_service_control_manager(scm_access) as hscm: hs = win32service.OpenService(hscm, service_name, service_access) - yield hs - finally: - if hs: + try: + yield hs + finally: win32service.CloseServiceHandle(hs) - win32service.CloseServiceHandle(hscm) + + @staticmethod + def _get_win32_start_type(start_mode): + start_type = WindowsUtils._SERVICE_START_TYPE_MAP.get(start_mode) + if not start_type: + raise exception.InvalidStateException( + "Invalid service start mode: %s" % start_mode) + return start_type + + def create_service(self, service_name, display_name, path, start_mode, + username=None, password=None): + LOG.debug('Creating service %s', service_name) + start_type = self._get_win32_start_type(start_mode) + + with WindowsUtils._get_service_control_manager( + scm_access=win32service.SC_MANAGER_CREATE_SERVICE) as hscm: + hs = win32service.CreateService( + hscm, service_name, display_name, + win32service.SERVICE_ALL_ACCESS, + win32service.SERVICE_WIN32_OWN_PROCESS, + start_type, + win32service.SERVICE_ERROR_NORMAL, + path, None, False, None, + username, password) + win32service.CloseServiceHandle(hs) + + def delete_service(self, service_name): + LOG.debug('Deleting service %s', service_name) + with self._get_service_handle( + service_name, win32service.SERVICE_ALL_ACCESS) as hs: + win32service.DeleteService(hs) def set_service_credentials(self, service_name, username, password): LOG.debug('Setting service credentials: %s', service_name) diff --git a/cloudbaseinit/tests/osutils/test_windows.py b/cloudbaseinit/tests/osutils/test_windows.py index d147b98c..42000fc4 100644 --- a/cloudbaseinit/tests/osutils/test_windows.py +++ b/cloudbaseinit/tests/osutils/test_windows.py @@ -879,16 +879,6 @@ class TestWindowsUtils(testutils.CloudbaseInitTestBase): ret_vals = [[1], [7]] self._test_wait_for_boot_completion(ret_vals=ret_vals) - def test_get_service(self): - conn = self._wmi_mock.WMI - conn.return_value.Win32_Service.return_value = ['fake name'] - - response = self._winutils._get_service('fake name') - - conn.assert_called_with(moniker='//./root/cimv2') - conn.return_value.Win32_Service.assert_called_with(Name='fake name') - self.assertEqual('fake name', response) - @mock.patch('cloudbaseinit.osutils.windows.WindowsUtils' '._get_service_handle') def test_check_service(self, mock_get_service_handle): @@ -934,6 +924,48 @@ class TestWindowsUtils(testutils.CloudbaseInitTestBase): close_service.assert_has_calls([mock.call(mock.sentinel.hs), mock.call(mock.sentinel.hscm)]) + @mock.patch('cloudbaseinit.osutils.windows.WindowsUtils' + '._get_service_control_manager') + def test_create_service(self, mock_get_service_control_manager): + mock_hs = mock.MagicMock() + mock_service_name = "fake name" + mock_start_mode = "Automatic" + mock_display_name = mock.sentinel.mock_display_name + mock_path = mock.sentinel.path + mock_get_service_control_manager.return_value = mock_hs + with self.snatcher: + self._winutils.create_service(mock_service_name, + mock_display_name, + mock_path, + mock_start_mode) + self.assertEqual(["Creating service fake name"], + self.snatcher.output) + + mock_get_service_control_manager.assert_called_once_with( + scm_access=self._win32service_mock.SC_MANAGER_CREATE_SERVICE) + self._win32service_mock.CreateService.assert_called_once_with( + mock_hs.__enter__(), mock_service_name, mock_display_name, + self._win32service_mock.SERVICE_ALL_ACCESS, + self._win32service_mock.SERVICE_WIN32_OWN_PROCESS, + self._win32service_mock.SERVICE_AUTO_START, + self._win32service_mock.SERVICE_ERROR_NORMAL, + mock_path, None, False, None, None, None) + + @mock.patch('cloudbaseinit.osutils.windows.WindowsUtils' + '._get_service_handle') + def test_delete_service(self, mock_get_service_handle): + mock_hs = mock.MagicMock() + fake_service_name = "fake name" + mock_get_service_handle.return_value = mock_hs + with self.snatcher: + self._winutils.delete_service(fake_service_name) + self.assertEqual(["Deleting service fake name"], + self.snatcher.output) + self._win32service_mock.DeleteService.assert_called_once_with( + mock_hs.__enter__()) + mock_get_service_handle.assert_called_once_with( + fake_service_name, self._win32service_mock.SERVICE_ALL_ACCESS) + @mock.patch('cloudbaseinit.osutils.windows.WindowsUtils' '._get_service_handle') def test_set_service_credentials(self, mock_get_service): @@ -1037,96 +1069,100 @@ class TestWindowsUtils(testutils.CloudbaseInitTestBase): service_username=".\\username") @mock.patch('cloudbaseinit.osutils.windows.WindowsUtils' - '._get_service') - def test_get_service_status(self, mock_get_service): - mock_service = mock.MagicMock() - mock_get_service.return_value = mock_service + '._get_service_handle') + def test_get_service_status(self, mock_get_service_handle): + mock_hs = mock.MagicMock() + fake_service_name = "fake name" + fake_status = {'CurrentState': 'fake-status'} + mock_get_service_handle.return_value = mock_hs + expected_log = ["Getting service status for: %s" % fake_service_name] + self._win32service_mock.QueryServiceStatusEx.return_value = fake_status + with self.snatcher: + response = self._winutils.get_service_status(fake_service_name) - response = self._winutils.get_service_status('fake name') - - self.assertEqual(mock_service.State, response) + self._win32service_mock.QueryServiceStatusEx.assert_called_once_with( + mock_hs.__enter__()) + mock_get_service_handle.assert_called_once_with( + fake_service_name, self._win32service_mock.SERVICE_QUERY_STATUS) + self.assertEqual(self.snatcher.output, expected_log) + self.assertEqual("Unknown", response) @mock.patch('cloudbaseinit.osutils.windows.WindowsUtils' - '._get_service') - def test_get_service_start_mode(self, mock_get_service): - mock_service = mock.MagicMock() - mock_get_service.return_value = mock_service + '._get_service_handle') + def test_get_service_start_mode(self, mock_get_service_handle): + mock_hs = mock.MagicMock() + fake_service_name = "fake name" + mock_mode = self._win32service_mock.SERVICE_AUTO_START + fake_status = ['', mock_mode] + mock_get_service_handle.return_value = mock_hs + expected_mode = "Automatic" + expected_log = [ + "Getting service start mode for: %s" % fake_service_name] + self._win32service_mock.QueryServiceConfig.return_value = fake_status - response = self._winutils.get_service_start_mode('fake name') + with self.snatcher: + response = self._winutils.get_service_start_mode(fake_service_name) - self.assertEqual(mock_service.StartMode, response) + self._win32service_mock.QueryServiceConfig.assert_called_once_with( + mock_hs.__enter__()) + mock_get_service_handle.assert_called_once_with( + fake_service_name, self._win32service_mock.SERVICE_QUERY_CONFIG) + self.assertEqual(self.snatcher.output, expected_log) + self.assertEqual(expected_mode, response) @mock.patch('cloudbaseinit.osutils.windows.WindowsUtils' - '._get_service') - def _test_set_service_start_mode(self, mock_get_service, ret_val): - mock_service = mock.MagicMock() - mock_get_service.return_value = mock_service - mock_service.ChangeStartMode.return_value = (ret_val,) - - if ret_val != 0: - self.assertRaises(exception.CloudbaseInitException, - self._winutils.set_service_start_mode, - 'fake name', 'fake mode') - else: - self._winutils.set_service_start_mode('fake name', 'fake mode') - - mock_service.ChangeStartMode.assert_called_once_with('fake mode') - - def test_set_service_start_mode(self): - self._test_set_service_start_mode(ret_val=0) - - def test_set_service_start_mode_exception(self): - self._test_set_service_start_mode(ret_val=1) + '._get_service_handle') + def test_set_service_start_mode(self, mock_get_service_handle): + mock_hs = mock.MagicMock() + fake_service_name = "fake name" + fake_start_mode = "Automatic" + mock_get_service_handle.return_value = mock_hs + with self.snatcher: + self._winutils.set_service_start_mode(fake_service_name, + fake_start_mode) + self.assertEqual(["Setting service start mode for: fake name"], + self.snatcher.output) + self._win32service_mock.ChangeServiceConfig.assert_called_once_with( + mock_hs.__enter__(), + self._win32service_mock.SERVICE_NO_CHANGE, + self._win32service_mock.SERVICE_AUTO_START, + self._win32service_mock.SERVICE_NO_CHANGE, + None, None, False, None, None, None, None) + mock_get_service_handle.assert_called_once_with( + fake_service_name, + self._win32service_mock.SERVICE_CHANGE_CONFIG) @mock.patch('cloudbaseinit.osutils.windows.WindowsUtils' - '._get_service') - def _test_start_service(self, mock_get_service, ret_val): - mock_service = mock.MagicMock() - mock_get_service.return_value = mock_service - mock_service.StartService.return_value = (ret_val,) - - if ret_val != 0: - self.assertRaises(exception.CloudbaseInitException, - self._winutils.start_service, - 'fake name') - else: - with self.snatcher: - self._winutils.start_service('fake name') + '._get_service_handle') + def test_start_service(self, mock_get_service_handle): + mock_hs = mock.MagicMock() + fake_service_name = "fake name" + mock_get_service_handle.return_value = mock_hs + with self.snatcher: + self._winutils.start_service(fake_service_name) self.assertEqual(["Starting service fake name"], self.snatcher.output) - - mock_service.StartService.assert_called_once_with() - - def test_start_service(self): - self._test_set_service_start_mode(ret_val=0) - - def test_start_service_exception(self): - self._test_set_service_start_mode(ret_val=1) + self._win32service_mock.StartService.assert_called_once_with( + mock_hs.__enter__(), fake_service_name) + mock_get_service_handle.assert_called_once_with( + fake_service_name, self._win32service_mock.SERVICE_START) @mock.patch('cloudbaseinit.osutils.windows.WindowsUtils' - '._get_service') - def _test_stop_service(self, mock_get_service, ret_val): - mock_service = mock.MagicMock() - mock_get_service.return_value = mock_service - mock_service.StopService.return_value = (ret_val,) - - if ret_val != 0: - self.assertRaises(exception.CloudbaseInitException, - self._winutils.stop_service, - 'fake name') - else: - with self.snatcher: - self._winutils.stop_service('fake name') + '._get_service_handle') + def test_stop_service(self, mock_get_service_handle): + mock_hs = mock.MagicMock() + fake_service_name = "fake name" + mock_get_service_handle.return_value = mock_hs + with self.snatcher: + self._winutils.stop_service(fake_service_name) self.assertEqual(["Stopping service fake name"], self.snatcher.output) - - mock_service.StopService.assert_called_once_with() - - def test_stop_service(self): - self._test_stop_service(ret_val=0) - - def test_stop_service_exception(self): - self._test_stop_service(ret_val=1) + self._win32service_mock.ControlService.assert_called_once_with( + mock_hs.__enter__(), + self._win32service_mock.SERVICE_CONTROL_STOP) + mock_get_service_handle.assert_called_once_with( + fake_service_name, self._win32service_mock.SERVICE_STOP | + self._win32service_mock.SERVICE_QUERY_STATUS) @mock.patch('cloudbaseinit.osutils.windows.WindowsUtils' '.stop_service')