File: //lib/python3.9/site-packages/ansible_collections/ansible/windows/plugins/modules/setup.ps1
#!powershell
# Copyright: (c) 2018, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#Requires -Module Ansible.ModuleUtils.AddType
#AnsibleRequires -CSharpUtil Ansible.Basic
$spec = @{
options = @{
# This is not meant to be publicly used, only for debugging how long it takes to capture a subset.
_measure_subset = @{ type = 'bool'; default = $false }
fact_path = @{ type = 'path' }
gather_subset = @{ type = 'list'; elements = 'str'; default = 'all' }
gather_timeout = @{ type = 'int'; default = 10 }
}
supports_check_mode = $true
}
# This module can be called by the gather_facts action plugin in ansible-base. While it shouldn't add any new options
# we need to make sure the module doesn't break if it does. To do this we need to add any options in the input args
if ($args.Length -gt 0) {
$params = Get-Content -LiteralPath $args[0] | ConvertFrom-AnsibleJson
}
else {
$params = $complex_args
}
if ($params) {
foreach ($param in $params.GetEnumerator()) {
if ($param.Key.StartsWith('_') -or $spec.options.ContainsKey($param.Key)) {
continue
}
$spec.options."$($param.Key)" = @{ type = 'raw' }
}
}
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
$measureSubset = $module.Params._measure_subset
$factPath = $module.Params.fact_path
$gatherSubset = $module.Params.gather_subset
$gatherTimeout = $module.Params.gather_timeout
$osversion = [Environment]::OSVersion.Version
if ($osversion -lt [version]"6.2") {
# Server 2008, 2008 R2, and Windows 7 are not tested in CI and we want to let customers know about it before
# removing support altogether.
$versionString = "{0}.{1}" -f ($osversion.Major, $osversion.Minor)
$module.Warn("The Windows version '$versionString' will no longer be supported or tested in future releases")
}
Add-CSharpType -AnsibleModule $module -References @'
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Text;
namespace Ansible.Windows.Setup
{
public class NativeHelpers
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DSROLE_PRIMARY_DOMAIN_INFO_BASIC
{
public Int32 MachineRole;
public UInt32 Flags;
public string DomainNameFlat;
public string DomainNameDns;
public string DomainForestName;
public Guid DomainGuid;
}
[StructLayout(LayoutKind.Sequential)]
public struct MEMORYSTATUSEX
{
public Int32 dwLength;
public UInt32 dwMemoryLoad;
public UInt64 ullTotalPhys;
public UInt64 ullAvailPhys;
public UInt64 ullTotalPageFile;
public UInt64 ullAvailPageFile;
public UInt64 ullTotalVirtual;
public UInt64 ullAvailVirtual;
public UInt64 ullAvailExtendedVirtual;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct OSVERSIONINFOEXW
{
public Int32 dwOSVersionInfoSize;
public UInt32 dwMajorVersion;
public UInt32 dwMinorVersion;
public UInt32 dwBuildNumber;
public UInt32 dwPlatformId;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string szCSDVersion;
public UInt16 wServicePackMajor;
public UInt16 wServicePackMinor;
public UInt16 wSuiteMask;
public byte wProductType;
public byte wReserved;
}
[StructLayout(LayoutKind.Sequential)]
public struct RawSMBIOSData
{
public byte Used20CallingMethod;
public byte SMBIOSMajorVersion;
public byte SMBIOSMinorVersion;
public byte DmiRevision;
public Int32 Length;
}
[StructLayout(LayoutKind.Sequential)]
public struct SMBIOSHeader
{
public byte Type;
public byte Length;
public UInt16 Handle;
}
// Both BIOSData and SystemInformation is defined in the standard below.
// https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.3.0.pdf
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct BIOSData
{
public byte Vendor;
public byte Version;
public UInt16 StartingAddressSegment;
public byte ReleaseDate;
// There are more fields but we only need up to ReleaseDate.
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct SystemInformation
{
public byte Manufacturer;
public byte ProductName;
public byte Version;
public byte SerialNumber;
// There are more fields but we only need up to SerialNumber.
}
[StructLayout(LayoutKind.Sequential)]
public struct SYSTEM_INFO
{
public UInt16 wProcessorArchitecture;
public UInt16 wReserved;
public UInt32 dwPageSize;
public IntPtr lpMinimumApplicationAddress;
public IntPtr lpMaximumApplicationAddress;
public UIntPtr dwActiveProcessorMask;
public UInt32 dwNumberOfProcessors;
public UInt32 dwProcessorType;
public UInt32 dwAllocationGranularity;
public UInt16 wProcessorLevel;
public UInt16 wProcessorRevision;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct WKSTA_INFO_100
{
public UInt32 wki100_platform_id;
public string wki100_computername;
public string wki100_langroup;
public UInt32 wki100_ver_major;
public UInt32 wki100_ver_minor;
}
}
public class NativeMethods
{
[DllImport("Netapi32.dll")]
public static extern void DsRoleFreeMemory(
IntPtr Buffer);
[DllImport("Netapi32.dll")]
public static extern Int32 DsRoleGetPrimaryDomainInformation(
[MarshalAs(UnmanagedType.LPWStr)] string lpServer,
UInt32 InfoLevel,
out SafeDsMemoryBuffer Buffer);
[DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool GetComputerNameExW(
UInt32 NameType,
[MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpBuffer,
ref Int32 nSize);
[DllImport("Kernel32.dll")]
public static extern void GetNativeSystemInfo(
ref NativeHelpers.SYSTEM_INFO lpSystemInfo);
[DllImport("Kernel32.dll", SetLastError = true)]
public static extern Int32 GetSystemFirmwareTable(
FirmwareProvider FirmwareTableProviderSignature,
UInt32 FirmwareTableID,
IntPtr pFirmwareTableBuffer,
Int32 BufferSize);
[DllImport("Kernel32.dll")]
public static extern Int64 GetTickCount64();
[DllImport("Kernel32.dll", SetLastError = true)]
public static extern bool GetVersionExW(
ref NativeHelpers.OSVERSIONINFOEXW lpVersionInformation);
[DllImport("Kernel32.dll", SetLastError = true)]
public static extern bool GlobalMemoryStatusEx(
ref NativeHelpers.MEMORYSTATUSEX lpBuffer);
[DllImport("Netapi32.dll")]
public static extern UInt32 NetApiBufferFree(
IntPtr Buffer);
[DllImport("Netapi32.dll", CharSet = CharSet.Unicode)]
public static extern Int32 NetWkstaGetInfo(
string servername,
UInt32 level,
out SafeNetAPIBuffer bufptr);
}
public class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeMemoryBuffer(int cb) : base(true)
{
base.SetHandle(Marshal.AllocHGlobal(cb));
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected override bool ReleaseHandle()
{
Marshal.FreeHGlobal(handle);
return true;
}
}
public class SafeDsMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeDsMemoryBuffer() : base(true) { }
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected override bool ReleaseHandle()
{
NativeMethods.DsRoleFreeMemory(this.handle);
return true;
}
}
public class SafeNetAPIBuffer : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeNetAPIBuffer() : base(true) { }
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected override bool ReleaseHandle()
{
NativeMethods.NetApiBufferFree(this.handle);
return true;
}
}
public class Win32Exception : System.ComponentModel.Win32Exception
{
private string _msg;
public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
public Win32Exception(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 Win32Exception(string message) { return new Win32Exception(message); }
}
public enum FirmwareProvider : uint
{
ACPI = 0x41435049,
FIRM = 0x4649524D,
RSMB = 0x52534D42
}
public class DomainInfo
{
public string Domain;
public Int32 DomainRole;
public bool PartOfDomain;
public DomainInfo()
{
SafeDsMemoryBuffer dsBuffer;
// 1 == DsRolePrimaryDomainInfoBasic
int res = NativeMethods.DsRoleGetPrimaryDomainInformation(null, 1, out dsBuffer);
if (res != 0)
throw new Win32Exception(res, "Failed to get domain information");
using (dsBuffer)
{
var domainInfo = (NativeHelpers.DSROLE_PRIMARY_DOMAIN_INFO_BASIC)Marshal.PtrToStructure(
dsBuffer.DangerousGetHandle(), typeof(NativeHelpers.DSROLE_PRIMARY_DOMAIN_INFO_BASIC));
Domain = domainInfo.DomainNameDns;
DomainRole = domainInfo.MachineRole;
PartOfDomain = !String.IsNullOrEmpty(Domain);
}
if (!PartOfDomain)
{
SafeNetAPIBuffer netBuffer;
res = NativeMethods.NetWkstaGetInfo(null, 100, out netBuffer);
if (res != 0)
throw new Win32Exception(res, "Failed to get workstation information");
using (netBuffer)
{
var netInfo = (NativeHelpers.WKSTA_INFO_100)Marshal.PtrToStructure(
netBuffer.DangerousGetHandle(), typeof(NativeHelpers.WKSTA_INFO_100));
Domain = netInfo.wki100_langroup;
}
}
}
}
public class MemoryInfo
{
public UInt64 TotalPhysical;
public UInt64 AvailablePhysical;
public MemoryInfo()
{
var memoryInfo = new NativeHelpers.MEMORYSTATUSEX();
memoryInfo.dwLength = Marshal.SizeOf(memoryInfo);
if (!NativeMethods.GlobalMemoryStatusEx(ref memoryInfo))
throw new Win32Exception("Failed to get memory info");
TotalPhysical = memoryInfo.ullTotalPhys;
AvailablePhysical = memoryInfo.ullAvailPhys;
}
}
public class OSVersionInfo
{
public byte ProductType;
public OSVersionInfo()
{
var versionInfo = new NativeHelpers.OSVERSIONINFOEXW() { };
versionInfo.dwOSVersionInfoSize = Marshal.SizeOf(versionInfo);
if (!NativeMethods.GetVersionExW(ref versionInfo))
throw new Win32Exception("Failed to get version info");
ProductType = versionInfo.wProductType;
}
}
public class SMBIOSInfo
{
public DateTime? ReleaseDate;
public string SMBIOSBIOSVersion;
public string Manufacturer;
public string Model;
public string SerialNumber;
public bool ProcessorCountFound = false;
public List<Tuple<int, int>> ProcessorInfo = new List<Tuple<int, int>>();
public SMBIOSInfo()
{
using (SafeMemoryBuffer buffer = GetSMBIOSBuffer())
{
var rawData = (NativeHelpers.RawSMBIOSData)Marshal.PtrToStructure(buffer.DangerousGetHandle(),
typeof(NativeHelpers.RawSMBIOSData));
IntPtr tablePtr = IntPtr.Add(buffer.DangerousGetHandle(), Marshal.SizeOf(rawData));
int headerSize = Marshal.SizeOf(typeof(NativeHelpers.SMBIOSHeader));
int offset = 0;
while (offset < rawData.Length)
{
var header = (NativeHelpers.SMBIOSHeader)Marshal.PtrToStructure(tablePtr,
typeof(NativeHelpers.SMBIOSHeader));
IntPtr headerDataPtr = IntPtr.Add(tablePtr, headerSize);
tablePtr = IntPtr.Add(tablePtr, header.Length);
offset += header.Length;
List<string> stringTable = ExtractStringTable(ref tablePtr, ref offset);
// We only care about the BIOS (0) or System Information (1) values.
if (header.Type == 0)
{
var biosInfo = (NativeHelpers.BIOSData)Marshal.PtrToStructure(headerDataPtr,
typeof(NativeHelpers.BIOSData));
ReleaseDate = ParseDateString(ExtractFromStringTable(stringTable, biosInfo.ReleaseDate));
SMBIOSBIOSVersion = ExtractFromStringTable(stringTable, biosInfo.Version);
}
else if (header.Type == 1)
{
var systemInfo = (NativeHelpers.SystemInformation)Marshal.PtrToStructure(headerDataPtr,
typeof(NativeHelpers.SystemInformation));
Manufacturer = ExtractFromStringTable(stringTable, systemInfo.Manufacturer);
Model = ExtractFromStringTable(stringTable, systemInfo.ProductName);
SerialNumber = ExtractFromStringTable(stringTable, systemInfo.SerialNumber);
}
else if (header.Type == 4)
{
// Section 7.5 Processor Information (Type 4)
// https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.4.0a.pdf
byte status = Marshal.ReadByte(headerDataPtr, 20);
if ((status & (1 << 6)) == 0)
{
// CPU Socket Unpopulated
continue;
}
else if ((status & 0x7) != 1)
{
// Not CPU Enabled
continue;
}
byte coreCount = 0;
ushort coreCount2 = 0;
byte threadCount = 0;
ushort threadCount2 = 0;
// SMBIOS 2.5 is when the core and thread count fields
// were added.
if (rawData.SMBIOSMajorVersion > 3 || (rawData.SMBIOSMajorVersion == 2 && rawData.SMBIOSMajorVersion > 4))
{
if ((header.Length - headerSize) >= 34)
{
ProcessorCountFound = true;
coreCount = Marshal.ReadByte(headerDataPtr, 31);
threadCount = Marshal.ReadByte(headerDataPtr, 33);
}
if ((header.Length - headerSize) >= 44)
{
ProcessorCountFound = true;
coreCount2 = (ushort)Marshal.ReadInt16(headerDataPtr, 38);
threadCount2 = (ushort)Marshal.ReadInt16(headerDataPtr, 42);
}
}
ProcessorInfo.Add(Tuple.Create(
CalculateCount(coreCount, coreCount2),
CalculateCount(threadCount, threadCount2)
));
}
else
continue;
}
}
}
private SafeMemoryBuffer GetSMBIOSBuffer()
{
Int32 size = NativeMethods.GetSystemFirmwareTable(FirmwareProvider.RSMB, 0, IntPtr.Zero, 0);
SafeMemoryBuffer buffer = new SafeMemoryBuffer(size);
NativeMethods.GetSystemFirmwareTable(FirmwareProvider.RSMB, 0, buffer.DangerousGetHandle(), size);
int res = Marshal.GetLastWin32Error();
if (res != 0)
throw new Win32Exception(res, "Failed to get SMBIOS buffer information");
return buffer;
}
private List<string> ExtractStringTable(ref IntPtr ptr, ref int offset)
{
// The string table is a list of null-terminated ASCII encoded strings right after the header.
List<string> stringTable = new List<string>();
while (true)
{
string stringValue = Marshal.PtrToStringAnsi(ptr);
ptr = IntPtr.Add(ptr, stringValue.Length + 1);
offset += stringValue.Length + 1;
if (String.IsNullOrEmpty(stringValue))
{
// If there were no string we still need to account for another null char.
if (stringTable.Count == 0)
{
ptr = IntPtr.Add(ptr, 1);
offset++;
}
break;
}
else
stringTable.Add(stringValue);
}
return stringTable;
}
private string ExtractFromStringTable(List<string> stringTable, byte index)
{
// StringTable indexes are 1 based, if the value is 0 then there is no value.
if (index == 0)
return null;
return stringTable[index - 1].Trim();
}
private DateTime? ParseDateString(string date)
{
if (String.IsNullOrEmpty(date))
return null;
// Older standards could use a 2 digit year that indicates 19yy.
string dateFormat = date.Length == 10 ? "MM/dd/yyyy" : "MM/dd/yy";
DateTime rawDateTime;
if (DateTime.TryParseExact(date, dateFormat, null, System.Globalization.DateTimeStyles.None, out rawDateTime)) {
return DateTime.SpecifyKind(rawDateTime, DateTimeKind.Utc);
}
else {
return null;
}
}
private int CalculateCount(byte count1, ushort count2)
{
return (int)(count2 == 0 ? count1 : count2);
}
}
public class SystemInfo
{
public string NetBIOSName;
public UInt32 NumberOfProcessors;
public UInt32 ProcessorArchitecture;
public SystemInfo()
{
var systemInfo = new NativeHelpers.SYSTEM_INFO();
NativeMethods.GetNativeSystemInfo(ref systemInfo);
NumberOfProcessors = systemInfo.dwNumberOfProcessors;
ProcessorArchitecture = systemInfo.wProcessorArchitecture;
StringBuilder buffer = new StringBuilder(0);
int size = 0;
// 0 == ComputerNameNetBIOS
NativeMethods.GetComputerNameExW(0, buffer, ref size);
buffer.EnsureCapacity(size);
if (!NativeMethods.GetComputerNameExW(0, buffer, ref size))
throw new Win32Exception("Failed to get ComputerName");
NetBIOSName = buffer.ToString();
}
}
}
'@
$factMeta = @(
@{
Subsets = 'all_ipv4_addresses', 'all_ipv6_addresses'
Code = {
$interfaces = [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces()
$ips = @(foreach ($interface in $interfaces) {
# Win32_NetworkAdapterConfiguration did not return the local IPs so we replicate that here.
$interface.GetIPProperties().UnicastAddresses | Where-Object {
$_.Address.ToString() -notin @('::1', '127.0.0.1')
} | ForEach-Object -Process {
$_.Address.ToString()
}
})
$ansibleFacts.ansible_ip_addresses = $ips
}
},
@{
Subsets = 'bios'
Code = {
$bios = New-Object -TypeName Ansible.Windows.Setup.SMBIOSInfo
$releaseDate = if ($bios.ReleaseDate) {
$bios.ReleaseDate.ToUniversalTime().ToString('MM/dd/yyyy')
}
$ansibleFacts.ansible_bios_date = $releaseDate
$ansibleFacts.ansible_bios_version = $bios.SMBIOSBIOSVersion
$ansibleFacts.ansible_product_name = $bios.Model
$ansibleFacts.ansible_product_serial = $bios.SerialNumber
}
},
@{
Subsets = 'date_time'
Code = {
$datetime = (Get-Date)
$datetimeUtc = $datetime.ToUniversalTime()
$epochDatetimeUtc = New-Object -TypeName DateTime -ArgumentList @(1970, 1, 1, 0, 0, 0, [DateTimeKind]::Utc)
$epoch = (New-TimeSpan -Start $epochDatetimeUtc -End $dateTimeUtc).TotalSeconds
$ansibleFacts.ansible_date_time = @{
date = $datetime.ToString("yyyy-MM-dd")
day = $datetime.ToString("dd")
epoch_local = (Get-Date ($datetime) -UFormat '+%s')
epoch = (Get-Date ($datetimeUtc) -UFormat '+%s')
epoch_int = [int]$epoch
hour = $datetime.ToString("HH")
iso8601 = $datetimeUtc.ToString("yyyy-MM-ddTHH:mm:ssZ")
iso8601_basic = $datetime.ToString("yyyyMMddTHHmmssffffff")
iso8601_basic_short = $datetime.ToString("yyyyMMddTHHmmss")
iso8601_micro = $datetimeUtc.ToString("yyyy-MM-ddTHH:mm:ss.ffffffZ")
minute = $datetime.ToString("mm")
month = $datetime.ToString("MM")
second = $datetime.ToString("ss")
time = $datetime.ToString("HH:mm:ss")
tz = ([System.TimeZoneInfo]::Local.Id)
tz_offset = $datetime.ToString("zzzz")
# Ensure that the weekday is in English
weekday = $datetime.ToString("dddd", [System.Globalization.CultureInfo]::InvariantCulture)
weekday_number = (Get-Date -UFormat "%w")
weeknumber = (Get-Date -UFormat "%W")
year = $datetime.ToString("yyyy")
}
}
},
@{
Subsets = 'distribution'
Code = {
$osversion = [Environment]::OSVersion.Version
$osProductType = (New-Object -TypeName Ansible.Windows.Setup.OSVersionInfo).ProductType
$productType = switch ($osProductType) {
1 { "workstation" }
2 { "domain_controller" }
3 { "server" }
default { "unknown" }
}
$osInfoParams = @{
LiteralPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
Name = 'InstallationType'
ErrorAction = 'SilentlyContinue'
}
$osInfo = Get-ItemProperty @osInfoParams
$ansibleFacts.ansible_distribution = $null
$ansibleFacts.ansible_distribution_version = $osversion.ToString()
$ansibleFacts.ansible_distribution_major_version = $osversion.Major.ToString()
$ansibleFacts.ansible_os_family = "Windows"
$ansibleFacts.ansible_os_name = $null
$ansibleFacts.ansible_os_product_type = $productType
$ansibleFacts.ansible_os_installation_type = $osInfo.InstallationType
# We cannot call WMI if we aren't an admin (on a network logon), conditionally set these facts.
$currentUser = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
if ($currentUser.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
# These values are localized and I cannot find where they are sourced, we just need to continue
# returning them for backwards compatibility.
$win32OS = Get-CimInstance -ClassName Win32_OperatingSystem -Property Caption, Name
$ansibleFacts.ansible_distribution = $win32OS.Caption
$ansibleFacts.ansible_os_name = ($win32OS.Name.Split('|')[0]).Trim()
}
}
},
@{
Subsets = 'env'
Code = {
$envVars = @{}
foreach ($item in Get-ChildItem -LiteralPath Env:) {
# Powershell ConvertTo-Json fails if string ends with \
$value = $item.Value.TrimEnd("\")
$envVars.Add($item.Name, $value)
}
$ansibleFacts.ansible_env = $envVars
}
},
@{
Subsets = 'facter'
Code = {
# Get-Command can be slow to enumerate the paths, do this ourselves
$facterDir = $env:PATH -split ([IO.Path]::PathSeparator) | Where-Object {
# https://github.com/ansible-collections/ansible.windows/pull/78#issuecomment-745229594
# PATHs with missing entries 'C:\Windows;;C:\Program Files' needs to be handled.
if ([String]::IsNullOrWhiteSpace($_)) {
return $false
}
# https://github.com/ansible-collections/ansible.windows/issues/364
# PATH may still contain invalid PATH characters, ignore them
try {
$facterPath = Join-Path -Path $_ -ChildPath facter.exe -ErrorAction Stop
Test-Path -LiteralPath $facterPath -ErrorAction SilentlyContinue
}
catch [ArgumentException], [System.Management.Automation.DriveNotFoundException] {
$false
}
} | Select-Object -First 1
if ($facterDir) {
# Get JSON from Facter, and parse it out.
$facter = Join-Path -Path $facterDir -ChildPath facter.exe
$facterOutput = &$facter -j
$facts = "$facterOutput" | ConvertFrom-Json
ForEach ($fact in $facts.PSObject.Properties) {
$ansibleFacts."facter_$($fact.Name)" = $fact.Value
}
}
}
},
@{
Subsets = 'interfaces'
Code = {
$interfaces = [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces()
$formattedNetCfg = @(foreach ($interface in $interfaces) {
$ipProps = $interface.GetIPProperties()
try {
$ipv4 = $ipProps.GetIPv4Properties()
}
catch [System.Net.NetworkInformation.NetworkInformationException] {
$ipv4 = $null
}
try {
$ipv6 = $ipProps.GetIPv6Properties()
}
catch [System.Net.NetworkInformation.NetworkInformationException] {
$ipv6 = $null
}
# Do not repo on either the loopback interface or any interfaces that did not have an IP address.
if (-not ($ipv4 -or $ipv6) -or $interface.NetworkInterfaceType -in @('Loopback', 'Tunnel')) {
continue
}
$defaultGateway = if ($ipProps.GatewayAddresses) {
$ipProps.GatewayAddresses[0].Address.IPAddressToString
}
$dnsDomain = $null
if ($ipProps.DnsSuffix) {
$dnsDomain = $ipProps.DnsSuffix
}
$index = if ($ipv4) {
$ipv4.Index
}
elseif ($ipv6) {
$ipv6.Index
}
$ipv4_address = $ipProps.UnicastAddresses | Where-Object {
$_.Address.AddressFamily -eq 2
} | Select-Object @{ N = 'address'; E = { $_.Address.ToString() } }, @{ N = 'prefix'; E = { $_.PrefixLength.ToString() } }
$ipv6_address = $ipProps.UnicastAddresses | Where-Object {
$_.Address.AddressFamily -eq 23
} | Select-Object @{ N = 'address'; E = { $_.Address.ToString() } }, @{ N = 'prefix'; E = { $_.PrefixLength.ToString() } }
$mac = ($interface.GetPhysicalAddress() -replace '(..)', '$1:').ToUpperInvariant().Trim(':')
@{
connection_name = $interface.Name
default_gateway = $defaultGateway
dns_domain = $dnsDomain
interface_index = $index
interface_name = $interface.Description
ipv4 = $ipv4_address
ipv6 = $ipv6_address
macaddress = $mac
mtu = $ipv4.Mtu
speed = $interface.Speed / 1000 / 1000
}
})
$ansibleFacts.ansible_interfaces = $formattedNetCfg
}
},
@{
Subsets = 'local'
Code = {
if (-not $factPath) {
return
}
if (Test-Path -Path $factPath) {
$factFiles = Get-ChildItem -Path $factpath | Where-Object {
-not $_.PSIsContainer -and @('.ps1', '.json') -ccontains $_.Extension
}
foreach ($factsFile in $factFiles) {
$out = if ($factsFile.Extension -eq '.ps1') {
& $($factsFile.FullName)
}
elseif ($factsFile.Extension -eq '.json') {
Get-Content -Raw -Path $factsFile.FullName | ConvertFrom-Json
}
if ($null -ne $out) {
$ansibleFacts."ansible_$($factsFile.BaseName)" = $out
}
}
}
else {
$module.Warn("Non existing path was set for local facts - $factPath")
}
}
},
@{
Subsets = 'memory'
Code = {
$mem = New-Object -TypeName Ansible.Windows.Setup.MemoryInfo
$ansibleFacts.ansible_memtotal_mb = ([Math]::Ceiling($mem.TotalPhysical / 1024 / 1024))
$ansibleFacts.ansible_memfree_mb = ([Math]::Ceiling($mem.AvailablePhysical / 1024 / 1024))
$ansibleFacts.ansible_swaptotal_mb = 0 # Swap memory does not apply to Windows
# Cannot figure out how to get this info outside of WMI. That means we can only run this when we are an
# an admin to avoid a 5 second execution time hit on a standard user logon.
$currentUser = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
if ($currentUser.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
$win32OS = Get-CimInstance -ClassName Win32_OperatingSystem -Property @(
'FreeSpaceInPagingFiles', 'SizeStoredInPagingFiles'
)
$ansibleFacts.ansible_pagefiletotal_mb = ([Math]::Round($win32OS.SizeStoredInPagingFiles / 1024))
$ansibleFacts.ansible_pagefilefree_mb = ([Math]::Round($win32OS.FreeSpaceInPagingFiles / 1024))
}
else {
$ansibleFacts.ansible_pagefiletotal_mb = $null
$ansibleFacts.ansible_pagefilefree_mb = $null
}
}
},
@{
Subsets = 'platform'
Code = {
$bios = New-Object -TypeName Ansible.Windows.Setup.SMBIOSInfo
$domainInfo = New-Object -TypeName Ansible.Windows.Setup.DomainInfo
$systemInfo = New-Object -TypeName Ansible.Windows.Setup.SystemInfo
$osVersion = [Environment]::OSVersion
# The Machine SID is stored in HKLM:\SECURITY\SAM\Domains\Account and is
# only accessible by the Local System account. This method get's the local
# admin account (ends with -500) and lops it off to get the machine sid.
$machineSid = $null
try {
$adminGroup = (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @(
"S-1-5-32-544")).Translate([System.Security.Principal.NTAccount]).Value
$namespace = 'System.DirectoryServices.AccountManagement'
Add-Type -AssemblyName $namespace
$context = New-Object -TypeName "$namespace.PrincipalContext" -ArgumentList @(
[System.DirectoryServices.AccountManagement.ContextType]::Machine)
$principal = New-Object -TypeName "$namespace.GroupPrincipal" -ArgumentList $context, $adminGroup
$searcher = New-Object -TypeName "$namespace.PrincipalSearcher" -ArgumentList $principal
$groups = $searcher.FindOne()
foreach ($user in $groups.Members) {
if ($user.Sid.Value.EndsWith("-500")) {
$machineSid = $user.Sid.AccountDomainSid.Value
break
}
}
}
catch {
$module.Warn("Error during machine sid retrieval: $($_.Exception.Message)")
}
$ipProps = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
$fqdn = $ipProps.HostName
if ($ipProps.DomainName) {
$fqdn = "$($fqdn).$($ipProps.DomainName)"
}
$domainSuffix = if ($domainInfo.PartOfDomain) { [string]$domainInfo.Domain } else { "" }
$ownerParams = @{
LiteralPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
Name = 'RegisteredOwner'
ErrorAction = 'SilentlyContinue'
}
$ownerName = [String](Get-ItemProperty @ownerParams)."$($ownerParams.Name)"
$descriptionParams = @{
LiteralPath = 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters'
Name = 'srvcomment'
ErrorAction = 'SilentlyContinue'
}
$description = [string](Get-ItemProperty @descriptionParams)."$($descriptionParams.Name)"
$ansibleFacts.ansible_domain = [String]$domainSuffix
$ansibleFacts.ansible_fqdn = $fqdn
$ansibleFacts.ansible_hostname = $ipProps.HostName
$ansibleFacts.ansible_netbios_name = $systemInfo.NetBIOSName
$ansibleFacts.ansible_kernel = $osVersion.Version.ToString()
$ansibleFacts.ansible_nodename = $fqdn
$ansibleFacts.ansible_machine_id = $machineSid
$ansibleFacts.ansible_owner_name = $ownerName
$ansibleFacts.ansible_system = $osVersion.Platform.ToString()
$ansibleFacts.ansible_system_description = $description
$ansibleFacts.ansible_system_vendor = $bios.Manufacturer
# Check if a reboot is pending, this is legacy behaviour from Legacy.psm1
$pendingParams = @{
LiteralPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager'
Name = 'PendingFileRenameOperations'
ErrorAction = 'SilentlyContinue'
}
$pendingRenames = (Get-ItemProperty @pendingParams)."$($pendingParams.Name)"
$cbsPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'
$cbsReboot = Test-Path -LiteralPath $cbsPath
$smParams = @{
LiteralPath = 'HKLM:\SOFTWARE\Microsoft\ServerManager\ServicingStorage\ServerComponentCache'
Name = 'RestartRequired'
ErrorAction = 'SilentlyContinue'
}
$smRestart = (Get-ItemProperty @smParams)."$($smParams.Name)"
$rebootPending = $pendingRenames -or $cbsReboot -or $smRestart
# Cannot run Get-CimInstance on non-admin account as it takes 5 seconds to come back with an access denied
# error. Set the original value to $null and use 'ansible_architecture2' instead.
$currentUser = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
if ($currentUser.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
$win32CS = Get-CimInstance -ClassName Win32_ComputerSystem -Property PrimaryOwnerContact
$win32OS = Get-CimInstance -ClassName Win32_OperatingSystem -Property OSArchitecture
$ansibleFacts.ansible_architecture = $win32OS.OSArchitecture
# I don't even know if this even returns anything on Windows, cannot find any documentation for it.
$ansibleFacts.ansible_owner_contact = ([string]$win32CS.PrimaryOwnerContact)
}
else {
$ansibleFacts.ansible_architecture = $null
$ansibleFacts.ansible_owner_contact = ""
}
$ansibleFacts.ansible_reboot_pending = $rebootPending
# This is a non-localized value that tries to match the POSIX setup.py values. We cannot replace
# ansible_architecture without a deprecation period so have both side by side. When we can deprecate facts
# returned by a module we should deprecate the format and rename this.
$ansibleFacts.ansible_architecture2 = switch ($systemInfo.ProcessorArchitecture) {
0 { 'i386' } # PROCESSOR_ARCHITECTURE_INTEL (x86)
5 { 'arm' } # PROCESSOR_ARCHITECTURE_ARM (ARM)
6 { 'ia64' } # PROCESSOR_ARCHITECTURE_IA64 (Intel Ithanium-based)
9 { 'x86_64' } # PROCESSOR_ARCHITECTURE_AMD64 (x64 (AMD or Intel))
12 { 'arm64' } # PROCESSOR_ARCHITECTURE_ARM64 (ARM664)
default { 'unknown' }
}
}
},
@{
Subsets = 'powershell_version'
Code = {
$ansibleFacts.ansible_powershell_version = $PSVersionTable.PSVersion.Major
}
},
@{
Subsets = 'processor'
Code = {
$bios = New-Object -TypeName Ansible.Windows.Setup.SMBIOSInfo
$systemInfo = New-Object -TypeName Ansible.Windows.Setup.SystemInfo
$getParams = @{
LiteralPath = 'HKLM:\HARDWARE\DESCRIPTION\System\CentralProcessor'
ErrorAction = 'SilentlyContinue'
}
$processorKeys = Get-ChildItem @getParams | Select-Object -Property @(
'PSPath', @{ N = 'ID'; E = { [int]$_.PSChildName } }
) | Sort-Object -Property Id
$processors = @(foreach ($proc in $processorKeys) {
$names = 'ProcessorNameString', 'VendorIdentifier'
$info = Get-ItemProperty -LiteralPath $proc.PSPath -Name $names -ErrorAction SilentlyContinue
[string]$proc.ID
$info.VendorIdentifier
$info.ProcessorNameString
})
$ansibleFacts.ansible_processor = $processors
$ansibleFacts.ansible_processor_count = $bios.ProcessorInfo.Count
$ansibleFacts.ansible_processor_vcpus = $systemInfo.NumberOfProcessors
if ($bios.ProcessorCountFound) {
$coresPerProcessor = $bios.ProcessorInfo[0].Item1
$threadsPerProcessor = $bios.ProcessorInfo[0].Item2
$ansibleFacts.ansible_processor_cores = $coresPerProcessor
$ansibleFacts.ansible_processor_threads_per_core = $threadsPerProcessor / $coresPerProcessor
}
else {
# SMBIOS version is too old to get this information. Fallback
# to using CIM for this case.
$win32Proc = Get-CimInstance -ClassName Win32_Processor -Property NumberOfCores, NumberOfLogicalProcessors |
Select-Object -First 1
$ansibleFacts.ansible_processor_cores = $win32Proc.NumberOfCores
$ansibleFacts.ansible_processor_threads_per_core = $win32Proc.NumberOfLogicalProcessors / $win32Proc.NumberOfCores
}
}
},
@{
Subsets = 'uptime'
Code = {
$ticks = [Ansible.Windows.Setup.NativeMethods]::GetTickCount64()
$ansibleFacts.ansible_lastboot = [DateTime]::Now.AddMilliseconds($ticks * -1).ToString('u')
$ansibleFacts.ansible_uptime_seconds = [Math]::Round($ticks / 1000)
}
},
@{
Subsets = 'user'
Code = {
$user = [Security.Principal.WindowsIdentity]::GetCurrent()
$ansibleFacts.ansible_user_dir = $env:userprofile
# Win32_UserAccount.FullName is probably the right thing here, but it can be expensive to get on large domains
$ansibleFacts.ansible_user_gecos = ""
$ansibleFacts.ansible_user_id = $env:username
$ansibleFacts.ansible_user_sid = $user.User.Value
}
},
@{
Subsets = 'windows_domain'
Code = {
$domainInfo = New-Object -TypeName Ansible.Windows.Setup.DomainInfo
$domainRole = switch ($domainInfo.DomainRole) {
0 { "Stand-alone workstation" }
1 { "Member workstation" }
2 { "Stand-alone server" }
3 { "Member server" }
4 { "Backup domain controller" }
5 { "Primary domain controller" }
default { "Unknown DomainRole $_" }
}
$ansibleFacts.ansible_windows_domain = $domainInfo.Domain
$ansibleFacts.ansible_windows_domain_member = $domainInfo.PartOfDomain
$ansibleFacts.ansible_windows_domain_role = $domainRole
}
},
@{
Subsets = 'winrm'
Code = {
try {
$certs = @(Get-ChildItem -Path WSMan:\localhost\Listener\* -ErrorAction SilentlyContinue | Where-Object {
'Transport=HTTPS' -in $_.Keys
} | Get-ChildItem | Where-Object Name -EQ 'CertificateThumbprint' | ForEach-Object {
Get-Item -LiteralPath Cert:\LocalMachine\My\$($_.Value)
})
}
catch {
$certs = @()
$module.Warn("Error during certificate expiration retrieval: $($_.Exception.Message)")
}
$certs | Sort-Object -Property NotAfter | Select-Object -First 1 | ForEach-Object -Process {
# this fact was renamed from ansible_winrm_certificate_expires due to collision with ansible_winrm_X connection var pattern
$ansibleFacts.ansible_win_rm_certificate_expires = $_.NotAfter.ToString('yyyy-MM-dd HH:mm:ss')
}
}
},
@{
Subsets = 'virtual'
Code = {
$bios = New-Object -TypeName Ansible.Windows.Setup.SMBIOSInfo
$modelMap = @{
kvm = @('KVM', 'KVM Server', 'Bochs', 'AHV')
RHEV = @('RHEV Hypervisor')
VMware = @('VMWare Virtual Platform', 'VMware7,1')
openstack = @('OpenStack Compute', 'OpenStack Nova')
xen = @('xen', 'HVM domU')
'Hyper-V' = @('Virtual Machine')
VirtualBox = @('VirtualBox')
}
foreach ($modelInfo in $modelMap.GetEnumerator()) {
if ($bios.Model -in $modelInfo.Value) {
$ansibleFacts.ansible_virtualization_role = 'guest'
$ansibleFacts.ansible_virtualization_type = $modelInfo.Key
return
}
}
$manufacturerMap = @{
kvm = @('QEMU', 'oVirt', 'Amazon EC2', 'DigitalOcean', 'Google', 'Scaleway', 'Nutanix')
KubeVirt = @('KubVirt')
parallels = @('Parallels Software International Inc.')
openstack = @('OpenStack Foundation')
}
foreach ($manufacturerInfo in $manufacturerMap.GetEnumerator()) {
if ($bios.Manufacturer -in $manufacturerInfo.Value) {
$ansibleFacts.ansible_virtualization_role = 'guest'
$ansibleFacts.ansible_virtualization_type = $manufacturerInfo.Key
return
}
}
$ansibleFacts.ansible_virtualization_role = 'NA'
$ansibleFacts.ansible_virtualization_type = 'NA'
}
}
)
$groupedSubsets = @{
min = [System.Collections.Generic.List[string]]@('date_time', 'distribution', 'dns', 'env', 'local', 'platform', 'powershell_version', 'user')
network = [System.Collections.Generic.List[string]]@('all_ipv4_addresses', 'all_ipv6_addresses', 'interfaces', 'windows_domain', 'winrm')
hardware = [System.Collections.Generic.List[string]]@('bios', 'memory', 'processor', 'uptime', 'virtual')
external = [System.Collections.Generic.List[string]]@('facter')
}
# build "all" set from everything mentioned in the group- this means every value must be in at least one subset to be considered legal
$allSet = [System.Collections.Generic.HashSet[string]]@()
foreach ($kv in $groupedSubsets.GetEnumerator()) {
$null = $allSet.UnionWith($kv.Value)
}
# dynamically create an "all" subset now that we know what should be in it
$groupedSubsets.all = [System.Collections.Generic.List[string]]$allSet
# start with all, build up gather and exclude subsets
$actualSubset = [System.Collections.Generic.HashSet[string]]$groupedSubsets.all
$explicitSubset = [System.Collections.Generic.HashSet[string]]@()
$excludeSubset = [System.Collections.Generic.HashSet[string]]@()
foreach ($item in $gatherSubset) {
if ($item.StartsWith('!')) {
$item = $item.Substring(1)
if ($item -eq "all") {
$allMinusMin = [System.Collections.Generic.HashSet[string]]@($allSet)
$null = $allMinusMin.ExceptWith($groupedSubsets.min)
$null = $excludeSubset.UnionWith($allMinusMin)
}
elseif ($groupedSubsets.ContainsKey($item)) {
$null = $excludeSubset.UnionWith($groupedSubsets[$item])
}
elseif ($allSet.Contains($item)) {
$null = $excludeSubset.Add($item)
}
# NB: invalid exclude values are ignored, since that's what posix setup does
}
else {
if ($groupedSubsets.ContainsKey($item)) {
$null = $explicitSubset.UnionWith($groupedSubsets.$item)
}
elseif ($allSet.Contains($item)) {
$null = $explicitSubset.Add($item)
}
else {
# NB: POSIX setup fails on invalid value; we warn, because we don't implement the same set as POSIX
# and we don't have platform-specific config for this...
$module.Warn("invalid value $item specified in gather_subset")
}
}
}
$null = $actualSubset.ExceptWith($excludeSubset)
$null = $actualSubset.UnionWith($explicitSubset)
$ansibleFacts = @{
gather_subset = $gatherSubset
module_setup = $true
}
if ($measureSubset) {
$ansibleFacts.measure_info = @{}
}
$module.Result.ansible_facts = $ansibleFacts
foreach ($meta in $factMeta.GetEnumerator()) {
$metaSubsets = [System.Collections.Generic.HashSet[String]]@($meta.Subsets)
$skip = $true
foreach ($subset in $metaSubsets) {
if ($actualSubset.Contains($subset)) {
$skip = $false
break
}
}
if ($skip) {
continue
}
# Originally tried running in parallel with a runspace pool, it didn't reduce the execution time and just added
# more complexity. Keep running each fact sequentially.
$ps = [PowerShell]::Create()
foreach ($varName in @('ansibleFacts', 'factPath', 'module')) {
$val = Get-Variable -Name $varName -ValueOnly
$null = $ps.AddCommand('Set-Variable').AddParameters(@{Name = $varName; Value = $val })
}
$name = $metaSubsets -join ', '
if ($measureSubset) {
$null = $ps.AddScript('$subset = $args[0]; $time = Get-Date').AddArgument($name)
$null = $ps.AddScript($meta.Code)
$null = $ps.AddScript(@'
$end = (Get-Date) - $time
$ansibleFacts.measure_info.$subset = $end.TotalSeconds
'@)
}
else {
$null = $ps.AddScript($meta.Code)
}
$asyncResult = $ps.BeginInvoke()
$null = $asyncResult.AsyncWaitHandle.WaitOne($gatherTimeout * 1000)
if ($asyncResult.IsCompleted) {
# We don't care about any actual output as each scriptblock sets the ansibleFacts hashtable in the code.
try {
$null = $ps.EndInvoke($asyncResult)
}
catch {
# We want to inner error record not the outer error from .EndInvoke()
$errorRecord = $_.Exception.InnerException.ErrorRecord
$err = "{0}`n{1}" -f (($errorRecord | Out-String), $errorRecord.ScriptStackTrace)
$module.Warn("Error when collecting $($name) facts: $err")
}
# Make sure we warn on any errors that may have occurred.
foreach ($errorRecord in $ps.Streams.Error) {
$err = "{0}`n{1}" -f (($errorRecord | Out-String), $errorRecord.ScriptStackTrace)
$module.Warn("Error when collecting $($name) facts: $err")
}
$ps.Dispose()
}
else {
# Give a best effort chance to stop it, we can't call .Stop() in case it blocks.
$null = $ps.BeginStop($null, $null)
$module.Warn("Failed to collection $($name) due to timeout")
}
}
$module.ExitJson()