Jamfのインベントリプリロード機能を活用してMacを効率的に管理する方法

この記事は [DENSOアドベントカレンダー2024](https://qiita.com/advent-calendar/2024/denso) の1日目の記事です。

## 目的
本記事では、Jamf Proを利用して、ゼロタッチデプロイメント前の在庫Macの物理的な場所や状態を効率的に管理する方法を紹介します。この手法により、在庫管理が簡素化され、手動での追跡作業を削減できます。

---

## 背景
Jamf Proは、利用者にデバイスを割り当てる機能が豊富ですが、物理的な在庫の追跡や状態の管理には特化していません。例えば、複数のオフィスや拠点にまたがる在庫を管理する場合、外部ツールを併用する運用となることもあります。しかし、Jamf Proの**インベントリプリロード機能**を活用すれば、CSVファイルを用いて物理的な場所や状態を直接登録し、Jamf Pro内で一元管理することが可能です。

---

## ソリューション概要
**インベントリプリロード機能**は、CSVファイルを使用してデバイス情報を一括登録・更新できるJamf Proの機能です。この機能を利用すると、以下の情報をJamf Proに登録して管理できます：

- 物理的な場所（例：本社、倉庫Aなど）
- デバイスの状態（例：新品、使用中）
- 固定資産番号などのカスタム情報

公式ドキュメント：[Jamf Pro Inventory Preload 機能](https://learn.jamf.com/ja-JP/bundle/jamf-pro-documentation-current/page/Inventory_Preload.html)

---

## 手順

### ステップ1: CSVファイルの準備

#### 手順
1. Jamf Proにログインします。
2. 左側メニューから「Settings」を選択します。
3. 「Global Management」セクションの「Inventory Preload」をクリックします。
4. 「Template」をクリックしてCSVテンプレートをダウンロードします。

#### CSVファイルの項目
CSVファイルには以下の情報を記載できます。必須項目に加え、カスタム項目（Extension Attributes）を追加して組織特有の情報を登録できます。

- **必須項目**:
  - シリアルナンバー（Serial Number）
  - デバイスタイプ（Device Type）："Computer"または"Mobile Device"
- **推奨項目**:
  - IT資産番号（Asset Tag）
  - 物理的な場所（EA Location）
  - デバイスの状態（EA Condition）

#### CSVファイルの例
以下の例では、デバイスの物理的な場所と状態を登録しています。

```csv
Serial Number,Device Type,EA Location,EA Condition,EA Asset Tag
DUMMY12345,Computer,Main Office,New,12345
DUMMY67890,Computer,Warehouse A,Used,67890
```

---

### ステップ2: CSVファイルのアップロード

1. **Jamf Proにアクセス**
   - Jamf Proの「Settings」→「Global Management」→「Inventory Preload」に進みます。
2. **CSVファイルをアップロード**
   - 「Upload CSV」をクリックし、準備したCSVファイルをアップロードします。

---

### ステップ3: データの確認と適用

1. **アップロード内容の確認**
   - CSVの内容が正しく反映されているかJamf Proの管理画面で確認します。
2. **デバイス情報の適用**
   - デバイスがインベントリ収集時に自動的に情報を反映します。

---

### ステップ4: 情報の更新

在庫情報を更新する場合、新しいCSVファイルを作成して再アップロードします。Jamf Pro上で直接編集するのではなく、常にCSVを基に更新を行うことで、ミスを防ぎ管理の一貫性を保てます。

---

## カスタム属性の活用

Jamf Proのカスタム属性（Extension Attributes）を使用することで、組織固有の情報を管理に追加できます。以下のような項目を拡張可能です：

- **IT資産番号（Fixed Asset Tag）**
- **利用用途（Usage）**
- **保管エリア（Location）**

CSVファイルのカスタム属性例：

```csv
Serial Number,Device Type,EA Location,EA Condition,EA Fixed Asset Tag
DUMMY12345,Computer,Main Office,New,FA12345
DUMMY67890,Computer,Warehouse A,Used,FA67890
```

---

## 注意点
- **在庫データの更新フロー**を構築することで、常に最新の情報を反映可能にします。

例えば、GitHub Actionsを使って自動化する場合、以下のようなスクリプトを用意します：

```python
"""
このスクリプトはJamfのインベントリプリロードを自動的に更新します。
OAuthトークンを取得し、既存のインベントリプリロードレコードを削除して、
在庫データを含む新しいCSVファイルをアップロードします。
"""

import logging
import os
import sys

import requests
from dotenv import load_dotenv

logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)

if os.path.exists(".env"):
    load_dotenv()


def validate_env_vars(env_vars):
    """
    必要な環境変数を検証します。
    """
    missing_vars = [var for var in env_vars if not os.getenv(var)]
    if missing_vars:
        logging.error(
            "必要な環境変数が不足しています: %s", ", ".join(missing_vars)
        )
        sys.exit(1)


class JamfRepository:
    """
    Jamf APIと対話するためのリポジトリクラス。
    """

    def __init__(self, base_url):
        self.base_url = base_url

    def get_oauth_token(self, client_id, client_secret):
        """
        Jamf APIからOAuthトークンを取得します。
        """
        token_url = f"{self.base_url}/api/oauth/token"
        token_data = {
            "client_id": client_id,
            "grant_type": "client_credentials",
            "client_secret": client_secret,
        }
        token_headers = {"Content-Type": "application/x-www-form-urlencoded"}

        try:
            response = requests.post(
                token_url, headers=token_headers, data=token_data, timeout=10
            )
            response.raise_for_status()
            logging.info("OAuthトークンを正常に取得しました。")
            return response.json().get("access_token")
        except requests.exceptions.HTTPError as http_err:
            logging.error(
                "HTTPエラーが発生しました: %s - レスポンス: %s", http_err, response.text
            )
            sys.exit(1)
        except requests.exceptions.RequestException as err:
            logging.error(
                "リクエストエラーが発生しました: %s - レスポンス: %s",
                err,
                err.response.text if err.response else "レスポンスなし",
            )
            sys.exit(1)

    def delete_all_inventory_records(self, access_token):
        """
        すべてのインベントリプリロードレコードを削除します。
        """
        delete_url = f"{self.base_url}/api/v2/inventory-preload/records/delete-all"
        delete_headers = {
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json",
        }

        try:
            response = requests.post(delete_url, headers=delete_headers, timeout=10)
            response.raise_for_status()
            logging.info("すべてのインベントリプリロードレコードを正常に削除しました。")
        except requests.exceptions.HTTPError as http_err:
            logging.error(
                "HTTPエラーが発生しました: %s - レスポンス: %s", http_err, response.text
            )
            sys.exit(1)
        except requests.exceptions.RequestException as err:
            logging.error(
                "リクエストエラーが発生しました: %s - レスポンス: %s",
                err,
                err.response.text if err.response else "レスポンスなし",
            )
            sys.exit(1)

    def upload_inventory_csv(self, access_token, csv_file_path):
        """
        インベントリプリロード用のCSVファイルをアップロードします。
        """
        upload_url = f"{self.base_url}/api/v2/inventory-preload/csv"
        upload_headers = {"Authorization": f"Bearer {access_token}"}

        try:
            with open(csv_file_path, "rb") as f:
                files = {"file": f}
                response = requests.post(
                    upload_url, headers=upload_headers, files=files, timeout=10
                )
                response.raise_for_status()
                logging.info("インベントリプリロード用CSVを正常にアップロードしました。")
        except requests.exceptions.HTTPError as http_err:
            logging.error(
                "HTTPエラーが発生しました: %s - レスポンス: %s", http_err, response.text
            )
            sys.exit(1)
        except requests.exceptions.RequestException as err:
            logging.error(
                "リクエストエラーが発生しました: %s - レスポンス: %s",
                err,
                err.response.text if err.response else "レスポンスなし",
            )
            sys.exit(1)


def main():
    """
    スクリプトを実行するメイン関数。
    """
    required_env_vars = ["JAMF_CLIENT_ID", "JAMF_CLIENT_SECRET", "CSV_FILE_PATH", "JAMF_BASE_URL"]
    validate_env_vars(required_env_vars)

    client_id = os.getenv("JAMF_CLIENT_ID")
    client_secret = os.getenv("JAMF_CLIENT_SECRET")
    csv_file_path = os.getenv("CSV_FILE_PATH")
    base_url = os.getenv("JAMF_BASE_URL")

    repository = JamfRepository(base_url=base_url)

    access_token = repository.get_oauth_token(client_id, client_secret)
    repository.delete_all_inventory_records(access_token)
    repository.upload_inventory_csv(access_token, csv_file_path)

    logging.info("Jamfのインベントリプリロードを正常に更新しました。")


if __name__ == "__main__":
    main()
```

このスクリプトを使って、自動化を実現します。

また、GitHub Actionsのワークフロー例は以下の通りです：

```yaml
name: Jamf Inventory Update

on:
  push:
    branches:
      - main
    paths:
      - '**/*.csv'

jobs:
  update-inventory:
    runs-on: ubuntu-latest

    steps:
      - name: リポジトリをチェックアウト
        uses: actions/checkout@v4

      - name: Pythonをセットアップ
        uses: actions/setup-python@v5
        with:
          python-version: '3.9'

      - name: 依存関係をインストール
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Jamf Inventory Updateを実行
        run: python scripts/jamf_inventory_update.py
        env:
          JAMF_CLIENT_ID: ${{ secrets.JAMF_CLIENT_ID }}
          JAMF_CLIENT_SECRET: ${{ secrets.JAMF_CLIENT_SECRET }}
          CSV_FILE_PATH: ${{ github.workspace }}/inventory.csv
          JAMF_BASE_URL: ${{ secrets.JAMF_BASE_URL }}
```

---

## 追加情報

詳細な使い方はJamf Pro公式ドキュメントをご参照ください：

- [Jamf Pro Inventory Preload Documentation](https://learn.jamf.com/ja-JP/bundle/jamf-pro-documentation-current/page/Inventory_Preload.html)

---

## まとめ

Jamf Proのインベントリプリロード機能を活用することで、ゼロタッチデプロイメント前の在庫Macの管理が効率化されます。物理的な場所や状態を一括登録・更新することで、管理の手間を大幅に削減し、情報の一元化が実現できます。これを機に、よりスマートな在庫管理に取り組んでみてはいかがでしょうか？