UiPath Unattendedの接続状態を監視する仕組み

# はじめに
OrchestratorとUnattendedを活用すると自動化できる業務の幅が広がりますが、無人実行するためには、それなりの運用を考慮しないといけません。運用を甘く見ると予期せぬ障害に見舞われることになります。
例えば、知らない間にUnattendedがOrchestratorの接続が切れていて、スケジュール済みのジョブが開始できないとか。とはいえ、Orchestratorの画面を見張っているのも効率が悪いので、監視・通知は可能な限り自動化する方が好まれます。
（Orchestratorにもロボット切断時のアラート機能がありますが、特定のロボットだけを監視するとか、既定の台数を閾値とした監視とかには向いていません。）
今回は、UnattendedがOrchestratorと接続され、実行可能な状態かをチェックする仕組みを考えてみます。

# つかうもの
今回は以下の使用して検証を行っています。クラウドは2022年3月時点のもの。
* UiPath Orchestrator Cloud

# 仕様
Orchestrator APIを使用して、特定フォルダ内のUnattendedがオンライン（接続状態）かオフライン（切断状態）かをチェックします。
オンラインのUnattendedが既定の閾値を下回ると警告通知（通知先はMicrosoft Teamsとします）を行います。
以上をPowerShellスクリプトで実装します。
※スクリプトはWindowsタスクスケジューラ等で繰り返し実行する前提です。統合監視の仕組みを持っている場合は、通知は監視ツールに頼り、本スクリプトはステータスを返すまでで十分です。
![仕組み.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89445/ebde9283-2cb4-809f-99cc-b02f80ee95df.png)

# 実装

## Orchestrator OAuth認証の準備
2021.4からOrchestrator APIの認証方法がOAuth対応になったため、従来のID/PW認証ではなく、OAuth認証で実装してみます（ID/PW認証も使えますが、非推奨ですので新しく実装する場合はOAuth認証を使ってください）。
今回は、以下2つのAPIを利用します。
* GET:/odata/Folders
* GET:/odata/Sessions/UiPath.Server.Configuration.OData.GetMachineSessionRuntimesByFolderId

まず、Swaggerでスコープを確認します。
今回は、OR.Folders.ReadとOR.Robots.Readが必要になるようです。
![OC_OAuthリソーススコープ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89445/9f28c5a4-4dfc-88be-b8f5-8ef1ea1bd902.png)

次に、AutomationCloudから「管理」>「外部アプリケーション」を選択し、アプリケーションを追加します。
「アプリケーションの種類」は「機密アプリケーション」、
「リソース」は「アプリケーションスコープ（OR.Folders.Read、OR.Robots.Read）」とします。
![OC_OAuth設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89445/ab42d62e-559e-a098-2799-f444c6183960.png)

アプリケーションを作成すると、「アプリケーションID」と「アプリケーションシークレット」が発行されるので、メモします。
※ シークレットは外部に流出しないように厳重に管理します。

## PowerShell
### 前提
モダンフォルダーの使用を前提としています。クラシックフォルダーの場合は、使用するAPIやロジックが異なります。

### 処理
大まかな処理の流れは以下の通り（詳細は割愛）。
1. OAuth認証（トークンを取得）
1. フォルダ名からフォルダIDを取得
1. 該当のフォルダ内のUnattendedセッションを取得
1. セッションがアクティブか否かを判定し、稼働率を算出
1. 警告またはエラーの閾値を超えていれば、Teamsへメッセージを送信

### コード
変数値の部分は適宜変更してください。
※ エラー処理など省略しています。

```URConnectionCheck.ps1
# 【閾値】警告・エラーを出すUR稼働率(%)
$warnRate = 70
$errRate = 50

# 接続先の環境設定
$url = "https://cloud.uipath.com"
$tenant = "/[組織名]/[テナント名]/orchestrator_"
$folderName = "[フォルダー名]"
$clientId = "[アプリケーションID]"
$clientSecret = "[アプリケーションシークレット]"
$scopes = "OR.Folders.Read OR.Robots.Read"

$body = @{
    grant_type = "client_credentials"
    client_id = $clientId
    client_secret = $clientSecret
    scope = $scopes
}

# 認証
$uri = $url + "/identity_/connect/token"
$token = (Invoke-RestMethod -Uri $uri -Method Post -Body $body -ContentType "application/x-www-form-urlencoded").access_token
$headers = @{"Authorization" = "Bearer $token"}

#フォルダーを検索
$uri = $url + $tenant + "/odata/Folders?%24filter=FullyQualifiedName%20eq%20'$folderName'"
$folder = (Invoke-RestMethod -Uri $uri -Method Get -Headers $headers).value
$folderId = $folder.Id

# UR接続状況を取得
$uri = $url + $tenant + "/odata/Sessions/UiPath.Server.Configuration.OData.GetMachineSessionRuntimesByFolderId(folderId=$folderId)?runtimeType=Unattended"
$session = (Invoke-RestMethod -Uri $uri -Method Get -Headers $headers).value

$online = 0
$offline = 0
$rate = 0
$machine = @()

forEach($value in $session){
    if($value.Status -eq "Available") {
        $online += 1
    } else {
        Write-Host $value.HostMachineName
        $offline += 1
        $machine += $value.HostMachineName
    }
}

$rate = [Math]::Round($online / ($online + $offline) * 100, [MidpointRounding]::AwayFromZero)

# 判定
if($rate -le $errRate) {
    Write-Host "[ERROR] Available UR $rate% (Available : $online  -  Unavailable : $offline)"
    $msgTitle = "【ERROR】接続中のURマシンがありません（稼働率：$rate%）"
    $msgColor = "dc3545"
} elseif($rate -le $warnRate) {
    Write-Host "[WARN] Available UR $rate% (Available : $online  -  Unavailable : $offline)"
    $msgTitle = "【WARN】接続中のURマシンが少なくなっています（稼働率：$rate%）"
    $msgColor = "ffc107"
} else {
    Write-Host "[INFO] Available UR $rate% (Available : $online  -  Unavailable : $offline)"
    $msgTitle = ""
}

# 通知
$teams = "[MS Teams Incoming WebhookのURL]"
$machine = $machine -join ", "

$body = @"
{
    "themeColor" : "$msgColor",
    "summary" : "UR Connection Check",
    "sections" : [{
        "activityTitle" : "$msgTitle",
        "facts" : [
            {"name" : "接続中のマシン", "value" : "$online 台"},
            {"name" : "切断中のマシン", "value" : "$offline 台（$machine）"}
        ]
    }]
}
"@

if($msgTitle -ne "") {
    $res = Invoke-RestMethod -Uri $teams -Method Post -Body ([System.Text.Encoding]::UTF8.GetBytes($body))
}

# EOF
```

# 動作を確認する
今回、Unattendedを3台用意し、WARN閾値を70%、ERROR閾値を50%に設定しました。
![全台有効.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89445/0c3e4031-b6bf-e90c-45eb-f0ffbce1921b.png)

## 1台接続を切る
3台の内、1台の接続を切ります。
![1台切断.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89445/ab659e74-a690-fa02-beef-cac367807a4e.png)

稼働率は67%となり、WARN閾値を下回ったので、Teamsには警告のメッセージが通知されました。
![警告.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89445/7835885d-442e-5063-c3f4-7464a73ae210.png)

## 2台接続を切る
続いて、もう1台の接続を切ります。オンラインのUnattendedは1/3になります。
![2台切断.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89445/09b48c81-a9a9-b37f-cb9a-db06eb7ac34b.png)

稼働率は更に下がり33%となり、ERROR閾値を下回ったので、Teamsにはエラーのメッセージが通知されました。
![エラー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89445/1680c3b0-97d7-08ca-7f98-cecccec7326e.png)

# さいごに
今回のようなUnattendedの接続状態の監視は、小規模運用環境では無くても構いませんが、大規模運用環境でクリティカルな業務をスケジュールを組んで自動化しているケースではあった方が良いと考えています。
今回は稼働率を求めて閾値を下回った場合にアラートを出す仕様にしましたが、ジョブが特定のマシンに固定アサインされている状況の場合は、ピンポイントに特定マシンの状態を監視したほうが良いです。