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


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

image


Oh, and here (color portion) is a look at where this post fits in to the overall example Solution Architecture:

image


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

  1. Settings Definition
  2. Poll for Requests
    • If found: Trigger worker Runbook
    • Checkpoint and sleep for remaining delay cycle (time between polls)
  3. 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

image


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

  1. The person who requested the share
  2. The account which was selected to own the share
  3. The new share’s name
  4. Whether or not the share should be a ‘secure’ share or not
  5. 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!