Cleaner PowerShell code for adding to array - arrays

I'm trying to create a cleaner code for my script. I'm first defining the array, which will be used to check if the user account has been already queried. Currently the code has two nested if statements, which I want to combine into one if possible. Here is my code:
function addUser {
[cmdletbinding()]
Param(
[Parameter(Mandatory=$true)]
[String]$userName
,
[Parameter(Mandatory=$true)]
[String]$userDomain
,
[Parameter(Mandatory=$true)]
[String]$userAccount
)
Begin {}
Process {
$adAttribute = (Get-ADUser -Identity $userName -Server ($userDomain + "<FQDN HERE>") -Properties "<AD ATTRIBUTE NAME HERE>")."<AD ATTRIBUTE NAME HERE>"
Write-Output (
[pscustomobject]#{
userName = $userName
userDomain = $userDomain
userAccount = $userAccount
<AD ATTRIBUTE NAME HERE> = $adAttribute
}
)
}
End {}
}
$userList = #()
Get-Content $sourceFile | % {
<SOME CODE HERE>
if ($userList.Length -eq 0) {
$userList += addUser -userName $userName -userDomain $userDomain -userAccount $userAccount
}
else {
if (-not $userList.userAccount.Contains($userAccount)){
$userList += addUser -userName $userName -userDomain $userDomain -userAccount $userAccount
}
}
$userAdAttribute = $userList.Where({$_.userAccount -eq $userAccount})."<AD ATTRIBUTE NAME HERE>"
}
<SOME CODE HERE>
As you can see the following code is repetative:
$userList += <SOME FUNCTION CODE HERE>
but I cannot figure out how to make it cleaner, as in the beginning the array is empty and I cannot validate it before adding, so I'm wondering if anyone can share a tip?

I think you could do better using a Hashtable or HashSet for that instead of adding to an array with += (which needs to rebuild the entire array in memory on every addition)
$userList = #{}
$userAccount = <SOME CODE HERE>
# add the $userAccount you get from the code block above to the hash
if (-not $userList.Contains($userAccount)) {
# this user has not been processed, so do that here
<CODE TO PROCESS USER>
# next add to the Hashtable
$userList[$userAccount] = $true # the value is not important
}
Using a HashSet:
# on PowerShell below 5.0 use
# $userList = New-Object -TypeName 'System.Collections.Generic.HashSet[String]' -ArgumentList ([StringComparer]::InvariantCultureIgnoreCase)
$userList = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::InvariantCultureIgnoreCase)
$userAccount = <SOME CODE HERE>
# add the $userAccount you get from the code block above to the hash
if (-not $userList.Contains($userAccount)) {
# this user has not been processed, so do that here
<CODE TO PROCESS USER>
# next add to the Hashtable
[void]$userList.Add($userAccount)
}
So, if it is not just about a string holding the userAccount (SamAccountName I gather), but about objects with multiple properties, you then need to use the Hashtable approach:
$userList = #{}
$userObject = <THE CODE THAT CALLS FUNCTION 'addUser' AND RETURNS THE USER OBJECT>
# add the $userObject you get from the code block above to the hash
if (-not $userList.Contains($userObject.userAccount)) {
# this user has not been processed, so do that here
<CODE TO PROCESS USER>
# next add to the Hashtable
$userList[$userObject.userAccount] = $userObject # the value is the userAccount Object
}

Related

Powershell filling array with function calling itself to loop through

tldr; I need to fill an array, which is populated in a function, within constricted language. Until now i found only ways, which are not do able in constricted language.
So basicly i want to loop through the AD and identify looping groups and where users are placed looping wise.
To Achive this i wrote a function which calls itslef. The function returns 4 diffrent objects. These objects are needed to handle the loop.
But the function scope needs to return the value to the script scope ("top most") as otherwise the script will loop infinitly on the first object already.
Unfortunatly this is in constrained language, which means the most common resolves wont work.
Shortend Code Sample
$ReturnValue1 = #()
$ReturnValue2 = #()
$ReturnValue3 = #()
$ReturnValue4 = #()
Function Get-ADInfos
{
Param(
$Entitys
)
foreach($Entity in $Entitys){
$Object = New-Object -TypeName PSObject
if($Entity.objectClass -eq "user"){
if($ReturnValue2.User.distinguishedName -contains $Entity)
#Do Something
$ReturnValue1 += $Object
Write-Host "$Entity is already scanned"
}else{
#Do something
$ReturnValue2 += $didsomething
Get-ADInfos $Values #looping
}
}elseif($Entity.objectClass -eq "group"){
if($ReturnValue4.Group.distinguishedName -contains $Entity){
#Do Something
$ReturnValue3 += $didsomething
}else{
#Do Something
$ReturnValue4 += $didsomething
Get-ADInfos $Values
}
}else{
write-host "finished"
}
}
Full Code for Repro (Older) #Note: To use constrained language for testing.
$User = #()
$Gruppen = #()
$LoopUser = #()
$LoopGroup = #()
Function Get-ADInfos
{
Param(
$Entry
)
#$Entry = Get-ADGroup "Domain Users"
if($Entry.objectClass -eq "user"){
$Entitys = Get-ADPrincipalGroupMembership $Entry
}elseif($Entry.objectClass -eq "group"){
$Entitys = Get-ADGroupMember $Entry
}else{}
foreach($Entity in $Entitys){
if($Entity.objectClass -eq "user"){
if($User.user -contains $Entity){
$Row = "" | Select User, Group
$Row.User = $Entity
$Row.Group = $Entry
$LoopUser += $Row #return to "master" scope
Write-Host "$Entity is already scanned"
}else{
$Row = "" | Select User, Group
$Row.User = $Entity
$Row.Group = $Entry
$User += $Row #return to "master" scope
Write-Host "$Entity is in $group"
Get-ADInfos $Entity
}
}elseif($Entity.objectClass -eq "group"){
if($Groups.group -contains $Entity){
$Row = "" | Select ScannedGroup, ParentGroup
$Row.ScannedGroup = $Entity
$Row.ParentGroup = $Entry
$LoopGroup += $Row #return to "master" scope
Write-Host "$Entity is already scanned"
}else{
$Row = "" | Select Group
$Row.Group = $Entity
$Groups += $Row #return to "master" scope
Write-Host "$Entity scanned"
Get-ADInfos $Entity
}
}else{
write-host "finished"
}
}
}
Get-ADGroup "Domain Users" | Get-ADInfos
PowerShell Arrays are immutable (fixed size collections):
$User = #()
$User.add('item')
MethodInvocationException: Exception calling "Add" with "1" argument(s): "Collection was of a fixed size."
This means that -besides the inefficient use of the increase assignment operator (+=)- it will create a new copy of the of array in each child scope when you try to change it:
$User = #()
function Test {
$User += 'Item'
Write-Host 'Child scope:' $User
}
Test
Write-Host 'Parent scope:' $User
Yields:
Child scope: Item
Parent scope:
Instead, I recommend you to use List<T> Class knowing that you can use the List<T>.Add(T) Method and the fact that objects are referenced by default:
$User = [Collections.Generic.List[object]]::new()
function Test { $User.Add('item') }
Test
$User # Yields: item
Addendum
As your script appears to run under constrained language mode, you will not be allowed to use the List<T> type or anything similar you might consider to use native PowerShell HashTables instead. See also: Mutable lists in Constrained Language Mode.
To apply this to your script:
Change all the arrays (that have a shared scope) to hashtables.
e.g.: $User = #() → $User = #{}
Change your assignments.
e.g.: $User += $Row → $User[$User.Count] = $Row
Change the conditions.
e.g.: $User.user -contains $Entity → $User.Values.user -contains $Entity

Scripting DBs backup through Powershell

I have created a PowerShell script that takes the back up of entire structure of database. When it comes to jobs backup, I cannot find a possible solution to that.
$v = [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO')
if ((($v.FullName.Split(','))[1].Split('='))[1].Split('.')[0] -ne '9')
{
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMOExtended') | out-null
}
[System.Reflection.Assembly]:: LoadWithPartialName('Microsoft.SqlServer.SmoEnum') | out-null
set-psdebug -strict # catch a few extra bugs
$ErrorActionPreference = "stop"
$My = 'Microsoft.SqlServer.Management.Smo'
$srv = new-object ("$My.Server") $ServerName # attach to the server
foreach($sqlDatabase in $srv.databases)
{
$databaseName=$sqlDatabase.name
if ($databaseName.count)
{
$scripter = new-object ("$My.Scripter") $srv # create the scripter
$scripter.Options.ToFileOnly = $true
# we now get all the object types except extended stored procedures
# first we get the bitmap of all the object types we want
$all =[long]
[Microsoft.SqlServer.Management.Smo.DatabaseObjectTypes]:: all -bxor
[Microsoft.SqlServer.Management.Smo.DatabaseObjectTypes]:: ExtendedStoredProcedure
# and we store them in a datatable
$d = new-object System.Data.Datatable
# get everything except the servicebroker object, the information schema and system views
$d = $srv.databases[$databaseName].EnumObjects([long]0x1FFFFFFF -band $all) | Where-Object {$_.Schema -ne 'sys'-and $_.Schema "information_schema"
#Saving it in a directory
}
}
This scripts takes the back up of the db but take the structural back up of msdb. I studied Microsoft.SqlServer.SMO that says it has a job server agent and job collection function but it doesn't seem to work.
For jobs, use the JobServer.Jobs collection. You can similarly script other server-level objects.
Below is an example.
$jobsCollection = $srv.JobServer.Jobs
$scriptingOptions = New-Object Microsoft.SqlServer.Management.Smo.ScriptingOptions
$scriptingOptions.IncludeIfNotExists = $true
$scriptingOptions.AppendToFile = $false
$scriptingOptions.ToFileOnly = $true
foreach ($job in $jobsCollection) {
Write-Host "Scripting $($job.Name)"
$scriptingOptions.FileName = "C:\ScriptFolder\Jobs.sql"
$job.Script($scriptingOptions)
$scriptingOptions.AppendToFile = $true
}
Although the answer given by Dan helped me but it wasn't creating a script in the folders. It was just creating folders with the jobs names. So, I did something like this :
foreach($sqlDatabase in $srv.JobServer.Jobs)
{ $databaseName=$sqlDatabase.name
write-host("I am her '$databaseName' ");
$scripter = new-object ("$My.Scripter") $srv # create the scripter
$scripter.Options.ToFileOnly = $true
$d = new-object System.Data.Datatable
$d=$srv.JobServer.Jobs[$databaseName]
$d| FOREACH-OBJECT {
trap [System.Management.Automation.MethodInvocationException]{
write-host ("ERROR: " + $_) -Foregroundcolor Red; Continue
}
# for every object we have in the datatable.
$SavePath="$($DirectoryToSaveTo)\$($ServerName)\$($databaseName)\$($_.DatabaseObjectTypes)"
# create the directory if necessary (SMO doesn't).
if (!( Test-Path -path $SavePath )) # create it if not existing
{Try { New-Item $SavePath -type directory | out-null }
Catch [system.exception]{
Write-Error "error while creating '$SavePath' $_"
return
}
}
# tell the scripter object where to write it
$scripter.Options.Filename = "$SavePath\$($_.name -replace '[\\\/\:\.]','-').sql";
# Create a single element URN array
$UrnCollection = new-object ('Microsoft.SqlServer.Management.Smo.urnCollection')
$URNCollection.add($_.urn)
# and write out the object to the specified file
$scripter.script($URNCollection)
}
}

Array of variables in PowerShell has null members

I have a PowerShell script, where I want to make sure certain variables have value before proceeding.
So I have the following:
$dataRow = $sheet.Cells.Find($country).Row
$serverCol = $sheet.Cells.Find($serverString).Column
$databaseCol = $sheet.Cells.Find($databaseString).Column
$userCol = $sheet.Cells.Find($userString).Column
$passwordCol = $sheet.Cells.Find($passString).Column
$partnerCol = $sheet.Cells.Find($partnerString).Column
#All variables in this array are required. If one is empty - the script cannot continue
$requiredVars = #($dataRow, $serverCol, $databaseCol, $userCol, $passwordCol, $partnerCol)
But when I foreach over the array like so:
foreach ($var in $requiredVars)
{
Write-Host DataRow = ($dataRow -eq $var)
Write-Host ServerCol = ($serverCol -eq $var)
Write-Host DatabaseCol = ($databaseCol -eq $var)
Write-Host UserCol = ($userCol -eq $var)
Write-Host PasswordCol = ($passwordCol -eq $var)
Write-Host PartnerCol = ($partnerCol -eq $var)
if ($var -eq $null)
{
[System.Windows.Forms.MessageBox]::Show("No data found for given string!")
$excel.Quit()
return
}
}
I always get the MessageBox. I added the "Write-Host" part to see the value of each variable, then changed it to see which variable was null but all variables have values in them and all the checks you see here return "False".
I'd like to know what I'm doing wrong and if the $requiredVars array only copies values, not references or something.
Instead of using separate variables, you may consider using a Hashtable to store them all.
This makes checking the individual items a lot simpler:
# get the data from Excel and store everything in a Hashtable
# to use any of the items, use syntax like $excelData.passwordCol or $excelData['passwordCol']
$excelData = #{
'dataRow' = $sheet.Cells.Find($country).Row
'serverCol' = $sheet.Cells.Find($serverString).Column
'databaseCol' = $sheet.Cells.Find($databaseString).Column
'userCol' = $sheet.Cells.Find($userString).Column
'passwordCol' = $sheet.Cells.Find($passString).Column
'partnerCol' = $sheet.Cells.Find($partnerString).Column
}
# check all items in the hash. If any item is $null then exit
foreach ($item in $excelData.Keys) {
# or use: if ($null -eq $excelData[$item])
if (-not $excelData[$item]) {
[System.Windows.Forms.MessageBox]::Show("No data found for item $item!")
$excel.Quit()
# IMPORTANT: clean-up used COM objects from memory when done with them
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($sheet) | Out-Null
# Your code doesn't show this, but you'll have a $workbook object in there too
# [System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbook) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
return
}
}
One way to directly solve your question is this:
$a = "foo"
$b = "bar"
$c = $null
$requiredVariables = $a, $b, $c
# How many total entries in array?
($requiredVariables).Count
# How many of them have a value?
($requiredVariables | Where-Object {$_}).Count
# So one option for a single check would be:
if (($requiredVariables.Count) -ne ($requiredVariables | Where-Object {$_}).Count) {
Write-Warning "Not all values provided"
}
However an alternative [and better] approach is to make your code in to a function that includes parameter validation
function YourCustomFunction {
Param (
[ValidateNotNullOrEmpty()]
$a
,
[ValidateNotNullOrEmpty()]
$b
,
[ValidateNotNullOrEmpty()]
$c
)
Process {
Write-Output "Your function code goes here..."
}
}
# Call your function with the params
YourCustomFunction -a $a -b $b -c $c
Example output:
Test-YourCustomFunction: Cannot validate argument on parameter 'c'. The argument is null or empty. Provide an argument that is not null or empty, and
then try the command again.
At line:39 char:48

Powershell-Azure WebApp IpRestrictions - WebApps Array

I have been struggling to come up with a working solution for days on this
What am I trying to achieve?
Foreach ($item in $webApps){
$WebAppConfig = (Get-AzureRmResource -ResourceType Microsoft.Web/sites/config -ResourceName $item -ResourceGroupName $resourceGroup -ApiVersion $APIVersion)
}
The issue is that "-resourceName" will not accept objects, but rather only a string
I am looking for a way to take the output of the following command, convert it to a string, so that it can satisfy –ResourceName, and loop through each item in the string
$webApps = (Get-AzureRmResourceGroup -Name $resourceGroup | Get-AzureRmWebApp).name
This returns a nice list of Azure WebApps that exist in a specified ResourceGroup, however they are in object form, which –ResourceName will not take
I have tried several ways to convert the output of $webApps to a string, add a comma to the end, then do a –split ',' but nothing seems to work for properly, where –ResourceName will accept it
Method 1:
[string]$webAppsArrays =#()
Foreach ($webApp in $webApps){
$webAp+',' -split ','
}
Method 2:
$
webApps | ForEach-Object {
$webApp = $_ + ","
Write-Host $webApp
}
Method 3:
$csvPath2 = 'C:\Users\Giann\Documents\_Git Repositorys\QueriedAppList2.csv'
$webApps = (Get-AzureRmResourceGroup -Name $resourceGroup | Get-AzureRmWebApp).name | out-file -FilePath $csvPath1 -Append
$csvFile2 = import-csv -Path $csvPath1 -Header Name
This ouputs a list in a CSV, however these are still objects, so I cannot pass each item into –ResourceName
I am going in circles trying to make the below a repeatable, looping script
The desired end result would be to use the below script, with an array of webApps, being queried from the provided resource group variable:
Any help would be greatly appreciated for how to use this script, but pull a dynamic list of WebApps from a specified Resource Group, keeping in mind the -ResourceName "String" restrictions in the $WebAppConfig variable
Here is the original script to create IP Restrictions for 1 Web App and 1 Resource Group, using properties from a CSV file:
#Create a Function to create IP Restrictions for 1 Web App and 1 Resource Group, using properties from the CSV file:
#Variables
$WebApp = ""
$resourceGroup =""
$subscription_Id = ''
#Login to Azure
Remove-AzureRmAccount -ErrorAction SilentlyContinue | Out-Null
Login-AzureRmAccount -EnvironmentName AzureUSGovernment -Subscription $subscription_Id
Function CreateIpRestriction {
Param (
[string] $name,
[string] $ipAddress,
[string] $subnetMask,
[string] $action,
[string] $priority
)
$APIVersion = ((Get-AzureRmResourceProvider -ProviderNamespace Microsoft.Web).ResourceTypes | Where-Object ResourceTypeName -eq sites).ApiVersions[0]
$WebAppConfig = (Get-AzureRmResource -ResourceType Microsoft.Web/sites/config -ResourceName $WebApp -ResourceGroupName $ResourceGroup -ApiVersion $APIVersion)
$ipRestriction = $WebAppConfig.Properties.ipSecurityRestrictions
$ipRestriction.name = $name
$ipRestriction.ipAddress = $ipAddress
$ipRestriction.subnetMask = $subnetMask
$ipRestriction.action = $action
$ipRestriction.priority = $priority
return $ipRestriction
}
#Set csv file path:
$csvPath5 = 'C:\Users\Giann\Documents\_Git Repositorys\ipRestrictions5.csv'
#import CSV Contents
$ipRestrictionArray = Import-Csv -Path $csvPath5
$ipRestrictions = #()
foreach($item in $ipRestrictionArray){
Write-Host "Adding ipRestriction properties for" $item.name
$newIpRestriction = CreateIpRestriction -name $item.name -ipAddress $item.ipAddress -subnetMask $item.subnetMask -action $item.action -priority $item.priority
$ipRestrictions += $newIpRestriction
}
#Set the new ipRestriction on the WebApp
Set-AzureRmResource -ResourceGroupName $resourceGroup -ResourceType Microsoft.Web/sites/config -ResourceName $WebApp/web -ApiVersion $APIVersion -PropertyObject $ipRestrictions
As continuation on the comments, I really need multiline, so here as an answer.
Note that I cannot test this myself
This page here shows that the Set-AzureRmResource -Properties parameter should be of type PSObject.
(instead of -Properties you may also use the alias -PropertyObject)
In your code, I don't think the function CreateIpRestriction returns a PSObject but tries to do too much.
Anyway, try like this:
Function CreateIpRestriction {
Param (
[string] $name,
[string] $ipAddress,
[string] $subnetMask,
[string] $action,
[string] $priority
)
# There are many ways to create a PSObject (or PSCustomObject if you like).
# Have a look at https://social.technet.microsoft.com/wiki/contents/articles/7804.powershell-creating-custom-objects.aspx for instance.
return New-Object -TypeName PSObject -Property #{
name = $name
ipAddress = $ipAddress
subnetMask = $subnetMask
action = $action
priority = $priority
}
}
#Set csv file path:
$csvPath5 = 'C:\Users\Giann\Documents\_Git Repositorys\ipRestrictions5.csv'
#import CSV Contents
$ipRestrictionArray = Import-Csv -Path $csvPath5
# create an new array of IP restrictions (PSObjects)
$newIpRestrictions = #()
foreach($item in $ipRestrictionArray){
Write-Host "Adding ipRestriction properties for" $item.name
$newIpRestrictions += (CreateIpRestriction -name $item.name -ipAddress $item.ipAddress -subnetMask $item.subnetMask -action $item.action -priority $item.priority )
}
# here we set the restrictions we collected in $newIpRestrictions in the $WebAppConfig.Properties.ipSecurityRestrictions array
$APIVersion = ((Get-AzureRmResourceProvider -ProviderNamespace Microsoft.Web).ResourceTypes | Where-Object ResourceTypeName -eq sites).ApiVersions[0]
$WebAppConfig = (Get-AzureRmResource -ResourceType Microsoft.Web/sites/config -ResourceName $WebApp -ResourceGroupName $ResourceGroup -ApiVersion $APIVersion)
$WebAppConfig.Properties.ipSecurityRestrictions = $newIpRestrictions
$WebAppConfig | Set-AzureRmResource -ApiVersion $APIVersion -Force | Out-Null
The code above will replace the ipSecurityRestrictions by a new set. You may want to consider first getting them and adding to the already existing list.
I found examples for Getting, Adding and Removing ipSecurityRestrictions here, but I can imagine there are more examples to be found.
Hope that helps.

Using Invoke-WebRequest on an array in PowerShell

I'm trying write a script that will grab the fortune 100 URLs from here, put those into an array, and then write a runspace that uses Invoke-WebRequest to get the content of those URLs and writes that content to a file. This is the code that I have so far:
#Importing Modules
Import-Module PoshRSJob
#variable declaration
$page = Invoke-WebRequest https://www.zyxware.com/articles/4344/list-of-fortune-500-companies-and-their-websites
$links = $page.Links
$tables = #($page.ParsedHtml.GetElementsByTagName("TABLE"))
$tableRows = $tables[0].Rows
#loops through the table to get only the top 100 urls.
$urlArray = #()
foreach ($tablerow in $tablerows) {
$urlArray += New-Object PSObject -Property #{'URLName' = $tablerow.InnerHTML.Split('"')[1]}
#Write-Host ($tablerow.innerHTML).Split('"')[1]
$i++
if ($i -eq 101) {break}
}
#Number of Runspaces to use
#$RunspaceThreads = 1
#Declaring Variables
$ParamList = #($urlArray)
$webRequest = #()
$urlArray | start-rsjob -ScriptBlock {
#$webRequest = (Invoke-WebRequest $using:ParamList)
#Invoke-WebRequest $urlArray
#Invoke-WebRequest {$urlArray}
#Get-Content $urlArray
}
The problem that I'm running into right now is that I can't get Invoke-WebRequest or Get-Content to give me the contents of the URLs that are actually contained in the array. You can see that in the scriptblock, I commented out some lines that didn't work.
My question is: using a runspace, what do I need to do to pull the data from all the URLs in the array using Get-Content, and then write that to a file?
You can adjust your current query to get the first 100 company names. This skips the empty company at the front. Consider using [PSCustomObject] #{ URLName = $url } which replaces the legacy New-Object PSObject.
$urlArray = #()
$i = 0
foreach ($tablerow in $tablerows) {
$url = $tablerow.InnerHTML.Split('"')[1]
if ($url) {
# Only add an object when the url exists
$urlArray += [PSCustomObject] #{ URLName = $url }
$i++
if ($i -eq 100) {break}
}
}
To run the requests in parallel use Start-RSJob with a script block. Invoke-Webrequest is then run in parallel. Note that in this example $_ refers to the current array element that is piped which consists of an object with a URLName property, but you need to be a little careful what variables you use inside the scriptblock because they might not be resovled they way you expect them to be.
# Run the webrequests in parallel
# $_ refers to a PSCustomObject with the #{ URLName = $url } property
$requests = ($urlArray | start-rsjob -ScriptBlock { Invoke-WebRequest -Uri $_.URLName })
You can then wait for all the jobs to complete and do some post processing of the results.
Here only the length of the website contents are written because the pages themself are lengthy.
# Get the results
# $_.Content.Length gets the length of the content to not spam the output with garbage
$result = Get-RSjob | Receive-RSJob | ForEach { $_.Content.Length }
Write-Host $result

Resources