Script return multiple values - powershell

We have a powershell script that download data from an url, verify the data using some generic and simple rules and return success or failed (0 or 1). Here is an example
Get-data -uri "http://www.google.com/some_path"
This script is used successfully in many situations. However, in some cases the simple rules implemented in the Get-data script is not enough to verify the data. We do not want to add a lot of domain specific rules into the Get-data. It would be much better if the parent script performed the additional verification but then it needs access to the raw data. How can we return both a boolean return value of success \ failed and a data object?

How about returning an object instead of a bool:
$props = #{
Success = $result
Data = $theData
}
$object = new-object psobject -Property $props
return $object
You can get the object like so:
$result = Get-data -uri "http://www.google.com/some_path"
if($result.success) {
# Do all the stuff you want with $result.data
}
Read more about creating objects here.

Related

Generate CLIXML string from a PowerShell object without serializing to disk first

I have the following code which exports an object to an XML file, then reads it back in and prints it on the Information stream.
try{
# Sample object
$Person = #{
Name = 'Bender'
Age = 'At least 1074'
}
$Person | Export-CliXml obj.xml
$cliXml = Get-Content -Raw ./obj.xml
Write-Host $cliXml
} finally {
if( Test-Path ./obj.xml ) {
Remove-Item -Force ./obj.xml -EV rError -EA SilentlyContinue
if( $rError ) {
Write-Warning "Failed to remove ./obj.xml: $($rError.Exception.Message)"
}
Remove-Variable -Force $rError -EA Continue
}
}
There is a system-local parent session which watches its STDOUT for this output and reconstructs it to an object in its own session.
NOTE: I know a local PSRemoting session would work, but I need this to also work on systems which PSRemoting has either not yet been configured or will not be.
I'd like to cut out the middleman and instead of writing the object to disk. Unfortunately,Import-CliXMl and Export-CliXml are the only cmdlets with CliXml in the name, and doing some .NET documentation sleuthing has turned up nothing so far.
Is there a way to simply serialize an object to a CliXml string without writing to disk first? I've considered using $Person | ConvertTo-Json -Compress -Depth 100 but this has two issues:
Only captures nested objects up to 100 levels deep. This is an edge case but still a limit I'd like to avoid. I could always use another library or another format, but;
I want these to be reconstructed into .NET objects of the same type they were before serialization. Recreating objects with CliXml is the only way I'm aware of that this can be done.
The CliXml serializer is exposed via the [PSSerializer] class:
$Person = #{
Name = 'Bender'
Age = 'At least 1074'
}
# produces the same XML ouput as `Export-CliXml $Person`
[System.Management.Automation.PSSerializer]::Serialize($Person)
To deserialize CliXml, use the Deserialize method:
$cliXml = [System.Management.Automation.PSSerializer]::Serialize($Person)
$deserializedPerson = [System.Management.Automation.PSSerializer]::Deserialize($cliXml)
To complement Mathias' helpful answer:
Introducing in-memory equivalents to the file-based Export-CliXml and Import-CliXml cmdlets - in the form of new ConvertTo-CliXml and ConvertFrom-CliXml cmdlets - has been green-lighted in principle, but is still awaiting implementation by the community (as of PowerShell 7.2.1) - see GitHub issue #3898 and the associated pending, but seemingly stalled community PR #12845
Note that [System.Management.Automation.PSSerializer]::Serialize() defaults to a recursion depth of 1, whereas Export-CliXml defaults to 2; use the overload that allows specifying the recursion depth explicitly, if needed (e.g., [System.Management.Automation.PSSerializer]::Serialize($Person, 2))

Powershell Script for Okta API to add single group to Multiple Apps

I have a group called Group1 and I have approx 50-60 similar apps that contain the same name like "ABC-423", "ABC-4242" etc.
What i want is to add this single group(Group-1) to this different apps "ABC*" by using Okta API with Powershell (API - https://github.com/gabrielsroka/OktaAPI.psm1)
here is the code I wrote so far
#Purpose of Code : We want to Add Group named GRPS1 to Okta Apps that's name contains ABC
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# [1] Connect to Okta. Do this before making any other calls. - done
Connect-Okta "<redacted>" "https://yourorg.oktapreview.com"
$url = "https://yourorg.oktapreview.com"
$appdata = #()
$groupid = "00gtx2yruqKg9diIJ0h7" #00gtx2yruqKg9diIJ0h7 => GRPS1
$group = 0
$i=0
$j=0
#Function to Get all Active OKTA Application List
function Get-AllActiveApps($url)
{
# [2] Now we want to Store JSON data from $appdata to Powershell Object - done
$appdata = #(Invoke-Method GET "/api/v1/apps?status eq 'Active'")
foreach($i in $appdata[0].label)
{
# [3.1] Use a For loop to get one by one application object checked with name contaning ABC
if ($i -like '*ABC*')
{
# [3.2] Assign Group to all apps that's name contains ABC
$appnumber = $appdata[0][$j].id
Add-OktaAppGroup($appnumber,$groupid,$group)
}
else
{
}
$j++
}
}
Get-AllActiveApps($url)
The problem is at function Add-OktaAppGroup($appnumber,$groupid,$group) because the loop it is passing all application-ids as one single array and the function can only process one group id at a time
Can anyone help how to solve this?
Your Get-AllActiveApps function is pretty much useless. After taking out all the parts that don't have anything to do with the task of getting all active apps, we would be left with
function Get-AllActiveApps
{
Invoke-Method GET '/api/v1/apps?filter=status eq "Active"'
}
and that function already exists in OktaAPI.psm1, in a better form:
function Get-OktaApps($filter, $limit = 20, $expand, $url = "/api/v1/apps?filter=$filter&limit=$limit&expand=$expand&q=$q", $q)
{
# ...
}
So let's use this instead, with the -filter parameter.
The rest of your task (filtering some more, and doing something for each item) is PowerShell 101. You don't need any custom functions or loops for that, a pipeline is enough:
Connect-Okta -baseUrl "https://yourorg.oktapreview.com" -token "<redacted>"
# using a hash for group IDs allows easy reference in the rest of the code
$groups = #{
GRPS1 = "00gtx2yruqKg9diIJ0h7"
}
$activeApps = Get-OktaApps -filter 'status eq "ACTIVE"' -limit 200
$activeApps.objects | Where-Object label -like '*ABC*' | Where-Object {
$appGroups = Get-OktaAppGroups -appid $_.id -limit 200
$appGroups.objects.id -notcontains $groups.GRPS1
} | ForEach-Object {
Add-OktaAppGroup -appid $_.id -groupid $groups.GRPS1
}
Notes
You don't need to set the [Net.ServicePointManager]::SecurityProtocol, Connect-Okta already does that.
App groups are not part of the API response when you get a list of apps, that's why the second Where-Object fetches the app groups for each app.
$appGroups.objects.id will be an array of the IDs of all returned groups. -notcontains $groups.GRPS1 produces $true or $false, depending. This will be the filter criterion for Where-Object, i.e. "all apps where the list of group IDs does not contain that particular ID".
Many Okta API responses are paged. 200 objects per page seems to be the upper limit, as per the API documentation. Pay attention to situations where there are more objects than that. Read the documentation to OktaAPI.psm1 to learn how to handle these cases, because the code above does not.
You should probably add some debugging output. Read about Write-Debug and the $DebugPreference global variable.
Note that the Okta Apps API can let you search by name using the -q parameter, eg
Get-OktaApps -q "ABC-"
It will return up to 200 apps; you can paginate for more.
See https://developer.okta.com/docs/reference/api/apps/#list-applications

How do I get events associated with Datastores and Datastore Clusters using PowerCLI?

This is a followup to this question I posted but running into an issue with enumerating events on some objects now. When I run the following code (or try any of the solutions in my prior question) to get the events from a datastore or datastore cluster for example:
Get-VMFolder FOLDER_NAME -Type Datastore | Get-DatastoreCluster | Get-VIEvent
I'm met with the following error for each event it tries to return:
Events can be retrieved only for inventory objects. The entity of type
'VMware.VimAutomation.ViCore.Impl.V1.DatastoreManagement.VmfsDatastoreImpl' will be ignored.
This is particularly annoying since the cmdlet clearly enumerates the events as I get this error for every event it attempts to return.
When I use the Get-TaskPlus function mentioned in the accepted answer to my prior question returns a different type conversion error:
Cannot process argument transformation on parameter 'Entity'. Cannot convert the "DATASTORE_CLUSTER_NAME" value of type "VMware.VimAutomation.ViCore.Impl.V1.DatastoreManagement.DatastoreClusterImpl" to type "VMware.VimAutomation.ViCore.Impl.V1.Inventory.InventoryItemImpl".
If I remove the type constraint on the $Entity argument in the function definition, the error goes away but I also don't get any results.
I'm not really looking for recommendations or tools here, but if Get-VIEvent to look for events on non-inventory objects through PowerCLI, is there a workaround or more nuanced way to retrieve this information with PowerCLI?
After I posted this I did some more digging and found that Luc Dekens wrote another function calledGet-VIEventPlus. This doesn't work out of the box because -Entity expects a type of VMware.VimAutomation.ViCore.Impl.V1.Inventory.InventoryItemImpl[], but datastores and datastore clusters have a different type under the VMWare.VimAutomation.ViCore.Impl.V1.DatastoreManagement namespace.
If we make one change to LucD's function to accept the base type VMware.VimAutomation.ViCore.Impl.V1.VIObjectImpl[] instead of InventoryItemImpl[], Get-VIEventPlus should work other vCenter object types:
function Get-VIEventPlus {
<#
.SYNOPSIS Returns vSphere events
.DESCRIPTION The function will return vSphere events. With
the available parameters, the execution time can be
improved, compered to the original Get-VIEvent cmdlet.
.NOTES Author: Luc Dekens
.PARAMETER Entity
When specified the function returns events for the
specific vSphere entity. By default events for all
vSphere entities are returned.
.PARAMETER EventType
This parameter limits the returned events to those
specified on this parameter.
.PARAMETER Start
The start date of the events to retrieve
.PARAMETER Finish
The end date of the events to retrieve.
.PARAMETER Recurse
A switch indicating if the events for the children of
the Entity will also be returned
.PARAMETER User
The list of usernames for which events will be returned
.PARAMETER System
A switch that allows the selection of all system events.
.PARAMETER ScheduledTask
The name of a scheduled task for which the events
will be returned
.PARAMETER FullMessage
A switch indicating if the full message shall be compiled.
This switch can improve the execution speed if the full
message is not needed.
.EXAMPLE
PS> Get-VIEventPlus -Entity $vm
.EXAMPLE
PS> Get-VIEventPlus -Entity $cluster -Recurse:$true
#>
param(
[VMware.VimAutomation.ViCore.Impl.V1.VIObjectImpl[]]$Entity,
[string[]]$EventType,
[DateTime]$Start,
[DateTime]$Finish = (Get-Date),
[switch]$Recurse,
[string[]]$User,
[Switch]$System,
[string]$ScheduledTask,
[switch]$FullMessage = $false
)
process {
$eventnumber = 100
$events = #()
$eventMgr = Get-View EventManager
$eventFilter = New-Object VMware.Vim.EventFilterSpec
$eventFilter.disableFullMessage = ! $FullMessage
$eventFilter.entity = New-Object VMware.Vim.EventFilterSpecByEntity
$eventFilter.entity.recursion = &{if($Recurse){"all"}else{"self"}}
$eventFilter.eventTypeId = $EventType
if($Start -or $Finish){
$eventFilter.time = New-Object VMware.Vim.EventFilterSpecByTime
if($Start){
$eventFilter.time.beginTime = $Start
}
if($Finish){
$eventFilter.time.endTime = $Finish
}
}
if($User -or $System){
$eventFilter.UserName = New-Object VMware.Vim.EventFilterSpecByUsername
if($User){
$eventFilter.UserName.userList = $User
}
if($System){
$eventFilter.UserName.systemUser = $System
}
}
if($ScheduledTask){
$si = Get-View ServiceInstance
$schTskMgr = Get-View $si.Content.ScheduledTaskManager
$eventFilter.ScheduledTask = Get-View $schTskMgr.ScheduledTask |
where {$_.Info.Name -match $ScheduledTask} |
Select -First 1 |
Select -ExpandProperty MoRef
}
if(!$Entity){
$Entity = #(Get-Folder -Name Datacenters)
}
$entity | %{
$eventFilter.entity.entity = $_.ExtensionData.MoRef
$eventCollector = Get-View ($eventMgr.CreateCollectorForEvents($eventFilter))
$eventsBuffer = $eventCollector.ReadNextEvents($eventnumber)
while($eventsBuffer){
$events += $eventsBuffer
$eventsBuffer = $eventCollector.ReadNextEvents($eventnumber)
}
$eventCollector.DestroyCollector()
}
$events
}
}
Update: The original answer changed the -Entity type to be an object[]. I have updated this function to make use of the VMware.VimAutomation.ViCore.Impl.V1.VIObjectImpl[] base type for -Entity instead.

Powershell: Multiple parameters for a TabExpansion++ ArgumentCompleter

I am working on a function to schedule a user's home drive transfer, I am going to use TabExpansion++ to allow the user to autocomplete the server name, which is populated from a CSV file. There will be parameters for both OldServer and NewServer.
Is it possible with TabExpansion++ to specify more than one parameter for a single autocompleter?
Here is what I have:
function HomeDriveSiteCompletion {
[ArgumentCompleter(
Parameter = 'OldServer',
Command = { 'Schedule-HomeTransfer' },
Description = 'Home drive transfer tool server name autocomplete')]
param($commandName,$parameterName,$wordToComplete,$commandAst,$fakeBoundParameter)
Import-Csv -Path $Global:ServersList | % {New-CompletionResult -ToolTip $_.Site -completiontext $_.Site}
}
Which works fine for OldServer. If I can save code by specifying both parameters in the same place, that would be ideal. I have tried both
Parameter = #('OldServer','NewServer')
and
Parameter = { 'OldServer','NewServer' }
Neither of which worked. Is there another way I could make this work?
Questions like this are why I love this site. I have not used TabExpansion++, but I have done some tab expansion stuff for parameters. I couldn't remember if I'd run into this exact question before so I went looking and discovered something that I haven't encountered in the PowerShell world before, DynamicParam. How have I not seen this before? The levels of awesome of it for situations like this are right off the charts! What it allows you to do is not declare a parameter, but then add that parameter before the actual scriptblock of the function, and do scripty kinds of things for validation of that parameter.
I asked Google for a little help, and it pointed me to this SO question (where Shay Levy gives the accepted answer recommending TabExpansion++), but the next answer goes on about DynamicParam. So I looked that up and found this blog on Microsoft's site that explains it further. Basically for your needs you would do something like:
DynamicParam {
$SrvList = Import-CSV $Global:ServerList | Select -Expand Site
$ParamNames = #('OldServer','NewServer')
#Create Param Dictionary
$ParamDictionary = new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
ForEach($Name in $ParamNames){
#Create a container for the new parameter's various attributes, like Manditory, HelpMessage, etc that usually goes in the [Parameter()] part
$ParamAttribCollecton = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
#Create each attribute
$ParamAttrib = new-object System.Management.Automation.ParameterAttribute
$ParamAttrib.Mandatory = $true
$ParamAttrib.HelpMessage = "Enter a server name"
#Create ValidationSet to make tab-complete work
$ParamValSet = New-Object -type System.Management.Automation.ValidateSetAttribute($SrvList)
#Add attributes and validationset to the container
$ParamAttribCollecton.Add($ParamAttrib)
$ParamAttribCollecton.Add($ParamValSet)
#Create the actual parameter, then add it to the Param Dictionary
$MyParam = new-object -Type System.Management.Automation.RuntimeDefinedParameter($Name, [String], $ParamAttribCollecton)
$ParamDictionary.Add($Name, $MyParam)
}
#Return the param dictionary so the function can add the parameters to itself
return $ParamDictionary
}
That would add the OldServer and NewServer parameters to your function. Both would tab-complete the servers listed in the Site column of the CSV located at $global:ServerList. Sure, it's not as short and sweet as TabExpansion++'s context, but on the other hand it does not require any additional modules or anything to be loaded on the system since it is all self contained and only using basic PowerShell features.
Now, that adds the parameters, but it doesn't actually assign them to variables, so we'll have to do that in the Begin part of the function. We'll list the parameters in PSBoundParameters.Keys and check if a variable already exists in the current scope, and if not we'll make one in the current scope, so as to mess with anything outside of the function. So, with a basic parameter of -User, the two dynamic parameters, and the addition of the variables for the dynamic parameters, we're looking at something like this for your function:
Function Schedule-HomeTransfer{
[CmdletBinding()]
Param([string]$User)
DynamicParam {
$SrvList = Import-CSV $Global:ServerList | Select -Expand Site
$ParamNames = #('OldServer','NewServer')
#Create Param Dictionary
$ParamDictionary = new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
ForEach($Name in $ParamNames){
#Create a container for the new parameter's various attributes, like Manditory, HelpMessage, etc that usually goes in the [Parameter()] part
$ParamAttribCollecton = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
#Create each attribute
$ParamAttrib = new-object System.Management.Automation.ParameterAttribute
$ParamAttrib.Mandatory = $true
$ParamAttrib.HelpMessage = "Enter a server name"
#Create ValidationSet to make tab-complete work
$ParamValSet = New-Object -type System.Management.Automation.ValidateSetAttribute($SrvList)
#Add attributes and validationset to the container
$ParamAttribCollecton.Add($ParamAttrib)
$ParamAttribCollecton.Add($ParamValSet)
#Create the actual parameter, then add it to the Param Dictionary
$MyParam = new-object -Type System.Management.Automation.RuntimeDefinedParameter($Name, [String], $ParamAttribCollecton)
$ParamDictionary.Add($Name, $MyParam)
}
#Return the param dictionary so the function can add the parameters to itself
return $ParamDictionary
}
Begin{$PSBoundParameters.Keys | Where{!(Get-Variable -name $_ -Scope 0 -ErrorAction SilentlyContinue)} | ForEach{New-Variable -Name $_ -Value $PSBoundParameters[$_]}}
Process{
"You chose to move $User from $OldServer to $NewServer"
}
}
That right there will allow for tab completion on -OldServer and -NewServer, and when I set $global:ServerList to "C:\Temp\new.csv' and populated that with a 'Site' column having 3 values, those popped right up for me to select (in the ISE it actually pops up a list to choose from, not just tab completion like in the console).

How do I write a PowerShell cmdlet to take either a HashTable or a PODO for input?

I have a powershell module that wraps around some web services. The web services take complex Plain Old Dot Net Objects (PODOs) and I have been using HashTables as in cmdlet parameters and New-Object MyPODO -Property $MyHashTable to transform the hashtable into the request object like so
function Get-Stuff ([HashTable]$WhatStuff) {
$service = New-ServiceProxy . . . .
$request = New-Object GetStuffRequest -Property $WhatStuff;
return $service.GetStuff($request);
$response;
}
However, sometimes I have a cmdlet whose response object can directly become a request object like so:
function Find-Stuff ([HashTable]$KindaStuff) {
$service = New-ServiceProxy . . . .
$request = New-Object GetStuffRequest -Property $KindaStuff;
return $service.SearchStuff($request);
}
Is there some sort of way to decorate the $WhatStuff parameter to accept either a HashTable or a PODO of a particular type?
James Tryand gave me this answer in a tweet.
The answer is to use Parameter Sets.
In one paramater set you accept a parameter of type HashTable, and in the other one, you accept the PODO type.
Maybe like below, depending on how you want to use it:
function Get-Stuff ($WhatStuff) {
if(($WhatStuff -isnot [HashTable]) -or ($WhatStuff -isnot [PODOType])){ throw "expect it to be Hashtable or object of type"}
...
}