I've got a CSV file with headers subnet, site, description. I want to import this into PoSH and then compare what is in AD Sites and Services. Now, this might be "null" or it might already contain subnet values. In either case, I need the subnets in AD compared to the CSV and updated/added as required.
I thought about If/Else, but that doesn't seem to handle the scenario where no subnets exist.
So I moved to try/catch and also Compare-Object.
Anyway, an example of what I've tried:
$csvSubnets = Import-Csv -Path C:\subnets.csv
$adSubnets = Get-ADReplicationSubnet -Filter *
Compare-Object -ReferenceObject #($csvSubnets | Select-Object) -DifferenceObject #($adSubnets | Select-Object) -Property Name | Where-Object {
$_.SideIndicator -eq '<='
} | ForEach-Object {
$csvSubnetName = $_.subnet
$csvSiteName = $_.site
$csvDescription = $_.description
$adSubnetName = $adSubnets.Where({$_.Name -eq $csvSubnetName})
New-ADReplicationSubnet -Site $_.site -Name $_.subnet -Description $_.description
}
This returns the following error:
New-ADReplicationSubnet : Cannot validate argument on parameter 'Site'. The argument is null
Would anyone have a suggestion as to how to fix this, or perhaps a more efficient way of achieving what I need? Doesn't need to be computationally efficient as there's probably never more than 10 sites (subnets)
The main thing I'd point out is you aren't passing the original objects coming out of the comparison to the ForEach-Object loop. Either reference the property $_.InputObject or preferably use the -PassThru parameter on the Compare-Object command:
Compare-Object -ReferenceObject #($csvSubnets | Select-Object) -DifferenceObject #($adSubnets | Select-Object) -Property Name -PassThru |
Where-Object { $_.SideIndicator -eq '<='} |
ForEach-Object {
$csvSubnetName = $_.subnet
$csvSiteName = $_.site
$csvDescription = $_.description
$adSubnetName = $adSubnets.Where({$_.Name -eq $csvSubnetName})
New-ADReplicationSubnet -Site $_.site -Name $_.subnet -Description $_.description
}
An aside:
The Compare-Object command is rather hard to read, given intermingled pipelines. It may be better to establish those as variables separately. However, you aren't specifying any properties in those Select-Object commands. I presume you are trying to level-set the typing to [PSCustomObject]'s in which case you should really only need to do that with the rich objects returned from Get-ADReplicationSubnet Import-Csv will always return [PSCustomObject]. So you could revise a little like:
$csvSubnets = Import-Csv -Path 'C:\subnets.csv'
$adSubnets = Get-ADReplicationSubnet -Filter * | Select-Object *
Compare-Object -ReferenceObject $csvSubnets -DifferenceObject $adSubnets -Property Name -PassThru |
Where-Object { $_.SideIndicator -eq '<='} |
ForEach-Object {
$csvSubnetName = $_.subnet
$csvSiteName = $_.site
$csvDescription = $_.description
$adSubnetName = $adSubnets.Where({$_.Name -eq $csvSubnetName})
New-ADReplicationSubnet -Site $_.site -Name $_.subnet -Description $_.description
}
I can't test your code but after solving the first problem you may have another with this line:
$adSubnetName = $adSubnets.Where({$_.Name -eq $csvSubnetName})
You aren't referencing the .Name property after you've isolated the AD Subnet you're interested in. I might be missing something, but if you do have that problem a quick fix might be something like:
$adSubnetName = ($adSubnets.Where({$_.Name -eq $csvSubnetName})).Name
Also, I like the .Where() method, but I'm a little iffy on it's return types, and on the readability of unnecessarily mixing and matching. You could replace with a regular Where{} clause:
$adSubnetName = ( $adSubnets | Where-Object{ $_.Name -eq $csvSubnetName } ).Name
Update:
It is true that if the difference object is null the Compare-Object command will fail. However, that approach stemmed from your original code. There are actually many ways to extract the difference between 2 lists. Here's a more concise example:
$csvSubnets = Import-Csv -Path 'C:\subnets.csv'
$adSubnets = (Get-ADReplicationSubnet -Filter *).Name
$csvSubnets |
Where-Object{ $_.Name -notin $adSubnets } |
ForEach-Object{
$SubnetParams = #{
Name = $_.subnet
Site = $_.site
Description = $_.description
}
New-ADReplicationSubnet #SubnetParams
}
In this case we use the -notin operator against a list of subnet names. $adSubnets isn't being used for anything else. We don't need it to be objects custom or otherwise. Now for each record in the CSV file just see if the name isn't on the list, and if not proceed to create the subnet.
Speed is not a likely concern in this case however you can actually make the above example a little more efficient using If logic in the loop and eliminating the Where{}.
$csvSubnets = Import-Csv -Path 'C:\subnets.csv'
$adSubnets = (Get-ADReplicationSubnet -Filter *).Name
$csvSubnets |
ForEach-Object{
If($_.Name -notin $adSubnets ) {
$SubnetParams = #{
Name = $_.subnet
Site = $_.site
Description = $_.description
}
New-ADReplicationSubnet #SubnetParams
}
}
In either example the key point is that -notin will work fine even if the right hand side of the comparison is null. It's simply return false and therefore the subnet will get created.
Firstly, thank you to the other responses here which lead me down a slightly different path to a solution.
$VerbosePreference = 'Continue'
$csvSubnets = Import-Csv -Delimiter "," -Path C:\subnets.csv
foreach ($subnet in $csvSubnets) {
$ADSubnetName = $subnet.subnet
if ($S = Get-ADReplicationSubnet -Filter {Name -eq $ADSubnetName}) {
Write-Verbose "Subnet $($ADSubnetName) already present"
Continue # to the next subnet
}
$SubnetParams = #{
Name = $subnet.subnet
Site = $subnet.site
Description = $subnet.description
}
Write-Verbose "Created subnet $($ADSubnetName)"
New-ADReplicationSubnet #SubnetParams
}
Related
I am currently trying to compare two CSV's. doc1 has 5000 strings in it, and doc2 has 100 strings in it. Every string in doc2 ends in "admin". I want to compare doc1 & doc2 and find all the strings that match up to the point it reaches "admin".
so as an example:
a string in doc1
a string in doc1 admin
it will output both of these to a new CSV
results = foreach ($OU in $OUs) {
Get-ADGroup -SearchBase $OU -Filter * -Properties CanonicalName,Created,Description,ManagedBy,Member,MemberOf,Members,ObjectGUID,whenChanged |
? {($_.ManagedBy -like $null) -and ($_.Description -notlike "*owner*") -and ($_.CanonicalName -notlike "*admin")}
}
$results | select CanonicalName,Description,ManagedBy,Member,MemberOf,Members,ObjectGUID,Created,whenChanged |
Export-Csv .\doc1.csv -NoTypeInformation
$results0 = foreach ($OU in $OUs) {
Get-ADGroup -SearchBase $OU -Filter * -Properties CanonicalName,Created,Description,ManagedBy,Member,MemberOf,Members,ObjectGUID,whenChanged |
? {($_.ManagedBy -like $null) -and ($_.Description -notlike "*owner*") -and ($_.CanonicalName -like "*admin")}
}
$results0 | select CanonicalName,Description,ManagedBy,Member,MemberOf,Members,ObjectGUID,Created,whenChanged |
Export-Csv .\doc2.csv -NoTypeInformation
$csvPath1 = ".\doc1.csv"
$csvPath2 = '.\doc2.csv'
$propertyToCompare = 'CanonicalName'
$csv1 = Import-Csv -Path $csvPath1
$csv2 = Import-Csv -Path $csvPath2
$duplicates = Compare-Object $csv1 $csv2 -Property $propertyToCompare -ExcludeDifferent -PassThru | Select-Object -ExpandProperty $propertyToCompare
$csv1 | Where-Object { $_.$propertyToCompare -in $duplicates } | Export-Csv -Path .\AdminsAndNotAdminsInOneFile.csv -NoTypeInformation
With Compare-Object I don't know how to make it ignore the last few characters in the string in doc2. Is there some way for me to modify the string?
You can take advantage of the fact that Compare-Object supports calculated properties as comparison properties (here, only a script block ({...}) is passed, which is the same as passing
#{ Expression = { ... } }):
Compare-Object $csv1 $csv2 -ExcludeDifferent -IncludeEqual -PassThru -Property {
$_.$propertyToCompare -replace ' admin$'
} | Select-Object -ExpandProperty $propertyToCompare
Note that -PassThru ensures that the input objects are passed through, which in the case of objects that compare as equal means that the LHS ($csv1) object is passed through.
Expression $_.$propertyToCompare -replace ' admin$', which is evaluated for each input object, uses regex admin$ to replace ' admin' at the end ($) of the value of property $propertyToCompare; if that string isn't present, the value is used as-is.
In effect, the objects are compared as if the trailing ' admin' weren't present.
I'm just having problems trying to export the following to a CSV, I've tried putting the Export-CSV within the Foreach-Object loop to no avail.
I want to put the server name $server in the first column and the description $description in the second.
In an earlier version, I was able to make a text file using Out-File -append .\outa.txt -InputObject $_.Name,$description but the formatting did not work well.
$server = Get-ADComputer -Filter {OperatingSystem -Like "*Server*" -and Name -Notlike "*DOM*"}
$server | ForEach-Object {
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$_.Name)
$RegKey = $Reg.OpenSubKey("SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters")
$description = $RegKey.GetValue("srvcomment")
} | Export-Csv .\out1.csv
A ForEach as oppose to ForEach-Object:
$server = Get-ADComputer -Filter {OperatingSystem -Like "*Server*" -and Name -Notlike "*DOM*"}
foreach($s in $server){
# replaced $_ with $s
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$s.Name)
$RegKey= $Reg.OpenSubKey("SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters")
$description = $RegKey.GetValue("srvcomment")
# casting as array is required to use +=
# disclaimer: the array is destroyed and rebuilt on each run.
[array]$myData += New-Object psobject -Property #{
Server = $s.Name
Description = $description
}
}
# If you want a CSV without the top info line, use the notypeinfo switch
# Select-Object gives you the column order you want.
$myData | Select-Object Server,Description | Export-Csv .\out1.csv -NoTypeInformation
Edit - comment answer
It is possible to do it without creating an array and from inside the loop, but an object is required by Export-Csv AFAIK.
ForEach-Object:
$server = Get-ADComputer -Filter {OperatingSystem -Like "*Server*" -and Name -Notlike "*DOM*"}
$server | ForEach-Object {
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$_.Name)
$RegKey = $Reg.OpenSubKey("SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters")
New-object psobject -Property #{Server=$_.Name;Description=$RegKey.GetValue("srvcomment")} | Export-Csv .\out1.csv -NoTypeInformation
}
ForEach:
$server = Get-ADComputer -Filter {OperatingSystem -Like "*Server*" -and Name -Notlike "*DOM*"}
foreach($s in $server){
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$s.Name)
$RegKey= $Reg.OpenSubKey("SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters")
New-object psobject -Property #{Server=$s.Name;Description=$RegKey.GetValue("srvcomment")} | Export-Csv .\out1.csv -NoTypeInformation
}
If you have problems with the values, brackets/subexpressions can help:
New-object psobject -Property #{Server=($_.Name);Description=($RegKey.GetValue("srvcomment"))} | Export-Csv .\out1.csv -NoTypeInformation
New-object psobject -Property #{Server=$($_.Name);Description=$($RegKey.GetValue("srvcomment"))} | Export-Csv .\out1.csv -NoTypeInformation
Your ForEach-Object only contains statements (variable assignments). It's not returning anything. You need to return something, otherwise you're passing an empty pipeline to Export-CSV.
I'm guessing that, instead of setting variables called $Reg, $RegKey, and $description (and then never using their values), what you actually want to do is create columns in the CSV called Reg, RegKey, and description. In that case, you want to yield a [PSCustomObject] (with those properties added to it) each time through the loop.
$server = Get-ADComputer -Filter {OperatingSystem -Like "*Server*" -and Name -Notlike "*DOM*"}
$server | ForEach-Object {
[PSCustomObject] #{
Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$_.Name)
RegKey = $Reg.OpenSubKey("SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters")
description = $RegKey.GetValue("srvcomment")
}
} | Export-Csv .\out1.csv
I have an empty array that's storing all my windows services that start with certain strings such as OPS-AmazonServer,not included in the code I provided is where I parse the service to just say it's application name.
I then have a CSV file with a list of service names labeled under 'Application Name'. It looks like this
ApplicationName,Instance,Priority
AuthBridge,,1
AmazonServer,,1
AmexEC,,1
What I want to do is compare the service stored in the array to the CSV list but I can't seem to figure out how the logic flows.
$services = get-service Centinel* -ComputerName $serverName | select -expand name
$centinelServices = #()
$services = get-service OPS* -ComputerName $serverName | select -expand name
$opsServices = #()
$services = #()
foreach($service in $centinelServices) {
$services += $service
}
foreach($service in $opsServices) {
$services += $service
}
$csvLocation = "\\logserver\Cardinal\OPS\QA\Task\conf\Centinel\app-restart.csv"
$masterList = import-csv $csvLocation
$applications = #()
$masterList | ForEach-Object {$applications += $_.ApplicationName}
forEach($service in $services){
forEach($application in $applications){
if($service -eq $application){
"$service match found"
}
else {
"$service match not found"
}
}
Ok, easiest way to do this is to use Compare-Object, and a little magic with Select.
I'm going to assume that the ApplicationName column in your CSV is a list of strings that match up with the Name property in your Windows Services list. So let's start by importing that CSV, and changing the property name of ApplicationName to just Name, so that it matches the related property on your Windows Service objects.
$masterList = Import-Csv $csvLocation | Select #{l='Name';e={$_.ApplicationName}}
Then we simply use Compare-Object to see what's in both lists:
Compare-Object (Get-Service) -DifferenceObject $masterList -Property Name -IncludeEqual
If you wanted to parse that you can always pipe it to a Where clause, or use combinations of -IncludeEqual and -ExcludeDifferent parameters:
$masterList = Import-Csv $csvLocation | Select #{l='Name';e={$_.ApplicationName}}
$myServices = Get-Service
$foundServices = Compare-Object $myServices -DifferenceObject $masterList -Property Name -IncludeEqual -ExcludeDifferent
$servicesNotInMaster = Compare-Object $myServices -DifferenceObject $masterList -Property Name | Where {$_.SideIndicator -eq '<='}
$servicesNotFoundLocally = Compare-Object $myServices -DifferenceObject $masterList -Property Name | Where {$_.SideIndicator -eq '=>'}
Or using the Switch cmdlet to do it all in one go:
$masterList = Import-Csv $csvLocation | Select #{l='Name';e={$_.ApplicationName}}
$myServices = Get-Service
Switch(Compare-Object $myServices -dif $masterList -prop Name -includeequal -PassThru){
{$_.SideIndicator -eq '<='} {[array]$servicesNotInMaster += $_}
{$_.SideIndicator -eq '=>'} {[array]$servicesNotFoundLocally += $_}
{$_.SideIndicator -eq '=='} {[array]$foundServices += $_}
}
Edit: Ok, updating from your addition to the OP. Looks like you could be well served by simply using a Where clause rather than getting services over and over.
$services = Get-Service -ComputerName $serverName | Where{$_.Name -like 'ops*' -or $_.Name -like 'Centinel*'} | Select -Expand Name
Then you import your CSV, and use Select -Expand again to get the value of the property, rather than looping through it like you were before.
$masterList = Import-Csv $csvLocation | Select -Expand ApplicationName
Now you just have two arrays of strings, so this actually gets even simpler than comparing objects... You can use the -in operator in a Where statement like this:
$services | Where{$_ -in $masterList} | ForEach{"$_ match found"}
That basically filters the $services array to look for any strings that are in the $masterList array. This will only work for exact matches though! So if the service is listed as 'OPS-AmazonServer', but in your CSV file it is listed at just 'AmazonServer' it will not work! I use that example specifically because you have that in your example in your question. You specifically call out the service named 'OPS-AmazonServer' and then in your CSV sample you list just 'AmazonServer'.
If the listings in the CSV are partial strings that you want to match against you could use RegEx to do it. This will probably make less sense if you aren't familiar with RegEx, but this would work:
$services = Get-Service -ComputerName $serverName | Where{$_.Name -like 'ops*' -or $_.Name -like 'Centinel*'} | Select -Expand Name
$masterList = (Import-Csv $csvLocation | ForEach{[regex]::escape($_.ApplicationName)}) -join '|'
$services | Where{ $_ -match $masterList } | ForEach{"$_ match found"}
I have been using for Office365 Licence Tracking. Actually it looks like good but but it takes too much time to complete the process. most of time is spent by Get-MsolUser it may be improved calculating them in parallel (while processing user 1 you're already fetching user 2's data and so on...) BTW we have about 3000+ cloud user How can I improve the speed of the script?
$T1 = #()
$O365Users = Get-MsolUser -All
ForEach ($O365User in $O365Users)
{
$ADuser = Get-ADUser -Filter { UserPrincipalName -eq $O365User.UserPrincipalName } -Properties whenCreated, Enabled, lastlogondate
$O365Stats = Get-MailboxStatistics $O365User.DisplayName -ErrorAction SilentlyContinue
$O365Smtp = Get-Recipient $O365User.DisplayName -ErrorAction SilentlyContinue
If ($O365Stats -and $O365Smtp) {
If (($ADUser.Enabled -eq $true) -and ($O365User.isLicensed -eq $true))
{
$T1 += New-Object psobject -Property #{
CollectDate = $(Get-Date);
ADUserUPN = $($ADUser.UserPrincipalName);
O365UserUPN = $($O365User.UserPrincipalName);
ADUserCreated = $($ADUser.whenCreated);
ADUserEnabled = $($ADUser.Enabled);
ADLastLogonDate = $($ADUser.LastLogonDate);
O365Licensed = $($O365User.isLicensed);
O365LastLogonTime = $($O365Stats.LastLogonTime);
O365SMTPAddress = $($O365Smtp.PrimarySMTPAddress)
}
}
}
}
$T1 = $T1 | Sort-Object -Property ADUserCreated
$T1 | Format-Table
$T1 | Export-Csv -Path $OutputFile -NoTypeInformation
Write-Host "Output to $OutputFile"
Using a pipeline, filtering early on, and avoiding appending to an array should already speed things up considerably:
Get-MsolUser -All | Where-Object {
$_.IsLicensed
} | ForEach-Object {
$upn = $_.UserPrincipalName
Get-ADUser -Filter "UserPrincipalName -eq '$upn'" -Properties whenCreated, Enabled, lastlogondate
} | Where-Object {
$_.Enabled
} | ForEach-Object {
$O365Stats = Get-MailboxStatistics $_.DisplayName -ErrorAction SilentlyContinue
$O365Smtp = Get-Recipient $_.DisplayName -ErrorAction SilentlyContinue
if ($O365Stats -and $O365Smtp) {
New-Object -Type PSObject -Property #{
'CollectDate' = Get-Date
'ADUserUPN' = $_.UserPrincipalName
'O365UserUPN' = $_.UserPrincipalName
'ADUserCreated' = $_.whenCreated
'ADUserEnabled' = $_.Enabled
'ADLastLogonDate' = $_.LastLogonDate
'O365Licensed' = $true
'O365LastLogonTime' = $O365Stats.LastLogonTime
'O365SMTPAddress' = $O365Smtp.PrimarySMTPAddress
}
}
} | Sort-Object -Property ADUserCreated | Export-Csv -Path $OutputFile -NoType
Also, why the heck is everybody so infatuated with subexpressions? Use them where you need them. Don't obfuscate your code with them when they're unnecessary.
To give you a set off with parallelism in Powershell.
I would like you to go through the PS Workflows.
We have -parallel in that which will help you in parallel call.
Apart from that, we have one function for Invoke-Parallel
This is the link for it : Invoke-Parallel Function
Note: Examples are mentioned inside the function itself . You can use get-help with that function also once compiled.
This morning some awesome people helped me make a script to move user accounts based on their displayName to a certain OU. I tested and it worked. I cannibalized the script to make another one that will rename the same accounts based off of the same criteria. I've gone through several errors but basically it all boils down to "I am having an identity crisis!". I can't seem to figure out exactly what I need to input as the $Identity. Here is what I have:
Import-Module ActiveDirectory
$Renames = #(
#{
Filter = 'DisplayName -like "*Supply*"'
NewName = "Supplies"
},
#{
Filter = 'DisplayName -like "*Accountant*"'
NewName = "Accounting"
}
) | ForEach-Object {New-Object -TypeName PSCustomObject -Property $_}
$OriginOU = "OU=Test,OU=Standard Users,OU=Domain Users,DC=com"
foreach ($Rename in $Renames) {
Get-ADUser -SearchBase $OriginOU -Filter $Rename.Filter -Properties displayName |
Where-Object {($_.Enabled -eq 'True') -and ($_.DistinguishedName -notlike '*DontTouch*')} |
%{Set-ADUser $_ -DisplayName {$_.DisplayName -replace '(.EPSILON ).+',"`$1$Rename.NewName"}}
}
You can't use the current object variable ($_) if you have Set-ADUser read directly from the pipeline. And since Set-ADUser apparently doesn't play nice with scriptblock arguments, you have to put the statement in a loop:
... | % { Set-ADUser $_ -DisplayName ($_.DisplayName -replace '(.EPSILON ).+',"`$1$($Rename.NewName)") }
Note that if you want to expand object properties inside a string you have to put $Rename.NewName in a subexpression ($()), otherwise the whole object $Rename would be stringified and the string ".NewName" would be appended to it.