プログラミングをせずに、コマンドラインツールの組み合わせでRedmineのAPIにアクセスしてみます。
* **curl**: HTTPリクエストを行う
* **jq**: JSONデータを整形・加工する
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
## 前提条件
* コマンドラインから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)
## ユーザー一覧の取得
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
```
※すごく遅いです
## レスポンスは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
.
.
```
* curl と jq を使えば、プログラミング無しでAPIを活用できる。案外簡単!
* Web UIでできないこともAPIを使えば実現できる場合あり
* ちょっとした自動化にも使えそう
**APIでRedmineをちょっと便利に!**