diff --git a/lib/ansible/module_utils/csharp/Ansible.Service.cs b/lib/ansible/module_utils/csharp/Ansible.Service.cs new file mode 100644 index 00000000000..be0f3db3f3b --- /dev/null +++ b/lib/ansible/module_utils/csharp/Ansible.Service.cs @@ -0,0 +1,1341 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Security.Principal; +using System.Text; +using Ansible.Privilege; + +namespace Ansible.Service +{ + internal class NativeHelpers + { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct ENUM_SERVICE_STATUSW + { + public string lpServiceName; + public string lpDisplayName; + public SERVICE_STATUS ServiceStatus; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct QUERY_SERVICE_CONFIGW + { + public ServiceType dwServiceType; + public ServiceStartType dwStartType; + public ErrorControl dwErrorControl; + [MarshalAs(UnmanagedType.LPWStr)] public string lpBinaryPathName; + [MarshalAs(UnmanagedType.LPWStr)] public string lpLoadOrderGroup; + public Int32 dwTagId; + public IntPtr lpDependencies; // Can't rely on marshaling as dependencies are delimited by \0. + [MarshalAs(UnmanagedType.LPWStr)] public string lpServiceStartName; + [MarshalAs(UnmanagedType.LPWStr)] public string lpDisplayName; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SC_ACTION + { + public FailureAction Type; + public UInt32 Delay; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SERVICE_DELAYED_AUTO_START_INFO + { + public bool fDelayedAutostart; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct SERVICE_DESCRIPTIONW + { + [MarshalAs(UnmanagedType.LPWStr)] public string lpDescription; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SERVICE_FAILURE_ACTIONS_FLAG + { + public bool fFailureActionsOnNonCrashFailures; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct SERVICE_FAILURE_ACTIONSW + { + public UInt32 dwResetPeriod; + [MarshalAs(UnmanagedType.LPWStr)] public string lpRebootMsg; + [MarshalAs(UnmanagedType.LPWStr)] public string lpCommand; + public UInt32 cActions; + public IntPtr lpsaActions; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SERVICE_LAUNCH_PROTECTED_INFO + { + public LaunchProtection dwLaunchProtected; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SERVICE_PREFERRED_NODE_INFO + { + public UInt16 usPreferredNode; + public bool fDelete; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SERVICE_PRESHUTDOWN_INFO + { + public UInt32 dwPreshutdownTimeout; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct SERVICE_REQUIRED_PRIVILEGES_INFOW + { + // Can't rely on marshaling as privileges are delimited by \0. + public IntPtr pmszRequiredPrivileges; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SERVICE_SID_INFO + { + public ServiceSidInfo dwServiceSidType; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SERVICE_STATUS + { + public ServiceType dwServiceType; + public ServiceStatus dwCurrentState; + public ControlsAccepted dwControlsAccepted; + public UInt32 dwWin32ExitCode; + public UInt32 dwServiceSpecificExitCode; + public UInt32 dwCheckPoint; + public UInt32 dwWaitHint; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SERVICE_STATUS_PROCESS + { + public ServiceType dwServiceType; + public ServiceStatus dwCurrentState; + public ControlsAccepted dwControlsAccepted; + public UInt32 dwWin32ExitCode; + public UInt32 dwServiceSpecificExitCode; + public UInt32 dwCheckPoint; + public UInt32 dwWaitHint; + public UInt32 dwProcessId; + public ServiceFlags dwServiceFlags; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SERVICE_TRIGGER + { + public TriggerType dwTriggerType; + public TriggerAction dwAction; + public IntPtr pTriggerSubtype; + public UInt32 cDataItems; + public IntPtr pDataItems; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SERVICE_TRIGGER_SPECIFIC_DATA_ITEM + { + public TriggerDataType dwDataType; + public UInt32 cbData; + public IntPtr pData; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SERVICE_TRIGGER_INFO + { + public UInt32 cTriggers; + public IntPtr pTriggers; + public IntPtr pReserved; + } + + public enum ConfigInfoLevel : uint + { + SERVICE_CONFIG_DESCRIPTION = 0x00000001, + SERVICE_CONFIG_FAILURE_ACTIONS = 0x00000002, + SERVICE_CONFIG_DELAYED_AUTO_START_INFO = 0x00000003, + SERVICE_CONFIG_FAILURE_ACTIONS_FLAG = 0x00000004, + SERVICE_CONFIG_SERVICE_SID_INFO = 0x00000005, + SERVICE_CONFIG_REQUIRED_PRIVILEGES_INFO = 0x00000006, + SERVICE_CONFIG_PRESHUTDOWN_INFO = 0x00000007, + SERVICE_CONFIG_TRIGGER_INFO = 0x00000008, + SERVICE_CONFIG_PREFERRED_NODE = 0x00000009, + SERVICE_CONFIG_LAUNCH_PROTECTED = 0x0000000c, + } + } + + internal class NativeMethods + { + [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool ChangeServiceConfigW( + SafeHandle hService, + ServiceType dwServiceType, + ServiceStartType dwStartType, + ErrorControl dwErrorControl, + string lpBinaryPathName, + string lpLoadOrderGroup, + IntPtr lpdwTagId, + string lpDependencies, + string lpServiceStartName, + string lpPassword, + string lpDisplayName); + + [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool ChangeServiceConfig2W( + SafeHandle hService, + NativeHelpers.ConfigInfoLevel dwInfoLevel, + IntPtr lpInfo); + + [DllImport("Advapi32.dll", SetLastError = true)] + public static extern bool CloseServiceHandle( + IntPtr hSCObject); + + [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern SafeServiceHandle CreateServiceW( + SafeHandle hSCManager, + string lpServiceName, + string lpDisplayName, + ServiceRights dwDesiredAccess, + ServiceType dwServiceType, + ServiceStartType dwStartType, + ErrorControl dwErrorControl, + string lpBinaryPathName, + string lpLoadOrderGroup, + IntPtr lpdwTagId, + string lpDependencies, + string lpServiceStartName, + string lpPassword); + + [DllImport("Advapi32.dll", SetLastError = true)] + public static extern bool DeleteService( + SafeHandle hService); + + [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool EnumDependentServicesW( + SafeHandle hService, + UInt32 dwServiceState, + SafeMemoryBuffer lpServices, + UInt32 cbBufSize, + out UInt32 pcbBytesNeeded, + out UInt32 lpServicesReturned); + + [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern SafeServiceHandle OpenSCManagerW( + string lpMachineName, + string lpDatabaseNmae, + SCMRights dwDesiredAccess); + + [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern SafeServiceHandle OpenServiceW( + SafeHandle hSCManager, + string lpServiceName, + ServiceRights dwDesiredAccess); + + [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool QueryServiceConfigW( + SafeHandle hService, + IntPtr lpServiceConfig, + UInt32 cbBufSize, + out UInt32 pcbBytesNeeded); + + [DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool QueryServiceConfig2W( + SafeHandle hservice, + NativeHelpers.ConfigInfoLevel dwInfoLevel, + IntPtr lpBuffer, + UInt32 cbBufSize, + out UInt32 pcbBytesNeeded); + + [DllImport("Advapi32.dll", SetLastError = true)] + public static extern bool QueryServiceStatusEx( + SafeHandle hService, + UInt32 InfoLevel, + IntPtr lpBuffer, + UInt32 cbBufSize, + out UInt32 pcbBytesNeeded); + } + + internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid + { + public UInt32 BufferLength { get; internal set; } + + public SafeMemoryBuffer() : base(true) { } + public SafeMemoryBuffer(int cb) : base(true) + { + BufferLength = (UInt32)cb; + base.SetHandle(Marshal.AllocHGlobal(cb)); + } + public SafeMemoryBuffer(IntPtr handle) : base(true) + { + base.SetHandle(handle); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + Marshal.FreeHGlobal(handle); + return true; + } + } + + internal class SafeServiceHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeServiceHandle() : base(true) { } + public SafeServiceHandle(IntPtr handle) : base(true) { this.handle = handle; } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + return NativeMethods.CloseServiceHandle(handle); + } + } + + [Flags] + public enum ControlsAccepted : uint + { + None = 0x00000000, + Stop = 0x00000001, + PauseContinue = 0x00000002, + Shutdown = 0x00000004, + ParamChange = 0x00000008, + NetbindChange = 0x00000010, + HardwareProfileChange = 0x00000020, + PowerEvent = 0x00000040, + SessionChange = 0x00000080, + PreShutdown = 0x00000100, + } + + public enum ErrorControl : uint + { + Ignore = 0x00000000, + Normal = 0x00000001, + Severe = 0x00000002, + Critical = 0x00000003, + } + + public enum FailureAction : uint + { + None = 0x00000000, + Restart = 0x00000001, + Reboot = 0x00000002, + RunCommand = 0x00000003, + } + + public enum LaunchProtection : uint + { + None = 0, + Windows = 1, + WindowsLight = 2, + AntimalwareLight = 3, + } + + [Flags] + public enum SCMRights : uint + { + Connect = 0x00000001, + CreateService = 0x00000002, + EnumerateService = 0x00000004, + Lock = 0x00000008, + QueryLockStatus = 0x00000010, + ModifyBootConfig = 0x00000020, + AllAccess = 0x000F003F, + } + + [Flags] + public enum ServiceFlags : uint + { + None = 0x0000000, + RunsInSystemProcess = 0x00000001, + } + + [Flags] + public enum ServiceRights : uint + { + QueryConfig = 0x00000001, + ChangeConfig = 0x00000002, + QueryStatus = 0x00000004, + EnumerateDependents = 0x00000008, + Start = 0x00000010, + Stop = 0x00000020, + PauseContinue = 0x00000040, + Interrogate = 0x00000080, + UserDefinedControl = 0x00000100, + Delete = 0x00010000, + ReadControl = 0x00020000, + WriteDac = 0x00040000, + WriteOwner = 0x00080000, + AllAccess = 0x000F01FF, + AccessSystemSecurity = 0x01000000, + } + + public enum ServiceStartType : uint + { + BootStart = 0x00000000, + SystemStart = 0x00000001, + AutoStart = 0x00000002, + DemandStart = 0x00000003, + Disabled = 0x00000004, + + // Not part of ChangeServiceConfig enumeration but built by the Srvice class for the StartType property. + AutoStartDelayed = 0x1000000 + } + + [Flags] + public enum ServiceType : uint + { + KernelDriver = 0x00000001, + FileSystemDriver = 0x00000002, + Adapter = 0x00000004, + RecognizerDriver = 0x00000008, + Driver = KernelDriver | FileSystemDriver | RecognizerDriver, + Win32OwnProcess = 0x00000010, + Win32ShareProcess = 0x00000020, + Win32 = Win32OwnProcess | Win32ShareProcess, + UserProcess = 0x00000040, + UserOwnprocess = Win32OwnProcess | UserProcess, + UserShareProcess = Win32ShareProcess | UserProcess, + UserServiceInstance = 0x00000080, + InteractiveProcess = 0x00000100, + PkgService = 0x00000200, + } + + public enum ServiceSidInfo : uint + { + None, + Unrestricted, + Restricted = 3, + } + + public enum ServiceStatus : uint + { + Stopped = 0x00000001, + StartPending = 0x00000002, + StopPending = 0x00000003, + Running = 0x00000004, + ContinuePending = 0x00000005, + PausePending = 0x00000006, + Paused = 0x00000007, + } + + public enum TriggerAction : uint + { + ServiceStart = 0x00000001, + ServiceStop = 0x000000002, + } + + public enum TriggerDataType : uint + { + Binary = 00000001, + String = 0x00000002, + Level = 0x00000003, + KeywordAny = 0x00000004, + KeywordAll = 0x00000005, + } + + public enum TriggerType : uint + { + DeviceInterfaceArrival = 0x00000001, + IpAddressAvailability = 0x00000002, + DomainJoin = 0x00000003, + FirewallPortEvent = 0x00000004, + GroupPolicy = 0x00000005, + NetworkEndpoint = 0x00000006, + Custom = 0x00000014, + } + + public class ServiceManagerException : System.ComponentModel.Win32Exception + { + private string _msg; + + public ServiceManagerException(string message) : this(Marshal.GetLastWin32Error(), message) { } + public ServiceManagerException(int errorCode, string message) : base(errorCode) + { + _msg = String.Format("{0} ({1}, Win32ErrorCode {2} - 0x{2:X8})", message, base.Message, errorCode); + } + + public override string Message { get { return _msg; } } + public static explicit operator ServiceManagerException(string message) + { + return new ServiceManagerException(message); + } + } + + public class Action + { + public FailureAction Type; + public UInt32 Delay; + } + + public class FailureActions + { + public UInt32? ResetPeriod = null; // Get is always populated, can be null on set to preserve existing. + public string RebootMsg = null; + public string Command = null; + public List Actions = null; + + public FailureActions() { } + + internal FailureActions(NativeHelpers.SERVICE_FAILURE_ACTIONSW actions) + { + ResetPeriod = actions.dwResetPeriod; + RebootMsg = actions.lpRebootMsg; + Command = actions.lpCommand; + Actions = new List(); + + int actionLength = Marshal.SizeOf(typeof(NativeHelpers.SC_ACTION)); + for (int i = 0; i < actions.cActions; i++) + { + IntPtr actionPtr = IntPtr.Add(actions.lpsaActions, i * actionLength); + + NativeHelpers.SC_ACTION rawAction = (NativeHelpers.SC_ACTION)Marshal.PtrToStructure( + actionPtr, typeof(NativeHelpers.SC_ACTION)); + + Actions.Add(new Action() + { + Type = rawAction.Type, + Delay = rawAction.Delay, + }); + } + } + } + + public class TriggerItem + { + public TriggerDataType Type; + public object Data; // Can be string, List, byte, byte[], or Int64 depending on Type. + + public TriggerItem() { } + + internal TriggerItem(NativeHelpers.SERVICE_TRIGGER_SPECIFIC_DATA_ITEM dataItem) + { + Type = dataItem.dwDataType; + + byte[] itemBytes = new byte[dataItem.cbData]; + Marshal.Copy(dataItem.pData, itemBytes, 0, itemBytes.Length); + + switch (dataItem.dwDataType) + { + case TriggerDataType.String: + string value = Encoding.Unicode.GetString(itemBytes, 0, itemBytes.Length); + + if (value.EndsWith("\0\0")) + { + // Multistring with a delimiter of \0 and terminated with \0\0. + Data = new List(value.Split(new char[1] { '\0' }, StringSplitOptions.RemoveEmptyEntries)); + } + else + // Just a single string with null character at the end, strip it off. + Data = value.Substring(0, value.Length - 1); + break; + case TriggerDataType.Level: + Data = itemBytes[0]; + break; + case TriggerDataType.KeywordAll: + case TriggerDataType.KeywordAny: + Data = BitConverter.ToUInt64(itemBytes, 0); + break; + default: + Data = itemBytes; + break; + } + } + } + + public class Trigger + { + // https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_trigger + public const string NAMED_PIPE_EVENT_GUID = "1f81d131-3fac-4537-9e0c-7e7b0c2f4b55"; + public const string RPC_INTERFACE_EVENT_GUID = "bc90d167-9470-4139-a9ba-be0bbbf5b74d"; + public const string DOMAIN_JOIN_GUID = "1ce20aba-9851-4421-9430-1ddeb766e809"; + public const string DOMAIN_LEAVE_GUID = "ddaf516e-58c2-4866-9574-c3b615d42ea1"; + public const string FIREWALL_PORT_OPEN_GUID = "b7569e07-8421-4ee0-ad10-86915afdad09"; + public const string FIREWALL_PORT_CLOSE_GUID = "a144ed38-8e12-4de4-9d96-e64740b1a524"; + public const string MACHINE_POLICY_PRESENT_GUID = "659fcae6-5bdb-4da9-b1ff-ca2a178d46e0"; + public const string NETWORK_MANAGER_FIRST_IP_ADDRESS_ARRIVAL_GUID = "4f27f2de-14e2-430b-a549-7cd48cbc8245"; + public const string NETWORK_MANAGER_LAST_IP_ADDRESS_REMOVAL_GUID = "cc4ba62a-162e-4648-847a-b6bdf993e335"; + public const string USER_POLICY_PRESENT_GUID = "54fb46c8-f089-464c-b1fd-59d1b62c3b50"; + + public TriggerType Type; + public TriggerAction Action; + public Guid SubType; + public List DataItems = new List(); + + public Trigger() { } + + internal Trigger(NativeHelpers.SERVICE_TRIGGER trigger) + { + Type = trigger.dwTriggerType; + Action = trigger.dwAction; + SubType = (Guid)Marshal.PtrToStructure(trigger.pTriggerSubtype, typeof(Guid)); + + int dataItemLength = Marshal.SizeOf(typeof(NativeHelpers.SERVICE_TRIGGER_SPECIFIC_DATA_ITEM)); + for (int i = 0; i < trigger.cDataItems; i++) + { + IntPtr dataPtr = IntPtr.Add(trigger.pDataItems, i * dataItemLength); + + var dataItem = (NativeHelpers.SERVICE_TRIGGER_SPECIFIC_DATA_ITEM)Marshal.PtrToStructure( + dataPtr, typeof(NativeHelpers.SERVICE_TRIGGER_SPECIFIC_DATA_ITEM)); + + DataItems.Add(new TriggerItem(dataItem)); + } + } + } + + public class Service : IDisposable + { + private const UInt32 SERVICE_NO_CHANGE = 0xFFFFFFFF; + + private SafeServiceHandle _scmHandle; + private SafeServiceHandle _serviceHandle; + private SafeMemoryBuffer _rawServiceConfig; + private NativeHelpers.SERVICE_STATUS_PROCESS _statusProcess; + + private NativeHelpers.QUERY_SERVICE_CONFIGW _ServiceConfig + { + get + { + return (NativeHelpers.QUERY_SERVICE_CONFIGW)Marshal.PtrToStructure( + _rawServiceConfig.DangerousGetHandle(), typeof(NativeHelpers.QUERY_SERVICE_CONFIGW)); + } + } + + // ServiceConfig + public string ServiceName { get; private set; } + + public ServiceType ServiceType + { + get { return _ServiceConfig.dwServiceType; } + set { ChangeServiceConfig(serviceType: value); } + } + + public ServiceStartType StartType + { + get + { + ServiceStartType startType = _ServiceConfig.dwStartType; + if (startType == ServiceStartType.AutoStart) + { + var value = QueryServiceConfig2( + NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_DELAYED_AUTO_START_INFO); + + if (value.fDelayedAutostart) + startType = ServiceStartType.AutoStartDelayed; + } + + return startType; + } + set + { + ServiceStartType newStartType = value; + bool delayedStart = false; + if (value == ServiceStartType.AutoStartDelayed) + { + newStartType = ServiceStartType.AutoStart; + delayedStart = true; + } + + ChangeServiceConfig(startType: newStartType); + + var info = new NativeHelpers.SERVICE_DELAYED_AUTO_START_INFO() + { + fDelayedAutostart = delayedStart, + }; + ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_DELAYED_AUTO_START_INFO, info); + } + } + + public ErrorControl ErrorControl + { + get { return _ServiceConfig.dwErrorControl; } + set { ChangeServiceConfig(errorControl: value); } + } + + public string Path + { + get { return _ServiceConfig.lpBinaryPathName; } + set { ChangeServiceConfig(binaryPath: value); } + } + + public string LoadOrderGroup + { + get { return _ServiceConfig.lpLoadOrderGroup; } + set { ChangeServiceConfig(loadOrderGroup: value); } + } + + public List DependentOn + { + get + { + StringBuilder deps = new StringBuilder(); + IntPtr depPtr = _ServiceConfig.lpDependencies; + + bool wasNull = false; + while (true) + { + // Get the current char at the pointer and add it to the StringBuilder. + byte[] charBytes = new byte[sizeof(char)]; + Marshal.Copy(depPtr, charBytes, 0, charBytes.Length); + depPtr = IntPtr.Add(depPtr, charBytes.Length); + char currentChar = BitConverter.ToChar(charBytes, 0); + deps.Append(currentChar); + + // If the previous and current char is \0 exit the loop. + if (currentChar == '\0' && wasNull) + break; + wasNull = currentChar == '\0'; + } + + return new List(deps.ToString().Split(new char[1] { '\0' }, + StringSplitOptions.RemoveEmptyEntries)); + } + set { ChangeServiceConfig(dependencies: value); } + } + + public IdentityReference Account + { + get + { + if (_ServiceConfig.lpServiceStartName == null) + // User services don't have the start name specified and will be null. + return null; + else if (_ServiceConfig.lpServiceStartName == "LocalSystem") + // Special string used for the SYSTEM account, this is the same even for different localisations. + return (NTAccount)new SecurityIdentifier("S-1-5-18").Translate(typeof(NTAccount)); + else + return new NTAccount(_ServiceConfig.lpServiceStartName); + } + set + { + string startName = null; + string pass = null; + + if (value != null) + { + // Create a SID and convert back from a SID to get the Netlogon form regardless of the input + // specified. + SecurityIdentifier accountSid = (SecurityIdentifier)value.Translate(typeof(SecurityIdentifier)); + NTAccount accountName = (NTAccount)accountSid.Translate(typeof(NTAccount)); + string[] accountSplit = accountName.Value.Split(new char[1] { '\\' }, 2); + + // SYSTEM, Local Service, Network Service + List serviceAccounts = new List { "S-1-5-18", "S-1-5-19", "S-1-5-20" }; + + // Well known service accounts and MSAs should have no password set. Explicitly blank out the + // existing password to ensure older passwords are no longer stored by Windows. + if (serviceAccounts.Contains(accountSid.Value) || accountSplit[1].EndsWith("$")) + pass = ""; + + // The SYSTEM account uses this special string to specify that account otherwise use the original + // NTAccount value in case it is in a custom format (not Netlogon) for a reason. + if (accountSid.Value == serviceAccounts[0]) + startName = "LocalSystem"; + else + startName = value.Translate(typeof(NTAccount)).Value; + } + + ChangeServiceConfig(startName: startName, password: pass); + } + } + + public string Password { set { ChangeServiceConfig(password: value); } } + + public string DisplayName + { + get { return _ServiceConfig.lpDisplayName; } + set { ChangeServiceConfig(displayName: value); } + } + + // ServiceConfig2 + + public string Description + { + get + { + var value = QueryServiceConfig2( + NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_DESCRIPTION); + + return value.lpDescription; + } + set + { + var info = new NativeHelpers.SERVICE_DESCRIPTIONW() + { + lpDescription = value, + }; + ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_DESCRIPTION, info); + } + } + + public FailureActions FailureActions + { + get + { + using (SafeMemoryBuffer b = QueryServiceConfig2( + NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_FAILURE_ACTIONS)) + { + NativeHelpers.SERVICE_FAILURE_ACTIONSW value = (NativeHelpers.SERVICE_FAILURE_ACTIONSW) + Marshal.PtrToStructure(b.DangerousGetHandle(), typeof(NativeHelpers.SERVICE_FAILURE_ACTIONSW)); + + return new FailureActions(value); + } + } + set + { + // dwResetPeriod and lpsaActions must be set together, we need to read the existing config if someone + // wants to update 1 or the other but both aren't explicitly defined. + UInt32? resetPeriod = value.ResetPeriod; + List actions = value.Actions; + if ((resetPeriod != null && actions == null) || (resetPeriod == null && actions != null)) + { + FailureActions existingValue = this.FailureActions; + + if (resetPeriod != null && existingValue.Actions.Count == 0) + throw new ArgumentException( + "Cannot set FailureAction ResetPeriod without explicit Actions and no existing Actions"); + else if (resetPeriod == null) + resetPeriod = (UInt32)existingValue.ResetPeriod; + + if (actions == null) + actions = existingValue.Actions; + } + + var info = new NativeHelpers.SERVICE_FAILURE_ACTIONSW() + { + dwResetPeriod = resetPeriod == null ? 0 : (UInt32)resetPeriod, + lpRebootMsg = value.RebootMsg, + lpCommand = value.Command, + cActions = actions == null ? 0 : (UInt32)actions.Count, + lpsaActions = IntPtr.Zero, + }; + + // null means to keep the existing actions whereas an empty list deletes the actions. + if (actions == null) + { + ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_FAILURE_ACTIONS, info); + return; + } + + int actionLength = Marshal.SizeOf(typeof(NativeHelpers.SC_ACTION)); + using (SafeMemoryBuffer buffer = new SafeMemoryBuffer(actionLength * actions.Count)) + { + info.lpsaActions = buffer.DangerousGetHandle(); + HashSet privileges = new HashSet(); + + for (int i = 0; i < actions.Count; i++) + { + IntPtr actionPtr = IntPtr.Add(info.lpsaActions, i * actionLength); + NativeHelpers.SC_ACTION action = new NativeHelpers.SC_ACTION() + { + Delay = actions[i].Delay, + Type = actions[i].Type, + }; + Marshal.StructureToPtr(action, actionPtr, false); + + // Need to make sure the SeShutdownPrivilege is enabled when adding a reboot failure action. + if (action.Type == FailureAction.Reboot) + privileges.Add("SeShutdownPrivilege"); + } + + using (new PrivilegeEnabler(true, privileges.ToList().ToArray())) + ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_FAILURE_ACTIONS, info); + } + } + } + + public bool FailureActionsOnNonCrashFailures + { + get + { + var value = QueryServiceConfig2( + NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_FAILURE_ACTIONS_FLAG); + + return value.fFailureActionsOnNonCrashFailures; + } + set + { + var info = new NativeHelpers.SERVICE_FAILURE_ACTIONS_FLAG() + { + fFailureActionsOnNonCrashFailures = value, + }; + ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_FAILURE_ACTIONS_FLAG, info); + } + } + + public ServiceSidInfo ServiceSidInfo + { + get + { + var value = QueryServiceConfig2( + NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_SERVICE_SID_INFO); + + return value.dwServiceSidType; + } + set + { + var info = new NativeHelpers.SERVICE_SID_INFO() + { + dwServiceSidType = value, + }; + ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_SERVICE_SID_INFO, info); + } + } + + public List RequiredPrivileges + { + get + { + using (SafeMemoryBuffer buffer = QueryServiceConfig2( + NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_REQUIRED_PRIVILEGES_INFO)) + { + var value = (NativeHelpers.SERVICE_REQUIRED_PRIVILEGES_INFOW)Marshal.PtrToStructure( + buffer.DangerousGetHandle(), typeof(NativeHelpers.SERVICE_REQUIRED_PRIVILEGES_INFOW)); + + int structLength = Marshal.SizeOf(value); + int stringLength = ((int)buffer.BufferLength - structLength) / sizeof(char); + + if (stringLength > 0) + { + string privilegesString = Marshal.PtrToStringUni(value.pmszRequiredPrivileges, stringLength); + return new List(privilegesString.Split(new char[1] { '\0' }, + StringSplitOptions.RemoveEmptyEntries)); + } + else + return new List(); + } + } + set + { + string privilegeString = String.Join("\0", value ?? new List()) + "\0\0"; + + using (SafeMemoryBuffer buffer = new SafeMemoryBuffer(Marshal.StringToHGlobalUni(privilegeString))) + { + var info = new NativeHelpers.SERVICE_REQUIRED_PRIVILEGES_INFOW() + { + pmszRequiredPrivileges = buffer.DangerousGetHandle(), + }; + ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_REQUIRED_PRIVILEGES_INFO, info); + } + } + } + + public UInt32 PreShutdownTimeout + { + get + { + var value = QueryServiceConfig2( + NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_PRESHUTDOWN_INFO); + + return value.dwPreshutdownTimeout; + } + set + { + var info = new NativeHelpers.SERVICE_PRESHUTDOWN_INFO() + { + dwPreshutdownTimeout = value, + }; + ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_PRESHUTDOWN_INFO, info); + } + } + + public List Triggers + { + get + { + List triggers = new List(); + + using (SafeMemoryBuffer b = QueryServiceConfig2( + NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_TRIGGER_INFO)) + { + var value = (NativeHelpers.SERVICE_TRIGGER_INFO)Marshal.PtrToStructure( + b.DangerousGetHandle(), typeof(NativeHelpers.SERVICE_TRIGGER_INFO)); + + int triggerLength = Marshal.SizeOf(typeof(NativeHelpers.SERVICE_TRIGGER)); + for (int i = 0; i < value.cTriggers; i++) + { + IntPtr triggerPtr = IntPtr.Add(value.pTriggers, i * triggerLength); + var trigger = (NativeHelpers.SERVICE_TRIGGER)Marshal.PtrToStructure(triggerPtr, + typeof(NativeHelpers.SERVICE_TRIGGER)); + + triggers.Add(new Trigger(trigger)); + } + } + + return triggers; + } + set + { + var info = new NativeHelpers.SERVICE_TRIGGER_INFO() + { + cTriggers = value == null ? 0 : (UInt32)value.Count, + pTriggers = IntPtr.Zero, + pReserved = IntPtr.Zero, + }; + + if (info.cTriggers == 0) + { + try + { + ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_TRIGGER_INFO, info); + } + catch (ServiceManagerException e) + { + // Can fail with ERROR_INVALID_PARAMETER if no triggers were already set on the service, just + // continue as the service is what we want it to be. + if (e.NativeErrorCode != 87) + throw; + } + return; + } + + // Due to the dynamic nature of the trigger structure(s) we need to manually calculate the size of the + // data items on each trigger if present. This also serializes the raw data items to bytes here. + int structDataLength = 0; + int dataLength = 0; + Queue dataItems = new Queue(); + foreach (Trigger trigger in value) + { + if (trigger.DataItems == null || trigger.DataItems.Count == 0) + continue; + + foreach (TriggerItem dataItem in trigger.DataItems) + { + structDataLength += Marshal.SizeOf(typeof(NativeHelpers.SERVICE_TRIGGER_SPECIFIC_DATA_ITEM)); + + byte[] dataItemBytes; + Type dataItemType = dataItem.Data.GetType(); + if (dataItemType == typeof(byte)) + dataItemBytes = new byte[1] { (byte)dataItem.Data }; + else if (dataItemType == typeof(byte[])) + dataItemBytes = (byte[])dataItem.Data; + else if (dataItemType == typeof(UInt64)) + dataItemBytes = BitConverter.GetBytes((UInt64)dataItem.Data); + else if (dataItemType == typeof(string)) + dataItemBytes = Encoding.Unicode.GetBytes((string)dataItem.Data + "\0"); + else if (dataItemType == typeof(List)) + dataItemBytes = Encoding.Unicode.GetBytes( + String.Join("\0", (List)dataItem.Data) + "\0"); + else + throw new ArgumentException(String.Format("Trigger data type '{0}' not a value type", + dataItemType.Name)); + + dataLength += dataItemBytes.Length; + dataItems.Enqueue(dataItemBytes); + } + } + + using (SafeMemoryBuffer triggerBuffer = new SafeMemoryBuffer( + value.Count * Marshal.SizeOf(typeof(NativeHelpers.SERVICE_TRIGGER)))) + using (SafeMemoryBuffer triggerGuidBuffer = new SafeMemoryBuffer( + value.Count * Marshal.SizeOf(typeof(Guid)))) + using (SafeMemoryBuffer dataItemBuffer = new SafeMemoryBuffer(structDataLength)) + using (SafeMemoryBuffer dataBuffer = new SafeMemoryBuffer(dataLength)) + { + info.pTriggers = triggerBuffer.DangerousGetHandle(); + + IntPtr triggerPtr = triggerBuffer.DangerousGetHandle(); + IntPtr guidPtr = triggerGuidBuffer.DangerousGetHandle(); + IntPtr dataItemPtr = dataItemBuffer.DangerousGetHandle(); + IntPtr dataPtr = dataBuffer.DangerousGetHandle(); + + foreach (Trigger trigger in value) + { + int dataCount = trigger.DataItems == null ? 0 : trigger.DataItems.Count; + var rawTrigger = new NativeHelpers.SERVICE_TRIGGER() + { + dwTriggerType = trigger.Type, + dwAction = trigger.Action, + pTriggerSubtype = guidPtr, + cDataItems = (UInt32)dataCount, + pDataItems = dataCount == 0 ? IntPtr.Zero : dataItemPtr, + }; + guidPtr = StructureToPtr(trigger.SubType, guidPtr); + + for (int i = 0; i < rawTrigger.cDataItems; i++) + { + byte[] dataItemBytes = dataItems.Dequeue(); + var rawTriggerData = new NativeHelpers.SERVICE_TRIGGER_SPECIFIC_DATA_ITEM() + { + dwDataType = trigger.DataItems[i].Type, + cbData = (UInt32)dataItemBytes.Length, + pData = dataPtr, + }; + Marshal.Copy(dataItemBytes, 0, dataPtr, dataItemBytes.Length); + dataPtr = IntPtr.Add(dataPtr, dataItemBytes.Length); + + dataItemPtr = StructureToPtr(rawTriggerData, dataItemPtr); + } + + triggerPtr = StructureToPtr(rawTrigger, triggerPtr); + } + + ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_TRIGGER_INFO, info); + } + } + } + + public UInt16? PreferredNode + { + get + { + try + { + var value = QueryServiceConfig2( + NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_PREFERRED_NODE); + + return value.usPreferredNode; + } + catch (ServiceManagerException e) + { + // If host has no NUMA support this will fail with ERROR_INVALID_PARAMETER + if (e.NativeErrorCode == 0x00000057) // ERROR_INVALID_PARAMETER + return null; + + throw; + } + } + set + { + var info = new NativeHelpers.SERVICE_PREFERRED_NODE_INFO(); + if (value == null) + info.fDelete = true; + else + info.usPreferredNode = (UInt16)value; + ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_PREFERRED_NODE, info); + } + } + + public LaunchProtection LaunchProtection + { + get + { + var value = QueryServiceConfig2( + NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_LAUNCH_PROTECTED); + + return value.dwLaunchProtected; + } + set + { + var info = new NativeHelpers.SERVICE_LAUNCH_PROTECTED_INFO() + { + dwLaunchProtected = value, + }; + ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel.SERVICE_CONFIG_LAUNCH_PROTECTED, info); + } + } + + // ServiceStatus + public ServiceStatus State { get { return _statusProcess.dwCurrentState; } } + + public ControlsAccepted ControlsAccepted { get { return _statusProcess.dwControlsAccepted; } } + + public UInt32 Win32ExitCode { get { return _statusProcess.dwWin32ExitCode; } } + + public UInt32 ServiceExitCode { get { return _statusProcess.dwServiceSpecificExitCode; } } + + public UInt32 Checkpoint { get { return _statusProcess.dwCheckPoint; } } + + public UInt32 WaitHint { get { return _statusProcess.dwWaitHint; } } + + public UInt32 ProcessId { get { return _statusProcess.dwProcessId; } } + + public ServiceFlags ServiceFlags { get { return _statusProcess.dwServiceFlags; } } + + public Service(string name) : this(name, ServiceRights.AllAccess) { } + + public Service(string name, ServiceRights access) : this(name, access, SCMRights.Connect) { } + + public Service(string name, ServiceRights access, SCMRights scmAccess) + { + ServiceName = name; + _scmHandle = OpenSCManager(scmAccess); + _serviceHandle = NativeMethods.OpenServiceW(_scmHandle, name, access); + if (_serviceHandle.IsInvalid) + throw new ServiceManagerException(String.Format("Failed to open service '{0}'", name)); + + Refresh(); + } + + private Service(SafeServiceHandle scmHandle, SafeServiceHandle serviceHandle, string name) + { + ServiceName = name; + _scmHandle = scmHandle; + _serviceHandle = serviceHandle; + + Refresh(); + } + + // EnumDependentServices + public List DependedBy + { + get + { + UInt32 bytesNeeded = 0; + UInt32 numServices = 0; + NativeMethods.EnumDependentServicesW(_serviceHandle, 3, new SafeMemoryBuffer(IntPtr.Zero), 0, + out bytesNeeded, out numServices); + + using (SafeMemoryBuffer buffer = new SafeMemoryBuffer((int)bytesNeeded)) + { + if (!NativeMethods.EnumDependentServicesW(_serviceHandle, 3, buffer, bytesNeeded, out bytesNeeded, + out numServices)) + { + throw new ServiceManagerException("Failed to enumerated dependent services"); + } + + List dependents = new List(); + Type enumType = typeof(NativeHelpers.ENUM_SERVICE_STATUSW); + for (int i = 0; i < numServices; i++) + { + var service = (NativeHelpers.ENUM_SERVICE_STATUSW)Marshal.PtrToStructure( + IntPtr.Add(buffer.DangerousGetHandle(), i * Marshal.SizeOf(enumType)), enumType); + + dependents.Add(service.lpServiceName); + } + + return dependents; + } + } + } + + public static Service Create(string name, string binaryPath, string displayName = null, + ServiceType serviceType = ServiceType.Win32OwnProcess, + ServiceStartType startType = ServiceStartType.DemandStart, ErrorControl errorControl = ErrorControl.Normal, + string loadOrderGroup = null, List dependencies = null, string startName = null, + string password = null) + { + SafeServiceHandle scmHandle = OpenSCManager(SCMRights.CreateService | SCMRights.Connect); + + if (displayName == null) + displayName = name; + + string depString = null; + if (dependencies != null && dependencies.Count > 0) + depString = String.Join("\0", dependencies) + "\0\0"; + + SafeServiceHandle serviceHandle = NativeMethods.CreateServiceW(scmHandle, name, displayName, + ServiceRights.AllAccess, serviceType, startType, errorControl, binaryPath, + loadOrderGroup, IntPtr.Zero, depString, startName, password); + + if (serviceHandle.IsInvalid) + throw new ServiceManagerException(String.Format("Failed to create new service '{0}'", name)); + + return new Service(scmHandle, serviceHandle, name); + } + + public void Delete() + { + if (!NativeMethods.DeleteService(_serviceHandle)) + throw new ServiceManagerException("Failed to delete service"); + Dispose(); + } + + public void Dispose() + { + if (_serviceHandle != null) + _serviceHandle.Dispose(); + + if (_scmHandle != null) + _scmHandle.Dispose(); + GC.SuppressFinalize(this); + } + + public void Refresh() + { + UInt32 bytesNeeded; + NativeMethods.QueryServiceConfigW(_serviceHandle, IntPtr.Zero, 0, out bytesNeeded); + + _rawServiceConfig = new SafeMemoryBuffer((int)bytesNeeded); + if (!NativeMethods.QueryServiceConfigW(_serviceHandle, _rawServiceConfig.DangerousGetHandle(), bytesNeeded, + out bytesNeeded)) + { + throw new ServiceManagerException("Failed to query service config"); + } + + NativeMethods.QueryServiceStatusEx(_serviceHandle, 0, IntPtr.Zero, 0, out bytesNeeded); + using (SafeMemoryBuffer buffer = new SafeMemoryBuffer((int)bytesNeeded)) + { + if (!NativeMethods.QueryServiceStatusEx(_serviceHandle, 0, buffer.DangerousGetHandle(), bytesNeeded, + out bytesNeeded)) + { + throw new ServiceManagerException("Failed to query service status"); + } + + _statusProcess = (NativeHelpers.SERVICE_STATUS_PROCESS)Marshal.PtrToStructure( + buffer.DangerousGetHandle(), typeof(NativeHelpers.SERVICE_STATUS_PROCESS)); + } + } + + private void ChangeServiceConfig(ServiceType serviceType = (ServiceType)SERVICE_NO_CHANGE, + ServiceStartType startType = (ServiceStartType)SERVICE_NO_CHANGE, + ErrorControl errorControl = (ErrorControl)SERVICE_NO_CHANGE, string binaryPath = null, + string loadOrderGroup = null, List dependencies = null, string startName = null, + string password = null, string displayName = null) + { + string depString = null; + if (dependencies != null && dependencies.Count > 0) + depString = String.Join("\0", dependencies) + "\0\0"; + + if (!NativeMethods.ChangeServiceConfigW(_serviceHandle, serviceType, startType, errorControl, binaryPath, + loadOrderGroup, IntPtr.Zero, depString, startName, password, displayName)) + { + throw new ServiceManagerException("Failed to change service config"); + } + + Refresh(); + } + + private void ChangeServiceConfig2(NativeHelpers.ConfigInfoLevel infoLevel, object info) + { + using (SafeMemoryBuffer buffer = new SafeMemoryBuffer(Marshal.SizeOf(info))) + { + Marshal.StructureToPtr(info, buffer.DangerousGetHandle(), false); + + if (!NativeMethods.ChangeServiceConfig2W(_serviceHandle, infoLevel, buffer.DangerousGetHandle())) + throw new ServiceManagerException("Failed to change service config"); + } + } + + private static SafeServiceHandle OpenSCManager(SCMRights desiredAccess) + { + SafeServiceHandle handle = NativeMethods.OpenSCManagerW(null, null, desiredAccess); + if (handle.IsInvalid) + throw new ServiceManagerException("Failed to open SCManager"); + + return handle; + } + + private T QueryServiceConfig2(NativeHelpers.ConfigInfoLevel infoLevel) + { + using (SafeMemoryBuffer buffer = QueryServiceConfig2(infoLevel)) + return (T)Marshal.PtrToStructure(buffer.DangerousGetHandle(), typeof(T)); + } + + private SafeMemoryBuffer QueryServiceConfig2(NativeHelpers.ConfigInfoLevel infoLevel) + { + UInt32 bytesNeeded = 0; + NativeMethods.QueryServiceConfig2W(_serviceHandle, infoLevel, IntPtr.Zero, 0, out bytesNeeded); + + SafeMemoryBuffer buffer = new SafeMemoryBuffer((int)bytesNeeded); + if (!NativeMethods.QueryServiceConfig2W(_serviceHandle, infoLevel, buffer.DangerousGetHandle(), bytesNeeded, + out bytesNeeded)) + { + throw new ServiceManagerException(String.Format("QueryServiceConfig2W({0}) failed", + infoLevel.ToString())); + } + + return buffer; + } + + private static IntPtr StructureToPtr(object structure, IntPtr ptr) + { + Marshal.StructureToPtr(structure, ptr, false); + return IntPtr.Add(ptr, Marshal.SizeOf(structure)); + } + + ~Service() { Dispose(); } + } +} diff --git a/lib/ansible/modules/windows/win_service_info.ps1 b/lib/ansible/modules/windows/win_service_info.ps1 new file mode 100644 index 00000000000..783d470f764 --- /dev/null +++ b/lib/ansible/modules/windows/win_service_info.ps1 @@ -0,0 +1,207 @@ +#!powershell + +# Copyright: (c) 2020, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#AnsibleRequires -CSharpUtil Ansible.Service + +$spec = @{ + options = @{ + name = @{ type = "str" } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$name = $module.Params.name + +$module.Result.exists = $false +$module.Result.services = @(foreach ($rawService in (Get-Service -Name $name -ErrorAction SilentlyContinue)) { + try { + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList @( + $rawService.Name, [Ansible.Service.ServiceRights]'EnumerateDependents, QueryConfig, QueryStatus' + ) + } catch [Ansible.Service.ServiceManagerException] { + # ERROR_ACCESS_DENIED, ignore the service and continue on. + if ($_.Exception.InnerException -and $_.Exception.InnerException.NativeErrorCode -eq 5) { + $module.Warn("Failed to access service '$($rawService.Name) to get more info, ignoring") + continue + } + + throw + } + $module.Result.exists = $true + + $controlsAccepted = @($service.ControlsAccepted.ToString() -split ',' | ForEach-Object -Process { + switch ($_.Trim()) { + Stop { 'stop' } + PauseContinue { 'pause_continue' } + Shutdown { 'shutdown' } + ParamChange { 'param_change' } + NetbindChange { 'netbind_change' } + HardwareProfileChange { 'hardware_profile_change' } + PowerEvent { 'power_event' } + SessionChange { 'session_change' } + PreShutdown { 'pre_shutdown' } + } + }) + + $rawFailureActions = $service.FailureActions + $failureActions = @(foreach ($action in $rawFailureActions.Actions) { + [Ordered]@{ + type = switch ($action.Type) { + None { 'none' } + Reboot { 'reboot' } + Restart { 'restart' } + RunCommand { 'run_command' } + } + delay_ms = $action.Delay + } + }) + + # LaunchProtection is only valid in Windows 8.1 (2012 R2) or above. + $launchProtection = 'none' + if ($service.LaunchProtection) { + $launchProtection = switch ($service.LaunchProtection) { + None { 'none' } + Windows { 'windows' } + WindowsLight { 'windows_light' } + AntimalwareLight { 'antimalware_light' } + } + } + + $serviceFlags = @($service.ServiceFlags.ToString() -split ',' | ForEach-Object -Process { + switch ($_.Trim()) { + RunsInSystemProcess { 'runs_in_system_process' } + } + }) + + # The ServiceType value can contain other flags which are represented by other properties, this strips them out + # so we don't include them in the service_type return value. + $serviceType = [uint32]$service.ServiceType -band -bnot [uint32][Ansible.Service.ServiceType]::InteractiveProcess + $serviceType = $serviceType -band -bnot [uint32][Ansible.Service.ServiceType]::UserServiceInstance + $serviceType = switch (([Ansible.Service.ServiceType]$serviceType).ToString()) { + KernelDriver { 'kernel_driver' } + FileSystemDriver { 'file_system_driver' } + Adapter { 'adapter' } + RecognizerDriver { 'recognizer_driver' } + Win32OwnProcess { 'win32_own_process' } + Win32ShareProcess { 'win32_share_process' } + UserOwnprocess { 'user_own_process' } + UserShareProcess { 'user_share_process' } + PkgService { 'pkg_service' } + } + + $startType = switch ($service.StartType) { + BootStart { 'boot_start' } + SystemStart { 'system_start' } + AutoStart { 'auto' } + DemandStart { 'manual' } + Disabled { 'disabled' } + AutoStartDelayed { 'delayed' } + } + + $state = switch ($service.State) { + Stopped { 'stopped' } + StartPending { 'start_pending' } + StopPending { 'stop_pending' } + Running { 'started' } + ContinuePending { 'continue_pending' } + PausePending { 'pause_pending' } + paused { 'paused' } + } + + $triggers = @(foreach ($trigger in $service.Triggers) { + [Ordered]@{ + action = switch($trigger.Action) { + ServiceStart { 'start_service' } + ServiceStop { 'stop_service' } + } + type = switch($trigger.Type) { + DeviceInterfaceArrival { 'device_interface_arrival' } + IpAddressAvailability { 'ip_address_availability' } + DomainJoin { 'domain_join' } + FirewallPortEvent { 'firewall_port_event' } + GroupPolicy { 'group_policy' } + NetworkEndpoint { 'network_endpoint' } + Custom { 'custom' } + } + sub_type = switch($trigger.SubType.ToString()) { + ([Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID) { 'named_pipe_event' } + ([Ansible.Service.Trigger]::RPC_INTERFACE_EVENT_GUID) { 'rpc_interface_event' } + ([Ansible.Service.Trigger]::DOMAIN_JOIN_GUID) { 'domain_join' } + ([Ansible.Service.Trigger]::DOMAIN_LEAVE_GUID) { 'domain_leave' } + ([Ansible.Service.Trigger]::FIREWALL_PORT_OPEN_GUID) { 'firewall_port_open' } + ([Ansible.Service.Trigger]::FIREWALL_PORT_CLOSE_GUID) { 'firewall_port_close' } + ([Ansible.Service.Trigger]::MACHINE_POLICY_PRESENT_GUID) { 'machine_policy_present' } + ([Ansible.Service.Trigger]::USER_POLICY_PRESENT_GUID) { 'user_policy_present' } + ([Ansible.Service.Trigger]::NETWORK_MANAGER_FIRST_IP_ADDRESS_ARRIVAL_GUID) { 'network_first_ip_arrival' } + ([Ansible.Service.Trigger]::NETWORK_MANAGER_LAST_IP_ADDRESS_REMOVAL_GUID) { 'network_last_ip_removal' } + default { 'custom' } + } + sub_type_guid = $trigger.SubType.ToString() + data_items = @(foreach ($dataItem in $trigger.DataItems) { + $dataValue = $dataItem.Data + + # We only need to convert byte and byte[] to a Base64 string, the rest can be serialised as is. + if ($dataValue -is [byte]) { + $dataValue = [byte[]]@($dataValue) + } + + if ($dataValue -is [byte[]]) { + $dataValue = [System.Convert]::ToBase64String($dataValue) + } + + [Ordered]@{ + type = switch ($dataItem.Type) { + Binary { 'binary' } + String { 'string' } + Level { 'level' } + KeywordAny { 'keyword_any' } + KeywordAll { 'keyword_all' } + } + data = $dataValue + } + }) + } + }) + + # These should closely reflect the options for win_service + [Ordered]@{ + checkpoint = $service.Checkpoint + controls_accepted = $controlsAccepted + dependencies = $service.DependentOn + dependency_of = $service.DependedBy + description = $service.Description + desktop_interact = $service.ServiceType.HasFlag([Ansible.Service.ServiceType]::InteractiveProcess) + display_name = $service.DisplayName + error_control = $service.ErrorControl.ToString().ToLowerInvariant() + failure_actions = $failureActions + failure_actions_on_non_crash_failure = $service.FailureActionsOnNonCrashFailures + failure_command = $rawFailureActions.Command + failure_reboot_msg = $rawFailureActions.RebootMsg + failure_reset_period_sec = $rawFailureActions.ResetPeriod + launch_protection = $launchProtection + load_order_group = $service.LoadOrderGroup + name = $service.ServiceName + path = $service.Path + pre_shutdown_timeout_ms = $service.PreShutdownTimeout + preferred_node = $service.PreferredNode + process_id = $service.ProcessId + required_privileges = $service.RequiredPrivileges + service_exit_code = $service.ServiceExitCode + service_flags = $serviceFlags + service_type = $serviceType + sid_info = $service.ServiceSidInfo.ToString().ToLowerInvariant() + start_mode = $startType + state = $state + triggers = $triggers + username = $service.Account.Value + wait_hint_ms = $service.WaitHint + win32_exit_code = $service.Win32ExitCode + } +}) + +$module.ExitJson() diff --git a/lib/ansible/modules/windows/win_service_info.py b/lib/ansible/modules/windows/win_service_info.py new file mode 100644 index 00000000000..a3fbf49d879 --- /dev/null +++ b/lib/ansible/modules/windows/win_service_info.py @@ -0,0 +1,294 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: win_service_info +version_added: '2.10' +short_description: Gather information about Windows services +description: +- Gather information about all or a specific installed Windows service(s). +options: + name: + description: + - If specified, this is used to match the C(name) or C(display_name) of the Windows service to get the info for. + - Can be a wildcard to match multiple services but the wildcard will only be matched on the C(name) of the service + and not C(display_name). + - If omitted then all services will returned. + type: str +seealso: +- module: win_service +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Get info for all installed services + win_service_info: + register: service_info + +- name: Get info for a single service + win_service_info: + name: WinRM + register: service_info + +- name: Get info for a service using its display name + win_service_info: + name: Windows Remote Management (WS-Management) + +- name: Find all services that start with 'win' + win_service_info: + name: win* +''' + +RETURN = r''' +exists: + description: Whether any services were found based on the criteria specified. + returned: always + type: bool + sample: true +services: + description: + - A list of service(s) that were found based on the criteria. + - Will be an empty list if no services were found. + returned: always + type: list + elements: dict + contains: + checkpoint: + description: + - A check-point value that the service increments periodically to report its progress. + type: int + sample: 0 + controls_accepted: + description: + - A list of controls that the service can accept. + - Common controls are C(stop), C(pause_continue), C(shutdown). + type: list + elements: str + sample: ['stop', 'shutdown'] + dependencies: + description: + - A list of services by their C(name) that this service is dependent on. + type: list + elements: str + sample: ['HTTP', 'RPCSS'] + dependency_of: + description: + - A list of services by their C(name) that depend on this service. + type: list + elements: str + sample: ['upnphost', 'WMPNetworkSvc'] + description: + description: + - The description of the service. + type: str + sample: Example description of the Windows service. + desktop_interact: + description: + - Whether the service can interact with the desktop, only valid for services running as C(SYSTEM). + type: bool + sample: false + display_name: + description: + - The display name to be used by SCM to identify the service. + type: str + sample: Windows Remote Management (WS-Management) + error_control: + description: + - The action to take if a service fails to start. + - Common values are C(critical), C(ignore), C(normal), C(severe). + type: str + sample: normal + failure_actions: + description: + - A list of failure actions to run in the event of a failure. + type: list + elements: dict + contains: + delay_ms: + description: + - The time to wait, in milliseconds, before performing the specified action. + type: int + sample: 120000 + type: + description: + - The action that will be performed. + - Common values are C(none), C(reboot), C(restart), C(run_command). + type: str + sample: run_command + failure_action_on_non_crash_failure: + description: + - Controls when failure actions are fired based on how the service was stopped. + type: bool + sample: false + failure_command: + description: + - The command line that will be run when a C(run_command) failure action is fired. + type: str + sample: runme.exe + failure_reboot_msg: + description: + - The message to be broadcast to server users before rebooting when a C(reboot) failure action is fired. + type: str + sample: Service failed, rebooting host. + failure_reset_period_sec: + description: + - The time, in seconds, after which to reset the failure count to zero. + type: int + sample: 86400 + launch_protection: + description: + - The protection type of the service. + - Common values are C(none), C(windows), C(windows_light), or C(antimalware_light). + type: str + sample: none + load_order_group: + description: + - The name of the load ordering group to which the service belongs. + - Will be an empty string if it does not belong to any group. + type: str + sample: My group + name: + description: + - The name of the service. + type: str + sample: WinRM + path: + description: + - The path to the service binary and any arguments used when starting the service. + - The binary part can be quoted to ensure any spaces in path are not treated as arguments. + type: str + sample: 'C:\Windows\System32\svchost.exe -k netsvcs -p' + pre_shutdown_timeout_ms: + description: + - The preshutdown timeout out value in milliseconds. + type: int + sample: 10000 + preferred_node: + description: + - The node number for the preferred node. + - This will be C(null) if the Windows host has no NUMA configuration. + type: int + sample: 0 + process_id: + description: + - The process identifier of the running service. + type: int + sample: 5135 + required_privileges: + description: + - A list of privileges that the service requires and will run with + type: list + elements: str + sample: ['SeBackupPrivilege', 'SeRestorePrivilege'] + service_exit_code: + description: + - A service-specific error code that is set while the service is starting or stopping. + type: int + sample: 0 + service_flags: + description: + - Shows more information about the behaviour of a running service. + - Currently the only flag that can be set is C(runs_in_system_process). + type: list + elements: str + sample: [ 'runs_in_system_process' ] + service_type: + description: + - The type of service. + - Common types are C(win32_own_process), C(win32_share_process), C(user_own_process), C(user_share_process), + C(kernel_driver). + type: str + sample: win32_own_process + sid_info: + description: + - The behavior of how the service's access token is generated and how to add the service SID to the token. + - Common values are C(none), C(restricted), or C(unrestricted). + type: str + sample: none + start_mode: + description: + - When the service is set to start. + - Common values are C(auto), C(manual), C(disabled), C(delayed). + type: str + sample: auto + state: + description: + - The current running state of the service. + - Common values are C(stopped), C(start_pending), C(stop_pending), C(started), C(continue_pending), + C(pause_pending), C(paused). + type: str + sample: started + triggers: + description: + - A list of triggers defined for the service. + type: list + elements: dict + contains: + action: + description: + - The action to perform once triggered, can be C(start_service) or C(stop_service). + type: str + sample: start_service + data_items: + description: + - A list of trigger data items that contain trigger specific data. + - A trigger can contain 0 or multiple data items. + type: list + elements: dict + contains: + data: + description: + - The trigger data item value. + - Can be a string, list of string, int, or base64 string of binary data. + type: complex + sample: named pipe + type: + description: + - The type of C(data) for the trigger. + - Common values are C(string), C(binary), C(level), C(keyword_any), or C(keyword_all). + type: str + sample: string + sub_type: + description: + - The trigger event sub type that is specific to each C(type). + - Common values are C(named_pipe_event), C(domain_join), C(domain_leave), C(firewall_port_open), and others. + type: str + sample: + sub_type_guid: + description: + - The guid which represents the trigger sub type. + type: str + sample: 1ce20aba-9851-4421-9430-1ddeb766e809 + type: + description: + - The trigger event type. + - Common values are C(custom), C(rpc_interface_event), C(domain_join), C(group_policy), and others. + type: str + sample: domain_join + username: + description: + - The username used to run the service. + - Can be null for user services and certain driver services. + type: str + sample: NT AUTHORITY\SYSTEM + wait_hint_ms: + description: + - The estimated time in milliseconds required for a pending start, stop, pause,or continue operations. + type: int + sample: 0 + win32_exitcode: + description: + - The error code returned from the service binary once it has stopped. + - When set to C(1066) then a service specific error is returned on C(service_exit_code). + type: int + sample: 0 +''' diff --git a/test/integration/targets/win_csharp_utils/library/ansible_service_tests.ps1 b/test/integration/targets/win_csharp_utils/library/ansible_service_tests.ps1 new file mode 100644 index 00000000000..6c8f729b2dd --- /dev/null +++ b/test/integration/targets/win_csharp_utils/library/ansible_service_tests.ps1 @@ -0,0 +1,937 @@ +#!powershell + +#AnsibleRequires -CSharpUtil Ansible.Basic +#AnsibleRequires -CSharpUtil Ansible.Service +#Requires -Module Ansible.ModuleUtils.ArgvParser +#Requires -Module Ansible.ModuleUtils.CommandUtil + +$module = [Ansible.Basic.AnsibleModule]::Create($args, @{}) + +$path = "$env:SystemRoot\System32\svchost.exe" + +Function Assert-Equals { + param( + [Parameter(Mandatory=$true, ValueFromPipeline=$true)][AllowNull()]$Actual, + [Parameter(Mandatory=$true, Position=0)][AllowNull()]$Expected + ) + + $matched = $false + if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array] -or $Actual -is [System.Collections.IList]) { + $Actual.Count | Assert-Equals -Expected $Expected.Count + for ($i = 0; $i -lt $Actual.Count; $i++) { + $actualValue = $Actual[$i] + $expectedValue = $Expected[$i] + Assert-Equals -Actual $actualValue -Expected $expectedValue + } + $matched = $true + } else { + $matched = $Actual -ceq $Expected + } + + if (-not $matched) { + if ($Actual -is [PSObject]) { + $Actual = $Actual.ToString() + } + + $call_stack = (Get-PSCallStack)[1] + $module.Result.test = $test + $module.Result.actual = $Actual + $module.Result.expected = $Expected + $module.Result.line = $call_stack.ScriptLineNumber + $module.Result.method = $call_stack.Position.Text + + $module.FailJson("AssertionError: actual != expected") + } +} + +Function Invoke-Sc { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [String] + $Action, + + [Parameter(Mandatory=$true)] + [String] + $Name, + + [Object] + $Arguments + ) + + $commandArgs = [System.Collections.Generic.List[String]]@("sc.exe", $Action, $Name) + if ($null -ne $Arguments) { + if ($Arguments -is [System.Collections.IDictionary]) { + foreach ($arg in $Arguments.GetEnumerator()) { + $commandArgs.Add("$($arg.Key)=") + $commandArgs.Add($arg.Value) + } + } else { + foreach ($arg in $Arguments) { + $commandArgs.Add($arg) + } + } + } + + $command = Argv-ToString -arguments $commandArgs + + $res = Run-Command -command $command + if ($res.rc -ne 0) { + $module.Result.rc = $res.rc + $module.Result.stdout = $res.stdout + $module.Result.stderr = $res.stderr + $module.FailJson("Failed to invoke sc with: $command") + } + + $info = @{ Name = $Name } + + if ($Action -eq 'qtriggerinfo') { + # qtriggerinfo is in a different format which requires some manual parsing from the norm. + $info.Triggers = [System.Collections.Generic.List[PSObject]]@() + } + + $currentKey = $null + $qtriggerSection = @{} + $res.stdout -split "`r`n" | Foreach-Object -Process { + $line = $_.Trim() + + if ($Action -eq 'qtriggerinfo' -and $line -in @('START SERVICE', 'STOP SERVICE')) { + if ($qtriggerSection.Count -gt 0) { + $info.Triggers.Add([PSCustomObject]$qtriggerSection) + $qtriggerSection = @{} + } + + $qtriggerSection = @{ + Action = $line + } + } + + if (-not $line -or (-not $line.Contains(':') -and $null -eq $currentKey)) { + return + } + + $lineSplit = $line.Split(':', 2) + if ($lineSplit.Length -eq 2) { + $k = $lineSplit[0].Trim() + if (-not $k) { + $k = $currentKey + } + + $v = $lineSplit[1].Trim() + } else { + $k = $currentKey + $v = $line + } + + if ($qtriggerSection.Count -gt 0) { + if ($k -eq 'DATA') { + $qtriggerSection.Data.Add($v) + } else { + $qtriggerSection.Type = $k + $qtriggerSection.SubType = $v + $qtriggerSection.Data = [System.Collections.Generic.List[String]]@() + } + } else { + if ($info.ContainsKey($k)) { + if ($info[$k] -isnot [System.Collections.Generic.List[String]]) { + $info[$k] = [System.Collections.Generic.List[String]]@($info[$k]) + } + $info[$k].Add($v) + } else { + $currentKey = $k + $info[$k] = $v + } + } + } + + if ($qtriggerSection.Count -gt 0) { + $info.Triggers.Add([PSCustomObject]$qtriggerSection) + } + + [PSCustomObject]$info +} + +$tests = [Ordered]@{ + "Props on service created by New-Service" = { + $actual = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + + $actual.ServiceName | Assert-Equals -Expected $serviceName + $actual.ServiceType | Assert-Equals -Expected ([Ansible.Service.ServiceType]::Win32OwnProcess) + $actual.StartType | Assert-Equals -Expected ([Ansible.Service.ServiceStartType]::DemandStart) + $actual.ErrorControl | Assert-Equals -Expected ([Ansible.Service.ErrorControl]::Normal) + $actual.Path | Assert-Equals -Expected ('"{0}"' -f $path) + $actual.LoadOrderGroup | Assert-Equals -Expected "" + $actual.DependentOn.Count | Assert-Equals -Expected 0 + $actual.Account | Assert-Equals -Expected ( + [System.Security.Principal.SecurityIdentifier]'S-1-5-18').Translate([System.Security.Principal.NTAccount] + ) + $actual.DisplayName | Assert-Equals -Expected $serviceName + $actual.Description | Assert-Equals -Expected $null + $actual.FailureActions.ResetPeriod | Assert-Equals -Expected 0 + $actual.FailureActions.RebootMsg | Assert-Equals -Expected $null + $actual.FailureActions.Command | Assert-Equals -Expected $null + $actual.FailureActions.Actions.Count | Assert-Equals -Expected 0 + $actual.FailureActionsOnNonCrashFailures | Assert-Equals -Expected $false + $actual.ServiceSidInfo | Assert-Equals -Expected ([Ansible.Service.ServiceSidInfo]::None) + $actual.RequiredPrivileges.Count | Assert-Equals -Expected 0 + # Cannot test default values as it differs per OS version + $null -ne $actual.PreShutdownTimeout | Assert-Equals -Expected $true + $actual.Triggers.Count | Assert-Equals -Expected 0 + $actual.PreferredNode | Assert-Equals -Expected $null + if ([Environment]::OSVersion.Version -ge [Version]'6.3') { + $actual.LaunchProtection | Assert-Equals -Expected ([Ansible.Service.LaunchProtection]::None) + } else { + $actual.LaunchProtection | Assert-Equals -Expected $null + } + $actual.State | Assert-Equals -Expected ([Ansible.Service.ServiceStatus]::Stopped) + $actual.Win32ExitCode | Assert-Equals -Expected 1077 # ERROR_SERVICE_NEVER_STARTED + $actual.ServiceExitCode | Assert-Equals -Expected 0 + $actual.Checkpoint | Assert-Equals -Expected 0 + $actual.WaitHint | Assert-Equals -Expected 0 + $actual.ProcessId | Assert-Equals -Expected 0 + $actual.ServiceFlags | Assert-Equals -Expected ([Ansible.Service.ServiceFlags]::None) + $actual.DependedBy.Count | Assert-Equals 0 + } + + "Service creation through util" = { + $testName = "$($serviceName)_2" + $actual = [Ansible.Service.Service]::Create($testName, '"{0}"' -f $path) + + try { + $cmdletService = Get-Service -Name $testName -ErrorAction SilentlyContinue + $null -ne $cmdletService | Assert-Equals -Expected $true + + $actual.ServiceName | Assert-Equals -Expected $testName + $actual.ServiceType | Assert-Equals -Expected ([Ansible.Service.ServiceType]::Win32OwnProcess) + $actual.StartType | Assert-Equals -Expected ([Ansible.Service.ServiceStartType]::DemandStart) + $actual.ErrorControl | Assert-Equals -Expected ([Ansible.Service.ErrorControl]::Normal) + $actual.Path | Assert-Equals -Expected ('"{0}"' -f $path) + $actual.LoadOrderGroup | Assert-Equals -Expected "" + $actual.DependentOn.Count | Assert-Equals -Expected 0 + $actual.Account | Assert-Equals -Expected ( + [System.Security.Principal.SecurityIdentifier]'S-1-5-18').Translate([System.Security.Principal.NTAccount] + ) + $actual.DisplayName | Assert-Equals -Expected $testName + $actual.Description | Assert-Equals -Expected $null + $actual.FailureActions.ResetPeriod | Assert-Equals -Expected 0 + $actual.FailureActions.RebootMsg | Assert-Equals -Expected $null + $actual.FailureActions.Command | Assert-Equals -Expected $null + $actual.FailureActions.Actions.Count | Assert-Equals -Expected 0 + $actual.FailureActionsOnNonCrashFailures | Assert-Equals -Expected $false + $actual.ServiceSidInfo | Assert-Equals -Expected ([Ansible.Service.ServiceSidInfo]::None) + $actual.RequiredPrivileges.Count | Assert-Equals -Expected 0 + $null -ne $actual.PreShutdownTimeout | Assert-Equals -Expected $true + $actual.Triggers.Count | Assert-Equals -Expected 0 + $actual.PreferredNode | Assert-Equals -Expected $null + if ([Environment]::OSVersion.Version -ge [Version]'6.3') { + $actual.LaunchProtection | Assert-Equals -Expected ([Ansible.Service.LaunchProtection]::None) + } else { + $actual.LaunchProtection | Assert-Equals -Expected $null + } + $actual.State | Assert-Equals -Expected ([Ansible.Service.ServiceStatus]::Stopped) + $actual.Win32ExitCode | Assert-Equals -Expected 1077 # ERROR_SERVICE_NEVER_STARTED + $actual.ServiceExitCode | Assert-Equals -Expected 0 + $actual.Checkpoint | Assert-Equals -Expected 0 + $actual.WaitHint | Assert-Equals -Expected 0 + $actual.ProcessId | Assert-Equals -Expected 0 + $actual.ServiceFlags | Assert-Equals -Expected ([Ansible.Service.ServiceFlags]::None) + $actual.DependedBy.Count | Assert-Equals 0 + } finally { + $actual.Delete() + } + } + + "Fail to open non-existing service" = { + $failed = $false + try { + $null = New-Object -TypeName Ansible.Service.Service -ArgumentList 'fake_service' + } catch [Ansible.Service.ServiceManagerException] { + # 1060 == ERROR_SERVICE_DOES_NOT_EXIST + $_.Exception.Message -like '*Win32ErrorCode 1060 - 0x00000424*' | Assert-Equals -Expected $true + $failed = $true + } + + $failed | Assert-Equals -Expected $true + } + + "Open with specific access rights" = { + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList @( + $serviceName, [Ansible.Service.ServiceRights]'QueryConfig, QueryStatus' + ) + + # QueryStatus can get the status + $service.State | Assert-Equals -Expected ([Ansible.Service.ServiceStatus]::Stopped) + + # Should fail to get the config because we did not request that right + $failed = $false + try { + $service.Path = 'fail' + } catch [Ansible.Service.ServiceManagerException] { + # 5 == ERROR_ACCESS_DENIED + $_.Exception.Message -like '*Win32ErrorCode 5 - 0x00000005*' | Assert-Equals -Expected $true + $failed = $true + } + + $failed | Assert-Equals -Expected $true + + } + + "Modfiy ServiceType" = { + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.ServiceType = [Ansible.Service.ServiceType]::Win32ShareProcess + + $actual = Invoke-Sc -Action qc -Name $serviceName + $service.ServiceType | Assert-Equals -Expected ([Ansible.Service.ServiceType]::Win32ShareProcess) + $actual.TYPE | Assert-Equals -Expected "20 WIN32_SHARE_PROCESS" + + $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{type="own"} + $service.Refresh() + $service.ServiceType | Assert-Equals -Expected ([Ansible.Service.ServiceType]::Win32OwnProcess) + } + + "Create desktop interactive service" = { + $service = New-Object -Typename Ansible.Service.Service -ArgumentList $serviceName + $service.ServiceType = [Ansible.Service.ServiceType]'Win32OwnProcess, InteractiveProcess' + + $actual = Invoke-Sc -Action qc -Name $serviceName + $actual.TYPE | Assert-Equals -Expected "110 WIN32_OWN_PROCESS (interactive)" + $service.ServiceType | Assert-Equals -Expected ([Ansible.Service.ServiceType]'Win32OwnProcess, InteractiveProcess') + + # Change back from interactive process + $service.ServiceType = [Ansible.Service.ServiceType]::Win32OwnProcess + + $actual = Invoke-Sc -Action qc -Name $serviceName + $actual.TYPE | Assert-Equals -Expected "10 WIN32_OWN_PROCESS" + $service.ServiceType | Assert-Equals -Expected ([Ansible.Service.ServiceType]::Win32OwnProcess) + + $service.Account = [System.Security.Principal.SecurityIdentifier]'S-1-5-20' + + $failed = $false + try { + $service.ServiceType = [Ansible.Service.ServiceType]'Win32OwnProcess, InteractiveProcess' + } catch [Ansible.Service.ServiceManagerException] { + $failed = $true + $_.Exception.NativeErrorCode | Assert-Equals -Expected 87 # ERROR_INVALID_PARAMETER + } + $failed | Assert-Equals -Expected $true + + $actual = Invoke-Sc -Action qc -Name $serviceName + $actual.TYPE | Assert-Equals -Expected "10 WIN32_OWN_PROCESS" + } + + "Modify StartType" = { + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.StartType = [Ansible.Service.ServiceStartType]::Disabled + + $actual = Invoke-Sc -Action qc -Name $serviceName + $service.StartType | Assert-Equals -Expected ([Ansible.Service.ServiceStartType]::Disabled) + $actual.START_TYPE | Assert-Equals -Expected "4 DISABLED" + + $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{start="demand"} + $service.Refresh() + $service.StartType | Assert-Equals -Expected ([Ansible.Service.ServiceStartType]::DemandStart) + } + + "Modify StartType auto delayed" = { + # Delayed start type is a modifier of the AutoStart type. It uses a separate config entry to define and this + # makes sure the util does that correctly from various types and back. + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.StartType = [Ansible.Service.ServiceStartType]::Disabled # Start from Disabled + + # Disabled -> Auto Start Delayed + $service.StartType = [Ansible.Service.ServiceStartType]::AutoStartDelayed + + $actual = Invoke-Sc -Action qc -Name $serviceName + $service.StartType | Assert-Equals -Expected ([Ansible.Service.ServiceStartType]::AutoStartDelayed) + $actual.START_TYPE | Assert-Equals -Expected "2 AUTO_START (DELAYED)" + + # Auto Start Delayed -> Auto Start + $service.StartType = [Ansible.Service.ServiceStartType]::AutoStart + + $actual = Invoke-Sc -Action qc -Name $serviceName + $service.StartType | Assert-Equals -Expected ([Ansible.Service.ServiceStartType]::AutoStart) + $actual.START_TYPE | Assert-Equals -Expected "2 AUTO_START" + + # Auto Start -> Auto Start Delayed + $service.StartType = [Ansible.Service.ServiceStartType]::AutoStartDelayed + + $actual = Invoke-Sc -Action qc -Name $serviceName + $service.StartType | Assert-Equals -Expected ([Ansible.Service.ServiceStartType]::AutoStartDelayed) + $actual.START_TYPE | Assert-Equals -Expected "2 AUTO_START (DELAYED)" + + # Auto Start Delayed -> Manual + $service.StartType = [Ansible.Service.ServiceStartType]::DemandStart + + $actual = Invoke-Sc -Action qc -Name $serviceName + $service.StartType | Assert-Equals -Expected ([Ansible.Service.ServiceStartType]::DemandStart) + $actual.START_TYPE | Assert-Equals -Expected "3 DEMAND_START" + } + + "Modify ErrorControl" = { + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.ErrorControl = [Ansible.Service.ErrorControl]::Severe + + $actual = Invoke-Sc -Action qc -Name $serviceName + $service.ErrorControl | Assert-Equals -Expected ([Ansible.Service.ErrorControl]::Severe) + $actual.ERROR_CONTROL | Assert-Equals -Expected "2 SEVERE" + + $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{error="ignore"} + $service.Refresh() + $service.ErrorControl | Assert-Equals -Expected ([Ansible.Service.ErrorControl]::Ignore) + } + + "Modify Path" = { + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.Path = "Fake path" + + $actual = Invoke-Sc -Action qc -Name $serviceName + $service.Path | Assert-Equals -Expected "Fake path" + $actual.BINARY_PATH_NAME | Assert-Equals -Expected "Fake path" + + $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{binpath="other fake path"} + $service.Refresh() + $service.Path | Assert-Equals -Expected "other fake path" + } + + "Modify LoadOrderGroup" = { + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.LoadOrderGroup = "my group" + + $actual = Invoke-Sc -Action qc -Name $serviceName + $service.LoadOrderGroup | Assert-Equals -Expected "my group" + $actual.LOAD_ORDER_GROUP | Assert-Equals -Expected "my group" + + $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{group=""} + $service.Refresh() + $service.LoadOrderGroup | Assert-Equals -Expected "" + } + + "Modify DependentOn" = { + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.DependentOn = @("HTTP", "WinRM") + + $actual = Invoke-Sc -Action qc -Name $serviceName + @(,$service.DependentOn) | Assert-Equals -Expected @("HTTP", "WinRM") + @(,$actual.DEPENDENCIES) | Assert-Equals -Expected @("HTTP", "WinRM") + + $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{depend=""} + $service.Refresh() + $service.DependentOn.Count | Assert-Equals -Expected 0 + } + + "Modify Account - service account" = { + $systemSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-18' + $systemName =$systemSid.Translate([System.Security.Principal.NTAccount]) + $localSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-19' + $localName = $localSid.Translate([System.Security.Principal.NTAccount]) + $networkSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-20' + $networkName = $networkSid.Translate([System.Security.Principal.NTAccount]) + + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.Account = $networkSid + + $actual = Invoke-Sc -Action qc -Name $serviceName + $service.Account | Assert-Equals -Expected $networkName + $actual.SERVICE_START_NAME | Assert-Equals -Expected $networkName.Value + + $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{obj=$localName.Value} + $service.Refresh() + $service.Account | Assert-Equals -Expected $localName + + $service.Account = $systemSid + $actual = Invoke-Sc -Action qc -Name $serviceName + $service.Account | Assert-Equals -Expected $systemName + $actual.SERVICE_START_NAME | Assert-Equals -Expected "LocalSystem" + } + + "Modify Account - user" = { + $currentSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User + + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.Account = $currentSid + $service.Password = 'password' + + $actual = Invoke-Sc -Action qc -Name $serviceName + + # When running tests in CI this seems to become .\Administrator + if ($service.Account.Value.StartsWith('.\')) { + $username = $service.Account.Value.Substring(2, $service.Account.Value.Length - 2) + $actualSid = ([System.Security.Principal.NTAccount]"$env:COMPUTERNAME\$username").Translate( + [System.Security.Principal.SecurityIdentifier] + ) + } else { + $actualSid = $service.Account.Translate([System.Security.Principal.SecurityIdentifier]) + } + $actualSid.Value | Assert-Equals -Expected $currentSid.Value + $actual.SERVICE_START_NAME | Assert-Equals -Expected $service.Account.Value + + # Go back to SYSTEM from account + $systemSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-18' + $service.Account = $systemSid + + $actual = Invoke-Sc -Action qc -Name $serviceName + $service.Account | Assert-Equals -Expected $systemSid.Translate([System.Security.Principal.NTAccount]) + $actual.SERVICE_START_NAME | Assert-Equals -Expected "LocalSystem" + } + + "Modify Account - virtual account" = { + $account = [System.Security.Principal.NTAccount]"NT SERVICE\$serviceName" + + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.Account = $account + + $actual = Invoke-Sc -Action qc -Name $serviceName + $service.Account | Assert-Equals -Expected $account + $actual.SERVICE_START_NAME | Assert-Equals -Expected $account.Value + } + + "Modify Account - gMSA" = { + # This cannot be tested through CI, only done on manual tests. + return + + $gmsaName = [System.Security.Principal.NTAccount]'gMSA$@DOMAIN.LOCAL' # Make sure this is UPN. + $gmsaSid = $gmsaName.Translate([System.Security.Principal.SecurityIdentifier]) + $gmsaNetlogon = $gmsaSid.Translate([System.Security.Principal.NTAccount]) + + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.Account = $gmsaName + + $actual = Invoke-Sc -Action qc -Name $serviceName + $service.Account | Assert-Equals -Expected $gmsaName + $actual.SERVICE_START_NAME | Assert-Equals -Expected $gmsaName + + # Go from gMSA to account and back to verify the Password doesn't matter. + $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().User + $service.Account = $currentUser + $service.Password = 'fake password' + $service.Password = 'fake password2' + + # Now test in the Netlogon format. + $service.Account = $gmsaSid + + $actual = Invoke-Sc -Action qc -Name $serviceName + $service.Account | Assert-Equals -Expected $gmsaNetlogon + $actual.SERVICE_START_NAME | Assert-Equals -Expected $gmsaNetlogon.Value + } + + "Modify DisplayName" = { + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.DisplayName = "Custom Service Name" + + $actual = Invoke-Sc -Action qc -Name $serviceName + $service.DisplayName | Assert-Equals -Expected "Custom Service Name" + $actual.DISPLAY_NAME | Assert-Equals -Expected "Custom Service Name" + + $null = Invoke-Sc -Action config -Name $serviceName -Arguments @{displayname="New Service Name"} + $service.Refresh() + $service.DisplayName | Assert-Equals -Expected "New Service Name" + } + + "Modify Description" = { + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.Description = "My custom service description" + + $actual = Invoke-Sc -Action qdescription -Name $serviceName + $service.Description | Assert-Equals -Expected "My custom service description" + $actual.DESCRIPTION | Assert-Equals -Expected "My custom service description" + + $null = Invoke-Sc -Action description -Name $serviceName -Arguments @(,"new description") + $service.Description | Assert-Equals -Expected "new description" + + $service.Description = $null + + $actual = Invoke-Sc -Action qdescription -Name $serviceName + $service.Description | Assert-Equals -Expected $null + $actual.DESCRIPTION | Assert-Equals -Expected "" + } + + "Modify FailureActions" = { + $newAction = [Ansible.Service.FailureActions]@{ + ResetPeriod = 86400 + RebootMsg = 'Reboot msg' + Command = 'Command line' + Actions = @( + [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::RunCommand; Delay = 1000}, + [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::RunCommand; Delay = 2000}, + [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::Restart; Delay = 1000}, + [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::Reboot; Delay = 1000} + ) + } + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.FailureActions = $newAction + + $actual = Invoke-Sc -Action qfailure -Name $serviceName + $actual.'RESET_PERIOD (in seconds)' | Assert-Equals -Expected 86400 + $actual.REBOOT_MESSAGE | Assert-Equals -Expected 'Reboot msg' + $actual.COMMAND_LINE | Assert-Equals -Expected 'Command line' + $actual.FAILURE_ACTIONS.Count | Assert-Equals -Expected 4 + $actual.FAILURE_ACTIONS[0] | Assert-Equals -Expected "RUN PROCESS -- Delay = 1000 milliseconds." + $actual.FAILURE_ACTIONS[1] | Assert-Equals -Expected "RUN PROCESS -- Delay = 2000 milliseconds." + $actual.FAILURE_ACTIONS[2] | Assert-Equals -Expected "RESTART -- Delay = 1000 milliseconds." + $actual.FAILURE_ACTIONS[3] | Assert-Equals -Expected "REBOOT -- Delay = 1000 milliseconds." + $service.FailureActions.Actions.Count | Assert-Equals -Expected 4 + + # Test that we can change individual settings and it doesn't change all + $service.FailureActions = [Ansible.Service.FailureActions]@{ResetPeriod = 172800} + + $actual = Invoke-Sc -Action qfailure -Name $serviceName + $actual.'RESET_PERIOD (in seconds)' | Assert-Equals -Expected 172800 + $actual.REBOOT_MESSAGE | Assert-Equals -Expected 'Reboot msg' + $actual.COMMAND_LINE | Assert-Equals -Expected 'Command line' + $actual.FAILURE_ACTIONS.Count | Assert-Equals -Expected 4 + $service.FailureActions.Actions.Count | Assert-Equals -Expected 4 + + $service.FailureActions = [Ansible.Service.FailureActions]@{RebootMsg = "New reboot msg"} + + $actual = Invoke-Sc -Action qfailure -Name $serviceName + $actual.'RESET_PERIOD (in seconds)' | Assert-Equals -Expected 172800 + $actual.REBOOT_MESSAGE | Assert-Equals -Expected 'New reboot msg' + $actual.COMMAND_LINE | Assert-Equals -Expected 'Command line' + $actual.FAILURE_ACTIONS.Count | Assert-Equals -Expected 4 + $service.FailureActions.Actions.Count | Assert-Equals -Expected 4 + + $service.FailureActions = [Ansible.Service.FailureActions]@{Command = "New command line"} + + $actual = Invoke-Sc -Action qfailure -Name $serviceName + $actual.'RESET_PERIOD (in seconds)' | Assert-Equals -Expected 172800 + $actual.REBOOT_MESSAGE | Assert-Equals -Expected 'New reboot msg' + $actual.COMMAND_LINE | Assert-Equals -Expected 'New command line' + $actual.FAILURE_ACTIONS.Count | Assert-Equals -Expected 4 + $service.FailureActions.Actions.Count | Assert-Equals -Expected 4 + + # Test setting both ResetPeriod and Actions together + $service.FailureActions = [Ansible.Service.FailureActions]@{ + ResetPeriod = 86400 + Actions = @( + [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::RunCommand; Delay = 5000}, + [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::None; Delay = 0} + ) + } + + $actual = Invoke-Sc -Action qfailure -Name $serviceName + $actual.'RESET_PERIOD (in seconds)' | Assert-Equals -Expected 86400 + $actual.REBOOT_MESSAGE | Assert-Equals -Expected 'New reboot msg' + $actual.COMMAND_LINE | Assert-Equals -Expected 'New command line' + # sc.exe does not show the None action it just ends the list, so we verify from get_FailureActions + $actual.FAILURE_ACTIONS | Assert-Equals -Expected "RUN PROCESS -- Delay = 5000 milliseconds." + $service.FailureActions.Actions.Count | Assert-Equals -Expected 2 + $service.FailureActions.Actions[1].Type | Assert-Equals -Expected ([Ansible.Service.FailureAction]::None) + + # Test setting just Actions without ResetPeriod + $service.FailureActions = [Ansible.Service.FailureActions]@{ + Actions = [Ansible.Service.Action]@{Type = [Ansible.Service.FailureAction]::RunCommand; Delay = 10000} + } + $actual = Invoke-Sc -Action qfailure -Name $serviceName + $actual.'RESET_PERIOD (in seconds)' | Assert-Equals -Expected 86400 + $actual.REBOOT_MESSAGE | Assert-Equals -Expected 'New reboot msg' + $actual.COMMAND_LINE | Assert-Equals -Expected 'New command line' + $actual.FAILURE_ACTIONS | Assert-Equals -Expected "RUN PROCESS -- Delay = 10000 milliseconds." + $service.FailureActions.Actions.Count | Assert-Equals -Expected 1 + + # Test removing all actions + $service.FailureActions = [Ansible.Service.FailureActions]@{ + Actions = @() + } + $actual = Invoke-Sc -Action qfailure -Name $serviceName + $actual.'RESET_PERIOD (in seconds)' | Assert-Equals -Expected 0 # ChangeServiceConfig2W resets this back to 0. + $actual.REBOOT_MESSAGE | Assert-Equals -Expected 'New reboot msg' + $actual.COMMAND_LINE | Assert-Equals -Expected 'New command line' + $actual.PSObject.Properties.Name.Contains('FAILURE_ACTIONS') | Assert-Equals -Expected $false + $service.FailureActions.Actions.Count | Assert-Equals -Expected 0 + + # Test that we are reading the right values + $null = Invoke-Sc -Action failure -Name $serviceName -Arguments @{ + reset = 172800 + reboot = "sc reboot msg" + command = "sc command line" + actions = "run/5000/reboot/800" + } + + $actual = $service.FailureActions + $actual.ResetPeriod | Assert-Equals -Expected 172800 + $actual.RebootMsg | Assert-Equals -Expected "sc reboot msg" + $actual.Command | Assert-Equals -Expected "sc command line" + $actual.Actions.Count | Assert-Equals -Expected 2 + $actual.Actions[0].Type | Assert-Equals -Expected ([Ansible.Service.FailureAction]::RunCommand) + $actual.Actions[0].Delay | Assert-Equals -Expected 5000 + $actual.Actions[1].Type | Assert-Equals -Expected ([Ansible.Service.FailureAction]::Reboot) + $actual.Actions[1].Delay | Assert-Equals -Expected 800 + } + + "Modify FailureActionsOnNonCrashFailures" = { + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.FailureActionsOnNonCrashFailures = $true + + $actual = Invoke-Sc -Action qfailureflag -Name $serviceName + $service.FailureActionsOnNonCrashFailures | Assert-Equals -Expected $true + $actual.FAILURE_ACTIONS_ON_NONCRASH_FAILURES | Assert-Equals -Expected "TRUE" + + $null = Invoke-Sc -Action failureflag -Name $serviceName -Arguments @(,0) + $service.FailureActionsOnNonCrashFailures | Assert-Equals -Expected $false + } + + "Modify ServiceSidInfo" = { + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.ServiceSidInfo = [Ansible.Service.ServiceSidInfo]::None + + $actual = Invoke-Sc -Action qsidtype -Name $serviceName + $service.ServiceSidInfo | Assert-Equals -Expected ([Ansible.Service.ServiceSidInfo]::None) + $actual.SERVICE_SID_TYPE | Assert-Equals -Expected 'NONE' + + $null = Invoke-Sc -Action sidtype -Name $serviceName -Arguments @(,'unrestricted') + $service.ServiceSidInfo | Assert-Equals -Expected ([Ansible.Service.ServiceSidInfo]::Unrestricted) + + $service.ServiceSidInfo = [Ansible.Service.ServiceSidInfo]::Restricted + + $actual = Invoke-Sc -Action qsidtype -Name $serviceName + $service.ServiceSidInfo | Assert-Equals -Expected ([Ansible.Service.ServiceSidInfo]::Restricted) + $actual.SERVICE_SID_TYPE | Assert-Equals -Expected 'RESTRICTED' + } + + "Modify RequiredPrivileges" = { + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.RequiredPrivileges = @("SeBackupPrivilege", "SeTcbPrivilege") + + $actual = Invoke-Sc -Action qprivs -Name $serviceName + ,$service.RequiredPrivileges | Assert-Equals -Expected @("SeBackupPrivilege", "SeTcbPrivilege") + ,$actual.PRIVILEGES | Assert-Equals -Expected @("SeBackupPrivilege", "SeTcbPrivilege") + + # Ensure setting to $null is the same as an empty array + $service.RequiredPrivileges = $null + + $actual = Invoke-Sc -Action qprivs -Name $serviceName + ,$service.RequiredPrivileges | Assert-Equals -Expected @() + ,$actual.PRIVILEGES | Assert-Equals -Expected @() + + $service.RequiredPrivileges = @("SeBackupPrivilege", "SeTcbPrivilege") + $service.RequiredPrivileges = @() + + $actual = Invoke-Sc -Action qprivs -Name $serviceName + ,$service.RequiredPrivileges | Assert-Equals -Expected @() + ,$actual.PRIVILEGES | Assert-Equals -Expected @() + + $null = Invoke-Sc -Action privs -Name $serviceName -Arguments @(,"SeCreateTokenPrivilege/SeRestorePrivilege") + ,$service.RequiredPrivileges | Assert-Equals -Expected @("SeCreateTokenPrivilege", "SeRestorePrivilege") + } + + "Modify PreShutdownTimeout" = { + $service = New-Object -TypeName Ansible.Service.Service -ArgumentList $serviceName + $service.PreShutdownTimeout = 60000 + + # sc.exe doesn't seem to have a query argument for this, just get it from the registry + $actual = ( + Get-ItemProperty -LiteralPath "HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName" -Name PreshutdownTimeout + ).PreshutdownTimeout + $actual | Assert-Equals -Expected 60000 + } + + "Modify Triggers" = { + $service = [Ansible.Service.Service]$serviceName + $service.Triggers = @( + [Ansible.Service.Trigger]@{ + Type = [Ansible.Service.TriggerType]::DomainJoin + Action = [Ansible.Service.TriggerAction]::ServiceStop + SubType = [Guid][Ansible.Service.Trigger]::DOMAIN_JOIN_GUID + }, + [Ansible.Service.Trigger]@{ + Type = [Ansible.Service.TriggerType]::NetworkEndpoint + Action = [Ansible.Service.TriggerAction]::ServiceStart + SubType = [Guid][Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID + DataItems = [Ansible.Service.TriggerItem]@{ + Type = [Ansible.Service.TriggerDataType]::String + Data = 'my named pipe' + } + }, + [Ansible.Service.Trigger]@{ + Type = [Ansible.Service.TriggerType]::NetworkEndpoint + Action = [Ansible.Service.TriggerAction]::ServiceStart + SubType = [Guid][Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID + DataItems = [Ansible.Service.TriggerItem]@{ + Type = [Ansible.Service.TriggerDataType]::String + Data = 'my named pipe 2' + } + }, + [Ansible.Service.Trigger]@{ + Type = [Ansible.Service.TriggerType]::Custom + Action = [Ansible.Service.TriggerAction]::ServiceStart + SubType = [Guid]'9bf04e57-05dc-4914-9ed9-84bf992db88c' + DataItems = @( + [Ansible.Service.TriggerItem]@{ + Type = [Ansible.Service.TriggerDataType]::Binary + Data = [byte[]]@(1, 2, 3, 4) + }, + [Ansible.Service.TriggerItem]@{ + Type = [Ansible.Service.TriggerDataType]::Binary + Data = [byte[]]@(5, 6, 7, 8, 9) + } + ) + } + [Ansible.Service.Trigger]@{ + Type = [Ansible.Service.TriggerType]::Custom + Action = [Ansible.Service.TriggerAction]::ServiceStart + SubType = [Guid]'9fbcfc7e-7581-4d46-913b-53bb15c80c51' + DataItems = @( + [Ansible.Service.TriggerItem]@{ + Type = [Ansible.Service.TriggerDataType]::String + Data = 'entry 1' + }, + [Ansible.Service.TriggerItem]@{ + Type = [Ansible.Service.TriggerDataType]::String + Data = 'entry 2' + } + ) + }, + [Ansible.Service.Trigger]@{ + Type = [Ansible.Service.TriggerType]::FirewallPortEvent + Action = [Ansible.Service.TriggerAction]::ServiceStop + SubType = [Guid][Ansible.Service.Trigger]::FIREWALL_PORT_CLOSE_GUID + DataItems = [Ansible.Service.TriggerItem]@{ + Type = [Ansible.Service.TriggerDataType]::String + Data = [System.Collections.Generic.List[String]]@("1234", "tcp", "imagepath", "servicename") + } + } + ) + + $actual = Invoke-Sc -Action qtriggerinfo -Name $serviceName + + $actual.Triggers.Count | Assert-Equals -Expected 6 + $actual.Triggers[0].Type | Assert-Equals -Expected 'DOMAIN JOINED STATUS' + $actual.Triggers[0].Action | Assert-Equals -Expected 'STOP SERVICE' + $actual.Triggers[0].SubType | Assert-Equals -Expected "$([Ansible.Service.Trigger]::DOMAIN_JOIN_GUID) [DOMAIN JOINED]" + $actual.Triggers[0].Data.Count | Assert-Equals -Expected 0 + + $actual.Triggers[1].Type | Assert-Equals -Expected 'NETWORK EVENT' + $actual.Triggers[1].Action | Assert-Equals -Expected 'START SERVICE' + $actual.Triggers[1].SubType | Assert-Equals -Expected "$([Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID) [NAMED PIPE EVENT]" + $actual.Triggers[1].Data.Count | Assert-Equals -Expected 1 + $actual.Triggers[1].Data[0] | Assert-Equals -Expected 'my named pipe' + + $actual.Triggers[2].Type | Assert-Equals -Expected 'NETWORK EVENT' + $actual.Triggers[2].Action | Assert-Equals -Expected 'START SERVICE' + $actual.Triggers[2].SubType | Assert-Equals -Expected "$([Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID) [NAMED PIPE EVENT]" + $actual.Triggers[2].Data.Count | Assert-Equals -Expected 1 + $actual.Triggers[2].Data[0] | Assert-Equals -Expected 'my named pipe 2' + + $actual.Triggers[3].Type | Assert-Equals -Expected 'CUSTOM' + $actual.Triggers[3].Action | Assert-Equals -Expected 'START SERVICE' + $actual.Triggers[3].SubType | Assert-Equals -Expected '9bf04e57-05dc-4914-9ed9-84bf992db88c [ETW PROVIDER UUID]' + $actual.Triggers[3].Data.Count | Assert-Equals -Expected 2 + $actual.Triggers[3].Data[0] | Assert-Equals -Expected '01 02 03 04' + $actual.Triggers[3].Data[1] | Assert-Equals -Expected '05 06 07 08 09' + + $actual.Triggers[4].Type | Assert-Equals -Expected 'CUSTOM' + $actual.Triggers[4].Action | Assert-Equals -Expected 'START SERVICE' + $actual.Triggers[4].SubType | Assert-Equals -Expected '9fbcfc7e-7581-4d46-913b-53bb15c80c51 [ETW PROVIDER UUID]' + $actual.Triggers[4].Data.Count | Assert-Equals -Expected 2 + $actual.Triggers[4].Data[0] | Assert-Equals -Expected "entry 1" + $actual.Triggers[4].Data[1] | Assert-Equals -Expected "entry 2" + + $actual.Triggers[5].Type | Assert-Equals -Expected 'FIREWALL PORT EVENT' + $actual.Triggers[5].Action | Assert-Equals -Expected 'STOP SERVICE' + $actual.Triggers[5].SubType | Assert-Equals -Expected "$([Ansible.Service.Trigger]::FIREWALL_PORT_CLOSE_GUID) [PORT CLOSE]" + $actual.Triggers[5].Data.Count | Assert-Equals -Expected 1 + $actual.Triggers[5].Data[0] | Assert-Equals -Expected '1234;tcp;imagepath;servicename' + + # Remove trigger with $null + $service.Triggers = $null + + $actual = Invoke-Sc -Action qtriggerinfo -Name $serviceName + $actual.Triggers.Count | Assert-Equals -Expected 0 + + # Add a single trigger + $service.Triggers = [Ansible.Service.Trigger]@{ + Type = [Ansible.Service.TriggerType]::GroupPolicy + Action = [Ansible.Service.TriggerAction]::ServiceStart + SubType = [Guid][Ansible.Service.Trigger]::MACHINE_POLICY_PRESENT_GUID + } + + $actual = Invoke-Sc -Action qtriggerinfo -Name $serviceName + $actual.Triggers.Count | Assert-Equals -Expected 1 + $actual.Triggers[0].Type | Assert-Equals -Expected 'GROUP POLICY' + $actual.Triggers[0].Action | Assert-Equals -Expected 'START SERVICE' + $actual.Triggers[0].SubType | Assert-Equals -Expected "$([Ansible.Service.Trigger]::MACHINE_POLICY_PRESENT_GUID) [MACHINE POLICY PRESENT]" + $actual.Triggers[0].Data.Count | Assert-Equals -Expected 0 + + # Remove trigger with empty list + $service.Triggers = @() + + $actual = Invoke-Sc -Action qtriggerinfo -Name $serviceName + $actual.Triggers.Count | Assert-Equals -Expected 0 + + # Add triggers through sc and check we get the values correctly + $null = Invoke-Sc -Action triggerinfo -Name $serviceName -Arguments @( + 'start/namedpipe/abc', + 'start/namedpipe/def', + 'start/custom/d4497e12-ac36-4823-af61-92db0dbd4a76/11223344/aabbccdd', + 'start/strcustom/435a1742-22c5-4234-9db3-e32dafde695c/11223344/aabbccdd', + 'stop/portclose/1234;tcp;imagepath;servicename', + 'stop/networkoff' + ) + + $actual = $service.Triggers + $actual.Count | Assert-Equals -Expected 6 + + $actual[0].Type | Assert-Equals -Expected ([Ansible.Service.TriggerType]::NetworkEndpoint) + $actual[0].Action | Assert-Equals -Expected ([Ansible.Service.TriggerAction]::ServiceStart) + $actual[0].SubType = [Guid][Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID + $actual[0].DataItems.Count | Assert-Equals -Expected 1 + $actual[0].DataItems[0].Type | Assert-Equals -Expected ([Ansible.Service.TriggerDataType]::String) + $actual[0].DataItems[0].Data | Assert-Equals -Expected 'abc' + + $actual[1].Type | Assert-Equals -Expected ([Ansible.Service.TriggerType]::NetworkEndpoint) + $actual[1].Action | Assert-Equals -Expected ([Ansible.Service.TriggerAction]::ServiceStart) + $actual[1].SubType = [Guid][Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID + $actual[1].DataItems.Count | Assert-Equals -Expected 1 + $actual[1].DataItems[0].Type | Assert-Equals -Expected ([Ansible.Service.TriggerDataType]::String) + $actual[1].DataItems[0].Data | Assert-Equals -Expected 'def' + + $actual[2].Type | Assert-Equals -Expected ([Ansible.Service.TriggerType]::Custom) + $actual[2].Action | Assert-Equals -Expected ([Ansible.Service.TriggerAction]::ServiceStart) + $actual[2].SubType = [Guid]'d4497e12-ac36-4823-af61-92db0dbd4a76' + $actual[2].DataItems.Count | Assert-Equals -Expected 2 + $actual[2].DataItems[0].Type | Assert-Equals -Expected ([Ansible.Service.TriggerDataType]::Binary) + ,$actual[2].DataItems[0].Data | Assert-Equals -Expected ([byte[]]@(17, 34, 51, 68)) + $actual[2].DataItems[1].Type | Assert-Equals -Expected ([Ansible.Service.TriggerDataType]::Binary) + ,$actual[2].DataItems[1].Data | Assert-Equals -Expected ([byte[]]@(170, 187, 204, 221)) + + $actual[3].Type | Assert-Equals -Expected ([Ansible.Service.TriggerType]::Custom) + $actual[3].Action | Assert-Equals -Expected ([Ansible.Service.TriggerAction]::ServiceStart) + $actual[3].SubType = [Guid]'435a1742-22c5-4234-9db3-e32dafde695c' + $actual[3].DataItems.Count | Assert-Equals -Expected 2 + $actual[3].DataItems[0].Type | Assert-Equals -Expected ([Ansible.Service.TriggerDataType]::String) + $actual[3].DataItems[0].Data | Assert-Equals -Expected '11223344' + $actual[3].DataItems[1].Type | Assert-Equals -Expected ([Ansible.Service.TriggerDataType]::String) + $actual[3].DataItems[1].Data | Assert-Equals -Expected 'aabbccdd' + + $actual[4].Type | Assert-Equals -Expected ([Ansible.Service.TriggerType]::FirewallPortEvent) + $actual[4].Action | Assert-Equals -Expected ([Ansible.Service.TriggerAction]::ServiceStop) + $actual[4].SubType = [Guid][Ansible.Service.Trigger]::FIREWALL_PORT_CLOSE_GUID + $actual[4].DataItems.Count | Assert-Equals -Expected 1 + $actual[4].DataItems[0].Type | Assert-Equals -Expected ([Ansible.Service.TriggerDataType]::String) + ,$actual[4].DataItems[0].Data | Assert-Equals -Expected @('1234', 'tcp', 'imagepath', 'servicename') + + $actual[5].Type | Assert-Equals -Expected ([Ansible.Service.TriggerType]::IpAddressAvailability) + $actual[5].Action | Assert-Equals -Expected ([Ansible.Service.TriggerAction]::ServiceStop) + $actual[5].SubType = [Guid][Ansible.Service.Trigger]::NETWORK_MANAGER_LAST_IP_ADDRESS_REMOVAL_GUID + $actual[5].DataItems.Count | Assert-Equals -Expected 0 + } + + # Cannot test PreferredNode as we can't guarantee CI is set up with NUMA support. + # Cannot test LaunchProtection as once set we cannot remove unless rebooting +} + +# setup and teardown should favour native tools to create and delete the service and not the util we are testing. +foreach ($testImpl in $tests.GetEnumerator()) { + $serviceName = "ansible_$([System.IO.Path]::GetRandomFileName())" + $null = New-Service -Name $serviceName -BinaryPathName ('"{0}"' -f $path) -StartupType Manual + + try { + $test = $testImpl.Key + &$testImpl.Value + } finally { + $null = Invoke-Sc -Action delete -Name $serviceName + } +} + +$module.Result.data = "success" +$module.ExitJson() diff --git a/test/integration/targets/win_csharp_utils/tasks/main.yml b/test/integration/targets/win_csharp_utils/tasks/main.yml index 374ec865533..5ac8aab3413 100644 --- a/test/integration/targets/win_csharp_utils/tasks/main.yml +++ b/test/integration/targets/win_csharp_utils/tasks/main.yml @@ -82,3 +82,12 @@ assert: that: - ansible_privilege_test.data == "success" + +- name: test Ansible.Service.cs + ansible_service_tests: + register: ansible_service_test + +- name: assert test Ansible.Service.cs + assert: + that: + - ansible_service_test.data == "success" diff --git a/test/integration/targets/win_service_info/aliases b/test/integration/targets/win_service_info/aliases new file mode 100644 index 00000000000..71fee861933 --- /dev/null +++ b/test/integration/targets/win_service_info/aliases @@ -0,0 +1,2 @@ +shippable/windows/group7 +shippable/windows/smoketest diff --git a/test/integration/targets/win_service_info/defaults/main.yml b/test/integration/targets/win_service_info/defaults/main.yml new file mode 100644 index 00000000000..0da553d9b28 --- /dev/null +++ b/test/integration/targets/win_service_info/defaults/main.yml @@ -0,0 +1,11 @@ +--- +test_path: '{{ remote_tmp_dir }}\win_service_info .ÅÑŚÌβŁÈ [$!@^&test(;)]' +service_url: https://ansible-ci-files.s3.amazonaws.com/test/integration/targets/win_service/SleepService.exe + +service_name1: ansible_service_info_test +service_name2: ansible_service_info_test2 +service_name3: ansible_service_info_other +service_names: +- '{{ service_name1 }}' +- '{{ service_name2 }}' +- '{{ service_name3 }}' diff --git a/test/integration/targets/win_service_info/handlers/main.yml b/test/integration/targets/win_service_info/handlers/main.yml new file mode 100644 index 00000000000..ee2d93eb197 --- /dev/null +++ b/test/integration/targets/win_service_info/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: remove test service + win_service: + name: '{{ item }}' + state: absent + loop: '{{ service_names }}' diff --git a/test/integration/targets/win_service_info/meta/main.yml b/test/integration/targets/win_service_info/meta/main.yml new file mode 100644 index 00000000000..9f37e96cd90 --- /dev/null +++ b/test/integration/targets/win_service_info/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_remote_tmp_dir diff --git a/test/integration/targets/win_service_info/tasks/main.yml b/test/integration/targets/win_service_info/tasks/main.yml new file mode 100644 index 00000000000..d393322236a --- /dev/null +++ b/test/integration/targets/win_service_info/tasks/main.yml @@ -0,0 +1,206 @@ +--- +- name: ensure test directory exists + win_file: + path: '{{ test_path }}' + state: directory + +- name: download test binary for services + win_get_url: + url: '{{ service_url }}' + dest: '{{ test_path }}\SleepService.exe' + +- name: create test service + win_service: + name: '{{ item }}' + path: '"{{ test_path }}\SleepService.exe"' + state: stopped + loop: '{{ service_names }}' + notify: remove test service + +- name: test we can get info for all services + win_service_info: + register: all_actual + check_mode: yes # tests that this will run in check mode + +- name: assert test we can get info for all services + assert: + that: + - not all_actual is changed + - all_actual.exists + - all_actual.services | length > 0 + +- name: test info on a missing service + win_service_info: + name: ansible_service_info_missing + register: missing_service + +- name: assert test info on a missing service + assert: + that: + - not missing_service is changed + - not missing_service.exists + +- name: test info on a single service + win_service_info: + name: '{{ service_name1 }}' + register: specific_service + +- name: assert test info on single service + assert: + that: + - not specific_service is changed + - specific_service.exists + - specific_service.services | length == 1 + - specific_service.services[0].checkpoint == 0 + - specific_service.services[0].controls_accepted == [] + - specific_service.services[0].dependencies == [] + - specific_service.services[0].dependency_of == [] + - specific_service.services[0].description == None + - specific_service.services[0].desktop_interact == False + - specific_service.services[0].display_name == service_name1 + - specific_service.services[0].error_control == 'normal' + - specific_service.services[0].failure_actions == [] + - specific_service.services[0].failure_actions_on_non_crash_failure == False + - specific_service.services[0].failure_command == None + - specific_service.services[0].failure_reboot_msg == None + - specific_service.services[0].failure_reset_period_sec == 0 + - specific_service.services[0].launch_protection == 'none' + - specific_service.services[0].load_order_group == "" + - specific_service.services[0].name == service_name1 + - specific_service.services[0].path == '"' ~ test_path + '\\SleepService.exe"' + - specific_service.services[0].pre_shutdown_timeout_ms is defined # Looks like the default for New-Service differs per OS version + - specific_service.services[0].preferred_node == None + - specific_service.services[0].process_id == 0 + - specific_service.services[0].required_privileges == [] + - specific_service.services[0].service_exit_code == 0 + - specific_service.services[0].service_flags == [] + - specific_service.services[0].service_type == 'win32_own_process' + - specific_service.services[0].sid_info == 'none' + - specific_service.services[0].start_mode == 'auto' + - specific_service.services[0].state == 'stopped' + - specific_service.services[0].triggers == [] + - specific_service.services[0].username == 'NT AUTHORITY\SYSTEM' + - specific_service.services[0].wait_hint_ms == 0 + - specific_service.services[0].win32_exit_code == 1077 + +- name: test info on services matching wildcard + win_service_info: + name: ansible_service_info_t* # should match service_name 1 and 2, but not 3 + register: wildcard_service + +- name: assert test info on services matching wildcard + assert: + that: + - not wildcard_service is changed + - wildcard_service.exists + - wildcard_service.services | length == 2 + - wildcard_service.services[0].name == service_name1 + - wildcard_service.services[1].name == service_name2 + +- name: modify service1 to depend on service 2 + win_service: + name: '{{ service_name1 }}' + state: stopped + dependencies: + - '{{ service_name2 }}' + +- name: edit basic settings for service 2 + win_service: + dependencies: + - '{{ service_name3 }}' + description: Service description + display_name: Ansible Service Display Name + name: '{{ service_name2 }}' + state: stopped + +# TODO: move this back into the above once win_service supports them +- name: edit complex settings for service 2 + win_command: sc.exe {{ item.action }} {{ service_name2 }} {{ item.args }} + with_items: + - action: config + args: type= share type= interact error= ignore group= "My group" start= delayed-auto + - action: failure + args: reset= 86400 reboot= "Reboot msg" command= "Command line" actions= run/500/run/600/restart/700/reboot/800 + - action: failureflag + args: 1 + - action: sidtype + args: unrestricted + - action: privs + args: SeBackupPrivilege/SeRestorePrivilege + - action: triggerinfo + args: start/namedpipe/abc start/namedpipe/def start/custom/0e0682e2-9951-4e6d-a36a-a0047e616f28/11223344/aabbccdd start/strcustom/c2961e88-c1f4-4d97-b581-219c852e1c7d/11223344/aabbccdd start/portopen/1234;tcp;imagepath;servicename + +- name: get info of advanced service using display name + win_service_info: + name: Ansible Service Display Name + register: adv_service + +- name: assert get info of advanced service using display_name + assert: + that: + - not adv_service is changed + - adv_service.exists + - adv_service.services | length == 1 + - adv_service.services[0].dependencies == [service_name3] + - adv_service.services[0].dependency_of == [service_name1] + - adv_service.services[0].description == 'Service description' + - adv_service.services[0].desktop_interact == True + - adv_service.services[0].error_control == 'ignore' + - adv_service.services[0].failure_actions | length == 4 + - adv_service.services[0].failure_actions[0].delay_ms == 500 + - adv_service.services[0].failure_actions[0].type == 'run_command' + - adv_service.services[0].failure_actions[1].delay_ms == 600 + - adv_service.services[0].failure_actions[1].type == 'run_command' + - adv_service.services[0].failure_actions[2].delay_ms == 700 + - adv_service.services[0].failure_actions[2].type == 'restart' + - adv_service.services[0].failure_actions[3].delay_ms == 800 + - adv_service.services[0].failure_actions[3].type == 'reboot' + - adv_service.services[0].failure_actions_on_non_crash_failure == True + - adv_service.services[0].failure_command == 'Command line' + - adv_service.services[0].failure_reboot_msg == 'Reboot msg' + - adv_service.services[0].failure_reset_period_sec == 86400 + - adv_service.services[0].load_order_group == 'My group' + - adv_service.services[0].required_privileges == ['SeBackupPrivilege', 'SeRestorePrivilege'] + - adv_service.services[0].service_type == 'win32_share_process' + - adv_service.services[0].sid_info == 'unrestricted' + - adv_service.services[0].start_mode == 'delayed' + - adv_service.services[0].triggers | length == 5 + - adv_service.services[0].triggers[0].action == 'start_service' + - adv_service.services[0].triggers[0].data_items | length == 1 + - adv_service.services[0].triggers[0].data_items[0].data == 'abc' + - adv_service.services[0].triggers[0].data_items[0].type == 'string' + - adv_service.services[0].triggers[0].sub_type == 'named_pipe_event' + - adv_service.services[0].triggers[0].sub_type_guid == '1f81d131-3fac-4537-9e0c-7e7b0c2f4b55' + - adv_service.services[0].triggers[0].type == 'network_endpoint' + - adv_service.services[0].triggers[1].action == 'start_service' + - adv_service.services[0].triggers[1].data_items | length == 1 + - adv_service.services[0].triggers[1].data_items[0].data == 'def' + - adv_service.services[0].triggers[1].data_items[0].type == 'string' + - adv_service.services[0].triggers[1].sub_type == 'named_pipe_event' + - adv_service.services[0].triggers[1].sub_type_guid == '1f81d131-3fac-4537-9e0c-7e7b0c2f4b55' + - adv_service.services[0].triggers[1].type == 'network_endpoint' + - adv_service.services[0].triggers[2].action == 'start_service' + - adv_service.services[0].triggers[2].data_items | length == 2 + - adv_service.services[0].triggers[2].data_items[0].data == 'ESIzRA==' + - adv_service.services[0].triggers[2].data_items[0].type == 'binary' + - adv_service.services[0].triggers[2].data_items[1].data == 'qrvM3Q==' + - adv_service.services[0].triggers[2].data_items[1].type == 'binary' + - adv_service.services[0].triggers[2].sub_type == 'custom' + - adv_service.services[0].triggers[2].sub_type_guid == '0e0682e2-9951-4e6d-a36a-a0047e616f28' + - adv_service.services[0].triggers[2].type == 'custom' + - adv_service.services[0].triggers[3].action == 'start_service' + - adv_service.services[0].triggers[3].data_items | length == 2 + - adv_service.services[0].triggers[3].data_items[0].data == '11223344' + - adv_service.services[0].triggers[3].data_items[0].type == 'string' + - adv_service.services[0].triggers[3].data_items[1].data == 'aabbccdd' + - adv_service.services[0].triggers[3].data_items[1].type == 'string' + - adv_service.services[0].triggers[3].sub_type == 'custom' + - adv_service.services[0].triggers[3].sub_type_guid == 'c2961e88-c1f4-4d97-b581-219c852e1c7d' + - adv_service.services[0].triggers[3].type == 'custom' + - adv_service.services[0].triggers[4].action == 'start_service' + - adv_service.services[0].triggers[4].data_items | length == 1 + - adv_service.services[0].triggers[4].data_items[0].data == ['1234', 'tcp', 'imagepath', 'servicename'] + - adv_service.services[0].triggers[4].data_items[0].type == 'string' + - adv_service.services[0].triggers[4].sub_type == 'firewall_port_open' + - adv_service.services[0].triggers[4].sub_type_guid == 'b7569e07-8421-4ee0-ad10-86915afdad09' + - adv_service.services[0].triggers[4].type == 'firewall_port_event'