Redmineをちょっと便利に!
プログラミング無しで使ってみるREST API


前田剛
(ファーエンドテクノロジー株式会社)

2018/05/26 redmine.tokyo

スライド公開URL:
https://vividtone.github.io/redmine-tokyo-slide-20180526/

プログラミングをせずに、コマンドラインツールの組み合わせでRedmineのAPIにアクセスしてみます。 * **curl**: HTTPリクエストを行う * **jq**: JSONデータを整形・加工する

RedmineのREST API

REST APIとは

Redmineが持つデータに、外部から
(Web UIを経由せず)アクセスできる仕組み。

アクセス方法

HTTPでリクエストすると、XMLまたはJSON形式のレスポンスが帰ってくる

## REST APIの何がうれしい? * 他システムからRedmineを操作できる - 自動化、チャットボットなど * Web UIの操作ではできないことが実現できる
## APIを利用したツール例 1. Redmineチケット★一括★ 2. Redmine Notifier
**①Redmineチケット★一括★** Excelファイルを読み込んでチケットを登録 ![](images/ticket-ikkatsu-exec.png) https://www.vector.co.jp/soft/winnt/util/se503347.html
**②Redmine Notifier** チケットの更新をデスクトップに通知 ![](images/redmine-notifier.png) https://github.com/emsk/redmine-notifier

REST APIの利用準備

## 前提条件 * コマンドラインからAPIを利用してみます * 扱いやすいJSON形式を利用します * APIへのアクセスを行う端末のOSは **Ubuntu** または **macOS** を想定しています * **Windows**の人は **Windows Subsystem for Linux** をインストールするなどしてください
## REST APIを有効にする 「管理」→「設定」→「API」タブの「**RESTによるWebサービスを有効にする**」をON ![](images/enable-rest-api.png)
## jq をPCにインストールする jq はJSON形式のテキストを整形・加工するツール。APIで取得したデータの処理に使う。 ### ubuntu: ``` bash sudo apt-get install jq ``` ### macOS: ``` bash brew install jq ```
## APIにアクセスできるかテスト ``` bash curl --user ログインID:パスワード 'http://redmine.test/issues.json?limit=1' | jq . ```
こんな感じの画面が出ればOK ![](images/test-api.png)

APIで遊んでみる

## ユーザー一覧の取得 Redmineのユーザー一覧をJSON形式で取得する ``` bash curl --user admin:パスワード 'http://redmine.test/users.json?limit=100' ```
``` json {"users":[{"id":1,"login":"admin","firstname":"Redmine","lastname":"Admin","mail":"admin@somenet.foo","created_on":"2006-07-19T17:12:21Z","last_login_on":"2018-05-25T20:24:22Z","custom_fields":[{"id":4,"name":"Phone number","value":""},{"id":5,"name":"Money","value":""}]},{"id":3,"login":"dlopper","firstname":"Dave","lastname":"Lopper","mail":"dlopper@somenet.foo","created_on":"2006-07-19T17:33:19Z","custom_fields":[{"id":4,"name":"Phone number","value":""},{"id":5,"name":"Money","value":""}]},{"id":22,"login":"example","firstname":"Some","lastname":"One","mail":"someone@example.jp","created_on":"2018-05-24T13:40:12Z","custom_fields":[{"id":4,"name":"Phone number","value":""},{"id":5,"name":"Money","value":""}]},{"id":2,"login":"jsmith","firstname":"John","lastname":"Smith","mail":"jsmith@somenet.foo","created_on":"2006-07-19T17:32:09Z","last_login_on":"2006-07-19T20:42:15Z","custom_fields":[{"id":4,"name":"Phone number","value":"01 42 50 00 00"},{"id":5,"name":"Money","value":""}]},{"id":8,"login":"miscuser8","firstname":"User","lastname":"Misc","mail":"miscuser8@foo.bar","created_on":"2006-07-19T17:33:19Z","custom_fields":[{"id":4,"name":"Phone number","value":""},{"id":5,"name":"Money","value":""}]},{"id":9,"login":"miscuser9","firstname":"User","lastname":"Misc","mail":"miscuser9@foo.bar","created_on":"2006-07-19T17:33:19Z","custom_fields":[{"id":4,"name":"Phone number","value":""},{"id":5,"name":"Money","value":""}]},{"id":4,"login":"rhill","firstname":"Robert","lastname":"Hill","mail":"rhill@somenet.foo","created_on":"2006-07-19T17:34:07Z","custom_fields":[{"id":4,"name":"Phone number","value":"01 23 45 67 89"},{"id":5,"name":"Money","value":""}]},{"id":7,"login":"someone","firstname":"Some","lastname":"One","mail":"someone@foo.bar","created_on":"2006-07-19T17:33:19Z","custom_fields":[{"id":4,"name":"Phone number","value":""},{"id":5,"name":"Money","value":""}]},{"id":15,"login":"user0001","firstname":"Redmine","lastname":"Admin","mail":"user0001@example.com","created_on":"2018-05-24T13:30:18Z","custom_fields":[{"id":4,"name":"Phone number","value":""},{"id":5,"name":"Money","value":""}]},{"id":16,"login":"user0002","firstname":"Dave","lastname":"Lopper","mail":"user0002@example.com","created_on":"2018-05-24T13:30:18Z","custom_fields":[{"id":4,"name":"Phone number","value":""},{"id":5,"name":"Money","value":""}]},{"id":17,"login":"user0003","firstname":"John","lastname":"Smith","mail":"user0003@example.com","created_on":"2018-05-24T13:30:18Z","custom_fields":[{"id":4,"name":"Phone number","value":""},{"id":5,"name":"Money","value":""}]},{"id":18,"login":"user0004","firstname":"User","lastname":"Misc","mail":"user0004@example.com","created_on":"2018-05-24T13:30:18Z","custom_fields":[{"id":4,"name":"Phone number","value":""},{"id":5,"name":"Money","value":""}]},{"id":19,"login":"user0005","firstname":"User","lastname":"Misc","mail":"user0005@example.com","created_on":"2018-05-24T13:30:18Z","custom_fields":[{"id":4,"name":"Phone number","value":""},{"id":5,"name":"Money","value":""}]},{"id":20,"login":"user0006","firstname":"Robert","lastname":"Hill","mail":"user0006@example.com","created_on":"2018-05-24T13:30:18Z","custom_fields":[{"id":4,"name":"Phone number","value":""},{"id":5,"name":"Money","value":""}]},{"id":21,"login":"user0007","firstname":"Some","lastname":"One","mail":"user0007@example.com","created_on":"2018-05-24T13:30:18Z","custom_fields":[{"id":4,"name":"Phone number","value":""},{"id":5,"name":"Money","value":""}]}],"total_count":15,"offset":0,"limit":100} ```
## jq で見やすく整形 ``` bash curl --user admin:パスワード 'http://redmine.test/users.json?limit=100' | jq . ```
``` json { "users": [ { "id": 1, "login": "admin", "firstname": "Redmine", "lastname": "Admin", "mail": "admin@somenet.foo", "created_on": "2006-07-19T17:12:21Z", "last_login_on": "2018-05-25T20:33:44Z", "custom_fields": [ { "id": 4, "name": "Phone number", "value": "" }, { "id": 5, "name": "Money", "value": "" } ] }, { "id": 3, "login": "dlopper", "firstname": "Dave", "lastname": "Lopper", "mail": "dlopper@somenet.foo", "created_on": "2006-07-19T17:33:19Z", "custom_fields": [ { "id": 4, "name": "Phone number", "value": "" }, { "id": 5, "name": "Money", "value": "" } ] }, { "id": 22, "login": "example", "firstname": "Some", "lastname": "One", "mail": "someone@example.jp", "created_on": "2018-05-24T13:40:12Z", "custom_fields": [ { "id": 4, "name": "Phone number", "value": "" }, { "id": 5, "name": "Money", "value": "" } ] }, { "id": 2, "login": "jsmith", "firstname": "John", "lastname": "Smith", "mail": "jsmith@somenet.foo", "created_on": "2006-07-19T17:32:09Z", "last_login_on": "2006-07-19T20:42:15Z", "custom_fields": [ { "id": 4, "name": "Phone number", "value": "01 42 50 00 00" }, { "id": 5, "name": "Money", "value": "" } ] }, { "id": 8, "login": "miscuser8", "firstname": "User", "lastname": "Misc", "mail": "miscuser8@foo.bar", "created_on": "2006-07-19T17:33:19Z", "custom_fields": [ { "id": 4, "name": "Phone number", "value": "" }, { "id": 5, "name": "Money", "value": "" } ] }, { "id": 9, "login": "miscuser9", "firstname": "User", "lastname": "Misc", "mail": "miscuser9@foo.bar", "created_on": "2006-07-19T17:33:19Z", "custom_fields": [ { "id": 4, "name": "Phone number", "value": "" }, { "id": 5, "name": "Money", "value": "" } ] }, { "id": 4, "login": "rhill", "firstname": "Robert", "lastname": "Hill", "mail": "rhill@somenet.foo", "created_on": "2006-07-19T17:34:07Z", "custom_fields": [ { "id": 4, "name": "Phone number", "value": "01 23 45 67 89" }, { "id": 5, "name": "Money", "value": "" } ] }, { "id": 7, "login": "someone", "firstname": "Some", "lastname": "One", "mail": "someone@foo.bar", "created_on": "2006-07-19T17:33:19Z", "custom_fields": [ { "id": 4, "name": "Phone number", "value": "" }, { "id": 5, "name": "Money", "value": "" } ] }, { "id": 15, "login": "user0001", "firstname": "Redmine", "lastname": "Admin", "mail": "user0001@example.com", "created_on": "2018-05-24T13:30:18Z", "custom_fields": [ { "id": 4, "name": "Phone number", "value": "" }, { "id": 5, "name": "Money", "value": "" } ] }, { "id": 16, "login": "user0002", "firstname": "Dave", "lastname": "Lopper", "mail": "user0002@example.com", "created_on": "2018-05-24T13:30:18Z", "custom_fields": [ { "id": 4, "name": "Phone number", "value": "" }, { "id": 5, "name": "Money", "value": "" } ] }, { "id": 17, "login": "user0003", "firstname": "John", "lastname": "Smith", "mail": "user0003@example.com", "created_on": "2018-05-24T13:30:18Z", "custom_fields": [ { "id": 4, "name": "Phone number", "value": "" }, { "id": 5, "name": "Money", "value": "" } ] }, { "id": 18, "login": "user0004", "firstname": "User", "lastname": "Misc", "mail": "user0004@example.com", "created_on": "2018-05-24T13:30:18Z", "custom_fields": [ { "id": 4, "name": "Phone number", "value": "" }, { "id": 5, "name": "Money", "value": "" } ] }, { "id": 19, "login": "user0005", "firstname": "User", "lastname": "Misc", "mail": "user0005@example.com", "created_on": "2018-05-24T13:30:18Z", "custom_fields": [ { "id": 4, "name": "Phone number", "value": "" }, { "id": 5, "name": "Money", "value": "" } ] }, { "id": 20, "login": "user0006", "firstname": "Robert", "lastname": "Hill", "mail": "user0006@example.com", "created_on": "2018-05-24T13:30:18Z", "custom_fields": [ { "id": 4, "name": "Phone number", "value": "" }, { "id": 5, "name": "Money", "value": "" } ] }, { "id": 21, "login": "user0007", "firstname": "Some", "lastname": "One", "mail": "user0007@example.com", "created_on": "2018-05-24T13:30:18Z", "custom_fields": [ { "id": 4, "name": "Phone number", "value": "" }, { "id": 5, "name": "Money", "value": "" } ] } ], "total_count": 15, "offset": 0, "limit": 100 } ```
## ユーザー一覧のJSONをCSVに変換 ``` bash curl --user admin:パスワード http://redmine.test/users.json | jq -r '.users[] | [.login, .mail, .firstname, .lastname] | @csv' ``` 1. users[] の値を取り出して、 2. 各ユーザーのデータから必要な値を取り出して配列に変換して、 3. CSVに変換
``` nohighlight "admin","admin@somenet.foo","Redmine","Admin" "dlopper","dlopper@somenet.foo","Dave","Lopper" "jsmith","jsmith@somenet.foo","John","Smith" "miscuser8","miscuser8@foo.bar","User","Misc" "miscuser9","miscuser9@foo.bar","User","Misc" "rhill","rhill@somenet.foo","Robert","Hill" "someone","someone@foo.bar","Some","One" ``` フォーマット: ログインID,メールアドレス,名,姓
## ユーザーを登録する ファイル newuser.json を用意して ``` nohighlight { "user": { "login": "maeda", "mail": "maeda@example.com", "firstname": "剛", "lastname": "前田", "password": "I3JlZG1pbmV0" } } ``` 実行 ``` bash cat newuser.json | curl 'http://redmine.test/users.json' --user admin:パスワード --header 'Content-type: application/json' --data @- ```
## ユーザーをCSVから一括登録する ファイル user.csv を用意して ``` nohighlight user0001,foo@example.com,Joe,Bloggs,HiaH4JJd user0002,bar@example.com,Jane,Public,9iQYyLn5 user0003,baz@example.com,Chris,Wong,dG9EFggG ``` 実行 ``` bash cat /tmp/users.csv | while read LINE do echo $LINE | jq -R 'gsub("\"";"") | split(",") | {"user": {"login": .[0], "mail": .[1], "firstname": .[2], "lastname": .[3], "password": .[4]}}' | curl -v 'http://redmine.test/users.json' --user admin:パスワード --header 'Content-type: application/json' --data @- done ```
1. 1行ずつ読み取りコマンド実行: **`cat ... while ...`** 2. ダブルクォーテーションを削除: **`gsub("\"";"")`** 3. コンマで分割: **`split(",")`** 4. JSONに変換: **`{"user": {"login": .[0], ...}}`** 5. 標準入力からJSONを受け取りAPIを呼び出してユーザー登録: **`curl ...`** ``` bash cat /tmp/users.csv | while read LINE do echo $LINE | jq -R 'gsub("\"";"") | split(",") | {"user": {"login": .[0], "mail": .[1], "firstname": .[2], "lastname": .[3], "password": .[4]}}' | curl 'http://redmine.test/users.json' --user admin:パスワード --header 'Content-type: application/json' --data @- done ```
## チケット操作 ``` bash # チケット一覧取得 curl http://ホスト名/issues.json --user ログインID:パスワード # 個別のチケット取得 curl http://ホスト名/issues/1.json --user ログインID:パスワード # チケット作成 curl http://ホスト名/issues.json --user ログインID:パスワード --header 'Content-type: application/json' --data '{"issue": {"project_id": 1, "tracker_id": 1, "subject": "件名", "description": "説明"}}' # チケット更新 curl http://ホスト名/issues/5.json --user ログインID:パスワード --request 'PUT' --header 'Content-type: application/json' --data '{"issue": {"subject": "変更後件名", "description": "変更後説明"}}' # チケット削除 curl http://ホスト名/issues/16.json --user ログインID:パスワード --request 'DELETE' ```
## 指定したチケットを取得する ``` bash curl --user ログインID:パスワード 'http://redmine.test/issues/1.json' | jq . ```
``` json { "issue": { "id": 1, "project": { "id": 1, "name": "eCookbook" }, "tracker": { "id": 1, "name": "Bug" }, "status": { "id": 1, "name": "New" }, "priority": { "id": 4, "name": "Low" }, "author": { "id": 2, "name": "John Smith" }, "category": { "id": 1, "name": "Printing" }, "subject": "Cannot print recipes", "description": "Unable to print recipes", "start_date": "2018-05-22", "due_date": "2018-06-02", "done_ratio": 0, "spent_hours": 154.25, "total_spent_hours": 154.25, "custom_fields": [ { "id": 2, "name": "Searchable field", "value": "125" }, { "id": 1, "name": "Database", "value": "" }, { "id": 6, "name": "Float field", "value": "2.1" }, { "id": 8, "name": "Custom date", "value": "2009-12-01" }, { "id": 9, "name": "Project 1 cf", "value": "" } ], "created_on": "2018-05-20T05:54:42Z", "updated_on": "2018-05-22T05:54:42Z" } } ```
## チケットを1行のテキストに変換 コマンド ``` bash curl --user ログインID:パスワード 'http://redmine.test/issues/1.json?limit=100' | jq '.issue | "\(.tracker.name) #\(.id) - \(.subject) (\(.status.name)) http://redmine.test/issues/\(.id)" | @text' ``` 結果 ``` nohighlight Bug #1 - Cannot print recipes (New) http://redmine.test/issues/1 ```
### 開始日が今日以前のチケットを抽出 コマンド ``` bash curl --user ログインID:パスワード "http://redmine.test/issues.json?limit=100&start_date=<=`date +%Y-%m-%d`&sort=start_date" | jq -r '.issues[] | "\(.tracker["name"]) #\(.id) - \(.subject) (\(.start_date))" | @text' ``` 結果 ``` nohighlight Bug #3 - Error 281 when updating a recipe (2018-05-08) Bug #7 - Issue due today (2018-05-13) Feature request #2 - Add ingredients categories (2018-05-21) Bug #1 - Cannot print recipes (2018-05-22) Bug #6 - Issue of a private subproject (2018-05-23) Bug #9 - Blocked Issue (2018-05-23) Bug #10 - Issue Doing the Blocking (2018-05-23) ```
## 未完了で、自分が担当で、更新日が7日以上前のチケットから1件をランダムに表示 コマンド ``` bash curl --user ログインID:パスワード "http://redmine.test/issues.json?limit=100&assigned_to_id=me&updated_on=<=`date -v-7d +%Y-%m-%d`" | jq -r '.issues[] | "\(.tracker.name) #\(.id) - \(.subject) (\(.updated_on)) http://redmine.test/issues/\(.id)" | @text' | python -c 'import sys, random; print(random.choice(sys.stdin.readlines()));' ``` 結果 ``` nohighlight Bug #3 - Error 281 when updating a recipe (2006-07-19T19:07:27Z) http://redmine.test/issues/3 ```
## 題名が正規表現にマッチするチケットを探す 1万件の未完了チケットの中から、題名が `/(交通|宿泊)/i` にマッチするチケットを探す ``` bash for OFFSET in `seq 0 100 10000`; do curl --user ログインID:ユーザー名 "https://ホスト名/issues.json?limit=100&offset=$OFFSET" | jq -r '.issues[] | select(.subject | test("交通|宿泊"; "i")) | "\(.tracker["name"]) #\(.id) - \(.subject)"'; done ``` ※すごく遅いです
# Redmine API 利用TIPS
## レスポンスはXMLかJSON * URLの拡張子でXMLかJSONか決まる * コマンドラインで使うなら **JSON** がおすすめ。 **jq** が強力で、また 登録用データを作るのもXMLより楽 ``` bash # XML curl --user ログインID:パスワード 'http://redmine.test/issues/1.xml' # JSON curl --user ログインID:パスワード 'http://redmine.test/issues/1.json' ```
## 1回のリクエストで取得できるのは最大100件 * デフォルトは25件。URLのパラメータ `limit` を指定して増やせるが、上限は100 * それ以上のデータを取得するには `offset` の値を変えながら複数のリクエストを行う ``` # 0..99 curl --user admin:パスワード 'http://redmine.test/users.json?limit=100' # 100..199 curl --user admin:パスワード 'http://redmine.test/users.json?limit=100&offset=100' # 200..299 curl --user admin:パスワード 'http://redmine.test/users.json?limit=100&offset=200' ```
## 認証はbasic認証またはAPIキーで行う ### basic認証 ``` bash curl --user ログインID:パスワード 'http://redmine.test/issues.json' ``` ### APIキー ``` bash curl --header 'X-Redmine-API-Key: APIキー' 'http://redmine.test/issues.json' ``` APIキーは「個人設定」画面のサイドバー内「APIアクセスキー」で確認する
offsetを 0 から 900 まで100ずつ増やしながら10回のリクエストを行う例 ``` for OFFSET in `seq 0 100 900`; do curl --user admin:パスワード "http://redmine.test/users.json?limit=100&offset=$OFFSET" | jq -r '.users[] | [.login, .mail, .firstname, .lastname] | @csv'; done > support-users.csv ```
## 一部のオブジェクトはシステム管理者権限が必要 ユーザー、グループ、カスタムフィールドの一覧など
## デバッグには curl の `-v` オプションが便利 ヘッダが見えるのでRedmineサーバとの通信で何が起こっているか把握しやすい ``` shell # システム管理者でないユーザーがユーザーの一覧にアクセス $ curl -v --user ログインID:パスワード "http://redmine.test/users.json" . (中略) . < HTTP/1.1 403 Forbidden < X-Frame-Options: SAMEORIGIN < X-XSS-Protection: 1; mode=block < X-Content-Type-Options: nosniff < Content-Type: application/json . . ```

情報源

Redmine API
http://www.redmine.org/projects/redmine/wiki/Rest_api
ドキュメント化されていない機能が一部ある
jq
https://stedolan.github.io/jq/manual/
# まとめ
* curl と jq を使えば、プログラミング無しでAPIを活用できる。案外簡単! * Web UIでできないこともAPIを使えば実現できる場合あり * ちょっとした自動化にも使えそう **APIでRedmineをちょっと便利に!**

ありがとうございました


前田剛 (@g_maeda)
ファーエンドテクノロジー株式会社
代表取締役

  • Redmine.JPというサイトを運営してます
  • 入門Redmine」という本を書きました
  • Redmineのコミッターとして開発を手伝っています