How to use "*" in nodeName - powershell

Say I have 2 roles in my DSC setup and I have variable amount of nodes in my setup:
$configdata = #{
AllNodes = #(
#{
NodeName = "*Web*" # < problem lies here
# can be prodWeb## or devWeb##
Role = "IIS", "basic"
}
#{
NodeName = "*"
Role = "basic"
}
)
}
DSC resource:
Configuration CFG
{
$AllNodes.where{ $_.Role.Contains("Basic") }.NodeName
{
...
}
$AllNodes.where{ $_.Role.Contains("IIS") }.NodeName
{
...
}
}
Can I achieve that?

The AllNodes entry in configuration data is an array of hashtables. Each hashtable needs to have a key NodeName. The value will get substituted when the expression evaluates. So a nodename like web* will not work

So, basically what I've done is this:
Configuration Windows
{
node $allnodes.NodeName {
switch ($Node.Role) {
"Role1" {
...
}
"Role2" {
...
}
"Role3" {
...
}
}
}
}
My configuration data:
#{ AllNodes = #( #{ NodeName = "web"; Role = "Role1", "Role2" } ) }
And for another set of nodes:
#{ AllNodes = #( #{ NodeName = "other"; Role = "Role1", "Role3" } ) }
I'm using Azure Automation to assign configurations to nodes, so it doesn't check the node name, it just applies whatever roles the configuration had at compile time.

Related

How to reverse traverse a nested hashtable in PowerShell

In a previous question, I was given a solution for finding the name of a key by its value. Unfortunately I neglected to consider the application of Security Groups
Old hashtable and working function
$Departments = #{
'Sales' = #{
'SAM' = 'Manager'
'SAP' = 'Person'
}
'IT' = #{
'ITM' = 'Manager'
'ITS' = 'Specialist'
'ITT' = 'Technician'
'ITC' = 'Consultant'
}
}
function Get-DepartmentOU {
Param (
[CmdletBinding()]
[Parameter(Mandatory = $true)]
[System.String]
$RoleCode
)
# Get the DictionaryEntry in the main Hashtable where the nested Hashtable value matches the role you are looking for.
$Department = $script:Departments.GetEnumerator() | Where-Object { $_.Value.ContainsKey($RoleCode) }
# Print the name of the DictionaryEntry (Your department) and retrieve the value from the Hashtable for the role.
"Department: $($Department.Name) Job Title: $($Department.Value[$RoleCode])"
}
$JobCode = "SAM"
$DepartmentInfo = Get-DepartmentOU -RoleCode $JobCode
$DepartmentInfo
OUTPUT: Department: Sales Job Title: Manager
The above works excellently, however I've now created a deeper hashtable and need to do the same thing, just another level to extract more information.
New hashtable
$Departments = #{
"Parts" = #{
"SG-Parts" = #{
"PAM" = "Parts Manager"
"PAA" = "Parts Advisor"
}
}
"Sales" = #{
"SG-Sales" = #{
"SAP" = "Sales Person"
"SAR" = "Receptionist"
}
"SG-Sales Managers" = #{
"SGM" = "General Manager"
"SAM" = "Sales Manager"
}
}
}
How should I change the working function to display the text contained in the key
OUTPUT: SG: SG-Sales Managers Department: Sales Job Title: Manager
This may help to visualize the data structure that #mathias-r.-jessen's 'flat role table/map' code generates:
$Rolemap = #{
"PAC" = #{
"Title" = "Parts Consultant"
"SG" = "SG-Parts"
"Department" = "Parts"
}
"PAA" = #{
"Title" = "Parts Advisor"
"SG" = "SG-Parts"
"Department" = "Parts"
}
"SGM" = #{
"Title" = "General Manager"
"SG" = "SG-Sales Managers"
"Department" = "Sales"
}
}
The original data structure is quicker for humans to modify as needed as it mimics a common small-scale Active Directory setup.
When using the original data format, each new role addition only has to be added to the correct 'group' within the hashtable. However, reversing the "SAM" = "Sales Manager" to "Sales Manager"="SAM" may help for legibility and logic / structure.
I used $RoleMap |Format-Custom and some manual typing to build the generated table visualization text.
Create a new flat role table from your OU-like hashtable structure:
$RoleMap = #{}
foreach($departmentCode in $Departments.psbase.Keys){
foreach($sgCode in $Departments[$departmentCode].psbase.Keys){
foreach($roleCode in $Departments[$departmentCode][$sgCode].psbase.Keys){
# Create a summary object that includes all 3 pieces of information
# Store in role table and use the "role code" as the key
$RoleMap[$roleCode] = [pscustomobject]#{
Title = $Departments[$departmentCode][$sgCode][$roleCode]
SG = $sgCode
Department = $departmentCode
}
}
}
}
Now you can avoid ...GetEnumerator() | Where-Object { ... } completely when resolving the role code:
function Get-DepartmentOU {
param(
[CmdletBinding()]
[Parameter(Mandatory = $true)]
[string]
$RoleCode
)
if($script:RoleMap.Contains($RoleCode)){
# `Where-Object` no longer needed!
$roleDescription = $script:RoleMap[$RoleCode]
"SG: $($roleDescription.SG) Department: $($roleDescription.Name) Job Title: $($roleDescription.Title)"
}
}

Possible to repeat an alias among different parameter sets in a powershell function?

Suppose I have:
function f {
[CmdletBinding(DefaultParameterSetName='x')]
param(
[Parameter(Mandatory,ParameterSetName='x')]
[Alias('a')]
[int]$Apple,
[Parameter(Mandatory,ParameterSetName='y')]
[Alias('b')]
[int]$Banana,
[Parameter(Mandatory,ParameterSetName='x')]
[Alias('b')]
[int]$Cherry
)
"Apple $Apple"
"Banana $Banana"
"Cherry $Cherry"
}
I'd like to be able to call f -Apple 1 -b 3 because including Apple means I'm certainly using Parameter Set x, but powershell complains that the alias b is declared multiple times.
Is it entirely impossible, or am I just missing a trick?
The non-trivial function I'm trying to write is a convenience wrapper for multiple external functions that have their own aliases, some of which can be the same for different named parameters, but the set of mandatory parameters would never be ambiguous.
I couldn't get it to work using regular params, but I found a workaround by defining Banana and Cherry as dynamic params. This way the alias b is only defined once, so PowerShell won't complain.
function f {
[CmdletBinding(DefaultParameterSetName='x')]
param(
[Parameter(Mandatory, ParameterSetName='x')]
[Alias('a')]
[int]$Apple
)
DynamicParam {
# If param 'Apple' exists, define dynamic param 'Cherry',
# else define dynamic param 'Banana', both using alias 'b'.
if( $PSBoundParameters.ContainsKey('Apple') ) {
$paramName = 'Cherry'
$paramSetName = 'x'
} else {
$paramName = 'Banana'
$paramSetName = 'y'
}
$aliasName = 'b'
$parameterAttribute = [System.Management.Automation.ParameterAttribute]#{
ParameterSetName = $paramSetName
Mandatory = $true
}
$aliasAttribute = [System.Management.Automation.AliasAttribute]::new( $aliasName )
$attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
$attributeCollection.Add( $parameterAttribute )
$attributeCollection.Add( $aliasAttribute )
$dynParam = [System.Management.Automation.RuntimeDefinedParameter]::new(
$paramName, [Int32], $attributeCollection
)
$paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
$paramDictionary.Add($paramName, $dynParam)
$paramDictionary
}
process {
"--- Parameter Set '$($PSCmdlet.ParameterSetName)' ---"
if( $PSBoundParameters.ContainsKey('Apple') ) {
"Apple $Apple"
}
if( $PSBoundParameters.ContainsKey('Banana') ) {
# Dynamic params require special syntax to read
$Banana = $PSBoundParameters.Banana
"Banana $Banana"
}
if( $PSBoundParameters.ContainsKey('Cherry') ) {
# Dynamic params require special syntax to read
$Cherry = $PSBoundParameters.Cherry
"Cherry $Cherry"
}
}
}
Calling the function:
f -Apple 1 -b 3
f -b 2
Output:
--- Parameter Set 'x' ---
Apple 1
Cherry 3
--- Parameter Set 'y' ---
Banana 2

Split PSCustomObjects in to strings/array

Hi I have n sum of objects for each Node like this:
NodeName : 11111
System.AreaId : 2375
System.AreaPath : Project
System.TeamProject : Project
System.NodeName : Project
System.AreaLevel1 : Project
Every node can have different objects in it.
How can I split them to an arrays/strings without specifying the object name so I can create foreach separate object loop?
mklement0 beat me to what I was going to post. Since I have the code drafted already I will post it.
Like mklement0 said in comments, you can access object properties through use of .psobject.Properties. Below in the code I am using a switch statement to check if an object contains a specific property.
$objs = #(
[pscustomobject]#{
AreaId = 2375
AreaPath = ''
TeamProject = 'Project2'
NodeName = ''
AreaLevel1 = ''
},
[pscustomobject]#{
AreaId = 342
AreaPath = ''
TeamProject = 'Project2'
Color = 'Red'
}
)
switch ($objs) {
{ $_.psobject.properties.name -contains 'Color' } {
'Object contains Color property'
}
{ $_.psobject.properties.name -contains 'NodeName' } {
'Object contains NodeName property'
}
Default {}
}

How to identify the default audio device in Powershell?

I am looking for a solution to get the default audio device via Powershell.
In best case, it could work via embedded C#-code to directly use IMMDeviceEnumerator::GetDefaultAudioEndpoint (see here IMMDeviceEnumertor).
But if it is easier to get this via RegKeys, then this is also OK.
I have seen a couple of code-snippets reading the keys from HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\Render or \Capture, but I still struggle to identify the DEFAULT device.
It seems, when I do modify the order of devices, then I can simply search for active devices (DeviceState=1) and then sort by the values "Level:0", "Level:1" and "Level:2", but the level-values are not available on a system, where the user has not modied the order manually. What is the sort-criteria in such case?
This is the code-snippet to solve it via RegKeys, but as mentioned - not working for all situations:
$regAudio = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio"
$nameId = "{b3f8fa53-0004-438e-9003-51a46e139bfc},6"
$classId = "{a45c254e-df1c-4efd-8020-67d146a850e0},2"
$driverDetails = "{83da6326-97a6-4088-9453-a1923f573b29},3"
function get-DefaultDevice($type) {
$activeDevices = foreach($key in Get-ChildItem "$regAudio\$type\") {
foreach($item in Get-ItemProperty $key.PsPath) {
if ($item.DeviceState -eq $activeState) {$item}
}
}
$defaultDevice = $activeDevices | Sort-Object -Property "Level:0","Level:1","Level:2" | select -last 1
$details = Get-ItemProperty "$($defaultDevice.PSPath)\Properties"
$name = "$($details.$classId) ($($details.$nameId))"
return #{
name = $name
driver = $details.$driverDetails
}
}
$OsRender = get-DefaultDevice "Render"
$OsCapture = get-DefaultDevice "Capture"
Is there any way to get this info "in a smart way" (without any external DLLs, of course)?
Finally I figured it out and I am happy to share the working code-snippet:
cls
Add-Type #'
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDevice {
int a(); int o();
int GetId([MarshalAs(UnmanagedType.LPWStr)] out string id);
}
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDeviceEnumerator {
int f();
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint);
}
[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] class MMDeviceEnumeratorComObject { }
public static string GetDefault (int direction) {
var enumerator = new MMDeviceEnumeratorComObject() as IMMDeviceEnumerator;
IMMDevice dev = null;
Marshal.ThrowExceptionForHR(enumerator.GetDefaultAudioEndpoint(direction, 1, out dev));
string id = null;
Marshal.ThrowExceptionForHR(dev.GetId(out id));
return id;
}
'# -name audio -Namespace system
function getFriendlyName($id) {
$reg = "HKLM:\SYSTEM\CurrentControlSet\Enum\SWD\MMDEVAPI\$id"
return (get-ItemProperty $reg).FriendlyName
}
$id0 = [audio]::GetDefault(0)
$id1 = [audio]::GetDefault(1)
write-host "Default Speaker: $(getFriendlyName $id0)"
write-host "Default Micro : $(getFriendlyName $id1)"
and in case you need a language-neutral international name of each device MMDEVICE-id and (optional) its driver then use this function:
# https://github.com/tpn/winsdk-10/blob/master/Include/10.0.16299.0/shared/devpkey.h
# https://github.com/EddieRingle/portaudio/blob/master/src/hostapi/wasapi/mingw-include/mmdeviceapi.h
$regId = "{b3f8fa53-0004-438e-9003-51a46e139bfc},2"
$regName = "{b3f8fa53-0004-438e-9003-51a46e139bfc},6"
$regFormFactor = "{1da5d803-d492-4edd-8c23-e0c0ffee7f0e},0"
# https://learn.microsoft.com/en-us/windows/win32/api/mmdeviceapi/ne-mmdeviceapi-endpointformfactor
$formFactor = #(
"RemoteNetworkDevice",
"Speakers",
"LineLevel",
"Headphones",
"Microphone",
"Headset",
"Handset",
"UnknownDigitalPassthrough",
"SPDIF",
"DigitalAudioDisplayDevice",
"UnknownFormFactor"
)
function getInternationalNameAndDriver($id) {
$guid = $id.Substring(17)
$subKey = #("Render","Capture")[[int]::Parse($id[5])]
$reg = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\MMDevices\Audio\$subKey\$guid\Properties"
$details = get-ItemProperty $reg -ea 0
if ($details) {
$id = $details.$regId.subString(4)
$name = $details.$regName
$form = $formFactor[$details.$regFormFactor]
$hardware = get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Enum\$id"
$regDrv = $hardware.Driver
$driver = Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Class\$regDrv"
$drvName = $driver.DriverDesc
$drvVersion = $driver.DriverVersion
}
return "$form ($name), driver: $drvName $drvVersion"
}
this gives you an output like this:
Default Speaker: Speakers (Realtek Audio), driver: Realtek Audio 6.0.1.6127
Default Micro : Microphone (Logitech BRIO), driver: USB Audio Device 10.0.22000.653

Get all IP Addresses assigned to an EC2 Instance

The function below as well as others that I have in the script to collect a complete inventory of everything we have in AWS, runs without any problems.
However, I am missing all of the IP addresses that are assigned to the instance after the first one when the instance has more than one interface.
How can I make sure to get all the ip addresses of every instance in the function below before writing the details into the excel worksheet?
function Create-EC2InstanceWorksheet {
#Creating EC2 Instances Worksheet
# Add Excel worksheet
$workbook.Worksheets.Add()
# We need to create a sheet for the Instances
$InstancesWorksheet = $workbook.Worksheets.Item(1)
$InstancesWorksheet.Name = 'Instances'
# Headers for the Instance worksheet
$InstancesWorksheet.Cells.Item(1,1) = 'Region'
$InstancesWorksheet.Cells.Item(1,2) = 'Instance Name'
$InstancesWorksheet.Cells.Item(1,3) = 'Image ID'
$InstancesWorksheet.Cells.Item(1,4) = 'Instance ID'
$InstancesWorksheet.Cells.Item(1,5) = 'PEM File'
$InstancesWorksheet.Cells.Item(1,6) = 'Instance Type'
$InstancesWorksheet.Cells.Item(1,7) = 'Private IP'
$InstancesWorksheet.Cells.Item(1,8) = 'Public IP'
$InstancesWorksheet.Cells.Item(1,9) = 'VPC ID'
$InstancesWorksheet.Cells.Item(1,10) = 'Subnet ID'
$InstancesWorksheet.Cells.Item(1,11) = 'State'
$InstancesWorksheet.Cells.Item(1,12) = 'Security Group Id'
$InstancesWorksheet.Cells.Item(1,13) = 'Source/Dest Check'
# Excel Cell Counter
$row_counter = 3
$column_counter = 1
# Get the Ec2 instances for each region
foreach($AWS_Locations_Iterator in $AWS_Locations){
$EC2Instances = Get-EC2Instance -Region $AWS_Locations_Iterator
# Iterating over each instance
foreach($EC2Instances_Iterator.Instances.NetworkInterfaces.PrivateIpAddresses.PrivateIpAddress in $EC2Instances){
foreach($EC2Instances_Iterator.Instances.NetworkInterfaces.Pr ...
+ ~
Missing 'in' after variable in foreach loop.
Remove the code above starting at foreach and used the suggestion provided by #AnthonyNeace. Replaced with the foreach below which does provide the additional ip addresses.
foreach($instance in $EC2Instances.Instances){
foreach($networkInterface in $instance.NetworkInterfaces){
"$($instance.InstanceID): $($networkInterface.PrivateIpAddresses.PrivateIpAddress)";
# Ignore if a region does not have any instances
if($EC2Instances_Iterator.count -eq $null) {
continue
}
# Populating the cells
$InstancesWorksheet.Cells.Item($row_counter,$column_counter++) = $AWS_Locations_Iterator
$InstancesWorksheet.Cells.Item($row_counter,$column_counter++) = $EC2Instances_Iterator.Instances.Tags.value
$InstancesWorksheet.Cells.Item($row_counter,$column_counter++) = $EC2Instances_Iterator.Instances.imageid
$InstancesWorksheet.Cells.Item($row_counter,$column_counter++) = $EC2Instances_Iterator.Instances.Instanceid
$InstancesWorksheet.Cells.Item($row_counter,$column_counter++) = $EC2Instances_Iterator.Instances.keyname.tostring()
$InstancesWorksheet.Cells.Item($row_counter,$column_counter++) = $EC2Instances_Iterator.Instances.Instancetype.Value
$InstancesWorksheet.Cells.Item($row_counter,$column_counter++) = $EC2Instances_Iterator.Instances.PrivateIpAddress
$InstancesWorksheet.Cells.Item($row_counter,$column_counter++) = $EC2Instances_Iterator.Instances.PublicIpAddress
$InstancesWorksheet.Cells.Item($row_counter,$column_counter++) = $EC2Instances_Iterator.Instances.VpcId
$InstancesWorksheet.Cells.Item($row_counter,$column_counter++) = $EC2Instances_Iterator.Instances.SubnetId
$InstancesWorksheet.Cells.Item($row_counter,$column_counter++) = $EC2Instances_Iterator.Instances.state.name.value
$InstancesWorksheet.Cells.Item($row_counter,$column_counter++) = $EC2Instances_Iterator.Instances.securitygroups.GroupId
$InstancesWorksheet.Cells.Item($row_counter,$column_counter++) = $EC2Instances_Iterator.Instances.SourceDestCheck
# Seting the row and column counter for next EC2 instance entry
$row_counter = $row_counter + 1
$column_counter = 1
}
# Iterating to the next region
$row_counter = $row_counter + 1
}
}
The network interface is included with each EC2 Instance in the Get-EC2Instance response, so taking the private ip addresses as an example... you could access the private IP addresses by simply iterating over each private ip address exposed on each network interface. Same for the IPv6 addresses.
Object Model
Amazon.EC2.Model.Instance
System.Collections.Generic.List<Amazon.EC2.Model.InstanceNetworkInterface>
System.Collections.Generic.List<Amazon.EC2.Model.InstancePrivateIpAddress>
PrivateIpAddress
System.Collections.Generic.List<Amazon.EC2.Model.InstanceIpv6Address>
Ipv6Address
IPv4 Example
$EC2Instances_Iterator.Instances.NetworkInterfaces.PrivateIpAddresses.PrivateIpAddress
IPv6 Example
$EC2Instances_Iterator.Instances.NetworkInterfaces.Ipv6Addresses.Ipv6Address
Example: Write addresses string using foreach loop
This example builds the addresses across each network interface into a comma-delimited string within the scope of the instances loop. So a string is created for each instance.
The powershell subexpression operator $() is used to resolve complex properties in the string.
$EC2Instances = Get-EC2Instance
foreach($instance in $EC2Instances.Instances){
$addresses = "";
foreach($networkInterface in $instance.NetworkInterfaces){
$addresses = $addresses, $networkInterface.PrivateIpAddresses.PrivateIpAddress -join ","
}
"$($instance.InstanceID): $($addresses.Trim(','))"
}
Further Reading
AWS Documentation - Get-EC2Instance