I'm attempting to create a function for a script module that verifies the previous parameter has been set before it shows the next parameter.
The parameters I need are Identity, Share and Quota. I always want Identity to show, I don't want Share to show until the Identity has been set and I don't want Quota to show until the Share has been set.
I'm able to easily access $Identity, I'm not able to access $Share from within DynamicParam{}. I stepped through the script using PowerGUI and I was only able to see $Share when I hit Begin{}.
I have a way to workaround this by just showing Share/Quota if Identity is set, but ultimately I would like to learn how to keep adding additional parameters based on the previously set parameter.
A copy of the function is below. personfileutility.exe is just an executable we use to interact with the various systems to provision and gather information on a user.
function New-PersonalFiles
{
[CmdletBinding()]
Param
(
# Param1 help description
[Parameter(Mandatory=$true)]
$Identity
)
DynamicParam
{
$paramDictionary = new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
if (($Identity -notlike "") -or ($Identity -notlike $null)){
$attributes = new-object System.Management.Automation.ParameterAttribute
$attributes.ParameterSetName = "__AllParameterSets"
$attributes.Mandatory = $true
$lookup = [xml](\\servername\personalfiles\personfileutility.exe -a $Identity)
$type = $lookup.User.user_directory.type.type
if ($type -like "other" -or $type -like "staff") {
$arguments = #()
$arguments += "\\fileserver\sharename"
}
elseif ($type -like "faculty") {
$arguments = #()
$arguments += "\\fileserver\sharename"
$arguments += "\\fileserver\sharename2"
}
elseif ($type -like "student") {
$arguments = #()
$arguments += "\\fileserver2\sharename"
}
$ParamOptions = New-Object System.Management.Automation.ValidateSetAttribute -ArgumentList $arguments
$attributeCollection = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
$attributeCollection.Add($attributes)
$attributeCollection.Add($ParamOptions)
$dynParam1 = new-object -Type System.Management.Automation.RuntimeDefinedParameter("Share", [String], $attributeCollection)
$paramDictionary.Add("Share", $dynParam1)
}
if (($Share -like "\\fileserver\*"))
{
$attributes2 = new-object System.Management.Automation.ParameterAttribute
$attributes2.ParameterSetName = "__AllParameterSets"
$attributes2.Mandatory = $true
$lookup = [xml](\\servername\personalfiles\personfileutility.exe -a $Identity)
$type = $lookup.User.user_directory.type.type
if ($type -like "other" -or $type -like "staff") {
$arguments = #()
$arguments += "15GB"
$arguments += "20GB"
}
elseif ($type -like "faculty") {
$arguments = #()
$arguments += "10GB"
$arguments += "15GB"
}
elseif ($type -like "student") {
$arguments = #()
$arguments += "5GB"
$arguments += "10GB"
}
$ParamOptions2 = New-Object System.Management.Automation.ValidateSetAttribute -ArgumentList $arguments2
$attributeCollection2 = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
$attributeCollection2.Add($attributes2)
$attributeCollection2.Add($ParamOptions2)
$dynParam2 = new-object -Type System.Management.Automation.RuntimeDefinedParameter("Quota", [String], $attributeCollection2)
$paramDictionary.Add("Quota", $dynParam2)
}
return $paramDictionary
}
<#
.Synopsis
Short description
.DESCRIPTION
Long description
.EXAMPLE
Example of how to use this cmdlet
.EXAMPLE
Another example of how to use this cmdlet
#>
Begin
{
}
Process
{
\\servername\personalfiles\personfileutility.exe -a $Identity -c -q ((Invoke-Expression $PSBoundParameters.Quota) / 1KB) -s $Share
}
End
{
}
}
I'm trying to replicate the "workaround" you're using, but I'm only seeing that I have access to the dynamic parameter Share (not Quota). In order to reproduce your setup, since I don't have access to personfileutility.exe I have commented out two lines and added a third where I hardcode $type to "faculty". For example, in two places I have updated the code to be:
#$lookup = [xml](\\servername\personalfiles\personfileutility.exe -a $Identity)
#$type = $lookup.User.user_directory.type.type
$type = "faculty"
With these changes in place, I can access the Share parameter after I specify an Identity. However, I cannot access the Quota. Do you expect Quota to be available?
If I'm understanding the problem you're asking about, you do not want Share to be accessible until Identity is accessible (which you currently have working). But you in addition, you don't want Quota to be accessible until both Identity and Share are filled out and you don't know how to make that work. Is that correct?
If I'm understanding the problem correctly, I don't believe that PowerShell offers a mechanism to achieve that with Commandlet binding. I think you could make that work by either using a GUI application, or interactively prompting the user for inputs.
Related
First off I would like to thank everyone for helping me work thru my issue.
Scope:
I am looking to write a script that will dynamically build the full set of permissions for each printer. As each printer has it's own Dynamic Group and is not allowed to have the everyone group applied to the printer.
Example:
Printer Name: PrinterA
AdGroup for Printer: gprt_PrinterA
Other groups assigned full (Print/Manage Doc/Manage Printer) permissions to the printer : Local Admin/Local Power User/Local Print Operator/Network Admins (Domain Group)
Other groups with Manage Documents and Print permissions to the printer: Endpoint (Domain Group)/Service Desk (Domain Group)/gprt_PrinterA (Domain Group)\
First what works and I see many examples about this across the web but does not meet my requirements:
$DefaultPrinterInfo = Get-Printer -Name PrinterA -Full
Set-Printer -Name PrinterB -PermissionSDDL ($DefaultPrinterInfo.PermissionSDDL)
IMPORTANT:
This however does not work to meet the required specifications. The reason is the gprt_PrinterA group can not exist on PrinterB. PrinterB must have the gprt_PrinterB Group.
In one example I have attempted to:
Set-Printer -Name PrinterB -PermissionSDDL "G:SYD:(A;;LCSWSDRCWDWO;;;BA)(A;OIIO;RPWPSDRCWDWO;;;BA)"
I have attempted to even dynamically create the default permission groups required and if this worked then it would be easy for me to just add 1 more group that is dynamically assigned:
(A;;LCSWSDRCWDWO;;;BA)(A;OIIO;RPWPSDRCWDWO;;;BA)
(A;;LCSWSDRCWDWO;;;PU)(A;OIIO;RPWPSDRCWDWO;;;PU)
(A;;LCSWSDRCWDWO;;;PO)(A;OIIO;RPWPSDRCWDWO;;;PO)
(A;;LCSWSDRCWDWO;;;S-1-5-21-51083937-621610274-1850952788-69794)(A;OIIO;RPWPSDRCWDWO;;;S-1-5-21-51083937-621610274-1850952788-69794)
(A;CIIO;RC;;;S-1-5-21-51083937-621610274-1850952788-69792)(A;OIIO;RPWPSDRCWDWO;;;S-1-5-21-51083937-621610274-1850952788-69792)(A;;SWRC;;;S-1-5-21-51083937-621610274-1850952788-69792)
(A;CIIO;RC;;;S-1-5-21-51083937-621610274-1850952788-69791)(A;OIIO;RPWPSDRCWDWO;;;S-1-5-21-51083937-621610274-1850952788-69791)(A;;SWRC;;;S-1-5-21-51083937-621610274-1850952788-69791)
I kept the groups clean for easy reading but essentially just make it a continuous line with "G:SYD:" in the beginning. Then replace the PermissionSDDL in the above powershell statement. Either way though, I keep getting the error: "[Set-Printer : Access was denied to the specific resource]"
I have even attempted to do the following:
SetSecurityDescriptor method of the Win32_Printer class
Set-PrinterPermission.ps1
The Security Descriptor Definition Language of Love (Part 2)
Adding Multiple Permissions to a Share
These did put me on the correct path! It lets me replace the permission on the printer. But it strips all existing permission, putting on only the single permission specified for the printer. I need to apply a whole set of permissions to the printer as you see above. I am a little out of my realm but learning how to build a Multi-ACL Package to apply to the printer.
I am ok with replacing all permissions, if I can assign a whole set of permissions, or simply add and remove to the existing permissions if they do or not exist.
What I have learned in my research the permission sets need to be:
Print/Manage this Printer
# G:SYD:(A;;LCSWSDRCWDWO;;;$SID)
Print
# G:SYD:(A;;SWRC;;;$SID)
Print/Manage this Printer/Manage Documents/Special Permissions
# G:SYD:(A;;LCSWSDRCWDWO;;;$SID)(A;OIIO;RPWPSDRCWDWO;;;$SID)
I hope someone the help me figure out a solution please.
Ok so after extensively researching I am getting closer.
The "Set-PrinterPermission" script is on the correct path. What I have had to do, is stripped out the ACE function from the script to place it into it's own function.
function New-PrinterACE
{
##[CmdletBinding(SupportsShouldProcess)]
Param (
[Parameter(
Mandatory = $true,
HelpMessage = "User/group to grant permissions"
)]
[String]$UserName,
[Parameter(
Mandatory = $true,
HelpMessage = "Permissions to apply"
)]
[ValidateSet('Takeownership', 'ReadPermissions', 'ChangePermissions', 'ManageDocuments', 'ManagePrinters', 'Print + ReadPermissions')]
[String]$Permission,
[Parameter(
Mandatory = $true,
HelpMessage = "Permissions to apply"
)]
[ValidateSet('Allow', 'Deny', 'System Audit')]
[String]$AccessType
)
$Ace = ([WMIClass] "Win32_Ace").CreateInstance()
$Trustee = ([WMIClass] "Win32_Trustee").CreateInstance()
Write-Verbose "Translating UserName (user or group) to SID"
$SID = (New-Object security.principal.ntaccount $UserName).translate([security.principal.securityidentifier])
Write-Verbose "Get binary form from SID and byte Array"
[byte[]]$SIDArray = , 0 * $SID.BinaryLength
$SID.GetBinaryForm($SIDArray, 0)
Write-Verbose "Fill Trustee object parameters"
$Trustee.Name = $UserName
$Trustee.SID = $SIDArray
Write-Verbose "Translating $Permission to the corresponding Access Mask"
Write-Verbose "Based on https://learn.microsoft.com/en-US/windows/win32/cimwin32prov/setsecuritydescriptor-method-in-class-win32-printer?redirectedfrom=MSDN"
Write-Verbose "https://social.technet.microsoft.com/Forums/Windows/en-US/a67e3ffd-5e41-4e2f-b1b9-c7c2f29a3a12/adding-permissions-to-an-existing-share"
switch ($Permission)
{
'Takeownership'
{
$Ace.AccessMask = "524288"
}
'ReadPermissions'
{
$Ace.AccessMask = "131072"
}
'ChangePermissions'
{
$Ace.AccessMask = "262144"
}
'ManageDocuments'
{
$Ace.AccessMask = "983088"
}
'ManagePrinters'
{
$Ace.AccessMask = "983052"
}
'Print + ReadPermissions'
{
$Ace.AccessMask = "131080"
}
}
Write-Verbose "Translating $AccessType to the corresponding numeric value"
Write-Verbose "Based on https://learn.microsoft.com/en-US/windows/win32/cimwin32prov/setsecuritydescriptor-method-in-class-win32-printer?redirectedfrom=MSDN"
switch ($AccessType)
{
"Allow"
{
$Ace.AceType = 0
$Ace.AceFlags = 0
}
"Deny"
{
$Ace.AceType = 1
$Ace.AceFlags = 1
}
"System Audit"
{
$Ace.AceType = 2
$Ace.AceFlags = 2
}
}
Write-Verbose "Write Win32_Trustee object to Win32_Ace Trustee property"
$Ace.Trustee = $Trustee
Return $ACE
}
$MyPrinterAces = #()
$MyPrinterAces += New-PrinterACE -UserName <DomainUserA> -Permission ManagePrinters -AccessType Allow
$MyPrinterAces += New-PrinterACE -UserName <DomainUserA> -Permission ManageDocuments -AccessType Allow
$MyPrinterAces += New-PrinterACE -UserName "DomainGroupA" -Permission ManageDocuments -AccessType Allow
$MyPrinterAces += New-PrinterACE -UserName "DomainGroupA" -Permission 'Print + ReadPermissions' -AccessType Allow
#https://learn.microsoft.com/en-us/windows/win32/wmisdk/wmi-security-descriptor-objects#example-checking-who-has-access-to-printers
#https://stackoverflow.com/questions/60261292/explicit-access-array-from-acl-win32-api
This, with a few other cosmetic modifications to the "Set-PrinterPermission" script to accommodate; So that it now references this function to build the ACE's it uses and to add the ability for it to accommodate an array of multiple users/groups with permissions types.
function Set-PrinterPermission
{
[CmdletBinding(SupportsShouldProcess)]
Param (
[Parameter(
Mandatory = $true,
HelpMessage = "Server or array of servers",
ParameterSetName = 'OnePrinter'
)]
[Parameter(
Mandatory = $true,
HelpMessage = "Server or array of servers",
ParameterSetName = 'AllPrinters'
)]
[string[]]$Servers,
[Parameter(
HelpMessage = "Name of the Printer",
ParameterSetName = 'OnePrinter'
)]
[String]$PrinterName,
$PrinterPermissions =
#(
#('Administrators', 'ManagePrinters','Allow'),
#('Power Users', 'ManagePrinters','Allow'),
#('Print Operators', 'ManagePrinters','Allow'),
#('OHD – Network Support Team', 'ManagePrinters','Allow'),
#("OHD – PC Support Team", 'Print + ReadPermissions','Allow'),
#("OHD - Service Desk Users", 'Print + ReadPermissions','Allow')
)
)
Begin
{
$greenCheck =
#{
Object = [Char]8730
ForegroundColor = 'Green'
NoNewLine = $true
}
ConvertFrom-SddlString -Sddl $printer.PermissionSDDL
#Write-Host "Status check... " -NoNewline
#Start-Sleep -Seconds 1
#Write-Host #greenCheck
#Write-Host " (Done)"
Write-Output "Beginning Treatment ..."
Write-Verbose "creating instances of necessary classes ..."
$SD = ([WMIClass] "Win32_SecurityDescriptor").CreateInstance()
$Aces = #()
Foreach ($PrinterPermission in $PrinterPermissions)
{
$Aces += New-PrinterACE -UserName $PrinterPermission[0] -Permission $PrinterPermission[1] -AccessType $PrinterPermission[2]
}
Write-Verbose "Write Win32_Ace and Win32_Trustee objects to SecurityDescriptor object"
$SD.DACL = $Aces
Write-Verbose "Set SE_DACL_PRESENT control flag"
$SD.ControlFlags = 0x0004
}
process
{
try
{
If ($PSCmdlet.ParameterSetName -eq "OnePrinter")
{
ForEach ($Server in $Servers)
{
$Printer = Get-Printer -ComputerName $Server -Name $PrinterName -ErrorAction Stop
$PrinterName = $Printer.name
Write-Output "Beginning treatment of: $PrinterName On: $Server"
Write-Verbose "Get printer object"
<#
It seems that i can't use the Filter parameter using a var
$PrinterWMI = Get-WMIObject -Class WIN32_Printer -Filter "name = $PrinterName"
I've also noticed that I've haven't the same result using Get-CimInstance in particular with
$PrinterCIM.psbase.scope
However I'm sure that using Get-CiMInstance will be better, but i don't know how to proceed
then I'm using the following "Legacy" approach
https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/the-security-descriptor-definition-language-of-love-part-1/ba-p/395202
https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/the-security-descriptor-definition-language-of-love-part-2/ba-p/395258
http://docs.directechservices.com/index.php/category-blog-menu/319-the-security-descriptor-definition-language-of-love
https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-strings?redirectedfrom=MSDN
https://learn.microsoft.com/en-us/windows/win32/secauthz/access-tokens
#>
#$PrinterWMI = (Get-WmiObject -Class WIN32_Printer | Where-Object -FilterScript { $_.Name -like "wilpa0p11" }).GetSecurityDescriptor().Descriptor.dacl
$PrinterWMI = Get-WmiObject -Class WIN32_Printer | Where-Object -FilterScript { $_.Name -like $PrinterName }
Write-Verbose "Enable SeSecurityPrivilege privilegies"
$PrinterWMI.psbase.Scope.Options.EnablePrivileges = $true
Write-Verbose "Invoke SetSecurityDescriptor method and write new ACE to specified"
$PrinterWMI.SetSecurityDescriptor($SD)
Write-Verbose "Treatment of $PrinterName : Completed"
}
} # end if OnePrinter Parameter Set
If ($PSCmdlet.ParameterSetName -eq "AllPrinters")
{
ForEach ($Server in $Servers)
{
$Printers = Get-Printer -ComputerName $Server | Where-Object { $_.Shared -eq $true } -ErrorAction Stop
ForEach ($Printer in $Printers)
{
$PrinterName = $Printer.name
Write-Output "Beginning treatment of : $PrinterName"
Write-Verbose "Get printer object"
<#
It seems that i can't use the Filter parameter using a var
$PrinterWMI = Get-WMIObject -Class WIN32_Printer -Filter "name = $PrinterName"
I've also noticed that I've haven't the same result using Get-CimInstance in particular with
$Printer.psbase.scope
then I'm using the following approach
However I'm sure that using Get-CiMInstance will be better
#>
$PrinterWMI = Get-WmiObject -Class WIN32_Printer | Where-Object -FilterScript { $_.Name -like $PrinterName }
Write-Verbose "Enable SeSecurityPrivilege privilegies"
$PrinterWMI.psbase.Scope.Options.EnablePrivileges = $true
Write-Verbose "Invoke SetSecurityDescriptor method and write new ACE to specified"
$PrinterWMI.SetSecurityDescriptor($SD)
Write-Output "Treatment of $PrinterName : Completed"
}
}
} # end if All Printers Parameter Set
} # End Try
catch
{
Write-Error "Hoops an error occured"
Write-Error $_.Exception.Message
}
}
end
{
Write-Output "All treatments : completed"
}
} # end function
Now this is working great I can easily add the dynamic group as a parameter and a ACE will get assigned to the security descriptor of the printer.
Now my problem is I am unable to add the "Manage Documents" permission to the printer. if anyone can help me with this I will have my project complete.
The permission is assigned correctly for Printing only, and Manage Printer.
Primary Issue needing help resolving:
I am so very close now... what am I doing wrong to apply the "Manage Documents" permission to the printer ACL?
The Image below is the results of the script trying to apply the "Manage Documents" Permissions.
Very Minor Cosmetic help:
is there a way to validate the $PrinterPermissions in the Parameters section of the code? My thinking is to validate the parameter in the begin section of the code and exit out if one of my validations fail. not sure if there is a better way.
I need to grab members in particular AD group and add them into array. Using net group I can easily get the members of AD group.
However, I am not familier with the filter on Windows. I just want to get the user name from output.
Group name test
Comment
Members
---------------------------------------------------------------------
mike tom jackie
rick jason nick
The command completed successfully.
I can't use Get-ADGroupMember command using PowerShell. If there is a way to get a data and filter using PowerShell, it is also OK.
Well, the good news is that there is rarely only one way to do things in PowerShell. Here's part of a larger script I have on hand for some group related things where I don't always have the AD module available (such as on servers that other teams own):
$Identity = 'test'
$LDAP = "dc="+$env:USERDNSDOMAIN.Replace('.',',dc=')
$Filter = "(&(sAMAccountName=$Identity)(objectClass=group))"
$Searcher = [adsisearcher]$Filter
$Searcher.SearchRoot = "LDAP://$LDAP"
'Member','Description','groupType' | %{$Searcher.PropertiesToLoad.Add($_)|Out-Null}
$Results=$Searcher.FindAll()
$GroupTypeDef = #{
1='System'
2='Global'
4='Domain Local'
8='Universal'
16='APP_BASIC'
32='APP_QUERY'
-2147483648='Security'
}
If($Results.Count -gt 0){
$Group = New-Object PSObject #{
'DistinguishedName'=[string]$Results.Properties.Item('adspath') -replace "LDAP\:\/\/"
'Scope'=$GroupTypeDef.Keys|?{$_ -band ($($Results.properties.item('GroupType')))}|%{$GroupTypeDef.get_item($_)}
'Description'=[string]$Results.Properties.Item('description')
'Members'=[string[]]$Results.Properties.Item('member')|% -Begin {$Searcher.PropertiesToLoad.Clear();$Searcher.PropertiesToLoad.Add('objectClass')|Out-Null} {$Searcher.Filter = "(distinguishedName=$_)";[PSCustomObject][ordered]#{'MemberType'=$Searcher.FindAll().Properties.Item('objectClass').ToUpper()[-1];'DistinguishedName'=$_}}
}
$Group|Select DistinguishedName,Scope,Description
$Group.Members|FT -AutoSize
}
Else{"Unable to find group '$Group' in '$env:USERDNSDOMAIN'.`nPlease check that you can access that domain from your current domain, and that the group exists."}
Here's one way to get the direct members of an AD group without using the AD cmdlets:
param(
[Parameter(Mandatory)]
$GroupName
)
$ADS_ESCAPEDMODE_ON = 2
$ADS_SETTYPE_DN = 4
$ADS_FORMAT_X500 = 5
function Invoke-Method {
param(
[__ComObject]
$object,
[String]
$method,
$parameters
)
$output = $object.GetType().InvokeMember($method,"InvokeMethod",$null,$object,$parameters)
if ( $output ) { $output }
}
function Set-Property {
param(
[__ComObject]
$object,
[String]
$property,
$parameters
)
[Void] $object.GetType().InvokeMember($property,"SetProperty",$null,$object,$parameters)
}
$Pathname = New-Object -ComObject "Pathname"
Set-Property $Pathname "EscapedMode" $ADS_ESCAPEDMODE_ON
$Searcher = [ADSISearcher] "(&(objectClass=group)(name=$GroupName))"
$Searcher.PropertiesToLoad.AddRange(#("distinguishedName"))
$SearchResult = $searcher.FindOne()
if ( $SearchResult ) {
$GroupDN = $searchResult.Properties["distinguishedname"][0]
Invoke-Method $Pathname "Set" #($GroupDN,$ADS_SETTYPE_DN)
$Path = Invoke-Method $Pathname "Retrieve" $ADS_FORMAT_X500
$Group = [ADSI] $path
foreach ( $MemberDN in $Group.member ) {
Invoke-Method $Pathname "Set" #($MemberDN,$ADS_SETTYPE_DN)
$Path = Invoke-Method $Pathname "Retrieve" $ADS_FORMAT_X500
$Member = [ADSI] $Path
"" | Select-Object `
#{
Name="group_name"
Expression={$Group.name[0]}
},
#{
Name="member_objectClass"
Expression={$member.ObjectClass[$Member.ObjectClass.Count - 1]}
},
#{
Name="member_sAMAccountName";
Expression={$Member.sAMAccountName[0]}
}
}
}
else {
throw "Group not found"
}
This version uses the Pathname COM object to handle name escaping and outputs the the object class and sAMAccountName for each member of the group.
I am working with PowerShell 4.0 and I am trying to pass a string array as one of the parameters for an Invoke-Command -ScriptBlock in which I am calling another PowerShell script on a remote server. When I do this, the string array seems to get flattened so that it appears as a single string value, rather than a string array.
Listed below is the 1st script, which is being called by a Bamboo deployment server that provides the initial parameters.
In the Debug section, the $SupportFolders string array is iterated by the FlowerBoxArrayText function and it properly writes the two folder paths to the console, as expected.
24-Oct-2017 14:59:33 *****************************************************************************
24-Oct-2017 14:59:33 **** E:\SRSFiles\SRSOutput
24-Oct-2017 14:59:33 **** E:\SRSFiles\SRSBad
24-Oct-2017 14:59:33 *****************************************************************************
Here is the initial part of the 1st script file, showing the input parameters, the string array creation and where I am calling the remote script via Invoke-Command;
[CmdletBinding(DefaultParametersetName='None')]
param (
# Allows you to specify Install, Delete or Check.
[ValidateSet("Install", "Delete", "Check")][string] $Action = "Check",
# Allows you to specify the remote server name.
[string] $ComputerName = "None",
# Allows you to specify the username to use for installing the service.
[string] $Username = "None",
# Allows you to specify the password to use for installing the service.
[string] $Password = "None",
# Allows you to specify the location of the support folders for the service, if used.
[string] $SupportFoldersRoot = "None"
)
Function CreateCredential()
{
$Pass = $Password | ConvertTo-SecureString -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential($Username, $Pass)
Return $Cred
}
Function FlowerBoxArrayText($TextArray, $TextColor="Yellow")
{
Write-Host "*****************************************************************************" -ForegroundColor $TextColor
foreach($TextLine in $TextArray)
{
IndentedText $TextLine $TextColor
}
Write-Host "*****************************************************************************" -ForegroundColor $TextColor
}
Function IndentedText($TextToInsert, $TextColor="Yellow")
{
Write-Host "**** $TextToInsert" -ForegroundColor $TextColor
}
$Credential = CreateCredential
[string[]] $ResultMessage = #()
[string] $Root = $SupportFoldersRoot.TrimEnd("/", "\")
[string[]] $SupportFolders = #("$Root\SRSOutput", "$Root\SRSBad")
#Debug
Write-Host "**** Starting debug in ManageAutoSignatureProcessorService ****"
FlowerBoxArrayText $SupportFolders -TextColor "Green"
Write-Host "**** Ending debug in ManageAutoSignatureProcessorService ****"
#End Debug
$ResultMessage = Invoke-Command -ComputerName $ComputerName -Credential $Credential -ScriptBlock {
param($_action,$_username,$_password,$_supportFolders) &"C:\Services\ManageService.ps1" `
-Action $_action `
-ComputerName DEV `
-Name DevProcessor `
-DisplayName 'DevProcessor' `
-Description 'DevProcessor' `
-BinaryPathName C:\Services\DevProcessor.exe `
-StartupType Manual `
-Username $_username `
-Password $_password `
-ServicePathName C:\Services `
-SupportFolders $_supportFolders `
-NonInteractive } -ArgumentList $Action,$Username,$Password,(,$SupportFolders)
if ($ResultMessage -like '*[ERROR]*')
{
FlowerBoxArrayText $ResultMessage -textColor "Red"
}
else
{
FlowerBoxArrayText $ResultMessage -textColor "Green"
}
Then, in the ManageService.ps1 script file on the remote server, I have the following;
[CmdletBinding(DefaultParametersetName='None')]
param (
# Allows you to specify Install, Delete or Check.
[ValidateSet("Install", "Delete", "Check")][string] $Action = "Check",
# Allows you to specify the name of the remote computer.
[string] $ComputerName = "None",
# Allows you to specify the service name.
[string] $Name = "None",
# Allows you to specify the service display name.
[string] $DisplayName = "None",
# Allows you to specify the service description.
[string] $Description = "None",
# Allows you to specify the path to the binary service executable file.
[string] $BinaryPathName = "None",
# Allows you to specify how the service will start, either manual or automatic.
[ValidateSet("Manual", "Automatic")][string] $StartupType = "Manual",
# Allows you to specify the domain username that the service will run under.
[string] $Username = "None",
# Allows you to specify the password for the domain username that the service will run under.
[string] $Password = "None",
# Allows you to specify the path to the service install scripts and service files on the remote server.
[string] $ServicePathName = "None",
# Allows you to specify the location of the support folders for the service, if used. The default value is an empty array
[string[]] $SupportFolders = #(),
# Disables human interaction, and allows all tests to be run even if they 'fail'.
[switch] $NonInteractive
)
Function CreateCredential()
{
$Pass = $Password | ConvertTo-SecureString -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential($Username, $Pass)
Return $Cred
}
[bool] $OkToInstall = $False
[string[]] $ResultMessage = #()
#Debug
$ResultMessage = $ResultMessage += "[DEBUG] ***************************************"
$ResultMessage = $ResultMessage += "[DEBUG] SupportFolders: [$SupportFolders] ."
foreach ($Folder in $SupportFolders)
{
$ResultMessage = $ResultMessage += "[DEBUG] SupportFolders Item: $Folder."
}
$Count = #($SupportFolders).Count
$ResultMessage = $ResultMessage += "[DEBUG] SupportFolders Count: $Count ."
$ResultMessage = $ResultMessage += "[DEBUG] ***************************************"
#End Debug
The line,
$ResultMessage = $ResultMessage += "[DEBUG] SupportFolders: [$SupportFolders] ."
shows the following result from the $ResultMessage value that is returned to the calling script;
**** [DEBUG] SupportFolders: [E:\SRSFiles\SRSOutput E:\SRSFiles\SRSBad] .
Notice that the array is flattened out.
The foreach loop that follows also only prints out one value instead of two;
"E:\SRSFiles\SRSOutput E:\SRSFiles\SRSBad"
I have spent considerable time researching a solution but have yet to find an answer.
Any ideas?
EDIT 1 using #Bacon Bits suggestion;
$Options = #{'Action' = $Action
'ComputerName' = 'DEV'
'Name' = 'DevProcessor'
'DisplayName' = 'DevProcessor'
'Description' = 'Generate daily processes'
'BinaryPathName' = 'C:\Services\DevProcessor\DevProcessor.exe'
'StartupType' = 'Manual'
'Username' = $Username
'Password' = $Password
'ServicePathName' = 'C:\Services\DevProcessor'
'SupportFolders' = $SupportFolders
}
$ScriptBlock = {
param($Options)
& {
param(
$Action,
$ComputerName,
$Name,
$DisplayName,
$Description,
$BinaryPathName,
$StartupType,
$Username,
$Password,
$ServicePathName,
$SupportFolders,
$NonInteractive
)
&powershell "C:\Services\DevProcessor\ManageService.ps1 $Action $ComputerName $Name $DisplayName $Description $BinaryPathName $StartupType $Username $Password $ServicePathName $SupportFolders"
} #Options;
}
$ResultMessage = Invoke-Command -ComputerName $ComputerName -Credential $Credential -ScriptBlock $ScriptBlock -ArgumentList $Options
If I run the code modified as it is listed above, I still get the flattened array for $SuppportFolders and the ManageService.ps1 script trips up over parameters that have spaces, even though they are quoted when I assign them.
The option to completely wrap the code in ManageService.ps1, as opposed to simply calling the remote script is not really viable because the ManagedService.ps1 script is fairly extensive and generic so I can call it from over 30 automation scripts in my deployment server.
I believe what #Bacon Bits is suggesting would work if it was feasible to wrap the ManageService script.
To pass a single array, you can do this:
Invoke-Command -Session $Session -ScriptBlock $ScriptBlock -ArgumentList (,$Array);
However, that only works if you only need to pass a single array. It can all fall apart as soon as you start to pass multiple arrays or multiple complex objects.
Sometimes, this will work:
Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList (, $Array1), (, $Array2), (, $Array3);
However, it can be inconsistent in my experience. Sometimes it flattens the arrays out again.
What you can do is something similar to this answer.
{param($Options)& <# Original script block (including {} braces)#> #options }
Basically what we do is:
Wrap the script in a scriptblock that accepts a single hashtable as an argument.
Put all our arguments into the hashtable.
Use the passed hashtable as a splat variable.
So it would be something like:
$Options = #{
Action = 'Check';
ComputerName = 'XYZ123456';
Name = 'MyName';
.
.
.
}
$ScriptBlock = {
param($Options)
& {
[CmdletBinding(DefaultParametersetName='None')]
param (
# Allows you to specify Install, Delete or Check.
[ValidateSet("Install", "Delete", "Check")][string] $Action = "Check",
# Allows you to specify the name of the remote computer.
[string] $ComputerName = "None",
# Allows you to specify the service name.
[string] $Name = "None",
.
.
.
.
#End Debug
} #Options;
}
Invoke-Command -ComputerName RemoteServer -ScriptBlock $ScriptBlock -ArgumentList $Options;
Here's a trivial working example:
$Options = #{
List1 = 'Ed', 'Frank';
List2 = 5;
List3 = 'Alice', 'Bob', 'Cathy', 'David'
}
$ScriptBlock = {
param($Options)
& {
param(
$List1,
$List2,
$List3
)
"List1"
$List1
''
"List2"
$List2
''
"List3"
$List3
} #Options;
}
Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $Options;
Output:
List1
Ed
Frank
List2
5
List3
Alice
Bob
Cathy
David
Note that I tested this on PowerShell v5. I no longer have a system with PowerShell v4 to test on.
I have a powershell script that measures download time on some pages, however I get the error above, I am unsure what I am doing wrong
error is
Cannot bind argument to parameter 'InputObject' because it is null.
function ResponseTime($CommonName,$URL, $environment)
{
$Times = 5
$i = 0
$TotalResponseTime = 0
Write-HOst $URL
While ($i -lt $Times) {
$Request = New-Object System.Net.WebClient
$Request.UseDefaultCredentials = $true
$Start = Get-Date
Write-HOst $URL
$PageRequest = $Request.DownloadString($URL)
$TimeTaken = ((Get-Date) - $Start).TotalMilliseconds
$Request.Dispose()
$i ++
$TotalResponseTime += $TimeTaken
}
$AverageResponseTime = $TotalResponseTime / $i
Write-Host Request to $CommonName took $AverageResponseTime ms in average -ForegroundColor Green
$details = #{
Date = get-date
AverageResponseTime = $AverageResponseTime
ResponseTime = $Destination
Environment = $environment
}
$results += New-Object PSObject -Property $details
$random = Get-Random -minimum 1 -maximum 30
Start-Sleep -s $random
}
#PRODUCTION
ResponseTime -commonname 'app homepage' -URL 'https://url1' -environment 'PRODUCTION'
ResponseTime -commonname 'department homepage' -URL 'https://url2' -environment 'PRODUCTION'
$results | export-csv -Path c:\so.csv -NoTypeInformation
Reviewing your last edit, it seems that $results simply returns $null (As your error says)
The only line setting $results is $results += New-Object PSObject -Property $details
It is not in the scope of your Export-CSV call and - even if it would, $results could be empty, if this line is not called.
You should IMHO set it to e.g. an ArrayList like follows:
$results = New-Object -TypeName System.Collections.ArrayList
And add items to it via
$times = ResponseTime -commonname '' #etc
$results.Add($times) | Out-Null
This gives you an ArrayList - even if there are no items in it - which can easily be transformed to CSV and other formats.
#Clijsters has given the correct answer; i.e. the issue being the scope of your $results variable.
This answer just provides a bit of a code review to help you with other bits going forwards...
function Get-ResponseTime {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]$CommonName
,
[Parameter(Mandatory = $true)]
[string]$URL
,
[Parameter(Mandatory = $true)]
[string]$Environment
,
[Parameter(Mandatory = $false)]
[int]$Times = 5
)
[System.Int64]$TotalResponseTime = 0
[System.Diagnostics.Stopwatch]$stopwatch = New-Object 'System.Diagnostics.Stopwatch'
Write-Verbose "Processing URL: $URL"
1..$times | foreach-object {
[System.Net.WebClient]$Request = New-Object 'System.Net.WebClient'
$Request.UseDefaultCredentials = $true
Write-Verboset "Call $_ to URL: $URL"
$stopwatch.Restart()
$PageRequest = $Request.DownloadString($URL)
$stopwatch.Stop()
$TimeTaken = $stopwatch.Elapsed.TotalMilliseconds
$Request.Dispose()
$TotalResponseTime += $TimeTaken
}
$AverageResponseTime = $TotalResponseTime / $Times
Write-Verbose "Request to $CommonName took $AverageResponseTime ms on average"
$details = #{
Date = get-date
AverageResponseTime = $AverageResponseTime
#ResponseTime = $Destination #this is not declared anywhere / don't know what this field's for
Environment = $environment
}
Write-Output (New-Object 'PSObject' -Property $details)
#do you really want a delay here? Doesn't make much sense... may make sense to include a delay in the above loop; i.e. to stagger your tests?
#$random = Get-Random -minimum 1 -maximum 30
#Start-Sleep -s $random
}
#PRODUCTION
[PSObject[]]$results = #(
(Get-ResponseTime -commonname 'app homepage' -URL 'https://url1' -environment 'PRODUCTION' -Verbose)
,(Get-ResponseTime -commonname 'department homepage' -URL 'https://url2' -environment 'PRODUCTION' -Verbose)
)
$results | Export-Csv -LiteralPath 'c:\so.csv' -NoTypeInformation
Use verb-noun function names (e.g. Get-Item). What is the naming convention for Powershell functions with regard to upper/lower case usage?
Use "Cmdlets" (Advanced Functions) instead of (Basic) Functions; they're basically the same thing, only tagged with [Cmdletbinding()]. The reason for this you get support for functionality such as verbose output. http://www.lazywinadmin.com/2015/03/standard-and-advanced-powershell.html
Use a stopwatch to time processes (you could also use measure-command; but any output would be suppressed / consumed by the measure-command function). Timing a command's execution in PowerShell
Have your cmdlet output its values to the pipeline via Write-Output (or you can leave off the function name; any output caused by placing a variable with nothing to process it will be fed to the pipeline; i.e. write-object $a is the same as a line solely consisting of $a).
Capture the output into your $results variable outside of the function, and handle the results there.
I use Powershell to run several reports on Microsoft SQL Report Services and to save the results to a Word doc. I have a script with functions that handle communications with the Report Server:
## File "qrap-functions.ps1"
function GetRSConnection($server, $instance)
{
$User = "xxxx"
$PWord = ConvertTo-SecureString -String "yyyy" -AsPlainText -Force
$c = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $User, $PWord
$reportServerURI = "http://" + $server + "/" + $instance + "/ReportExecution2005.asmx?WSDL"
$RS = New-WebServiceProxy -Class 'RS' -NameSpace 'RS' -Uri $reportServerURI -Credential $c
$RS.Url = $reportServerURI
return $RS
}
function GetReport($RS, $reportPath)
{
$reportPath = "/" + $reportPath
#$reportPath
$Report = $RS.GetType().GetMethod("LoadReport").Invoke($RS, #($reportPath, $null))
$parameters = #()
$RS.SetExecutionParameters($parameters, "nl-nl") > $null
return $report
}
function AddParameter($params, $name, $val)
{
$par = New-Object RS.ParameterValue
$par.Name = $name
$par.Value = $val
$params += $par
return ,$params
}
function GetReportInFormat($RS, $report, $params, $format, $saveas)
{
$deviceInfo = "<DeviceInfo><NoHeader>True</NoHeader></DeviceInfo>"
$extension = ""
$mimeType = ""
$encoding = ""
$warnings = $null
$streamIDs = $null
$RS.SetExecutionParameters($params, "nl-nl") > $null
$RenderOutput = $RS.Render($format,
$deviceInfo,
[ref] $extension,
[ref] $mimeType,
[ref] $encoding,
[ref] $warnings,
[ref] $streamIDs
)
$Stream = New-Object System.IO.FileStream($saveas), Create, Write
$Stream.Write($RenderOutput, 0, $RenderOutput.Length)
$Stream.Close()
}
Then, I have a script that executes a report containing the financial quarterly data. This script runs fine:
## File "qrap-financieel.ps1"
. "./qrap-functions.ps1"
$saveas = "e:\test\financieel.doc"
$RS = GetRSConnection -server "MSZRDWH" -instance "reportserver_acc"
$report = GetReport -RS $RS -reportPath "kwartaalrapportage/kwartaalrapportage financieel"
$params = #()
$kwartaal = "[Periode Maand].[Jaar Kwartaal].&[2015-2]"
$kptc = "[Kostenplaats].[Team code].&[2003]"
$params = AddParameter -params $params -name "PeriodeMaandJaarKwartaal" -val $kwartaal
$params = AddParameter -params $params -name "KostenplaatsTeamcode" -val $kptc
GetReportInformat -RS $RS -report $report -params $params -format "WORD" -saveas $saveas
The values for $kwartaal and $kptc are hard-coded here, but are parameters in the actual version of this script. Besides the financial quarterly, we have three other quarterly reports that need to be output by this script.
Two of these run fine, in the fourth I can't seem to get one of the parameters right. The script for that one is:
## File "qrap-zorglog.ps1"
. "./qrap-functions.ps1"
$RS = GetRSConnection -server "MSZRDWH" -instance "reportserver_acc"
$report = GetReport -RS $RS -reportPath "kwartaalrapportage/kwartaalrapportage zorglogistiek"
$s = "Urologie"
$saveas = "e:\test\ZL Urologie.doc"
$params = #()
$kwartaal = "[Periode Maand].[Jaar Kwartaal].&[2015-2]"
$params = AddParameter -params $params -name "HoofdspecialismeSpecialismeOms" -val "[Hoofdspecialisme].[Specialisme Oms].&[$s]"
$params = AddParameter -params $params -name "PeriodeMaandJaarKwartaal" -val $kwartaal
$params = AddParameter -params $params -name "WachttijdenSpecialismeSpecialisme" -val "[Wachttijden Specialisme].[Specialisme].&[$s]"
$params = AddParameter -params $params -name "SpecialisatieGroeperingSpecialisatieGroeperingOms" -val "[Specialistie Groepering].[Specialistie Groepering Oms].&[$s]"
$params = AddParameter -params $params -name "AanvragendSpecialismeSpecialismeOms" -val "[AanvragendSpecialisme].[Specialisme Oms].&[$s]"
GetReportInformat -RS $RS -report $report -params $params -format "WORD" -saveas $saveas
When I execute this script, I get this error:
Exception calling "Render" with "7" argument(s): "System.Web.Services.Protocols.SoapException: This report requires a
default or user-defined value for the report parameter 'HoofdspecialismeSpecialismeOms'. To run or subscribe to this
report, you must provide a parameter value. ---> Microsoft.ReportingServices.Diagnostics.Utilities.ReportParameterValueNot
SetException: This report requires a default or user-defined value for the report parameter
'HoofdspecialismeSpecialismeOms'. To run or subscribe to this report, you must provide a parameter value.
I clearly DO supply a value for 'HoofdspecialismeSpecialismeOms'; I've previously noticed that this error also is thrown when the parameter is not in the expected format. This format, since the
report filter is based on a hierarchy in an SSAS cube, looks like this: [hierarchy].[sub-level].&[member]. I've ensured that [Hoofdspecialisme].[Specialisme Oms].&[$s] is the correct format by
looking it up in the query that populates the prompt in SSRS. The report does display data when run through SSRS - and taking a parameter from the prompt.
I did notice that this parameter allows multiple selection. However, I don't believe this leads to the error because that is also true for AanvragendSpecialismeSpecialismeOms.
Any idea why this one parameter fails to be fed into the report when calling GetReportInformat?
Have you tried
function AddParameter($params, $name, $val)
{
$par = New-Object RS.ParameterValue
$par.Name = $name
$par.Value = $val
$params += $par
return ,$params
# ^Removing this comma?
}
As well as declaring the data types explicitly for your parameters?
function AddParameter([Array]$params, [String]$name, [String]$val)
{
$par = New-Object RS.ParameterValue
$par.Name = $name
$par.Value = $val
$params += $par
return ,$params
}
Also, with so many user-defined helper functions calling imported types that call methods and set properties to a report we can't see, it can get a little difficult to help troubleshoot in-depth for this specific report you're getting an error on. It looks like you've tried moving the line around in the order which sounds to me like you might have an issue with how that specific report parses the values you input through RS.ParameterValue so maybe take a look at if it accepts the string you set in -val for your AddParameter user defined function.
Edit:
From https://social.msdn.microsoft.com/Forums/sqlserver/en-US/e38b4a34-c780-43bb-8321-15f96d0938a9/exception-calling-render-systemwebservicesprotocolssoapexception-one-or-more-data-source?forum=sqlreportingservices
This error is generated when you are attempting to run a report in which one or more of the data sources are set to "prompt" credentials. This means we do not use your Windows credentials automatically, but rather you need to supply a different set of credentials which are used only for the data source.
Sounds like you might need to put aside the script and check if the report is different.
I've finally figured it out: The failing prompt had a multi-select enabled. And when filling in a multi-select, SSRS expects a list of values. When only given one string, the string is ignored and the parameter is assumed blank.
To feed it a list, we must do:
$multival = New-Object System.Collections.Specialized.StringCollection
$multival.Add("[Hoofdspecialisme].[Specialisme Oms].&[$s]")
[snip]
$params = AddParameter -params $params -name "HoofdspecialismeSpecialismeOms" -val $multival
Found the answer thanks to this question:
How to pass multiple value parameter to reporting services report via powershell