Automation–MVP Spotlight Series–SMA: DFS Share Creation Request Walkthrough In Depth
Okay Readers – we have the next post ready to go!
Here is the fourth of six posts in the MVP Spotlight Series for the Automation Track…
–MVP Spotlight Series–
SMA: DFS Share Creation Request Walkthrough In Depth
by Ryan Andorfer
Assumptions
- You have followed the first blog post in this series
- You have a solid foundation of PowerShell workflow and Service Management Automation (SMA)
- PowerShell Workflow
- Introduction (TechNet / PowerShell Team Blog)
- Running PowerShell Commands in Workflow (TechNet)
- Running in Parallel (Hey, Scripting Guy! Blog)
- Restrictions (Hey, Scripting Guy! Blog)
- SMA
- Getting Started (Building Clouds Blog)
- Runbook Authoring (Orchestrator Team Blog)
- Controlling Runbook Streams for Testing and Troubleshooting (Orchestrator Team Blog)
- Using InlineScript blocks (Building Clouds Blog)
- Assets (Orchestrator Team Blog)
- Runbook Input, Output, and Nested Runbooks (Orchestrator Team Blog)
- Runbook Concepts (TechNet)
- CheckPointing (Orchestrator Team Blog)
- Continuous TFS Integration (Building Cloud Blog)
Core Concepts
We consider SMA to be a platform for executing our PowerShell workflows. To this end we author our workflows to be executable outside of SMA environments. This helps us debug and author outside of SMA using normal local development tools. We check all of our work into TFS (and actually track it against User Stories / Tasks) and utilize the continuous TFS integration solution above to sync our work to our SMA environments. We run our SMA environment in a low privilege model which means the service account that our Runbook service executes as has no permissions beyond domain user outside of the SMA environment. We use inlinescript blocks to elevate portions of code to run as a different user as needed.
Attempt to treat workflow authoring much like you would web development. Three versions of each script are recommended (Dev / QA / Production) and should live in three different SMA environments. To facilitate moving between these environments you should structure your workflows to leverage global variables for information that will be changed when they are promoted from Dev to QA to Production so that the workflow itself doesn’t ever need to be changed (which would, in theory, invalidate the testing you have done).
Next Blog Post
If all this code is a bit too much and you want just a walkthrough of how to import this solution into your SMA environment stay tuned for the next blog post where we will walk through taking the export files, importing them into a SMA environment and configuring the global settings to work in an environment! This next post will also expand on what the variable values setup as in our environment for the scripts below.
Overview
The solution contains two PowerShell Scripts and one approval action in SharePoint
Oh, and here (color portion) is a look at where this post fits in to the overall example Solution Architecture:
Monitor SharePoint for Approved Request
The basic idea of a monitor is rather simple; every so often go out and look at something and see if there is work there to be done. In Orchestrator we had activities that were labeled as monitors and did this sort of functionality for us. As a part of our migration to SMA we have carried this concept forward and created a basic reusable pattern for ‘monitors’. The pattern consists of
- Settings Definition
- Poll for Requests
- If found: Trigger worker Runbook
- Checkpoint and sleep for remaining delay cycle (time between polls)
- Re-launch monitor Runbook after global delay cycle
Since this is a very generic pattern we create a ‘shared’ Runbook in SMA that can be referenced by all of the other Runbooks that will need to use this functionality. We have called this shared Runbook Monitor-SharePointList. In this way if we have multiple things that need to monitor list items we they can all share the basic pattern
Monitor-SharePointList
This is the generic pattern for monitoring a SharePoint lists. It contains two main section, a section related to ‘looking’ for new SharePoint list items in a given status and a section that re launches the monitor Runbook after a defined period of time
Polling for Requests
This action occurs inside of a while block. The while block ends after a timeout period (the length of time our monitor are active before they refresh themselves). Inside of this area we query whatever data source has the information we need to determine if there is work to be done and gather the requisite information from that data source to initiate the worker Runbook. In the example below we are querying a SharePoint list for items with a status field set to a particular value (new in this case). If a request is found we start a new instance of the worker Runbook using Start-SmaRunbook. This causes the Runbook to be executed independently of the Monitor Runbook which is a very important concept. Having the worker logic separate from the monitor logic allows us to be constantly be looking for new work to do without waiting for any individual request to fully process. This means that we are not creating any relationships between request, each request is handled independently. After polling is complete calculate the remaining cycle time and sleep for that amount of time. In this way if we set DelayCycle to 30 seconds each poll will happen in a 30 second time window.
While( $MonitorActive ) { InlineScript { # To ease debugging declare variables that will be passed into # the inlinescript at the beginning of the inlinescript. This allows # you to run the code in a normal PowerShell session if you declare the # variables by hand $runCred = $Using:runCred $NewRequestURI = $Using:NewRequestURI $SPProperty = $Using:SPProperty $NextValue = $Using:NextValue $ExecutionRunbook = $Using:ExecutionRunbook $SMAWebserviceEndpoint = $Using:SMAWebserviceEndpoint Try { # Query SharePoint list for new items $Box = ( Invoke-RestMethod -URI $NewRequestURI -Credential $runCred ).ID # Put the results in an arraylist $NewRequests = New-Object -TypeName System.Collections.ArrayList ForEach ( $Item in $Box ) { $NewRequests += $Item } Write-Debug -Message "`$NewRequests.Count [$($NewRequests.count)]" # If any new requests were found, for each new request... ForEach ( $NewRequest in $NewRequests ) { # Change request Status to $NextValue $Invoke = Invoke-RestMethod -Method Merge ` -URI $NewRequest ` -Body "{$($SPProperty): '$NextValue'}" ` -ContentType "application/json" ` -Headers @{ "If-Match"="*" } ` -Credential $runCred Write-Debug -Message "Calling $ExecutionRunbook for $NewRequest." $Launch = Start-SmaRunbook -Name $ExecutionRunbook ` -Parameters @{ "NewRequestURI" = $NewRequest } ` -WebServiceEndpoint $SMAWebserviceEndpoint } } Catch [System.Net.WebException] { # Capture SharePoint errors without crashing monitor Write-Error -Message "SharePoint request returned error [$($Error[0].Exception.Message)]" Write-Error -Message "SharePoint may be down!" } } # Sleep for the rest of the $DelayCycle, with a checkpoint every $DelayCheckpoint seconds [int]$RemainingDelay = $DelayCycle - (Get-Date).TimeOfDay.TotalSeconds % $DelayCycle If ( $RemainingDelay -eq 0 ) { $RemainingDelay = $DelayCycle } Write-Debug -Message "Sleeping for $RemainingDelay seconds." Checkpoint-Workflow While ( $RemainingDelay -gt 0 ) { Start-Sleep -Seconds ( [math]::Min( $RemainingDelay, $DelayCheckpoint ) ) Checkpoint-Workflow $RemainingDelay -= $DelayCheckpoint } # Calculate if we should continue running or if we should start a new instance of this monitor $MonitorActive = ( Get-Date ) -lt $MonitorRefreshTime } |
Re-launch the Monitor Runbook
Initially we would have our monitor Runbooks running indefinitely. This caused a number of issues with the built in SMA grooming jobs and was not ideal for shutting down our environment (if you have Runbooks that run forever then the SMA Runbook Worker service will timeout on its stop operation and ungracefully kill the monitor). Furthermore it complicated our TFS continuous integration strategy, since monitor Runbooks never restarted they would never pick up the newly deployed code and we would have to go out to the environment and stop and then start them by hand. This section of code runs after the polling section and is rather simple.
# Relaunch this monitor Write-Debug -Message "Reached end of monitor lifespan. Relaunching this monitor, $MonitorRunbook." $Launch = Start-SmaRunbook -Name $MonitorRunbook ` -WebServiceEndpoint $SMAWebServiceEndpoint |
Final Monitor-SharePointList
<# .SYNOPSIS Monitors a sharepoint list item on a sharepoint site using REST methods. Foreach new list item matching the given filter criteria a new runbook is launched and the list item's identifier is passed target runbook in the parameter with the name NewRequestURI .DESCRIPTION This function is designed to be used as a reference workflow for unqiue monitors. Pass in the required parameters and it will begin monitoring the list This function is desgined to be used inside of Service Management Automation .PARAMETER runCred The credential object to use for monitoring SharePoint and initiating runbooks in the SMA environment .PARAMETER SPSite The path to the SharePoint site to monitor Ex: https://scorchsp01 .PARAMETER SPList The name of the SharePoint list to monitor .PARAMETER SPProperty The property of the SharePoint list that contains the status field to trigger off of. This is the field name that will have the monitor value applied to and when a match if found will initiate a new instance of Execution Runbook .PARAMETER MonitorValue The value of the SPProperty to filter on. Usually this is a status such as 'New' or 'Approved' .PARAMETER MonitorRunbook The name of the Monitor Runbook calling this workflow. Used to 'restart' once the monitor life span has been reached .PARAMETER NextValue The value to set the found SharePoint list item to .PARAMETER ExecutionRunbook The runbook to initiate if a list item is found to be acted on. Must have NewRequestURI as an input parameter. This will be the pointer to the SharePoint list item that met the filter criteria .PARAMETER DelayCycle The amount of time between monitor cycles in seconds Default time is 30 seconds .PARAMETER DelayCheckpoint The amount of time between monitor checkpoints in seconds Default time is 30 seconds .PARAMETER MonitorLifeSpan The amount of time that each instance of the monitor should run for. Default time 20 minutes .PARAMETER SMAWebserviceEndpoint The webservice URL to the target SMA environment. In format https://SMA_SERVER_NAME Default value is HTTPS://localhost .EXAMPLE Begin Monitoring a SharePoint list item Monitor-SharepointList -RunCred $cred ` -SPSite "https://scorchsp01" ` -SPList "NewDFSShare" ` -SPProperty "StatusValue" ` -MonitorValue "Approved" ` -MonitorRunbook "Monitor-Approved-DFSSHare" ` -NextValue "In Progress" ` -ExecutionRunbook "New-DFSShare" .EXAMPLE Begin Monitoring a SharePoint list item with a non Monitor Life Span(example is 1 hour) Monitor-SharepointList -RunCred $cred ` -SPSite "https://scorchsp01" ` -SPList "NewDFSShare" ` -SPProperty "StatusValue" ` -MonitorValue "Approved" ` -MonitorRunbook "Monitor-Approved-DFSSHare" ` -NextValue "In Progress" ` -ExecutionRunbook "New-DFSShare" ` -MonitorLifeSpan 60 #> workflow Monitor-SharepointList { Param( [pscredential] $runCred, [string] $SPSite, [string] $SPList, [string] $SPProperty, [string] $MonitorValue, [string] $MonitorRunbook, [string] $NextValue, [string] $ExecutionRunbook, [int] $DelayCycle = 30, [Parameter(Mandatory=$false)] [int] $DelayCheckpoint = 30, [Parameter(Mandatory=$false)] [int] $MonitorLifeSpan = 20, [Parameter(Mandatory=$false)] [string] $SMAWebserviceEndpoint = "https://localhost") #region initial setup Write-Debug -Message "`$runCredName [$($runCred.UserName)]" Write-Debug -Message "`$SPSite [$SPSite]" Write-Debug -Message "`$SPList [$SPList]" Write-Debug -Message "`$SPProperty [$SPProperty]" Write-Debug -Message "`$MonitorValue [$MonitorValue]" Write-Debug -Message "`$MonitorRunbook [$MonitorRunbook]" Write-Debug -Message "`$NextValue [$NextValue]" Write-Debug -Message "`$ExecutionRunbook [$ExecutionRunbook]" Write-Debug -Message "`$DelayCycle [$DelayCycle]" Write-Debug -Message "`$DelayCheckpoint [$DelayCheckpoint]" Write-Debug -Message "`$MonitorLifeSpan [$MonitorLifeSpan]" Write-Debug -Message "`$SMAWebserviceEndpoint [$SMAWebserviceEndpoint]" # Define SharePoint query $SPFilter = "$SPProperty eq '$MonitorValue'" $NewRequestURI = "$SPSite/_vti_bin/listdata.svc/$($SPList)?`$filter=$SPFilter" Write-Debug -Message "`$SPFilter [$SPFilter]" Write-Debug -Message "`$NewRequestURI [$NewRequestURI]" Write-Debug -Message "Monitoring SharePoint list $SPList on site $SPSite for new items." $MonitorRefreshTime = ( Get-Date ).AddMinutes( $MonitorLifeSpan ) $MonitorActive = ( Get-Date ) -lt $MonitorRefreshTime Write-Debug -Message "`$MonitorRefreshTime [$MonitorRefreshTime]" Write-Debug -Message "`$MonitorActive [$MonitorActive]" #endregion While( $MonitorActive ) { InlineScript { # To ease debugging declare variables that will be passed into # the inlinescript at the beginning of the inlinescript. This allows # you to run the code in a normal PowerShell session if you declare the # variables by hand $runCred = $Using:runCred $NewRequestURI = $Using:NewRequestURI $SPProperty = $Using:SPProperty $NextValue = $Using:NextValue $ExecutionRunbook = $Using:ExecutionRunbook $SMAWebserviceEndpoint = $Using:SMAWebserviceEndpoint Try { # Query SharePoint list for new items $Box = ( Invoke-RestMethod -URI $NewRequestURI -UseDefaultCredentials ).ID # Put the results in an arraylist $NewRequests = New-Object -TypeName System.Collections.ArrayList ForEach ( $Item in $Box ) { $NewRequests += $Item } Write-Debug -Message "`$NewRequests.Count [$($NewRequests.count)]" # If any new requests were found, for each new request... ForEach ( $NewRequest in $NewRequests ) { # Change request Status to $NextValue $Invoke = Invoke-RestMethod -Method Merge ` -URI $NewRequest ` -Body "{$($SPProperty): '$NextValue'}" ` -ContentType "application/json" ` -Headers @{ "If-Match"="*" } ` -UseDefaultCredentials Write-Debug -Message "Calling $ExecutionRunbook for $NewRequest." $Launch = Start-SmaRunbook -Name $ExecutionRunbook ` -Parameters @{ "NewRequestURI" = $NewRequest } } } Catch [System.Net.WebException] { # Capture SharePoint errors without crashing monitor Write-Error -Message "SharePoint request returned error [$($Error[0].Exception.Message)]" Write-Error -Message "SharePoint may be down!" } } -PSCredential $runCred # Sleep for the rest of the $DelayCycle, with a checkpoint every $DelayCheckpoint seconds [int]$RemainingDelay = $DelayCycle - (Get-Date).TimeOfDay.TotalSeconds % $DelayCycle If ( $RemainingDelay -eq 0 ) { $RemainingDelay = $DelayCycle } Write-Debug -Message "Sleeping for $RemainingDelay seconds." Checkpoint-Workflow While ( $RemainingDelay -gt 0 ) { Start-Sleep -Seconds ( [math]::Min( $RemainingDelay, $DelayCheckpoint ) ) Checkpoint-Workflow $RemainingDelay -= $DelayCheckpoint } # Calculate if we should continue running or if we should start a new instance of this monitor $MonitorActive = ( Get-Date ) -lt $MonitorRefreshTime } # Relaunch this monitor Write-Debug -Message "Reached end of monitor lifespan. Relaunching this monitor, $MonitorRunbook." $Launch = Start-SmaRunbook -Name $MonitorRunbook ` -WebServiceEndpoint $SMAWebServiceEndpoint } |
Monitor-DFSShare-Approved
Now that we have the common pattern in our environment we can call a unique instance of it to monitor our ‘DFS Share’ list that contains requests for a new DFS Share. To facilitate this we want to store the unique information about the SharePoint list that we will be monitoring in SMA’s asset store so we can easily change it if we want to move the scripts between SMA environments (only need to change the asset values, do not need to change the workflows). Once we have pulled the variable values from the store we simply invoke the shared Monitor-SharePointListRunbook.
Settings Definition
In this section we poll the SMA environment for all assets we will use during execution. These assets commonly include global variables (items which are pulled out into a configuration file and changed when we migrate from Dev to QA to Prod – very akin to the sorts of variables pulled out into a web.config in web development) and credentials from the credential store. All ‘magic numbers’ should be evaluated for pulling out into global configuration. After we pull the information we output it to the debug stream for debugging purposes.
#region settings # Get our credential from the Cred Store $SharePointCredName = ( Get-SMAVariable -Name "DFSShare-SharePointCredName" ` -WebServiceEndpoint $WebServiceEndpoint ).Value Write-Debug -Message "`$SharePointCredName [$SharePointCredName]" $SPCred = Get-AutomationPSCredential -Name $SharePointCredName Write-Debug -Message "`$SPCred.UserName [$($SPCred.UserName)]" $SPSite = ( Get-SMAVariable -Name "DFSShare-SharePointSite" ` -WebServiceEndpoint $WebServiceEndpoint ).Value $SPList = ( Get-SMAVariable -Name "DFSShare-SPList" ` -WebServiceEndpoint $WebServiceEndpoint ).Value $SPProperty = ( Get-SMAVariable -Name "DFSShare-SPProperty" ` -WebServiceEndpoint $WebServiceEndpoint ).Value $MonitorValue = ( Get-SMAVariable -Name "DFSShare-MonitorApproved-MonitorValue" ` -WebServiceEndpoint $WebServiceEndpoint ).Value $NextValue = ( Get-SMAVariable -Name "DFSShare-MonitorApproved-NextValue" ` -WebServiceEndpoint $WebServiceEndpoint ).Value $ExecutionRunbook = ( Get-SMAVariable -Name "DFSShare-MonitorApproved-ExecutionRunbook" ` -WebServiceEndpoint $WebServiceEndpoint ).Value $MonitorName = ( Get-SMAVariable -Name "DFSShare-MonitorApproved-MonitorName" ` -WebServiceEndpoint $WebServiceEndpoint ).Value $DelayCycle = [int]( Get-SMAVariable -Name "DFSShare-DelayCycle" ` -WebServiceEndpoint $WebServiceEndpoint ).Value $DelayCheckpoint = [int]( Get-SMAVariable -Name "DFSShare-DelayCheckpoint" ` -WebServiceEndpoint $WebServiceEndpoint ).Value Write-Debug -Message "`$SPList [$SPList]" Write-Debug -Message "`$SPProperty [$SPProperty]" Write-Debug -Message "`$MonitorValue [$MonitorValue]" Write-Debug -Message "`$NextValue [$NextValue]" Write-Debug -Message "`$ExecutionRunbook [$ExecutionRunbook]" Write-Debug -Message "`$DelayCycle [$DelayCycle]" Write-Debug -Message "`$DelayCheckpoint [$DelayCheckpoint]" Write-Debug -Message "`$MonitorRunbook [$MonitorRunbook]" #endregion |
Start Monitor Section
Now that we have all of the unique values we starting the shared monitor code is very simple.
Monitor-SharepointList -RunCred $SPCred ` -SPSite $SPSite ` -SPList $SPList ` -SPProperty $SPProperty ` -MonitorValue $MonitorValue ` -MonitorRunbook $MonitorRunbook ` -NextValue $NextValue ` -ExecutionRunbook $ExecutionRunbook |
Final Monitor-DFSShare-Approved
# Monitor-DFSShare-Approved # Functionality # Every 30 seconds... # Query SharePoint for any "Approved" requests in list DFSShareRequest # If any requests found... # Set request status to "In progress" # Launch workflow New-DFSShare # Trigger # Always running (monitor) # Dependencies # Workflow - New-DFSShare # SMA Variables # DFSShare-SharePointCredName # DFSShare-SPList # DFSShare-SPProperty # DFSShare-DelayCycle # DFSShare-DelayCheckpoint # DFSShare-MonitorApproved-MonitorValue # DFSShare-MonitorApproved-NextValue # DFSShare-MonitorApproved-ExecutionRunbook # DFSShare-MonitorApproved-MonitorValue # DFSShare-MonitorApproved-MonitorRunbook workflow Monitor-DFSShare-Approved { Param( $WebServiceEndpoint = "https://localhost" ) #region settings # Get our credential from the Cred Store $SharePointCredName = ( Get-SMAVariable -Name "DFSShare-SharePointCredName" ` -WebServiceEndpoint $WebServiceEndpoint ).Value Write-Debug -Message "`$SharePointCredName [$SharePointCredName]" $SPCred = Get-AutomationPSCredential -Name $SharePointCredName Write-Debug -Message "`$SPCred.UserName [$($SPCred.UserName)]" $SPSite = ( Get-SMAVariable -Name "DFSShare-SharePointSite" ` -WebServiceEndpoint $WebServiceEndpoint ).Value $SPList = ( Get-SMAVariable -Name "DFSShare-SPList" ` -WebServiceEndpoint $WebServiceEndpoint ).Value $SPProperty = ( Get-SMAVariable -Name "DFSShare-SPProperty" ` -WebServiceEndpoint $WebServiceEndpoint ).Value $MonitorValue = ( Get-SMAVariable -Name "DFSShare-MonitorApproved-MonitorValue" ` -WebServiceEndpoint $WebServiceEndpoint ).Value $NextValue = ( Get-SMAVariable -Name "DFSShare-MonitorApproved-NextValue" ` -WebServiceEndpoint $WebServiceEndpoint ).Value $ExecutionRunbook = ( Get-SMAVariable -Name "DFSShare-MonitorApproved-ExecutionRunbook" ` -WebServiceEndpoint $WebServiceEndpoint ).Value $MonitorName = ( Get-SMAVariable -Name "DFSShare-MonitorApproved-MonitorName" ` -WebServiceEndpoint $WebServiceEndpoint ).Value $DelayCycle = [int]( Get-SMAVariable -Name "DFSShare-DelayCycle" ` -WebServiceEndpoint $WebServiceEndpoint ).Value $DelayCheckpoint = [int]( Get-SMAVariable -Name "DFSShare-DelayCheckpoint" ` -WebServiceEndpoint $WebServiceEndpoint ).Value Write-Debug -Message "`$SPList [$SPList]" Write-Debug -Message "`$SPProperty [$SPProperty]" Write-Debug -Message "`$MonitorValue [$MonitorValue]" Write-Debug -Message "`$NextValue [$NextValue]" Write-Debug -Message "`$ExecutionRunbook [$ExecutionRunbook]" Write-Debug -Message "`$DelayCycle [$DelayCycle]" Write-Debug -Message "`$DelayCheckpoint [$DelayCheckpoint]" Write-Debug -Message "`$MonitorRunbook [$MonitorRunbook]" #endregion Monitor-SharepointList -RunCred $SPCred ` -SPSite $SPSite ` -SPList $SPList ` -SPProperty $SPProperty ` -MonitorValue $MonitorValue ` -MonitorRunbook $MonitorRunbook ` -NextValue $NextValue ` -ExecutionRunbook $ExecutionRunbook } |
Create DFS Share
This is the script that will actually setup our DFS Share. This script is split up into multiple regions each with its own purpose. As usual the first region is settings related, we define the parameters that calling workflows must supply while providing default values for optional parameter, access SMAs asset store to pull out global variables and access SharePoint to retrieve the rest of the needed information to carry out the task. Once all of this information is gathered we create an Active Directory (AD) group which will be used to secure the share then create the share on a file server and finally send out communications.
Settings
We access a number of settings for this script.
Credentials
We first access two sets of credentials, one for accessing SharePoint and a different one for carrying out the automation tasks (creating the AD group and share). In this way we can use a low privilege account for accessing SharePoint and a higher privilege account for carrying out automation tasks – remember one huge advantage of PowerShell workflow is the ease of switching user contexts making running in a low privilege mode a real possibility.
Windows Server List and Restricted Drives
This is a list of windows file servers for us to build the share on. We have logic to choose the server that has the highest free space available on it. Restricted Drives contains a list of drives that we should not use for creating the share on (C drive, Temporary Drives etch)
ADDomain
This is the domain that the AD group will be built in. For Dev and QA we build the automation in a non-production domain, this allows us to change that without modify the code as we promote the workflow
SPList and SPProperty
The SharePoint list name and the field name that contains the status property. These are generic constructs for us with our functions that access SharePoint. The list corresponds to the list name for this request and the property corresponds to the field name that contains the requests current status. This field is used to update the request as we take action on it.
SharePoint Request Data
We also pull additional information from the SharePoint List
- The person who requested the share
- The account which was selected to own the share
- The new share’s name
- Whether or not the share should be a ‘secure’ share or not
- A list of users who will have access to the share initially (initial AD Group members)
Active Directory (AD) Group
This section is rather simple. For illustrative purposes we run this whole section as the domain credential accessed from the store above. We also could have just passed this credential objects to each AD command individually. PSPersist is set to true which causes a checkpoint to occur after this action has completed. This means that if the workflow encounters an error later it will not re-attempt to create on a resume the group if this was successful. For more information check out TechNet.
$CreateGroup = InlineScript { $DC = $Using:DC $OwnerDN = $Using:OwnerDN $GroupName = $Using:GroupName $GroupDescription = $Using:GroupDescription $GroupOU = $Using:GroupOU $MemberList = $Using:MemberList Try { # Create the group Write-Debug -Message "Creating the group object" $NewGroup = New-ADGroup -Name $GroupName ` -Path $GroupOU ` -GroupCategory Security ` -GroupScope Universal ` -Description $GroupDescription ` -ManagedBy $OwnerDN ` -PassThru ` -Server $DC # Add the members to the group Write-Debug -Message "Adding members to the group" Add-ADGroupMember -Identity $NewGroup -Members $MemberList Write-Verbose -Message "AD security group created successfully." Return "Complete" } Catch { Write-Verbose -Message "AD security group creation failed." Return $Error[0].Exception.Message } } -PSCredential $DomainCred -PSPersist $true |
DFS Share Creation
We take the necessary actions to create a DFS share inside of an InlineScript block that runs as the DomainCred credential accessed from the SMA credential store. After this action completes we checkpoint to ensure we do not attempt to create the share again if this workflow suspends and is resumed.
$CreateShare = InlineScript { $SecureShare = $Using:SecureShare $GroupFullName = $Using:GroupFullName $GroupName = $Using:GroupName $NewShare = $Using:NewShare $WindowsServers = $Using:WindowsServers $WindowsServerRestrictedDrives = $Using:WindowsServerRestrictedDrives $DC = $Using:DC Try { # Query Windows file servers for the file share volume with the most space available. $WindowsDrives = Get-WMIObject -Class Win32_LogicalDisk ` -Filter "DriveType=3" ` -ComputerName $WindowsServers ` | Where-Object -Property DeviceID -notin $WindowsServerRestrictedDrives Write-Debug -Message "`$WindowsDrives.Count [$($WindowsDrives.Count)]" $LargestWindowsDrive = $WindowsDrives | Sort-Object -Property FreeSpace -Descending | Select-Object -First 1 Write-Debug -Message "`$LargestWindowsDrive.FreeSpace [$($LargestWindowsDrive.FreeSpace.ToString("#,0"))]" $Share = $GroupName $Path = "\\$($LargestWindowsDrive.PSComputerName)\$($LargestWindowsDrive.DeviceID.Replace(":", "$"))" $Folder = $LargestWindowsDrive.DeviceID + "\" + $GroupName $LocalShare = "\\$($LargestWindowsDrive.PSComputerName)\$GroupName$" Write-Debug -Message "`$Share [$Share]" Write-Debug -Message "`$Path [$Path]" Write-Debug -Message "`$Folder [$Folder]" Write-Debug -Message "`$LocalShare [$LocalShare]" # Create the Folder on the drive Write-Debug -Message "Creating folder $Path\$Share on Windows file server." New-Item -ItemType "Directory" -Path $Path -Name $Share # Create share and set share permissions Write-Debug -Message "Sharing folder $Path\$Share as $LocalShare." Invoke-Command -ComputerName $LargestWindowsDrive.PSComputerName -ArgumentList $Share, $Folder -ScriptBlock ` { Param( $Share, $Folder ) $Result = net SHARE $Share$=$Folder /GRANT:Everyone`,FULL /UNLIMITED } # Add the share to the DFS namespace $DFSShare = $NewShare Write-Debug -Message "Adding share [$LocalShare] to DFS namespace as [$DFSShare]." C:\Windows\System32\dfsutil.exe link add $DFSShare $LocalShare $GroupAccount = Get-ADGroup -Filter { Name -like $GroupName } -Server $DC $GroupSID = $GroupAccount.SID # Grant NTFS permissions Write-Debug -Message "Granting NTFS permissions on $LocalShare." $ShareACL = Get-ACL -Path $LocalShare # Turn off inheritance Write-Debug -Message "Turn off inheritance." $ShareACL.SetAccessRuleProtection($True, $False) # Remove any existing access rules that didn't go away in the above command Write-Debug -Message "Remove access rules." ForEach ( $Rule in $ShareACL.Access ) { Write-Debug -Message "Remove access rule [$Rule]." $ShareACL.RemoveAccessRule( $Rule ) } # Grant full control to admnistrators Write-Debug -Message "Grant full control to admnistrators." $ShareACL.AddAccessRule(( New-Object System.Security.AccessControl.FileSystemAccessRule( ` "BUILTIN\Administrators", ` "FullControl", ` "ContainerInherit, ObjectInherit", ` "None", ` "Allow") )) # Grant full control to SYSTEM Write-Debug -Message "Grant full control to SYSTEM." $ShareACL.AddAccessRule(( New-Object System.Security.AccessControl.FileSystemAccessRule( "NT AUTHORITY\SYSTEM", ` "FullControl", ` "ContainerInherit, ObjectInherit", ` "None", ` "Allow" ) )) # Grant full control to the AD security group we created above Write-Debug -Message "Grant full control to the AD security group we created above [$GroupFullName]." $ShareACL.AddAccessRule(( New-Object System.Security.AccessControl.FileSystemAccessRule( $GroupSID, ` "FullControl", ` "ContainerInherit, ObjectInherit", ` "None", ` "Allow" ) )) # If this is not a secure group... # grant ReadOnly to authenticated users If ( -not $SecureShare ) { Write-Debug -Message "Grant ReadOnly to authenticated users." $ShareACL.AddAccessRule(( New-Object System.Security.AccessControl.FileSystemAccessRule( "NT AUTHORITY\Authenticated Users", ` "ReadAndExecute", ` "ContainerInherit, ObjectInherit", ` "None", ` "Allow" ) )) } Write-Debug -Message "Set-ACL." Set-ACL -Path $LocalShare -ACLObject $ShareACL Return "Complete" } Catch { Write-Verbose -Message "Share creation failed." Return $Error[0].Exception.Message } } -PSCredential $DomainCred -PSPersist $true |
Final Communications
After everything is complete we update SharePoint as appropriate. This is also where you could add in additional notifications on errors. In our production environment this section contains information on emailing support teams in the case of error to initiate manual resolution steps.
#region final communications #Add in additional steps to email out or handle error cases here If ( $CreateShare -eq "Complete" ) { $FinalStatus = "Complete" } # Failed - duplicate security group Else { $FinalStatus = "Failed" } Write-Verbose -Message "Updating request (list item) status in SharePoint." $Invoke = Invoke-RestMethod -Method Merge ` -URI $NewRequestURI ` -Body "{$($SPProperty): '$FinalStatus'}" ` -ContentType "application/json" ` -Headers @{ "If-Match"="*" } ` -Credential $SPCred #endregion |
Final New-DFSShare
# New-DFSShare # Description # Create new DFS file share and corresponding AD security group # Functionality # Query Sharepoint for details of request # Query AD for existing security group and share owner # If share owner exists in AD and security group does not... # Create the AD security group (for share access) # Query $WindowsServers for the file server volume with the most space available # Create the folder # Share the folder # Add the share to DFS name space # Grant read/write NTFS rights to security group, system, admins # If not secure group... # Grant read NTFS rights to Everyone # Send email to requester, share owner and/or support # Set final request status in SharePoint # Trigger # Monitor-DFSShare-Approved # which was triggered by manual approval in SharePoint # Dependencies # Module - Active Directory # Parameters # $NewRequestURI - string - URI of the SharePoint list item to be processed # SMA Variables # DFSShare-SharePointCredName - string - cred name with rights to administer SharePoint list CreateANewDFSShare # DFSShare-DomainCredName - string - cred name with rights to administer AD security groups, file servers, file shares, and DFS # DFSShare-ADDomain - string - domain in which to create the access group and DFS share # DFSShare-NewDFSShare-WindowsServerRestrictedDrives - comma delimited string - list of Drives to ignore # DFSShare-NewDFSShare-WindowsServerList - comma delimited string - Windows file servers # DFSShare-SPList - string - name of the SharePoint list to access request from # DFSShare-SPProperty - string - name of the list field containing request status workflow New-DFSShare { Param( # From calling monitor [string]$NewRequestURI, # From SMA $WebServiceEndpoint = "https://localhost" ) #region settings # Get our SharePoint credential from the Cred Store $SharePointCredName = ( Get-SMAVariable -Name "DFSShare-SharePointCredName" -WebServiceEndpoint $WebServiceEndpoint ).Value Write-Debug -Message "`$SharePointCredName [$SharePointCredName]" $SPCred = Get-AutomationPSCredential -Name $SharePointCredName Write-Debug -Message "`$SPCred.UserName [$($SPCred.UserName)]" # Get our Domain credential from the Cred Store $DomainCredName = ( Get-SMAVariable -Name "DFSShare-DomainCredName" -WebServiceEndpoint $WebServiceEndpoint ).Value Write-Debug -Message "`$DomainCredName [$DomainCredName]" $DomainCred = Get-AutomationPSCredential -Name $DomainCredName Write-Debug -Message "`$DomainCred.UserName [$($DomainCred.UserName)]" $WindowsServerList = ( Get-SMAVariable -Name "DFSShare-NewDFSShare-WindowsServerList" -WebServiceEndpoint $WebServiceEndpoint ).Value Write-Debug -Message "`$WindowsServerList [$WindowsServerList]" $WindowsServers = $WindowsServerList.Split(",;") If ( $Debug ) { InlineScript { $Using:WindowsServers | ForEach -Begin { $i = 0 } -Process { Write-Debug -Message "`$WindowsServers[$i] [$_]" ; $i++ } } } $WindowsServerRestrictedDrivesList = ( Get-SMAVariable -Name "DFSShare-NewDFSShare-WindowsServerRestrictedDrives" -WebServiceEndpoint $WebServiceEndpoint ).Value Write-Debug -Message "`$WindowsServerRestrictedDrivesList [$WindowsServerRestrictedDrivesList]" $WindowsServerRestrictedDrives = $WindowsServerRestrictedDrivesList.Split(",;") If ( $Debug ) { InlineScript { $Using:WindowsServerRestrictedDrives | ForEach -Begin { $i = 0 } -Process { Write-Debug -Message "`$WindowsServerRestrictedDrives[$i] [$_]" ; $i++ } } } $ADDomain = ( Get-SMAVariable -Name "DFSShare-ADDomain" -WebServiceEndpoint $WebServiceEndpoint ).Value $ADDomainDN = "DC=" + $ADDomain.Replace( ".", ",DC=" ) Write-Debug -Message "`$ADDomain [$ADDomain]" Write-Debug -Message "`$ADDomainDN [$ADDomainDN]" # SharePoint variables $SPList = ( Get-SMAVariable -Name "DFSShare-SPList" -WebServiceEndpoint $WebServiceEndpoint ).Value $SPProperty = ( Get-SMAVariable -Name "DFSShare-SPProperty" -WebServiceEndpoint $WebServiceEndpoint ).Value Write-Debug -Message "`$SPList [$SPList]" Write-Debug -Message "`$SPProperty [$SPProperty]" # Get the new DFS Share request item from SharePoint $Request = Invoke-RestMethod -URI "$NewRequestURI" -Credential $SPCred $Requester = Invoke-RestMethod -URI "$NewRequestURI/CreatedBy" -Credential $SPCred $Owner = Invoke-RestMethod -URI "$NewRequestURI/ShareOwners" -Credential $SPCred # Pull the information out of the new DFS Share request $NewShare = "$($Request.Entry.Content.Properties.RootShareValue)$($Request.entry.content.properties.ShareName)" $SecureShare = $Request.Entry.Content.Properties.SecureShare.'#text' -eq "true" Write-Debug -Message "`$NewShare [$NewShare]" Write-Debug -Message "`$SecureShare [$SecureShare]" $RequesterName = $Requester.Entry.Content.Properties.Name Write-Debug -Message "`$RequesterName [$RequesterName]" $OwnerName = $Owner.Content.Properties.Name $OwnerAccountName = $Owner.Content.Properties.Account $OwnerUserName = $OwnerAccountName.Split( "\" )[1] $OwnerDomain = $OwnerAccountName.Split( "|\" )[1].ToLower() + ".com" $OwnerAccount = Get-ADUser -Filter { sAMAccountName -eq $OwnerUserName } -Server $OwnerDomain $OwnerDN = $OwnerAccount.DistinguishedName $OwnerUPN = $OwnerAccount.UserprincipalName Write-Debug -Message "`$OwnerName [$OwnerName]" Write-Debug -Message "`$OwnerUserName [$OwnerUserName]" Write-Debug -Message "`$OwnerAccountName [$OwnerAccountName]" Write-Debug -Message "`$OwnerDomain [$OwnerDomain]" Write-Debug -Message "`$OwnerAccount.Name [$($OwnerAccount.Name)]" Write-Debug -Message "`$OwnerDN [$OwnerDN]" Write-Debug -Message "`$OwnerUPN [$OwnerUPN]" $RootShare = $NewShare.Split( "\" )[3] $ShareName = $NewShare.Split( "\" )[-1] $GroupName = ( "$RootShare-$ShareName" ).ToUpper() $GroupFullName = "$ADDomain\$GroupName" $GroupDescription = "Full Access to $NewShare" $GroupOU = ( Get-SMAVariable -Name "DFSShare-NewDFSShare-GroupOU" -WebServiceEndpoint $WebServiceEndpoint ).Value Write-Debug -Message "`$RootShare [$RootShare]" Write-Debug -Message "`$ShareName [$ShareName]" Write-Debug -Message "`$GroupName [$GroupName]" Write-Debug -Message "`$GroupFullName [$GroupFullName]" Write-Debug -Message "`$GroupDescription [$GroupDescription]" Write-Debug -Message "`$GroupOU [$GroupOU]" # Extract the group membership from SharePoint $MemberList = @() $MemberNameList = @() $Members = InlineScript { [array](( Invoke-RestMethod -URI "$Using:NewRequestURI/ShareMembers" -Credential $Using:SPCred ).ID ) } ForEach ( $Member in $Members ) { $MemberDetails = Invoke-RestMethod -URI $Member -Credential $SPCred $MemberAccount = $MemberDetails.Entry.Content.Properties.Account $MemberDomain = $MemberAccount.Split( "\" )[0] $MemberName = $MemberAccount.Split( "\" )[1] $MemberADAccount = Get-ADUser -Filter { sAMAccountName -eq $MemberName } -Server $MemberDomain $MemberList += $MemberADAccount.DistinguishedName $MemberNameList += $MemberADAccount.Name } # Add the owners to the group membership as needed If ( $OwnerDN -notin $MemberList ) { $MemberList += $OwnerDN ; $MemberNameList += $OwnerName } Write-Debug -Message "`$MemberList.Count [$($MemberList.Count)]" If ( $Debug ) { InlineScript { $Using:MemberList | ForEach -Begin { $i = 0 } -Process { Write-Debug -Message "`$MemberList[$i] [$_]" ; $i++ } } } Write-Debug -Message "`$MemberNameList.Count [$($MemberNameList.Count)]" If ( $Debug ) { InlineScript { $Using:MemberNameList | ForEach -Begin { $i = 0 } -Process { Write-Debug -Message "`$MemberNameList[$i] [$_]" ; $i++ } } } #endregion #region Create AD Group for share access $DC = ( Get-ADDomainController -Domain $ADDomain -Discover ).HostName[0] Write-Debug -Message "`$DC [$DC]" Write-Verbose -Message "Creating AD group $ADDomain\$GroupName." # Check if the group already exists $ExistingGroup = Get-ADGroup -Filter { Name -eq $GroupName } -Properties ManagedBy -Server $DC # If group does already exist... If ( $ExistingGroup ) { Write-Debug -Message "Existing Group Found: `$ExistingGroup.Name [$($ExistingGroup.Name)]" $CreateGroup = "Duplicate" Write-Debug -Message "`$CreateGroup [$CreateGroup]" Write-Debug -Message "AD group $ADDomain\$GroupName already exists." } # Otherwise continue... Else { Write-Debug -Message "New Group" # If Primary Owner does not exist... If ( -not $OwnerAccount ) { Write-Verbose -Message "Owner $OwnerAccountName not found in Active Directory." $CreateGroup = "Failed - Unable to find Owner information in Active Directory" Write-Debug -Message "`$CreateGroup [$CreateGroup]" } # Otherwise continue... Else { $CreateGroup = InlineScript { $DC = $Using:DC $OwnerDN = $Using:OwnerDN $GroupName = $Using:GroupName $GroupDescription = $Using:GroupDescription $GroupOU = $Using:GroupOU $MemberList = $Using:MemberList Try { # Create the group Write-Debug -Message "Creating the group object" $NewGroup = New-ADGroup -Name $GroupName ` -Path $GroupOU ` -GroupCategory Security ` -GroupScope Universal ` -Description $GroupDescription ` -ManagedBy $OwnerDN ` -PassThru ` -Server $DC # Add the members to the group Write-Debug -Message "Adding members to the group" Add-ADGroupMember -Identity $NewGroup -Members $MemberList -Server $DC Write-Verbose -Message "AD security group created successfully." Return "Complete" } Catch { Write-Verbose -Message "AD security group creation failed." Return $Error[0].Exception.Message } } -PSCredential $DomainCred -PSPersist $true } } #endregion #region create share # If the security group was created without error... # continue If ( $CreateGroup -eq "Complete" ) { Write-Verbose -Message "Creating share $ShareName." $CreateShare = InlineScript { $SecureShare = $Using:SecureShare $GroupFullName = $Using:GroupFullName $GroupName = $Using:GroupName $NewShare = $Using:NewShare $WindowsServers = $Using:WindowsServers $WindowsServerRestrictedDrives = $Using:WindowsServerRestrictedDrives $DC = $Using:DC Try { # Query Windows file servers for the file share volume with the most space available. $WindowsDrives = Get-WMIObject -Class Win32_LogicalDisk ` -Filter "DriveType=3" ` -ComputerName $WindowsServers ` | Where-Object -Property DeviceID -notin $WindowsServerRestrictedDrives if($WindowsDrives.Count) { Write-Debug -Message "`$WindowsDrives.Count [$($WindowsDrives.Count)]" } else { Write-Debug -Message "`$WindowsDrives.Count [1]" } $LargestWindowsDrive = $WindowsDrives | Sort-Object -Property FreeSpace -Descending | Select-Object -First 1 Write-Debug -Message "`$LargestWindowsDrive.FreeSpace [$($LargestWindowsDrive.FreeSpace.ToString("#,0"))]" $Share = $GroupName $Path = "\\$($LargestWindowsDrive.PSComputerName)\$($LargestWindowsDrive.DeviceID.Replace(":", "$"))" $Folder = $LargestWindowsDrive.DeviceID + "\" + $GroupName $LocalShare = "\\$($LargestWindowsDrive.PSComputerName)\$GroupName$" Write-Debug -Message "`$Share [$Share]" Write-Debug -Message "`$Path [$Path]" Write-Debug -Message "`$Folder [$Folder]" Write-Debug -Message "`$LocalShare [$LocalShare]" # Create the Folder on the drive Write-Debug -Message "Creating folder $Path\$Share on Windows file server." New-Item -ItemType "Directory" -Path $Path -Name $Share # Create share and set share permissions Write-Debug -Message "Sharing folder $Path\$Share as $LocalShare." Invoke-Command -ComputerName $LargestWindowsDrive.PSComputerName -ArgumentList $Share, $Folder -ScriptBlock ` { Param( $Share, $Folder ) $Result = net SHARE $Share$=$Folder /GRANT:Everyone`,FULL /UNLIMITED } # Add the share to the DFS namespace $DFSShare = $NewShare Write-Debug -Message "Adding share [$LocalShare] to DFS namespace as [$DFSShare]." C:\Windows\System32\dfsutil.exe link add $DFSShare $LocalShare $GroupAccount = Get-ADGroup -Filter { Name -like $GroupName } -Server $DC $GroupSID = $GroupAccount.SID # Grant NTFS permissions Write-Debug -Message "Granting NTFS permissions on $LocalShare." $ShareACL = Get-ACL -Path $LocalShare # Turn off inheritance Write-Debug -Message "Turn off inheritance." $ShareACL.SetAccessRuleProtection($True, $False) # Remove any existing access rules that didn't go away in the above command Write-Debug -Message "Remove access rules." ForEach ( $Rule in $ShareACL.Access ) { Write-Debug -Message "Remove access rule [$Rule]." $ShareACL.RemoveAccessRule( $Rule ) } # Grant full control to admnistrators Write-Debug -Message "Grant full control to admnistrators." $ShareACL.AddAccessRule(( New-Object System.Security.AccessControl.FileSystemAccessRule( ` "BUILTIN\Administrators", ` "FullControl", ` "ContainerInherit, ObjectInherit", ` "None", ` "Allow") )) # Grant full control to SYSTEM Write-Debug -Message "Grant full control to SYSTEM." $ShareACL.AddAccessRule(( New-Object System.Security.AccessControl.FileSystemAccessRule( "NT AUTHORITY\SYSTEM", ` "FullControl", ` "ContainerInherit, ObjectInherit", ` "None", ` "Allow" ) )) # Grant full control to the AD security group we created above Write-Debug -Message "Grant full control to the AD security group we created above [$GroupFullName]." $ShareACL.AddAccessRule(( New-Object System.Security.AccessControl.FileSystemAccessRule( $GroupSID, ` "FullControl", ` "ContainerInherit, ObjectInherit", ` "None", ` "Allow" ) )) # If this is not a secure group... # grant ReadOnly to authenticated users If ( -not $SecureShare ) { Write-Debug -Message "Grant ReadOnly to authenticated users." $ShareACL.AddAccessRule(( New-Object System.Security.AccessControl.FileSystemAccessRule( "NT AUTHORITY\Authenticated Users", ` "ReadAndExecute", ` "ContainerInherit, ObjectInherit", ` "None", ` "Allow" ) )) } Write-Debug -Message "Set-ACL." Set-ACL -Path $LocalShare -ACLObject $ShareACL Return "Complete" } Catch { Write-Verbose -Message "Share creation failed." Return $Error[0].Exception.Message } } -PSCredential $DomainCred -PSPersist $true } #endregion #region final communications #Add in additional steps to email out or handle error cases here If ( $CreateShare -eq "Complete" ) { $FinalStatus = "Complete" } # Failed - duplicate security group Else { $FinalStatus = "Failed" } Write-Verbose -Message "Updating request (list item) status in SharePoint." $Invoke = Invoke-RestMethod -Method Merge ` -URI $NewRequestURI ` -Body "{$($SPProperty): '$FinalStatus'}" ` -ContentType "application/json" ` -Headers @{ "If-Match"="*" } ` -Credential $SPCred #endregion } |
And now a few notes from me (Charles)…
Be sure to check out Ryan’s session from TechEd North America 2014!
DCIM-B363 Automated Service Requests with Microsoft System Center 2012 R2
In this session, see a real-world implementation of a fully automated IT service catalog developed by a Fortune 500 company for supporting self-service requests. This service catalog is based in Microsoft SharePoint and utilizes the newly released Service Management Automation (SMA) engine. During the session we look at how the solution is architected, cover integration between SMA and SharePoint, build a new service offering from the ground up, and share the best practices we have developed for doing work with SMA along the way. So what’s the best part? You get access to the solution we create, so you leave with access to a working solution to help get you started! Speakers: Ryan Andorfer, Mike Roberts Link on TechEd NA 2014 Channel 9 Recording: DCIM-B363 Automated Service Requests with Microsoft System Center 2012 R2 |
And finally - As always, for more information, tips/tricks, and example solutions for Automation within System Center, Windows Azure Pack, Windows Azure, etc., be sure to check out the other blog posts from Building Clouds in the Automation Track (and https://aka.ms/IntroToSMA), the great work over at the System Center Orchestrator Engineering Blog, and of course, Ryan’s Blog over at https://opalis.wordpress.com!
enJOY!