none
Runbook looping

    Question

  • We are trying to build a runbook for creating AD users using information from our CRM and report to them

    When I run this runbook if all steps finished success everything is OK, but if one step failed (3rd step for example can return that User already exist) this error message collect at Fail Message Collector and run next step Report to our CRM

    When runbook finished it start automatically from the begin. If we Disable last step looping stops and everything is OK but we need to take error message in our CRM. We tried to combine Fail Collector and CRM Result to one block but runbook still looping.


    The opinion expressed by me is not an official position of Microsoft


    • Edited by Vector BCO Friday, March 10, 2017 1:18 PM
    Friday, March 10, 2017 1:17 PM

All replies

  • Share the code of Get-Aduser..

    Regards


    Priyabrata

    Friday, March 10, 2017 4:56 PM
  • Hi,

    I guess the Runbook is triggered from another Runbook  with "Invoke Runbook" Activity, which has looping enbaled an exit condition like this:

    Ig you don't want the looping disable in the other Runbook  the loop at"Invoke Runbook".

    Regards,

    Stefan


    Visit go2azure.eu and my blog at www.sc-orchestrator.eu !

    Monday, March 13, 2017 8:33 AM
    Answerer
  • Dear Stefan, I have only one runbook, and start it manually (in test mode) from Web console.

    In web console I chouse 2 parameters - Runbook server, and number of action in our CRM.

    On second step "Request parameters", I found action by number from Init_D, and get parameters like FirstName, LastName, PhoneNuber etc.

    On third step "Validation" I try to find users with this parameters, if user exist I invoke exception

    If user exist - 4st step is "Fail message collector" - on this step I put error message to the string like "On a first validation action faild. Error: $FirstValidationError"

    On a last step I try to write message from step 4 to our CRM. I found CRM action using number from Init_D, and put message to the CRM action log.

    When I run my runbook manually and user exist I take alot messages in my CRM

    If user does not exist everything run once


    The opinion expressed by me is not an official position of Microsoft



    • Edited by Vector BCO Monday, March 13, 2017 9:04 AM
    Monday, March 13, 2017 8:38 AM
  • Share the code of Get-Aduser..

    Regards


    Priyabrata

    Dear Priyabrata, code from validation block
    $GivenName = "{GivenName from "Request Parameters from CRM"}"
    $Surname = "{Surname from "Request Parameters from CRM"}"
    $SamAccountName = "{SamAccountName from "Request Parameters from CRM"}"
    
     
    try {
        $ErrorActionPreference = "Stop"
        
        "$(get-date -Format u)  :  Start" | out-file C:\tmp\trace.log
           
        $argsArray = @()
    
        "$(get-date -Format u)  :  args arr init" | out-file C:\tmp\trace.log -Append
        
        $argsArray += "$GivenName"
        $argsArray += "$Surname"
        $argsArray += "$SamAccountName"
        
        "$(get-date -Format u)  :  Arr: $argsArray" | out-file C:\tmp\trace.log -Append
        
    
        $dc = Invoke-Command -ComputerName localhost -ScriptBlock {
            (([system.directoryservices.activedirectory.Forest]::GetCurrentForest()).Domains.Domaincontrollers).name | foreach {
                if (!([string]::IsNullOrEmpty($_))){
                    IF (Test-Connection $_ -Count 2 -Quiet){$dc = $_}
                } # End If
            } # End Foreach
            if ([string]::IsNullOrEmpty($dc)){
                Throw "Cannot find valid DC"
            } # End If
            else {$dc}
        } # End Scriptblock
    
        "$(get-date -Format u)  :  Before Session create. Computername: '$dc'" | out-file C:\tmp\trace.log -Append
    
        # Establish an external session (to localhost) to ensure 64bit PowerShell runtime using the latest version of PowerShell installed on the runbook server
        # Use this session to perform all work to ensure latest PowerShell features and behavior available
        $Session = New-PSSession -ComputerName $dc -Authentication Kerberos
        if (!([string]::IsNullOrEmpty($Session))){
            "$(get-date -Format u)  :  Session created succesfully" | out-file C:\tmp\trace.log  -Append
        }
            
        # Invoke-Command used to start the script in the external session. Variables returned by script are then stored in the $ReturnArray variable
        $ReturnArray = Invoke-Command -Session $Session -Argumentlist $argsArray -ScriptBlock {
            # Define a parameter to accept each data bus input value. Recommend matching names of parameters and data bus input variables above
            Param(
    
            [ValidateNotNullOrEmpty()]
                [string]$GivenName,
    
            [Parameter(Mandatory = $false)]
                [string]$Surname,
        
            [Parameter(Mandatory = $false)]
                [string]$SamAccountName
    
            ) # End Param
    
            Import-Module ActiveDirectory 
    
            $retry = 0
            $UserEnabled = $false
            $UserDoesNotExist = $false
    
            do {
                try {
                    if (! [string]::IsNullOrEmpty($SamAccountName)){
                        $AdUser = Get-ADUser $SamAccountName -Properties * -ErrorAction stop
                
                        Switch ($AdUser.UserAccountControl) {
                            514{ # If we are here, then user now Disabled
                                if (! ([string]::IsNullOrEmpty($AdUser.memberOf))){
                                    foreach ($group in $AdUser.memberOf){
                                        $group -match 'CN=(?''GRP''[^,]+),' | Out-Null
                                        Remove-ADGroupMember -Identity $Matches['GRP'] -Members $SamAccountName -Confirm:$false
                                    } # End Foreach
                                } # End If
                            } # End If User Disabled
                            512 { # If we are here, then user now Enabled
                                Throw "User '$SamAccountName' exist and enabled."
                            } # End ElseIf (User Enabled)
                            default {
                                # User Exist but something is incorrect (Maybe SUZ)
                                Throw "User '$SamAccountName' exist and status is different as expected.$($AdUser.UserAccountControl)"
                            } # End Else (User Exist but something is incorrect)
                        }
                    } # End If SamAccountName Exist
                    Else {
                        Throw "SamAccountName is empty"
                    }
                } # End try
                Catch{
                    switch -Regex ($Error[0].Exception.Message){
                        "Cannot find an object with identity: '$SamAccountName'" {
                            $UserDoesNotExist = $true
                        } # End If
                        "User '$SamAccountName' exist and enabled."{
                            $UserEnabled = $true
                        }
                        Default { 
                            Start-Sleep -s 2
                        }  # End Else
                    } # End Switch
                    $ErrorMessage = $Error[0].Exception.Message
                } # End try
                $retry++
            } Until (($UserDoesNotExist) -or (! [string]::IsNullOrEmpty($AdUser)) -or ($retry -ge 5))
    
            if (($retry -ge 5) -or ($UserEnabled)) {
                $ErrorMessage = $Error[0].Exception
                Throw $ErrorMessage
            } # End If
    
            $TmpDisplayName = "$GivenName $Surname"
            Try {
                $i = 1
                $AdUsers = Get-ADUser -Filter * -Properties DisplayName
                do {
                   if ($($AdUsers | where {($_.DisplayName -eq $TmpDisplayName) -and !($_.SamAccountName -eq $SamAccountName)} | Measure-Object).count -ge 1){
                        $TmpDisplayName = "$TmpDisplayName $i"
                        $i++
                    } # End If
                    else {
                        $DisplayName = $TmpDisplayName
                    } # 
                } Until (!([string]::IsNullOrEmpty($DisplayName)))
            } # End try
            Catch {
                #$error[0] | out-file C:\tmp\trace.log -Append
                $ErrorMessage = $Error[0].Exception
                Throw $ErrorMessage
            } # End Catch
    
            if ($UserDoesNotExist){
                $AccountExist = 0
            } # End If
            Elseif (!($UserDoesNotExist) -and !($UserEnabled)) {
                $AccountExist = 1
            } # End Else
    
            $resultArray = @()
            $resultArray += $AccountExist
            $resultArray += $DisplayName
            return  $resultArray  
        } # End Invoke-Command
    
        $AccountExist = $ReturnArray[0]
        $DisplayName = $ReturnArray[1]
        
        Remove-PSSession $Session -ErrorAction "SilentlyContinue"
    
    } # End Try
    Catch {
        $ErrorMessage = $Error[0]
        Remove-PSSession $Session -ErrorAction "SilentlyContinue"
        Throw $ErrorMessage
    } # End Catch


    The opinion expressed by me is not an official position of Microsoft


    • Edited by Vector BCO Monday, March 13, 2017 11:28 AM
    Monday, March 13, 2017 11:18 AM
  • Have anyone any ideas?

    The opinion expressed by me is not an official position of Microsoft

    Monday, March 13, 2017 2:52 PM
  • Thanks Vector, for sharing the code. I was busy attending Summit in Singapore :)

    Analyzed the code. You cannot have have 2 Invoke Sessions and enter-pssession  in a .NET Script. SCORCH is a 32 Bit PS.

    You need to break the code. 

    1> Get the details-> as Inputs

    2> Get the DC. (here you need to add a break command to get the first available DC. No need to loop and wait for all. 

    3> Pass this valued to to the Next .Net activity. 

    Remember that Orchestrator will use 32 bit powershell version if you use the built in powershell script function.

    Therefore you do miss some functions from powershell v3 and so on..

    You might not need this, you can try to log an activity in the return Array value.

       # Establish an external session (to localhost) to ensure 64bit PowerShell runtime using the latest version of PowerShell installed on the runbook server
        # Use this session to perform all work to ensure latest PowerShell features and behavior available
        $Session = New-PSSession -ComputerName $dc -Authentication Kerberos
        if (!([string]::IsNullOrEmpty($Session))){
            "$(get-date -Format u)  :  Session created succesfully" | out-file C:\tmp\trace.log  -Append
        }


    4> This might not work within Invoke command due to double hop. Its a great PS script but will not work in SCORCH unless and until, you break it. 

    $error[0] | out-file C:\tmp\trace.log -Append

    5> Invoke-Command -ComputerName localhost -ScriptBlock instead write Invoke-Command -ScriptBlock {}

    SCORCH goes hayware and at times does not understand -computername localhost

    If you are still stuck, let us know. 

    Regards



    Priyabrata

    Tuesday, March 14, 2017 10:55 AM
  • Or you might be interested in this.  :) . This will solve all the purpose of your issues. (No need to invoke or PSSEssion)

    By Default  SCORCh 2012/2016 will start 32bit PowerShell version 2 with "Run .Net Script" Activity.

    You can set this Registry-Value on the Orchestrator Runbook Service to make "Run .Net Script" Activity to execute the highest version of PowerShell on the Orchestrator Runbook Server.

     

    HKLM\SOFTWARE\Wow6432Node\Microsoft\.NETFramework

    Reg_DWORD: OnlyUseLatestCLR

    Value: 1

     

    Regards,


    Priyabrata

    Tuesday, March 14, 2017 11:00 AM
  • Thanks Vector, for sharing the code. I was busy attending Summit in Singapore :)

    Analyzed the code. You cannot have have 2 Invoke Sessions and enter-pssession  in a .NET Script. SCORCH is a 32 Bit PS.

    You need to break the code. 

    1> Get the details-> as Inputs

    2> Get the DC. (here you need to add a break command to get the first available DC. No need to loop and wait for all. 

    3> Pass this valued to to the Next .Net activity. 

    Remember that Orchestrator will use 32 bit powershell version if you use the built in powershell script function.

    Therefore you do miss some functions from powershell v3 and so on..

    You might not need this, you can try to log an activity in the return Array value.

       # Establish an external session (to localhost) to ensure 64bit PowerShell runtime using the latest version of PowerShell installed on the runbook server
        # Use this session to perform all work to ensure latest PowerShell features and behavior available
        $Session = New-PSSession -ComputerName $dc -Authentication Kerberos
        if (!([string]::IsNullOrEmpty($Session))){
            "$(get-date -Format u)  :  Session created succesfully" | out-file C:\tmp\trace.log  -Append
        }


    4> This might not work within Invoke command due to double hop. Its a great PS script but will not work in SCORCH unless and until, you break it. 

    $error[0] | out-file C:\tmp\trace.log -Append

    5> Invoke-Command -ComputerName localhost -ScriptBlock instead write Invoke-Command -ScriptBlock {}

    SCORCH goes hayware and at times does not understand -computername localhost

    If you are still stuck, let us know. 

    Regards



    Priyabrata

    Dear Priyabrata,

    I know that SCO run PowerShell script in 32 bit process in lowest version - it is strange, by the way.
    I tried modify registry as you wrote at the top, but it broke all. I take errors on first step with message "You cannot call method on a null-value expression".
    I"m rotated back my values in registry, and try split my code in validation part to the 2 Activities (its not looks like believable, but i try), and I got the same problems with looping as before.

    Code of this Activitie (Validation) writen for test, and it work clearly, but i got looping of all RunBook, and got now ideas why this sh#t happen. Some of yours proposal optimizations will be accepted in the production version, but now it does not take sense. Can you give a proufe of this "You cannot have have 2 Invoke Sessions and enter-pssession  in a .NET Script. SCORCH is a 32 Bit PS."

    I use only one PSSession in my code block in a second of time, when I open second session previous session already closen. By the way in my script block I do bot use double hope constructions

    In this scheme i got looping

    In this scheme I have no looping but its looks like ugly


    The opinion expressed by me is not an official position of Microsoft




    • Edited by Vector BCO Tuesday, March 14, 2017 3:46 PM
    Tuesday, March 14, 2017 1:37 PM
  • Hello Vector,

    I have bit messed around your code to make few changes. My apologies for this. 

    I have changed the script of finding the First available DC. 

    Your code was looping around to get all the DC and was continuing to populate all the available DC as an array. 

    So is the reason, it was looping. 

        $dc = Invoke-Command -ScriptBlock {
            $arrayListofDC = (([system.directoryservices.activedirectory.Forest]::GetCurrentForest()).Domains.Domaincontrollers).name 
            foreach ($AvailableDC in $arrayListofDC)
            {
            IF (Test-Connection $AvailableDC -Count 2 -Quiet)
            {
            Return $AvailableDC
            }
            Break;
            } # End If
            if ([string]::IsNullOrEmpty($AvailableDC)){
                Throw "Cannot find valid DC"
            } # End If
            else {$AvailableDC}
        } # End Scriptblock
    

    Modified the above code.

    Instead of remoting into the server using a PS Session , pls use the AD PS Options.

    #Remove-ADGroupMember -Identity $Matches['GRP'] -Members $SamAccountName -Confirm:$false
    #My Code
    Remove-ADGroupMember -Identity $Matches['GRP'] -Members $SamAccountName -Server $dc -Confirm:$false 
    Suit yourself to Run the code. Should work fine. :)

    $GivenName = "{GivenName from "Request Parameters from CRM"}"
    $Surname = "{Surname from "Request Parameters from CRM"}"
    $SamAccountName = "{SamAccountName from "Request Parameters from CRM"}"
    
     
    try {
        $ErrorActionPreference = "Stop"
        
        "$(get-date -Format u)  :  Start" | out-file C:\temp\trace.log
           
        $argsArray = @()
    
        "$(get-date -Format u)  :  args arr init" | out-file C:\temp\trace.log -Append
        
        $argsArray += "$GivenName"
        $argsArray += "$Surname"
        $argsArray += "$SamAccountName"
        
        "$(get-date -Format u)  :  Arr: $argsArray" | out-file C:\temp\trace.log -Append
        
        # This will Retun the First Available DC
          $dc = Invoke-Command -ScriptBlock {
            $arrayListofDC = (([system.directoryservices.activedirectory.Forest]::GetCurrentForest()).Domains.Domaincontrollers).name 
            foreach ($AvailableDC in $arrayListofDC)
            {
            IF (Test-Connection $AvailableDC -Count 2 -Quiet)
            {
            Return $AvailableDC
            }
            Break;
            } # End If
            if ([string]::IsNullOrEmpty($AvailableDC)){
                Throw "Cannot find valid DC"
            } # End If
            else {$AvailableDC}
        } # End Scriptblock
    
        "$(get-date -Format u)  :  Before Session create. Computername: '$dc'" | out-file C:\temp\trace.log -Append
    
            
        # Invoke-Command used to start the script in the external session. Variables returned by script are then stored in the $ReturnArray variable
        $ReturnArray = Invoke-Command -Argumentlist $argsArray -ScriptBlock {
            # Define a parameter to accept each data bus input value. Recommend matching names of parameters and data bus input variables above
            Param(
    
            [ValidateNotNullOrEmpty()]
                [string]$GivenName,
    
            [Parameter(Mandatory = $false)]
                [string]$Surname,
        
            [Parameter(Mandatory = $false)]
                [string]$SamAccountName
    
            ) # End Param
    
            Import-Module ActiveDirectory 
    
            $retry = 0
            $UserEnabled = $false
            $UserDoesNotExist = $false
    
            do {
                try {
                    if (! [string]::IsNullOrEmpty($SamAccountName)){
                        $AdUser = Get-ADUser $SamAccountName -Properties * -ErrorAction stop -Server $dc
                
                        Switch ($AdUser.UserAccountControl) {
                            514{ # If we are here, then user now Disabled
                                if (! ([string]::IsNullOrEmpty($AdUser.memberOf))){
                                    foreach ($group in $AdUser.memberOf){
                                        $group -match 'CN=(?''GRP''[^,]+),' | Out-Null
                                        #Remove-ADGroupMember -Identity $Matches['GRP'] -Members $SamAccountName -Confirm:$false
                                        #My Code
                                        Remove-ADGroupMember -Identity $Matches['GRP'] -Members $SamAccountName -Server $dc -Confirm:$false 
                                    } # End Foreach
                                } # End If
                            } # End If User Disabled
                            512 { # If we are here, then user now Enabled
                                Throw "User '$SamAccountName' exist and enabled."
                            } # End ElseIf (User Enabled)
                            default {
                                # User Exist but something is incorrect (Maybe SUZ)
                                Throw "User '$SamAccountName' exist and status is different as expected.$($AdUser.UserAccountControl)"
                            } # End Else (User Exist but something is incorrect)
                        }
                    } # End If SamAccountName Exist
                    Else {
                        Throw "SamAccountName is empty"
                    }
                } # End try
                Catch{
                    switch -Regex ($Error[0].Exception.Message){
                        "Cannot find an object with identity: '$SamAccountName'" {
                            $UserDoesNotExist = $true
                        } # End If
                        "User '$SamAccountName' exist and enabled."{
                            $UserEnabled = $true
                        }
                        Default { 
                            Start-Sleep -s 2
                        }  # End Else
                    } # End Switch
                    $ErrorMessage = $Error[0].Exception.Message
                } # End try
                $retry++
            } Until (($UserDoesNotExist) -or (! [string]::IsNullOrEmpty($AdUser)) -or ($retry -ge 5))
    
            if (($retry -ge 5) -or ($UserEnabled)) {
                $ErrorMessage = $Error[0].Exception
                Throw $ErrorMessage
            } # End If
    
            $TmpDisplayName = "$GivenName $Surname"
            Try {
                $i = 1
                $AdUsers = Get-ADUser -Filter * -Properties DisplayName
                do {
                   if ($($AdUsers | where {($_.DisplayName -eq $TmpDisplayName) -and !($_.SamAccountName -eq $SamAccountName)} | Measure-Object).count -ge 1){
                        $TmpDisplayName = "$TmpDisplayName $i"
                        $i++
                    } # End If
                    else {
                        $DisplayName = $TmpDisplayName
                    } # 
                } Until (!([string]::IsNullOrEmpty($DisplayName)))
            } # End try
            Catch {
                #$error[0] | out-file C:\temp\trace.log -Append
                $ErrorMessage = $Error[0].Exception
                Throw $ErrorMessage
            } # End Catch
    
            if ($UserDoesNotExist){
                $AccountExist = 0
            } # End If
            Elseif (!($UserDoesNotExist) -and !($UserEnabled)) {
                $AccountExist = 1
            } # End Else
    
            $resultArray = @()
            $resultArray += $AccountExist
            $resultArray += $DisplayName
            return  $resultArray  
        } # End Invoke-Command
    
        $AccountExist = $ReturnArray[0]
        $DisplayName = $ReturnArray[1]
        
        #Remove-PSSession $Session -ErrorAction "SilentlyContinue"
    
    } # End Try
    Catch {
        $ErrorMessage = $Error[0]
        #Remove-PSSession $Session -ErrorAction "SilentlyContinue"
        Throw $ErrorMessage
    } # End Catch

    Regards,


    Priyabrata

    Wednesday, March 15, 2017 3:18 AM
  • Dear Priyabrata,

    1) Find DC block is non optimized, but it return only one result or Error. Please prove something if you say that is that code is incorrect.

    Here is my prove:

    2) Remove-ADGroupMember with out invoke-command will fail because in PoSh v1 32bit dont now nothing about that command

    Remove-ADGroupMember -Identity $Matches['GRP'] -Members $SamAccountName -Server $dc -Confirm:$false

    Remove-ADGroupMember with invoke-command will fail because it will be doublehope operation, and I do not want add credentials in this script


    The opinion expressed by me is not an official position of Microsoft

    Thursday, March 16, 2017 4:01 PM
  • Hi,

    I guess "Throw $ErrorMessage" will kill the process of the Runbook Instances. Bu I can't reproduce this here (System Center 2016 running on Windows 2016).

    Perhaps you can look at "C:\ProgramData\Microsoft System Center 2012\Orchestrator\PolicyModule.exe\Logs" to check.

    I would not use "Throw $ErrorMessage" but I would define ErrorMessage in the Publishe Data tab and go to the error branch if ErrorMessage is not empty

    Regards,

    Stefan


    Visit go2azure.eu and my blog at www.sc-orchestrator.eu !

    Friday, March 17, 2017 10:23 PM
    Answerer
  • Dear Stefan, sorry for a long respond!

    We try to modify our scripts (remove throw $ErrorMessage), and build logical matches based on $ErrorMessage, as you show in a prewious post. But we still take a loop after runbook finished :-(

    I think something can be wrong in a CRM activity. If we disable them - everything is ok


    The opinion expressed by me is not an official position of Microsoft

    Wednesday, March 22, 2017 8:33 AM