Today I quickly needed to split users into migration batches for Office 365, and with that I needed to create many buckets of 100 user objects each. The following script can help you out if you need something similar.
Continue reading “Splitting objects into buckets using PowerShell”Tag: PowerShell
Checking Azure AD tenant id using PowerShell
This is a short blog post with a PowerShell cmdlet that will return you the Azure AD tenant id for a given domain.
function Get-AzureADTenantId
{
[CmdletBinding()]
[Alias()]
[OutputType([string])]
Param
(
# Param1 help description
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
$DomainName
)
Begin
{
Load-Assembly System.Xml.Linq | Out-Null
}
Process
{
$FederationMetadata = Get-AzureADFederationMetadata -Domain $DomainName
$FederationMetadata.EntityDescriptor.entityID -split "/" | where{$_ -match "^[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}$"}
}
End
{
}
}
Get-AzureADTenantId microsoft.com
Get-AzureADTenantId microsoft.onmicrosoft.com
How Active Directory vNext and the PAM feature works under the hood
So, i decided to figure out how the new PAM feature in MIM worked as a POC for a customer. I configured two forests (red.goodworkaround.com and blue.goodworkaround.com), where RED was the Privileged Access Management forest.
I got MIM PAM up and running in a matter of minutes using the TechNet Configuring the MIM Environment for Privileged Access Managment. As PAM is still very new, I figure it would be a good idea to follow a guide for once.
I imported some groups using the code below, so far so good. The groups was created in the RED forest with sidHistory from the BLUE forest.
$cred = get-credential -UserName RED\marius -Message "RED forest domain admin credentials" New-PAMGroup -SourceGroupName "Service Admin - Service G" -SourceDomain blue.goodworkaround.com -Credentials $cred -SourceDC blue-pc.blue.goodworkaround.com
I tested with the Domain Admins group, and it failed with the error message about limitations in sidHistory not allowing this type of group. I already knew this going in, but I figured I wanted to try.
My customer really want to control their Domain Admins group, so we digged further into the new functionality in Windows Server vNext / Threshold. Here there is a new feature which they sometimes call “Foreign Principal Groups” and sometimes “Shadow Groups”. Searching around the web gave very, very few answers, but I found a few interesting sites:
What’s New and Changed (Windows Server vNext)
A pdf with some details
3.1.1.13.5 ExpandShadowPrincipal
2.453 Attribute msDS-ShadowPrincipalSid
From these sites I was able to deduce that what the MIM PAM feature is actually doing is creating msDS-ShadowPrincipal objects, which are basically groups with an additional attribute msDS-ShadowPrincipalSID. These are created in the “Shadow Principal Configuration” container (which is actually a msDs-ShadowPrincipalContainer) under the Services node in AD Sites and Services.
The following PowerShell however, yielded an error “New-ADObject : The specified method is not supported”:
$g = Get-ADGroup -Identity "Domain Admins" -Server blue.goodworkaround.com New-ADObject -Type msDS-ShadowPrincipal -OtherAttributes @{ "msDS-ShadowPrincipalSid" = $g.SID } -Path "CN=Shadow Principal Configuration,CN=Services,CN=Configuration,DC=red,DC=goodworkaround,DC=com" -Name "BLUE.Domain Admins" $g = Get-ADGroup -Identity "My Global Group" -Server blue.goodworkaround.com New-ADObject -Type msDS-ShadowPrincipal -OtherAttributes @{ "msDS-ShadowPrincipalSid" = $g.SID } -Path "CN=Shadow Principal Configuration,CN=Services,CN=Configuration,DC=red,DC=goodworkaround,DC=com" -Name "BLUE.My Global Group"
Digging a bit more around I found that this PAM feature is an optional feature that has to be enabled, just as the AD Recycle Bin. Enabling the feature was easy:
Get-ADOptionalFeature -Filter {Name -eq "Privileged Access Management Feature"} | Enable-ADOptionalFeature -Scope ForestOrConfigurationSet -Target red.goodworkaround.com
Now, the PowerShell above worked and the shadow principals was created:
I have not added any members though. So I did that through the GUI:
So what I have now is the following code
Get-ADObject -Filter {objectClass -eq "msDS-ShadowPrincipal"} -SearchBase "CN=Shadow Principal Configuration,CN=Services,CN=Configuration,DC=red,DC=goodworkaround,DC=com" -Properties member
That returns the following shadow principals (with member as you can see):
And when logging on as this user and running whoami /groups, you will see these as added groups:
As you can imagine, this will also completely remove the requirement to use ADMT (or the library of ADMT) to migrate SIDs for groups
Now, one heads up that I have asked Microsoft is about: Domain Admins does not work over the trust with this method (the SID is probably filtered away on the other side). I will update the article when I have more info.
Update: So, I talked to Mark Wahl about this and it turns out that it is planned a QFE for older Windows Server versions to allow for built-in groups over trusts, but this will probably not happen until Windows Server vNext is released. This means that the PAM feature will not work for built-in groups until this QFE is released.
Office 2013 with ADAL not working with Single Sign-On
I am currently testing out Office 2013 with ADAL which is currently in preview. With ADAL, the Office applications support “Modern Authentication” which means web redirects instead of using the old basic authentication and “proxying credentials” through Office 365. I followed the guidance and enabled ADAL. However, despite of using ADFS and having the adfs website added as an “intranett site” in security settings in IE, all I got was forms based authentication and not single sign-on as I expected. I contacted the Microsoft product group and verified that this was indeed supposed to work and was one of the primary use cases.
If you enable the TCOTrace registry key, the %temp%\outlook.exe.txt logfile is created and here I found the following entry:
ADAL: message=’Could not discover endpoint for Integrate Windows Authentication. Check your ADFS settings. It should support Integrate Widows Authentication for WS-Trust 1.3.’, additionalInformation=’Authority: https://login.windows.net/common
To fix this, you need to enable an ADFS endpoint that is disabled by default. To do this you need to run the following PowerShell cmdlet and restart the ADFS service on all servers in the farm.
Enable-AdfsEndpoint -TargetAddressPath "/adfs/services/trust/13/windowstransport"
Now it should work, with ADAL giving you perfect SSO from your Office applications.
Another bug
I also encountered a bug that Microsoft is fixing (also verified after contacting the product group) in the April update. If you find a log line saying CheckADUser: Not AD user found in the log file, even though you are a domain user, you have encountered this bug. To fix, close all Office apps and delete the following registry key below and try again: HKCU\Software\Microsoft\Office\15.0\Common\Identity\SignedOutADUser
After deleting the registry key, ADAL should not try Integrated Windows Authentication instead of Forms Based Authentication.
Using the Azure AD Graph API with PowerShell
I am implementing a custom synchronization solution between a member register and Office 365, as well as using a custom identity provider. I therefore need to create, update and delete users in Azure AD using the Graph API, here is how I did it.
Start by downloading the NuGet.exe tool to a folder. I will be using C:\GraphAPI in these examples. If you are not familiar with NuGet, this is a tool for downloading libraries and their dependencies, used a lot by Microsoft. Open a PowerShell and run the following.
cd c:\GraphAPI
.\nuget.exe install Microsoft.IdentityModel.Clients.ActiveDirectory
You should see the following:
After running the commands, the folder where you run nuget.exe from should contain some new folders and some files. The following file should now exist (the version number might be different): C:\GraphAPI\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll.
Now, in order to access the Graph API we need to create an application in the Azure AD that you are accessing. Let us start by creating a brand new Azure AD for demo purposes.
You should end up with an Azure AD like this:
Go to Applications and click “Add an Application”:
Choose “Add an application my organization is developing”:
Give the application a name of your choice and choose “WEB APPLICATION AND/OR WEB API”:
Input a url for your application. This url is never used and does not need to be working or anything, it is just an identifier for your application.
Your new application should display. Go to the configuration tab of the new application.
Scroll down until you find the Client ID. Copy this, we will use this later.
In the Keys section, create a new key and save the application.
As soon as you save the application, the key will appear.This is the only time you can see the key so make sure you copy it.
A little note here. As you can see the max lifetime of a key is 2 years, meaning that your application will stop working after two years. What you should do then is to create a new key, input this key into your application and let the old key expire.
Last thing to configure on the application is permissions. Go down to the “permissions to other applications” section and change the following to “Read and write directory data”. This operation can take a few minutes to complete (even though it already says completed), so you should wait a few minutes before you try the PowerShell examples below.
As a side note, here you can actually also give permissions to other applications such as Exchange Online to query the API there.
You are now finished configuring the application. Now, here is an example PowerShell for you. You need to make sure the path, the client id (which we copied earlier), the key (which we copied earlier) and the tenant name is changed. The rest should be pretty self explanatory.
# # PowerShell examples created by Marius Solbakken - https://goodworkaround.com/node/73 # # Change to correct file location Add-Type -Path "C:\GraphAPI\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll" # Change these three values to your application and tenant settings $clientID = "26b2e067-291d-4ad7-9cd2-2e1fae15c905" # CLIENT ID for application $clientSecret = "qxUG3anGzOi9mfDoV7tHVNWOOM9k2FKo08Xs3bG4APs=" # KEY for application $tenant = "goodworkarounddemo.onmicrosoft.com" # The tenant domain name # Static values $resAzureGraphAPI = "https://graph.windows.net"; $serviceRootURL = "https://graph.windows.net/$tenant" $authString = "https://login.windows.net/$tenant"; # Creates a context for login.windows.net (Azure AD common authentication) [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext]$AuthContext = [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext]$authString # Creates a credential from the client id and key [Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential]$clientCredential = New-Object -TypeName "Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential"($clientID, $clientSecret) # Requests a bearer token $authenticationResult = $AuthContext.AcquireToken($resAzureGraphAPI, $clientCredential); # Output the token object Write-Host -ForegroundColor Yellow "Token object:" $authenticationResult | Format-List # Example to get all users Write-Host -ForegroundColor Yellow "Getting all users" $users = Invoke-RestMethod -Method GET -Uri "$serviceRootURL/users?api-version=1.5" -Headers @{Authorization=$authenticationResult.CreateAuthorizationHeader()} -ContentType "application/json" $users.value | Format-Table UserPrincipalName,DisplayName # Example to create a user Write-Host -ForegroundColor Yellow "Creating user" $newUserJSONObject = @{ "accountEnabled" = $true "displayName" = "Donald Duck" "mailNickname" = "donald.duck" "passwordProfile" = @{ "password" = "Test1234" "forceChangePasswordNextLogin" = $false } "userPrincipalName" = "donald.duck@$tenant" } | ConvertTo-Json Invoke-RestMethod -Method POST -Uri "$serviceRootURL/users?api-version=1.5" -Headers @{Authorization=$authenticationResult.CreateAuthorizationHeader()} -ContentType "application/json" -Body $newUserJSONObject # Example to update a user Write-Host -ForegroundColor Yellow "Updating user" $updateUserJSONObject = @{ "givenName" = "Donald" "surname" = "Duck" } | ConvertTo-Json Invoke-RestMethod -Method PATCH -Uri "$serviceRootURL/users/donald.duck@${tenant}?api-version=1.5" -Headers @{Authorization=$authenticationResult.CreateAuthorizationHeader()} -ContentType "application/json" -Body $updateUserJSONObject # Example to get a single user Write-Host -ForegroundColor Yellow "Getting user" $user = Invoke-RestMethod -Method GET -Uri "$serviceRootURL/users/donald.duck@${tenant}?api-version=1.5" -Headers @{Authorization=$authenticationResult.CreateAuthorizationHeader()} -ContentType "application/json" $user # Example to delete a user - please note that this requires a special permissions set with the MsOnline PowerShell module Write-Host -ForegroundColor Yellow "Deleting user" Invoke-RestMethod -Method DELETE -Uri "$serviceRootURL/users/donald.duck@${tenant}?api-version=1.5" -Headers @{Authorization=$authenticationResult.CreateAuthorizationHeader()} -ContentType "application/json"
Custom ADFS cmdlets I use all the time
I don’t know about your habits, but one of mine is filling my PowerShell profile with all kinds of good stuff. Here are a few of my favorites for ADFS.
function Copy-ADFSClaimRules { [CmdletBinding()] Param ( # Param1 help description [Parameter(Mandatory=$true, ValueFromPipeline=$false, Position=0)] [string] $SourceRelyingPartyTrustName, [Parameter(Mandatory=$true, ValueFromPipeline=$false, Position=1)] [string] $DestinationRelyingPartyTrustName ) Begin { } Process { $SourceRPT = Get-AdfsRelyingPartyTrust -Name $SourceRelyingPartyTrustName $DestinationRPT = Get-AdfsRelyingPartyTrust -Name $DestinationRelyingPartyTrustName if(!$SourceRPT) { Write-Error "Could not find $SourceRelyingPartyTrustName" } elseif(!$DestinationRPT) { Write-Error "Could not find $DestinationRelyingPartyTrustName" } Set-AdfsRelyingPartyTrust -TargetRelyingParty $DestinationRPT -IssuanceTransformRules $SourceRPT.IssuanceTransformRules -IssuanceAuthorizationRules $SourceRPT.IssuanceAuthorizationRules -DelegationAuthorizationRules $SourceRpT.DelegationAuthorizationRules } End { } } function Get-AdfsTokenSigningThumbprint { [CmdletBinding()] Param ( # Param1 help description [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$false, Position=0)] $ADFS ) Begin { } Process { $metadata = Invoke-RestMethod -Uri ("https://{0}/FederationMetadata/2007-06/FederationMetadata.xml" -f $ADFS) $tempfile = "{0}\adfsTempCert.cer" -f $env:temp $metadata.EntityDescriptor.Signature.KeyInfo.X509Data.X509Certificate | Set-Content -Path $tempfile $cert = (New-Object System.Security.Cryptography.X509Certificates.X509Certificate2) $cert.Import($tempfile) return $cert.Thumbprint } End { } } function Copy-AdfsRelyingPartyTrust { [CmdletBinding()] [OutputType([int])] Param ( [Parameter(Mandatory=$true, ValueFromPipeline=$false, Position=0)] $SourceRelyingPartyTrustName, [Parameter(Mandatory=$true, ValueFromPipeline=$false, Position=1)] $NewRelyingPartyTrustName, [Parameter(Mandatory=$true, ValueFromPipeline=$false, Position=2)] $NewRelyingPartyTrustIdentifier ) Begin { } Process { $SourceRelyingPartyTrust = Get-AdfsRelyingPartyTrust -Name $SourceRelyingPartyTrustName $exceptedAttributes = @("ConflictWithPublishedPolicy","OrganizationInfo","ProxyEndpointMappings","LastUpdateTime","PublishedThroughProxy","LastMonitoredTime") $parameters = @{} $SourceRelyingPartyTrust | Get-Member -MemberType Property | where{$_.name -notin $exceptedAttributes} | foreach { if($SourceRelyingPartyTrust.($_.Name) -ne $null) { $parameters[$_.Name] = $SourceRelyingPartyTrust.($_.Name) } } $parameters.Name = $NewRelyingPartyTrustName $parameters.Identifier = $NewRelyingPartyTrustIdentifier Add-AdfsRelyingPartyTrust @parameters } End { } }
Lessons learned while configuring the SharePoint Services Connector for FIM 2010 R2
I have now configured many SharePoint Management Agents, and initially I had severe problems finding out which attributes to populate with what. Here is the lessons I learned during this investigation.
Application ID
During configuration of the Management Agent, you are requested to input Application ID. I have never used it, and i guess it is used when you have multiple User Profile Service Applications.
Anchor
Do not bother with anchors. Instead just provision a connector space object and let it get the default anchor. You will never see the anchor anywhere except in FIM and internally in the SharePoint databases.
Manager attribute populating bug
There is a bug in SharePoint, where the manager attribute won’t be populated in the User Profile Service, even though you are flowing it with FIM. The reason is that the timer job “User Profile Service Application – User Profile ActiveDirectory Import Job” is not created if you configure “Enable External Identity Manager” directly. Instead, you have to first choose “Use SharePoint Active Directory Import” on the “Configure Synchronization Settings”, and let this job be created (takes 15 minutes), then switch to “Enable External Identity Manager”.
Parallellism
It is not supported to run multiple SharePoint MAs simultaneously. Not sure why, but a little bit of code snooping shows this is true.
Pictures
Pictures can be a bit difficult, especially when trying with limited permissions. First of all, if you use fiddler the attribute is actually called “PictureURL”. Also, technically it seems as though what actually happens when you use this connector and export a picture, you transfer the binary data (as base64 ofc) out in “PictureURL / Picture” and the API you talk to uploads these data as an original to your mysite, at the location “http://mysitehost.goodworkaround.com/User photos”. And then it stores the url of the picture in the User Profile Service.
First of all, the MySite host MUST BE IN THE SAME FARM. It is not possible to have pictures uploaded to a separate SharePoint farm. Second, there is a requirement for permissions on the mysitehost. You can grant these permissions with the following cmdlet:
$w = Get-SPWebApplication -Identity http://mysitehost.goodworkaround.com
$w.GrantAccessToProcessIdentity("gwrnd\managementAgentAccount")
If you do not give this permissions, FIM will not get any error message from SharePoint saying “sorry, we could not store this picture”. It will simply be “ok” even though the picture was not saved.
Also, as you can see in this TechNet article you need to run a cmdlet to actually generate the thumbnail photos.
ADFS authentication
To configure ADFS authentication the following attributes needs to be flowed from FIM to SharePoint:
SharePoint attribute | Value |
SPS-ClaimProviderID | Name of the trusted identity provider in SP (case sensitive): “SAML Users” |
SPS-ClaimProviderType | Constant: “Trusted” |
SPS-ClaimID | Unique identifier – mail, userPrincipalname, employeeID etc. Must be what comes in the nameidentifier claim from ADFS |
SID | Do not flow anything |
ProfileIdentifier | someprefix:unique – where “unique” is the same as SPS-ClaimID (not required, but make it unique) |
UserName | Do not flow anything |
AccountName | Do not flow anything |
Example user
SharePoint attribute | Value |
SPS-ClaimProviderID | SAML Users |
SPS-ClaimProviderType | Trusted |
SPS-ClaimID | marius@goodworkaround.com |
SID | no flow |
ProfileIdentifier | gwrnd:marius@goodworkaround.com |
UserName | no flow |
AccountName | no flow – SharePoint will automatically populate this with something like “i:0\.t|SAML Users|marius@goodworkaround.com“ |
Windows authentication
To configure Windows authentication the following attributes needs to be flowed from FIM to SharePoint:
SharePoint attribute | Value |
SPS-ClaimProviderID | Constant: “Windows” |
SPS-ClaimProviderType | Constant: “Windows” |
SID | ObjectSID from Active Directory |
ProfileIdentifier | DOMAIN\sAMAccountName from Active Directory |
UserName | sAMAccountName from Active Directory |
AccountName | Do not flow anything |
Example user
SharePoint attribute | Value |
SPS-ClaimProviderID | Windows |
SPS-ClaimProviderType | Windows |
SID | – binary data – |
ProfileIdentifier | GWRND\marius |
UserName | marius |
AccountName | no flow |
That’s, hope it saves you some time.
Configuring the SharePoint Services Connector for FIM 2010 R2 for ADFS authentication
Here is a quick article on how to configure the SharePoint Services Connector for provisioning user profiles for ADFS authenticated users. I did not find any particularly good articles on the attributes required, so here is a quick reference on what I did no make things work with ADFS authentication.
This is not a guide on how to configure the MA. You should find good information on how to do that here.
There are 5 attributes that are important. Here is a table for you.
Attribute | Initial only | Description |
---|---|---|
SPS-ClaimID | This is the value of the identifier claim. This means that if you use userPrincipalname as identifier, this should be marius@goodworkaround.com, or if you use EmployeeID this should be 10032. | |
SPS-ClaimProviderID | This is the case sensitive name of the Trusted Identity Provider configured in SharePoint. If your Trusted Identity Provider is called “SAML Users”, this value should be “SAML Users”. | |
SPS-ClaimProviderType | When doing ADFS authentication, this should be the constant “Trusted”. (Btw, if you are doing Windows authentication, this should be “Windows”) | |
ProfileIdentifier | This value is a bit weird when it comes to ADFS authentication. It is required, and it must be unique, and it mst be on the form “someting:unique” (something colon unique). I usually fill this with “ID:value of SPS-ClaimID”; for example “ID:10032” or “ID:marius@goodworkaround.com“. | |
Anchor | yes | Another required value that must be unique. I use the same value as the SPS-ClaimID, so marius@goodworkaround.com or 10032. The reason this attribute must be configure as initial only, is that the Anchor will actually change and overwriting it may cause some strange behavior. |
Functions for base 64 in PowerShell
There are no default functions available in PowerShell for encoding and decoding base 64 strings. However, the .NET libraries contain this. Here are two wrapper functions ConvertFrom-Base64 and ConvertTo-Base64 that you can use to make this easier.
function ConvertTo-Base64 { [CmdletBinding(DefaultParameterSetName='String')] [OutputType([String])] Param ( # String to convert to base64 [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='String')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string] $String, # Param2 help description [Parameter(ParameterSetName='ByteArray')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [byte[]] $ByteArray ) if($String) { return [System.Convert]::ToBase64String(([System.Text.Encoding]::UTF8.GetBytes($String))); } else { return [System.Convert]::ToBase64String($ByteArray); } } function ConvertFrom-Base64 { [CmdletBinding()] Param( [Parameter(Mandatory=$True, Position=0, ValueFromPipeline=$true)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string] $Base64String ) return [System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($Base64String))); }
These two functions are a part of my PowerShell $profile, which I will create an article about later.
Graphing with PowerShell done easy
PowerShell is nice for getting textual output, csv output, xml output, etc, but there are no built in charting tools. However, luckily PowerShell is based on .NET, which means all the libraries for .NET is available to us. In this article I will show you how to use the Microsoft Chart Controls together with PowerShell to create charts.
The first thing you need to do is to install the Microsoft Chart Controls from here. After this, you can verify with the following lines of code whether you are able to load the assemblies.
[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms.DataVisualization")
This should hopefully just return without output. If so, you are good to go.
To make things a bit easier than working directly with objects, I have made a simple module to create new charts, add datasets to them and display them. The following can either be just pasted into a PowerShell, or better, added to a separate “.psm1” file and loaded with Import-Module.
# Load assembly for Microsoft Chart Controls for Microsoft .NET Framework 3.5 Write-Verbose "Loading assemblies" [void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") [void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms.DataVisualization") function New-Chart { [CmdletBinding()] [OutputType([System.Windows.Forms.DataVisualization.Charting.ChartArea])] Param ( # Dataset [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false, Position=0)] $Dataset, # Width [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false)] [int]$Width = 500, # Height [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false)] [int]$Height = 500, # X Interval [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false)] [int]$XInterval, # Y Interval [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false)] [int]$YInterval, # X Title [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false)] [string]$XTitle, # Y Title [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false)] [string]$YTitle, # Title [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false)] [string]$Title ) # Create chart Write-Verbose "Creating chart $Width x $Height" $Chart = New-object System.Windows.Forms.DataVisualization.Charting.Chart $Chart.Width = $Width $Chart.Height = $Height $Chart.Left = 0 $Chart.Top = 0 # Add chart area to chart $ChartArea = New-Object System.Windows.Forms.DataVisualization.Charting.ChartArea $Chart.ChartAreas.Add($ChartArea) # Set titles and lables if($Title) { Write-Verbose "Setting title: $Title" [void]$Chart.Titles.Add($Title) } else { Write-Verbose "No title provided" } if($YTitle) { Write-Verbose "Setting Ytitle: $YTitle" $ChartArea.AxisY.Title = $YTitle } else { Write-Verbose "No Ytitle provided" } if($XTitle) { Write-Verbose "Setting Xtitle: $XTitle" $ChartArea.AxisX.Title = $XTitle } else { Write-Verbose "No Xtitle provided" } if($YInterval) { Write-Verbose "Setting Y Interval to $YInterval" $ChartArea.AxisY.Interval = $YInterval } if($XInterval) { Write-Verbose "Setting X Interval to $XInterval" $ChartArea.AxisX.Interval = $XInterval } if($Dataset) { Write-Verbose "Dataset provided. Adding this as ""default dataset"" with chart type line." [void]$Chart.Series.Add("default dataset") $Chart.Series["default dataset"].Points.DataBindXY($Dataset.Keys, $Dataset.Values) $Chart.Series["default dataset"].ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Line } return $Chart } function Add-ChartDataset { [CmdletBinding()] [OutputType([System.Windows.Forms.DataVisualization.Charting.ChartArea])] Param ( # Chart [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0)] $Chart, # Dataset [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$false, Position=0)] $Dataset, # DatasetName [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false, Position=1)] [string]$DatasetName = "Added dataset", # SeriesChartType = http://msdn.microsoft.com/en-us/library/system.windows.forms.datavisualization.charting.seriescharttype(v=vs.110).aspx [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$false, Position=2)] [string]$SeriesChartType = "Line" ) Write-Verbose "Adding series $Datasetname" [void]$Chart.Series.Add($DatasetName) Write-Verbose "Adding data binding" $Chart.Series[$DatasetName].Points.DataBindXY($Dataset.Keys, $Dataset.Values) Write-Verbose "Setting chart type to $SeriesChartType" $Chart.Series[$DatasetName].ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::$SeriesChartType return $Chart } function Show-Chart { [CmdletBinding()] [OutputType([void])] Param ( # Chart [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0)] $Chart ) # display the chart on a form $Chart.Anchor = [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Right -bor [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left $Form = New-Object Windows.Forms.Form $Form.Text = "PowerShell Chart" $Form.Width = $chart.Width $Form.Height = $chart.Height + 50 $Form.controls.add($Chart) $Form.Add_Shown({$Form.Activate()}) $Form.ShowDialog() | Out-Null }
The functions are documented in the module, and available with Get-Help.
Here is a demo script using the functions:
Import-Module .\GoodWorkaroundCharts-v0.1.psm1 -Force # Create simple dataset $simpleDataset = @{ "Microsoft" = 800 "Apple" = 250 "Google" = 400 "RIM" = 0 } # Create chart and show it New-Chart -Dataset $simpleDataset | Show-Chart # Create ordered hashmap $osloTemperature = [ordered]@{} # Request weather data for Oslo, and put into dataset [xml]$weather = (Invoke-WebRequest -Uri http://www.yr.no/place/Norway/Oslo/Oslo/Oslo/varsel.xml).Content $weather.weatherdata.forecast.tabular.time | foreach { $osloTemperature[$_.from] = $_.temperature.value } # Create chart, add dataset and show New-Chart -Title "Temperature in Oslo" -XInterval 4 -YInterval 2 -Width 1200 | Add-ChartDataset -Dataset $osloTemperature -DatasetName "Temperature" -SeriesChartType Spline -OutVariable tempChart | Show-Chart # Save the chart as a PNG to the desktop $tempChart.SaveImage($Env:USERPROFILE + "\Desktop\Chart.png", "PNG")
Hope that helps someone!