From e5b4fb393fc5c3eb0aabdc4a02f67ac0ba849125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20G=C3=B3mez=20Garc=C3=ADa?= Date: Fri, 29 Nov 2019 02:23:56 +0100 Subject: [PATCH] Actor v3 --- actor/src/udsactor/linux/operations.py | 7 +- actor/src/udsactor/linux/service.py | 33 +-- actor/src/udsactor/service.py | 31 ++- actor/src/udsactor/windows/operations.py | 21 +- actor/src/udsactor/windows/service.py | 285 ++++++++--------------- 5 files changed, 143 insertions(+), 234 deletions(-) diff --git a/actor/src/udsactor/linux/operations.py b/actor/src/udsactor/linux/operations.py index 29d52eccc..76b72e4d3 100644 --- a/actor/src/udsactor/linux/operations.py +++ b/actor/src/udsactor/linux/operations.py @@ -149,8 +149,13 @@ def loggoff() -> None: # subprocess.call(['/usr/bin/systemctl', 'reboot', '-i']) -def renameComputer(newName: str) -> None: +def renameComputer(newName: str) -> bool: + ''' + Changes the computer name + Returns True if reboot needed + ''' rename(newName) + return False def joinDomain(domain: str, ou: str, account: str, password: str, executeInOneStep: bool = False): diff --git a/actor/src/udsactor/linux/service.py b/actor/src/udsactor/linux/service.py index 223a517ff..f225f12fe 100644 --- a/actor/src/udsactor/linux/service.py +++ b/actor/src/udsactor/linux/service.py @@ -33,7 +33,6 @@ import signal import typing from . import operations -from . import renamer from . import daemon from ..log import logger @@ -59,37 +58,6 @@ class UDSActorSvc(daemon.Daemon, CommonService): def markForExit(self, signum, frame) -> None: self._isAlive = False - def rename( # pylint: disable=unused-argument - self, - name: str, - userName: typing.Optional[str] = None, - oldPassword: typing.Optional[str] = None, - newPassword: typing.Optional[str] = None - ) -> None: - ''' - Renames the computer, and optionally sets a password for an user - before this - ''' - hostName = operations.getComputerName() - - if hostName.lower() == name.lower(): - logger.info('Computer name is already {}'.format(hostName)) - return - - logger.debug('Data: {}'.format((name, userName, oldPassword, newPassword))) - - # Check for password change request for an user - if userName and oldPassword and newPassword: - logger.info('Setting password for user {}'.format(userName)) - try: - operations.changeUserPassword(userName, oldPassword, newPassword) - except Exception as e: - # We stop here without even renaming computer, because the - # process has failed - raise Exception('Could not change password for user {} (maybe invalid current password is configured at broker): {} '.format(userName, e)) - - renamer.rename(name) - def joinDomain( # pylint: disable=unused-argument, too-many-arguments self, name: str, @@ -106,6 +74,7 @@ class UDSActorSvc(daemon.Daemon, CommonService): # Linux daemon will continue running unless something is requested to if not self.initialize(): + self.notifyStop() return # Stop daemon if initializes told to do so logger.debug('Initialized, setting ready') diff --git a/actor/src/udsactor/service.py b/actor/src/udsactor/service.py index f3bbbc3de..c68af4ab2 100644 --- a/actor/src/udsactor/service.py +++ b/actor/src/udsactor/service.py @@ -202,9 +202,6 @@ class CommonService: # No ip changed, log exception for info logger.warn('Checking ips faield: {}'.format(e)) - # *************************************************** - # Methods that ARE overriden by linux & windows Actor - # *************************************************** def rename( # pylint: disable=unused-argument self, name: str, @@ -216,8 +213,28 @@ class CommonService: Invoked when broker requests a rename action default does nothing ''' - logger.info('Base renamed invoked: {}, {}'.format(name, userName)) + hostName = platform.operations.getComputerName() + if hostName.lower() == name.lower(): + logger.info('Computer name is already {}'.format(hostName)) + return + + # Check for password change request for an user + if userName and newPassword: + logger.info('Setting password for user {}'.format(userName)) + try: + platform.operations.changeUserPassword(userName, oldPassword or '', newPassword) + except Exception as e: + raise Exception('Could not change password for user {} (maybe invalid current password is configured at broker): {} '.format(userName, str(e))) + + if platform.operations.renameComputer(name): + # Reboot just after renaming + logger.info('Rebooting computer to activate new name {}'.format(name)) + self.reboot() + + # ****************************************************** + # Methods that can be overriden by linux & windows Actor + # ****************************************************** def joinDomain( # pylint: disable=unused-argument, too-many-arguments self, name: str, @@ -248,7 +265,7 @@ class CommonService: ''' logger.info('Service is being stopped') - def preConnect(self, user: str, protocol: str) -> str: # pylint: disable=unused-argument + def preConnect(self, userName: str, protocol: str) -> str: # pylint: disable=unused-argument ''' Invoked when received a PRE Connection request via REST Base preconnect executes the preconnect command @@ -258,5 +275,5 @@ class CommonService: return 'ok' - def onLogout(self, user: str) -> None: - logger.debug('On logout invoked for {}'.format(user)) + def onLogout(self, userName: str) -> None: + logger.debug('On logout invoked for {}'.format(userName)) diff --git a/actor/src/udsactor/windows/operations.py b/actor/src/udsactor/windows/operations.py index 5f2ea4854..eef990164 100644 --- a/actor/src/udsactor/windows/operations.py +++ b/actor/src/udsactor/windows/operations.py @@ -86,7 +86,7 @@ def getDomainName() -> str: return domain -def getWindowsVersion() -> str: +def getWindowsVersion() -> typing.Tuple[int, int, int, int, str]: return win32api.GetVersionEx() EWX_LOGOFF = 0x00000000 @@ -106,7 +106,11 @@ def reboot(flags: int = EWX_FORCEIFHUNG | EWX_REBOOT) -> None: def loggoff() -> None: win32api.ExitWindowsEx(EWX_LOGOFF) -def renameComputer(newName: str) -> None: +def renameComputer(newName: str) -> bool: + ''' + Changes the computer name + Returns True if reboot needed + ''' # Needs admin privileges to work if ctypes.windll.kernel32.SetComputerNameExW(DWORD(win32con.ComputerNamePhysicalDnsHostname), LPCWSTR(newName)) == 0: # @UndefinedVariable # win32api.FormatMessage -> returns error string @@ -115,6 +119,7 @@ def renameComputer(newName: str) -> None: error = getErrorMessage() computerName = win32api.GetComputerNameEx(win32con.ComputerNamePhysicalDnsHostname) raise Exception('Error renaming computer from {} to {}: {}'.format(computerName, newName, error)) + return True NETSETUP_JOIN_DOMAIN = 0x00000001 NETSETUP_ACCT_CREATE = 0x00000002 @@ -171,16 +176,18 @@ def joinDomain(domain: str, ou: str, account: str, password: str, executeInOneSt raise Exception('Error joining domain {}, with credentials {}/*****{}: {}, {}'.format(domain, account, ', under OU {}'.format(ou) if ou is not None else '', res, error)) def changeUserPassword(user: str, oldPassword: str, newPassword: str) -> None: - lpUser = LPCWSTR(user) - lpOldPassword = LPCWSTR(oldPassword) - lpNewPassword = LPCWSTR(newPassword) + # lpUser = LPCWSTR(user) + # lpOldPassword = LPCWSTR(oldPassword) + # lpNewPassword = LPCWSTR(newPassword) - res = ctypes.windll.netapi32.NetUserChangePassword(None, lpUser, lpOldPassword, lpNewPassword) + # res = ctypes.windll.netapi32.NetUserChangePassword(None, lpUser, lpOldPassword, lpNewPassword) + # Try to set new password "a las bravas", ignoring old one. This will not work with domain users + res = win32net.NetUserSetInfo(None, user, 1003, {'password': newPassword}) if res != 0: # Log the error, and raise exception to parent error = getErrorMessage(res) - raise Exception('Error changing password for user {}: {} {}'.format(lpUser.value, res, error)) + raise Exception('Error changing password for user {}: {} {}'.format(user, res, error)) class LASTINPUTINFO(ctypes.Structure): # pylint: disable=too-few-public-methods _fields_ = [ diff --git a/actor/src/udsactor/windows/service.py b/actor/src/udsactor/windows/service.py index f804c7235..d05cc7665 100644 --- a/actor/src/udsactor/windows/service.py +++ b/actor/src/udsactor/windows/service.py @@ -33,6 +33,7 @@ import struct import subprocess import os import stat +import typing import win32serviceutil import win32service @@ -43,12 +44,11 @@ import win32com.client import pythoncom import servicemanager -from udsactor import operations -from udsactor import store -from udsactor.service import CommonService -from udsactor.service import initCfg +from . import operations +from . import store +from ..service import CommonService -from udsactor.log import logger +from ..log import logger from .SENS import SensLogon from .SENS import logevent @@ -57,70 +57,44 @@ from .SENS import SENSGUID_PUBLISHER from .SENS import PROGID_EventSubscription from .SENS import PROGID_EventSystem -POST_CMD = 'c:\\windows\\post-uds.bat' - +REMOTE_USERS_SID = 'S-1-5-32-555' # Well nown sid for remote desktop users class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService): ''' This class represents a Windows Service for managing actor interactions with UDS Broker and Machine ''' + # ServiceeFramework related _svc_name_ = "UDSActorNG" _svc_display_name_ = "UDS Actor Service" _svc_description_ = "UDS Actor Management Service" # 'System Event Notification' is the SENS service _svc_deps_ = ['EventLog', 'SENS'] + _user: typing.Optional[str] + _hWaitStop: typing.Any + def __init__(self, args): win32serviceutil.ServiceFramework.__init__(self, args) CommonService.__init__(self) - self.hWaitStop = win32event.CreateEvent(None, 1, 0, None) + + self._hWaitStop = win32event.CreateEvent(None, 1, 0, None) self._user = None - def SvcStop(self): + def SvcStop(self) -> None: self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) - self.isAlive = False + self._isAlive = False win32event.SetEvent(self.hWaitStop) SvcShutdown = SvcStop - def notifyStop(self): - servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, - servicemanager.PYS_SERVICE_STOPPED, - (self._svc_name_, '')) + def notifyStop(self) -> None: + servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STOPPED, (self._svc_name_, '')) - def doWait(self, miliseconds): + def doWait(self, miliseconds: int) -> None: win32event.WaitForSingleObject(self.hWaitStop, miliseconds) - def rename(self, name, user=None, oldPassword=None, newPassword=None): - ''' - Renames the computer, and optionally sets a password for an user - before this - ''' - hostName = operations.getComputerName() - - if hostName.lower() == name.lower(): - logger.info('Computer name is now {}'.format(hostName)) - self.setReady() - return - - # Check for password change request for an user - if user is not None: - logger.info('Setting password for user {}'.format(user)) - try: - operations.changeUserPassword(user, oldPassword, newPassword) - except Exception as e: - # We stop here without even renaming computer, because the - # process has failed - raise Exception( - 'Could not change password for user {} (maybe invalid current password is configured at broker): {} '.format(user, str(e))) - - operations.renameComputer(name) - # Reboot just after renaming - logger.info('Rebooting computer to activate new name {}'.format(name)) - self.reboot() - - def oneStepJoin(self, name, domain, ou, account, password): + def oneStepJoin(self, name: str, domain: str, ou: str, account: str, password: str) -> None: ''' Ejecutes the join domain in exactly one step ''' @@ -129,176 +103,126 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService): # name will not change if currName.lower() == name.lower(): self.multiStepJoin(name, domain, ou, account, password) - else: - operations.renameComputer(name) - logger.debug('Computer renamed to {} without reboot'.format(name)) - operations.joinDomain( - domain, ou, account, password, executeInOneStep=True) - logger.debug( - 'Requested join domain {} without errors'.format(domain)) - self.reboot() + return - def multiStepJoin(self, name, domain, ou, account, password): + operations.renameComputer(name) + logger.debug('Computer renamed to {} without reboot'.format(name)) + operations.joinDomain(domain, ou, account, password, executeInOneStep=True) + logger.debug('Requested join domain {} without errors'.format(domain)) + self.reboot() + + def multiStepJoin(self, name: str, domain: str, ou: str, account: str, password: str) -> None: currName = operations.getComputerName() if currName.lower() == name.lower(): currDomain = operations.getDomainName() - if currDomain is not None: + if currDomain: # logger.debug('Name: "{}" vs "{}", Domain: "{}" vs "{}"'.format(currName.lower(), name.lower(), currDomain.lower(), domain.lower())) logger.info('Machine {} is part of domain {}'.format(name, domain)) self.setReady() else: - operations.joinDomain( - domain, ou, account, password, executeInOneStep=False) + operations.joinDomain(domain, ou, account, password, executeInOneStep=False) self.reboot() else: operations.renameComputer(name) - logger.info( - 'Rebooting computer got activate new name {}'.format(name)) + logger.info('Rebooting computer got activate new name {}'.format(name)) self.reboot() - def joinDomain(self, name, domain, ou, account, password): - ver = operations.getWindowsVersion() - ver = ver[0] * 10 + ver[1] - logger.debug('Starting joining domain {} with name {} (detected operating version: {})'.format( - domain, name, ver)) - # If file c:\compat.bin exists, joind domain in two steps instead one - + def joinDomain( # pylint: disable=unused-argument, too-many-arguments + self, + name: str, + domain: str, + ou: str, + account: str, + password: str + ) -> None: + versionData = operations.getWindowsVersion() + versionInt = versionData[0] * 10 + versionData[1] + logger.debug('Starting joining domain {} with name {} (detected operating version: {})'.format(domain, name, versionData)) # Accepts one step joinDomain, also remember XP is no more supported by # microsoft, but this also must works with it because will do a "multi # step" join - if ver >= 60 and store.useOldJoinSystem() is False: + if versionInt >= 60 and not store.useOldJoinSystem(): self.oneStepJoin(name, domain, ou, account, password) else: logger.info('Using multiple step join because configuration requests to do so') self.multiStepJoin(name, domain, ou, account, password) - def preConnect(self, user, protocol): + def preConnect(self, userName: str, protocol: str) -> str: logger.debug('Pre connect invoked') - if protocol != 'rdp': # If connection is not using rdp, skip adding user - return 'ok' - # Well known SSID for Remote Desktop Users - REMOTE_USERS_SID = 'S-1-5-32-555' - p = win32security.GetBinarySid(REMOTE_USERS_SID) - groupName = win32security.LookupAccountSid(None, p)[0] + if protocol == 'rdp': # If connection is not using rdp, skip adding user + # Well known SSID for Remote Desktop Users + groupName = win32security.LookupAccountSid(None, win32security.GetBinarySid(REMOTE_USERS_SID))[0] - useraAlreadyInGroup = False - resumeHandle = 0 - while True: - users, _, resumeHandle = win32net.NetLocalGroupGetMembers(None, groupName, 1, resumeHandle, 32768) - if user.lower() in [u['name'].lower() for u in users]: - useraAlreadyInGroup = True - break - if resumeHandle == 0: - break + useraAlreadyInGroup = False + resumeHandle = 0 + while True: + users, _, resumeHandle = win32net.NetLocalGroupGetMembers(None, groupName, 1, resumeHandle, 32768) + if userName.lower() in [u['name'].lower() for u in users]: + useraAlreadyInGroup = True + break + if resumeHandle == 0: + break - if useraAlreadyInGroup is False: - logger.debug('User not in group, adding it') - self._user = user - try: - userSSID = win32security.LookupAccountName(None, user)[0] - win32net.NetLocalGroupAddMembers(None, groupName, 0, [{'sid': userSSID}]) - except Exception as e: - logger.error('Exception adding user to Remote Desktop Users: {}'.format(e)) - else: - self._user = None - logger.debug('User {} already in group'.format(user)) - - # Now try to run pre connect command - try: - pre_cmd = store.preApplication() - if os.path.isfile(pre_cmd): - if (os.stat(pre_cmd).st_mode & stat.S_IXUSR) != 0: - subprocess.call([pre_cmd, user, protocol]) - else: - logger.info('PRECONNECT file exists but it it is not executable (needs execution permission by root)') + if not useraAlreadyInGroup: + logger.debug('User not in group, adding it') + self._user = userName + try: + userSSID = win32security.LookupAccountName(None, userName)[0] + win32net.NetLocalGroupAddMembers(None, groupName, 0, [{'sid': userSSID}]) + except Exception as e: + logger.error('Exception adding user to Remote Desktop Users: {}'.format(e)) else: - logger.info('PRECONNECT file not found & not executed') - except Exception as e: - # Ignore output of execution command - logger.error('Executing preconnect command give') + self._user = None + logger.debug('User {} already in group'.format(userName)) - return 'ok' + return super().preConnect(userName, protocol) - def ovLogon(self, username, password): + def ovLogon(self, username: str, password: str) -> str: + """ + Logon on oVirt agent + currently not used. + """ # Compose packet for ov - ub = username.encode('utf8') - up = username.encode('utf8') - packet = struct.pack('!I', len(ub)) + ub + struct.pack('!I', len(up)) + up + usernameBytes = username.encode() + passwordBytes = username.encode() + packet = struct.pack('!I', len(usernameBytes)) + usernameBytes + struct.pack('!I', len(passwordBytes)) + passwordBytes # Send packet with username/password to ov pipe operations.writeToPipe("\\\\.\\pipe\\VDSMDPipe", packet, True) return 'done' - def onLogout(self, user): + def onLogout(self, userName) -> None: logger.debug('Windows onLogout invoked: {}, {}'.format(user, self._user)) try: - REMOTE_USERS_SID = 'S-1-5-32-555' p = win32security.GetBinarySid(REMOTE_USERS_SID) groupName = win32security.LookupAccountSid(None, p)[0] except Exception: logger.error('Exception getting Windows Group') return - if self._user is not None: + if self._user: try: win32net.NetLocalGroupDelMembers(None, groupName, [self._user]) except Exception as e: logger.error('Exception removing user from Remote Desktop Users: {}'.format(e)) - def SvcDoRun(self): # pylint: disable=too-many-statements, too-many-branches + def SvcDoRun(self) -> None: # pylint: disable=too-many-statements, too-many-branches ''' Main service loop ''' - try: - initCfg() + logger.debug('running SvcDoRun') + servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STARTED, (self._svc_name_, '')) - logger.debug('running SvcDoRun') - servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, - servicemanager.PYS_SERVICE_STARTED, - (self._svc_name_, '')) + # call the CoInitialize to allow the registration to run in an other + # thread + logger.debug('Initializing com...') + + pythoncom.CoInitialize() - # call the CoInitialize to allow the registration to run in an other - # thread - logger.debug('Initializing com...') - pythoncom.CoInitialize() - - # ******************************************************** - # * Ask brokers what to do before proceding to main loop * - # ******************************************************** - while True: - brokerConnected = self.interactWithBroker() - if brokerConnected is False: - logger.debug('Interact with broker returned false, stopping service after a while') - self.notifyStop() - win32event.WaitForSingleObject(self.hWaitStop, 5000) - return - elif brokerConnected is True: - break - - # If brokerConnected returns None, repeat the cycle - self.doWait(16000) # Wait for a looong while - - if self.interactWithBroker() is False: - logger.debug('Interact with broker returned false, stopping service after a while') - self.notifyStop() - win32event.WaitForSingleObject(self.hWaitStop, 5000) - return - - if self.isAlive is False: - logger.debug('The service is not alive after broker interaction, stopping it') - self.notifyStop() - return - - if self.rebootRequested is True: - logger.debug('Reboot has been requested, stopping service') - self.notifyStop() - return - - self.initIPC() - except Exception: # Any init exception wil be caught, service must be then restarted - logger.exception() - logger.debug('Exiting service with failure status') - os._exit(-1) # pylint: disable=protected-access + if not self.initialize(): + self.notifyStop() + win32event.WaitForSingleObject(self.hWaitStop, 5000) + return # Stop daemon if initializes told to do so # ******************************** # * Registers SENS subscriptions * @@ -319,18 +243,11 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService): event_system.Store(PROGID_EventSubscription, event_subscription) - logger.debug('Registered SENS, running main loop') + logger.debug('Registered SENS') + logger.debug('Initialized, setting ready') - # Execute script in c:\\windows\\post-uds.bat after interacting with broker, if no reboot is requested ofc - # This will be executed only when machine gets "ready" - try: - if os.path.isfile(POST_CMD): - subprocess.call([POST_CMD, ]) - else: - logger.info('POST file not found & not executed') - except Exception as e: - # Ignore output of execution command - logger.error('Executing post command give') + # Initialization is done, set machine to ready for UDS, communicate urls, etc... + self.setReady() # ********************* # * Main Service loop * @@ -338,17 +255,15 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService): # Counter used to check ip changes only once every 10 seconds, for # example counter = 0 - while self.isAlive: + while self._isAlive: counter += 1 - # Process SENS messages, This will be a bit asyncronous (1 second - # delay) + # Process SENS messages, This will be a bit asyncronous (1 second delay) pythoncom.PumpWaitingMessages() - if counter >= 15: # Once every 15 seconds + + if counter >= 10: # Once every 15 seconds counter = 0 - try: - self.checkIpsChanged() - except Exception as e: - logger.error('Error checking ip change: {}'.format(e)) + self.checkIpsChanged() + # In milliseconds, will break win32event.WaitForSingleObject(self.hWaitStop, 1000) @@ -357,10 +272,6 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService): # ******************************************* # * Remove SENS subscription before exiting * # ******************************************* - event_system.Remove( - PROGID_EventSubscription, "SubscriptionID == " + subscription_guid) - - self.endIPC() # Ends IPC servers - self.endAPI() # And deinitializes REST api if needed + event_system.Remove(PROGID_EventSubscription, "SubscriptionID == " + subscription_guid) self.notifyStop()