I have function with a foreach inside a foreach. My hashtable set inside the foreach is not accessible outside and I don't understand why.
The scope of sampletab is in "Script"
function Generate(){
$script:sampletab=#{}
$y=0
$samples = Invoke-Sqlcmd -Query "SELECT * FROM ..."
foreach ($row in $samples)
{
$paramID = $row["xxx"]
$query = "SELECT * FROM ... ${paramID};"
$parameters = Invoke-Sqlcmd -Query $query
foreach ($row in $parameters)
{
$name = "coucou"
$name = $row["name"]
$sampletab[$y] = #{
$name = $row["value"]
}
}
$y++
break
}
}
Generate
echo $sampletab[0].Keys # gives me only 1 key, should be 20 keys
Okay I found it. It's missing the "+="
$name = $row["name"]
$sampletab[$y] += #{
$name = $row["value"]
}
thank you !
Related
I am trying to convert the below code to a PowerShell function as I will be using it multiple times in other scripts. I am trying to figure out how I can allow the user to simply pass a SQL Query (a String) to this script that I will turn into a function and have my hash table $row define the keys dynamically based on the column headers and then assign the values when multiple rows are returned by my SQL Query variable $QueryResults. Also, I will have two common variables in each hash table, which are "DateTime" and "ServerName" so those will have to be added as well.
$endpoint = "myPowerBIEndpoint"
$Query = "
myQueryThatReturnsMultipleRecords
"
$GetDate = (Get-Date).AddMinutes(-1)
while($true)
{
if((Get-Date) -gt $GetDate)
{
#Only run the below every 5 minutes
$GetDate = (Get-Date).AddMinutes(5)
#Get list of server to iterate through
$ServerList = Invoke-DbaQuery -SqlInstance "" -Database "" -Query "
EXEC [dbo].[myProc] #MonitorType" -SqlParameters #{MonitorType = "AGHealth"}
}
$ServerResults = #() # Results from each of the servers will be stored here
foreach ($Server in $ServerList)
{
$QueryResults = Invoke-DbaQuery -SqlInstance $ServerList.Cname -Database "" -Query $Query
$DateTime = Get-Date -DisplayHint Datetime -Format "MM/dd/yyyy HH:mm:ss"
foreach ($object in $QueryResults)
{
Write-Host 'Building Payload for' $object.replica_server_name
$row = #{
"DateTime" = $DateTime
"Replica_Server" = $object.replica_server_name
"Log_Send_Queue_Size_mb" = $object.total_log_send_queue_size_mb
"Redo_Queue_Size_mb" = $object.total_redo_queue_size_mb
"Sync_Health" = $object.synchronization_health
"Is_Primary_Replica" = $object.is_primary_replica
"AG_Name" = $object.ag_name
"ServerName" = $Server.Cname
}
$ServerResults += $row
}
}
$payload = #{ "rows" = $ServerResults }
Write-Host 'Invoking Rest Method'
Invoke-RestMethod -Method Post -Uri "$endpoint" -Body (ConvertTo-Json $payload)
(ConvertTo-Json $payload)
sleep 60
}
As Invoke-DbaQueryis somehow a wrapper of Invoke-SqlCmd, we can use the schema information stored in the returned PSObject [DataRow] for each row. Each one has a Table member filled by the command describing the resulting schema for the row. In this [DataTable]Table member, we have the [DataColumnCollection]Columns which describes each column of the row as [DataColmun]. We can then use that to feed a hash with the ColumnName member as a key and use the indexed array Item of the row for this ColumnName as value.
The specific corresponding PowerShell code, we add DateTime and ServerName distinctively because they are not coming from the [DataRow] itself :
$row = #{}
$object.Table.Columns | %{ $row.Add($_.ColumnName, $object.Item($_.ColumnName) ) }
$row.Add("DateTime", $DateTime)
$row.Add("ServerName", $Server.Cname)
The resulting script :
$GetDate = (Get-Date).AddMinutes(-1)
while($true)
{
if((Get-Date) -gt $GetDate)
{
#Only run the below every 5 minutes
$GetDate = (Get-Date).AddMinutes(5)
#Get list of server to iterate through
$ServerList = Invoke-DbaQuery -SqlInstance "" -Database "" -Query "
EXEC [dbo].[myProc] #MonitorType" -SqlParameters #{MonitorType = "AGHealth"}
}
$ServerResults = #() # Results from each of the servers will be stored here
foreach ($Server in $ServerList)
{
$QueryResults = Invoke-DbaQuery -SqlInstance $ServerList.Cname -Database "" -Query $Query
$DateTime = Get-Date -DisplayHint Datetime -Format "MM/dd/yyyy HH:mm:ss"
foreach ($object in $QueryResults)
{
Write-Host 'Building Payload for' $object.replica_server_name
$row = #{}
$object.Table.Columns | %{ $row.Add($_.ColumnName, $object.Item($_.ColumnName) ) }
$row.Add("DateTime", $DateTime)
$row.Add("ServerName", $Server.Cname)
$ServerResults += $row
}
}
$payload = #{ "rows" = $ServerResults }
Write-Host 'Invoking Rest Method'
Invoke-RestMethod -Method Post -Uri "$endpoint" -Body (ConvertTo-Json $payload)
(ConvertTo-Json $payload)
sleep 60
}
I am in the process of re-writing the script below to be able to run in parallel, as can be seen in the code, an array of servers is passed to the script, and then it loads it onto a hash table, loops through each server at a time to do the deployment, for each server there are files to execute in a particular order (see array of files). Looking at the structure, I feel workspace is the way to go here but I could be wrong.
Where the performance gains can be seen in my opinion or having the code such that multiple servers can be executed at thesame time rather than waiting for each server to complete and move onto the next one. foreach parallel
I ran a test to call a function declared outside a workspace, it worked.Is this good practice to call a function declared outside a workspace ? I ask this because I would like to reuse some functions outside the workspace, or is it generally better to put all the code in the workspace even ones that are not intended for parallel workloads i.e one off calls to the code. ?
The below is the code I am testing with.
Function Check-Instance-Connection{
param
(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
$sql_server,
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=1)]
$db_name
)
try
{
#Return extra useful info by using custom objects
$check_outcome = "" | Select-Object -Property log_date, stage, status, error_message
$check_outcome.log_date = (Get-Date)
$check_outcome.stage = 'Ping SQL instance for $sql_server'
#test connection for a sql instance
$connectionstring = "Data Source=$sql_server;Integrated Security =true;Initial Catalog=$db_name;Connect Timeout=5;"
$sqllconnection = New-Object System.Data.SqlClient.SqlConnection $connectionstring
$sqllconnection.Open();
$check_outcome.status = $true
$check_outcome.error_message = ''
return $check_outcome
}
Catch
{
$check_outcome.status = $false
$check_outcome.error_message = $_.Exception.Message
return $check_outcome
}
finally{
$sqllconnection.Close();
}
}
$file_list = #("deployment_1.sql","deployment_2.sql","deployment_3.sql","deployment_4.sql","deployment_5.sql")
$x = (1,"Server1",3,1),(4,"Server2",6,2),(3,"Server3",4,3)
$k = 'serverid','servername','locationid','appid' # key names correspond to data positions in each array in $x
$h = #{}
For($i=0;$i -lt $x[0].length; $i++){
$x |
ForEach-Object{
[array]$h.($k[$i]) += [string]$_[$i]
}
}
$folder = "F:\Files\"
$database_name = "Test"
foreach ($server_id in $all_server_ids)
{
$severid = $h["serverid"][$all_server_ids.indexof($server_id)]
$servername = $h["servername"][$all_server_ids.indexof($server_id)]
$locationid = $h["locationid"][$all_server_ids.indexof($server_id)]
$message = 'ServerID {0} has a servername of {1} and a location id of {2}' -f $server_id, $h["servername"][$all_server_ids.indexof($server_id)],$h["locationid"][$all_server_ids.indexof($server_id)]
Write-Output $message
Write-Output "This $severid and this $servername and this $locationid"
foreach ($file in $file_list)
{
$is_instance_ok = Check-Instance-Connection $servername $database_name
if ($is_instance_ok.check_outcome -eq $true){
invoke-sqlcmd -ServerInstance "$servername" -inputfile $folder$file -Database "$database_name" -Querytimeout 60 -OutputSqlErrors $true -ConnectionTimeout 10 -ErrorAction Continue -Errorvariable generated_error | Out-Null
}
}
}
Thanks, I did a lot more research and looked at a lot of examples on how workflows work. This is what I have come up with.
Workflow RunExecution
{
Function Check-Instance-Connection{
param
(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
$sql_server,
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=1)]
$db_name
)
try
{
#Return extra useful info by using custom objects
$check_outcome = "" | Select-Object -Property log_date, stage, status, error_message
$check_outcome.log_date = (Get-Date)
$check_outcome.stage = 'Ping SQL instance for $sql_server'
#test connection for a sql instance
$connectionstring = "Data Source=$sql_server;Integrated Security =true;Initial Catalog=$db_name;Connect Timeout=5;"
$sqllconnection = New-Object System.Data.SqlClient.SqlConnection $connectionstring
$sqllconnection.Open();
$check_outcome.status = $true
$check_outcome.error_message = ''
return $check_outcome
}
Catch
{
$check_outcome.status = $false
$check_outcome.error_message = $_.Exception.Message
return $check_outcome
}
finally{
$sqllconnection.Close();
}
}
$file_list = #("deployment_1.sql","deployment_2.sql","deployment_3.sql","deployment_4.sql","deployment_5.sql")
$x = (1,"server1\DEV3",3,1),(4,"serer1\DEV2",6,2),(3,"serer2\DEV1",4,3)
$k = 'serverid','servername','locationid','appid'
$h = #{}
For($i=0;$i -lt $x[0].length; $i++){
$x |
ForEach-Object{
[array]$h.($k[$i]) += [string]$_[$i]
}
}
$folder = "C:\Temp\"
$database_name = "Test"
$all_server_ids = $h['serverid']
foreach -parallel ($server_id in $all_server_ids)
{
$severid = $h["serverid"][$all_server_ids.indexof($server_id)]
$servername = $h["servername"][$all_server_ids.indexof($server_id)]
$locationid = $h["locationid"][$all_server_ids.indexof($server_id)]
foreach ($file in $file_list)
{
# $check_fine = $is_instance_ok.check_outcome
# if ($check_fine = $true){
invoke-sqlcmd -ServerInstance "$servername" -inputfile $folder$file -Database "$database_name" -Querytimeout 60 -OutputSqlErrors $true -ConnectionTimeout 10 -ErrorAction Continue
write-output "invoke-sqlcmd -ServerInstance $servername -inputfile $folder$file -Database $database_name -Querytimeout 60 -OutputSqlErrors $true -ConnectionTimeout 10 -ErrorAction Continue "
# }
}
}
}
RunExecution
I have an array of hashtables and I need to find if there are elements who has the same Name.
I have this HasDuplicate function which return True or False if the array contains duplicate or not.
What I am doing here is that I am iterating through each element and add Name of it to another array, and then check it if it exists. But this code does not looks good, and I was thinking if there is another way of achieving this
# object looks like this
$array = #(
#{ Name = 'First', Passed = $True }
#{ Name = 'First', Passed = $False }
)
Function HasDuplicate
{
param($array)
$all = #()
foreach($item in $array)
{
$item_name = $item.Name
if($all -contains $item_name)
{
Write-Error "Duplicate name ""$item_name"""
return $True
}
else
{
$all += $item_name
}
}
return $False
}
Group-Object is probably the easiet, something like this:
$array = #(
#{ Name = 'First'; Passed = $True }
#{ Name = 'First'; Passed = $False }
)
$array.Name | Group-Object | Where-Object Count -GT 1
Another way you could do it using an hash table:
$array = #(
#{ Name = 'First'; Passed = $True }
#{ Name = 'First'; Passed = $False }
)
$h = #{}
$array | % {$h[$_.Name] += 1 }
$h.GetEnumerator() | Where value -GT 1
This might not be very good looking compared to the other answers, but you could just count your names in another hashtable then output the duplicates afterwards.
$array = #(
#{ Name = 'First'; Passed = $True }
#{ Name = 'First'; Passed = $False }
);
# Count names in array
$counts = #{}
foreach ($object in $array) {
$name = $object.Name
if (-not $counts.ContainsKey($name)) {
$counts[$name] = 0
}
$counts[$name] += 1
}
# Output duplicates
foreach ($name in $counts.Keys) {
if ($counts[$name] -gt 1) {
Write-Output ("Duplicate Name: " + $name)
}
}
Output:
Duplicate Name: First
I would like to create groups out of two hash tables
$var_computer = #{
"Notebooks" = "NB";
"PCs" = "PC";
}
$var_os = #{
"Windows10" = "Windows 10*";
"Windows8.1" = "Windows 8.1*";
}
function create_groups ($var_computer, $var_os, $path){
ForEach ($key in ($var_computer).Keys) {
$new_group_name = "_DEFAULT_" + $key
if ((Get-ADGroup -Filter {Name -eq $new_group_name} -SearchBase $path) -eq $null) {
New-ADGroup -name $new_group_name -GroupScope Global -Path $path
}
So that i get
_DEFAULT_Notebooks_Windows10
_DEFAULT_Notebooks_Windows8.1
_DEFAULT_PCs_Windows10
...
How could i add another key in my $new_group_name so that it doesn't start at the first key in my second hash table.
Or would it be easier to have nested hash tables?
Is this what you need? Foreach OS foreach computer-type
$var_computer = #{
"Notebooks" = "NB";
"PCs" = "PC";
}
$var_os = #{
"Windows10" = "Windows 10*";
"Windows8.1" = "Windows 8.1*";
}
foreach ($type in $var_computer.Keys) {
foreach ($os in $var_os.Keys) {
$new_group_name = "_DEFAULT_$($type)_$($os)"
$new_group_name
#Create group
}
}
Output:
_DEFAULT_Notebooks_Windows8.1
_DEFAULT_Notebooks_Windows10
_DEFAULT_PCs_Windows8.1
_DEFAULT_PCs_Windows10
I've created two functions in PowerShell, one that creates the Namespace ROOT\CustomCMClasses and one that creates the class Test. This piece works fine:
Param (
$Namespace = 'CustomCMClasses',
$Class = 'Test'
)
Function New-WMINamespaceHC{
if (Get-WmiObject -Namespace 'root' -Class '__NAMESPACE' | Where-Object {$_.Name -eq $Namespace}) {
Write-Verbose "WMI Namespace 'root\$Namespace' exists"
}
else {
Write-Verbose "Create WMI namespace 'root\$Namespace'"
$Ns = [WMICLASS]'root:__Namespace'
$NewNamespace = $Ns.createInstance()
$NewNamespace.Name = $Namespace
$NewNamespace.Put()
}
}
Function New-WMIClassHC {
if (Get-WmiObject -List -Namespace "root\$Namespace" | Where-Object {$_.Name -eq $Class}) {
Write-Verbose "WMI Class '$Class' exists"
}
else {
Write-Verbose "Create WMI Class '$Class'"
$NewClass = New-Object System.Management.ManagementClass ("root\$Namespace", [String]::Empty, $Null);
$NewClass['__CLASS'] = $Class
$NewClass.Qualifiers.Add('Static', $true)
$NewClass.Properties.Add('Key', [System.Management.CimType]::String, $false)
$NewClass.Properties['Key'].Qualifiers.Add('Key', $true)
$NewClass.Properties.Add('Value1', [System.Management.CimType]::UInt32, $false)
$NewClass.Properties.Add('Value2', [System.Management.CimType]::String, $false)
$NewClass.Put()
}
}
At this point I need to grant Domain users permissions on the new namespace, so they are able to write data to it on their local client (no remoting needed). This is the point where I find a lot of information but am now stuck.
On the Microsoft blog there is a script I tried to tweak, but it's overly complicated for my needs and it failed so I found and tweaked the following code:
Function Add-WMIPermissions {
[CmdLetBinding()]
Param (
[String]$Principal = 'DOMAIN.NET\Domain users',
[String]$Namespace = 'CustomCMClasses'
)
Function Get-Sid {
Param (
$DSIdentity
)
$ID = new-object System.Security.Principal.NTAccount($DSIdentity)
Return $ID.Translate([System.Security.Principal.SecurityIdentifier]).toString()
}
$Sid = Get-Sid $Principal
$WMISDDL = "A;CI;CCWP;;;$Sid"
$WMISDDLPartialMatch = "A;\w*;\w+;;;$Sid"
$security = Get-WmiObject -Namespace root\$Namespace -Class __SystemSecurity
$binarySD = #($null)
$result = $security.PsBase.InvokeMethod('GetSD',$binarySD)
$converter = New-Object system.management.ManagementClass Win32_SecurityDescriptorHelper
$CurrentWMISDDL = $converter.BinarySDToSDDL($binarySD[0])
if (($CurrentWMISDDL.SDDL -match $WMISDDLPartialMatch) -and ($CurrentWMISDDL.SDDL -notmatch $WMISDDL)) {
$NewWMISDDL = $CurrentWMISDDL.SDDL -replace $WMISDDLPartialMatch, $WMISDDL
}
else {
$NewWMISDDL = $CurrentWMISDDL.SDDL += '(' + $WMISDDL + ')'
}
$WMIbinarySD = $converter.SDDLToBinarySD($NewWMISDDL)
$WMIconvertedPermissions = ,$WMIbinarySD.BinarySD
if ($CurrentWMISDDL.SDDL -match $WMISDDL) {
Write-Verbose 'Current WMI Permissions matches desired value'
}
else {
$result = $security.PsBase.InvokeMethod('SetSD',$WMIconvertedPermissions)
if($result='0'){
Write-Verbose 'WMI permissions applied'
}
}
}
Add-WMIPermissions -Verbose
It's clearly stating that the permissions are correctly applied but the user still can't write data to WMI. Working with WMI is new to me, so any help is greatly appreciated.
The test code to see if a regular user (Domain users) has permissions:
$WMIClass = [WMICLASS]('root\' + $Namespace + ':' + $Class)
$WMIInstance = $WMIClass.CreateInstance()
$WMIInstance.Key = 'Unique value identifier 5'
$WMIInstance.Value1 = 101
$WMIInstance.Value2 = 'Status Ok'
$WMIInstance.Put()
Solved the problem for Partial Write:
Function Set-WMIPermissionsHC {
Param (
[String]$Namespace = 'CustomCMClasses',
[String]$Class = 'Test',
[String]$Account = 'DOMAIN\Domain users',
[String]$Computer = $env:COMPUTERNAME
)
Function Get-Sid {
Param (
$Account
)
$ID = New-Object System.Security.Principal.NTAccount($Account)
Return $ID.Translate([System.Security.Principal.SecurityIdentifier]).toString()
}
$SID = Get-Sid $Account
$SDDL = "A;CI;CCSWWP;;;$SID"
$DCOMSDDL = "A;;CCDCRP;;;$SID"
$Reg = [WMICLASS]"\\$Computer\root\default:StdRegProv"
$DCOM = $Reg.GetBinaryValue(2147483650,'software\microsoft\ole','MachineLaunchRestriction').uValue
$Security = Get-WmiObject -ComputerName $Computer -Namespace "root\$Namespace" -Class __SystemSecurity
$Converter = New-Object System.Management.ManagementClass Win32_SecurityDescriptorHelper
$BinarySD = #($null)
$Result = $Security.PsBase.InvokeMethod('GetSD', $BinarySD)
$OutSDDL = $Converter.BinarySDToSDDL($BinarySD[0])
$OutDCOMSDDL = $Converter.BinarySDToSDDL($DCOM)
$NewSDDL = $OutSDDL.SDDL += '(' + $SDDL + ')'
$NewDCOMSDDL = $OutDCOMSDDL.SDDL += '(' + $DCOMSDDL + ')'
$WMIbinarySD = $Converter.SDDLToBinarySD($NewSDDL)
$WMIconvertedPermissions = ,$WMIbinarySD.BinarySD
$DCOMbinarySD = $Converter.SDDLToBinarySD($NewDCOMSDDL)
$DCOMconvertedPermissions = ,$DCOMbinarySD.BinarySD
$Result = $Security.PsBase.InvokeMethod('SetSD', $WMIconvertedPermissions)
$Result = $Reg.SetBinaryValue(2147483650,'software\microsoft\ole','MachineLaunchRestriction', $DCOMbinarySD.binarySD)
Write-Verbose 'WMI Permissions set'
}
Thanks to this blog and the Microsoft blog for explaining the permissions