Redmine APIで、プロジェクトにユーザ登録して本人に案内を送るところまでを自動化する
Redmine Rest API経由でアカウント発行とユーザのプロジェクトメンバー登録、本人に通知メールを送るところまでを自動化した話。
動機
自分の勤務先では、案件のプロジェクト進行から営業の引き合い、忘年会の計画に至るまでなんでもかんでもRedmineで管理している。
開発案件ではクライアントや外部スタッフに新しく参加してもらう場合があるが、都度、手動でアカウント発行して、プロジェクトへのメンバー追加をし、本人への案内メールを書くのが地味に面倒だった。
そこで、RedmineAPI経由で登録と本人通知までを自動化することにした。
今までも、メンバー登録 要請が 登録依頼チケットの形で来ることがあり、そうでない場合でも、まずチケットに登録者リストをとりまとめてから登録作業に入っていた。自動化するにあたっても、登録依頼チケットから登録に必要な情報を取得するフローとした。さらに、本人宛に案内を送る部分を、Redmine 自身のチケット更新通知機能に任せることにした。これ専用にメール送信機能を用意しないで済むし、さらに登録依頼チケット自身に作業履歴が残って好都合だ。
システム概要
- 動作環境: MacBook Pro, Jupyter notebook
社内での需要がディレクターである自分一人なので、ローカルで稼働すれば十分だった。 - 処理言語: Python
技術要件:
- Rest api - Redmine
- ライブラリ:
- Python-Redmine — Python-Redmine documentation --- Redmine API をサポートするPythonライブラリ。
- configparser --- 設定ファイルのパーサー — Python 3.7.5rc1 ドキュメント - 環境固有値を外部ファイル化するために利用。
ファイル構成:
システム構成図と動作フロー --
処理の流れ
1. configファイルを読み込んで定数値をセットする
- 外部設定ファイルから定数値をセットする。
- ConfigParserを使うと、Windows の INI ファイル風の外部設定ファイルから値を読み書きしてdict型のデータのように扱える。これにより スクリプトに直書きしたくないAPI キーだの環境固有値だの一式を、外部ファイル化しておける。
- configparser --- 設定ファイルのパーサー — Python 3.7.5rc1 ドキュメント
設定ファイルの中身
[Redmine] # general url = RedmineのルートURLを書く api_key = マイページから取得したAPIキーを書く id = (管理者権限者のID) passwd = (管理者権限者のパスワード) # User Initial password user_initial_passwd = 初期パスワードを書く # User Role ID role_admin = 3 role_developer = 4 role_client = 5 # Ticket Status ID to_verify = 3 [BASIC Auth] id = BASIC認証ID passwd = BASIC認証パスワード
処理スクリプト本体の方(Jupyter notebook側)
#!/usr/bin/env python # coding: utf-8 """ Redmine ユーザ登録とプロジェクトへのメンバー追加の自動化スクリプト Redmineの登録依頼チケットに従って、Redmineへのユーザ新規登録とプロジェクトへの メンバー追加を行う。当事者にはRedmineから通知メールが送信される。 """ import configparser import re from redminelib import Redmine #===================================== # configファイルを読み込んで定数値をセット #===================================== # In[104]: config = configparser.ConfigParser() config.read('config.ini') # 定数のセット REDMINE_URL = config['Redmine']['url'] REDMINE_API_KEY = config['Redmine']['api_key'] REDMINE_ID = config['Redmine']['id'] REDMINE_PW = config['Redmine']['passwd'] ROLE_CLIENT = config['Redmine']['role_client'] ROLE_DEVELOPER = config['Redmine']['role_developer'] ROLE_ADMIN = config['Redmine']['role_admin'] KAKUNIN_MACHI = config['Redmine']['to_verify'] # 確認待ちステータス INITIAL_PW = config['Redmine']['user_initial_passwd'] # ユーザ初期パスワード
2. Redmineに接続する。
- Redmine APIに認証を通す方法は4種類ある。今回はID/passwordを渡す方式で実装した。
- 当初APIキーにより接続予定であったが、Python-Redmineで 前段のBASIC認証を通す方法がわからなかった。
# set Redmine managers
redmine = Redmine(REDMINE_URL, username=REDMINE_ID, password=REDMINE_PW)
3. 登録依頼チケットから登録情報を取得する
- 登録者情報は、空のUserクラスを構造体的に使って格納する。
class User: # 空のクラス定義を 構造体的に使う # user.name : 氏名 # user.company : 会社名 # user.email : メールアドレス # user.login : Redmine ログイン名 # user.membership_id : Redmine プロジェクト メンバーID pass
- まず、登録依頼チケットのチケット番号を聞いて、チケットの内容を読み込む。
- チケット本文には、あらかじめ決まった書式で、登録者情報が一覧してある。これを読み込んでに new_members に格納する。
- チケットの所属プロジェクトを調べて、追加対象プロジェクトを特定する。
#================================ # 登録依頼チケットから登録情報取得 #================================ # In[105]: # 登録依頼チケット番号を入力する ticket_id = int(input("登録依頼チケットのチケット番号 #")) # プロジェクト情報を取得 issue = redmine.issue.get(resource_id=ticket_id) project_id = issue.project.id project_name = issue.project.name project_path = redmine.project.get(project_id).identifier # チケット本文から登録者情報を取得する # チケットには次の書式(一人分)で、必要人数分書く # * id: yagi-51250test # ** name: 八木 てすと # ** company: 株式会社xxxxx # ** email: yagi-51250@example.jp # (1行空行) body = issue.description.replace('\r\n', '\n').split('\n\n') pattern = r'\* id: (.+?)\n\*\* name: (.+?)\n\*\* company: (.+?)\n\*\* email: (.+?)$' new_members = [] for u in (b for b in body if b.startswith('* id: ')): m = re.match(pattern, u) user = User() user.login = m.group(1) user.name = m.group(2) user.company = m.group(3) user.email = m.group(4) user.membership_id = None new_members.append(user) # 検証用 # print("登録依頼チケット:") # print(f" #{issue.id} {issue.subject}") # print(f" {issue.url}") # print("-"*20) # print("登録先プロジェクト:") # print(f" {project_name}") # print(f" {REDMINE_URL+'projects/'+project_path+'/issues'}") # print("-"*20) # print(f"新規ユーザ:") # for user in new_members: # print(f" {user.login}{user.name} <{user.email}>, membership_id:{user.membership_id}")
Redmineアカウント発行と、プロジェクトへのメンバー追加
- 対象者が既存アカウントを持っていないかチェックして、なければRedmineの新規アカウントを発行する。
- 続いて、対象者をプロジェクトにメンバー追加する。
#================================================= # Redmineアカウント発行と、プロジェクトへのメンバー追加 #================================================= # In[106]: # 通知メッセージに埋め込むテキストデータ。新メンバーの名前を格納しておく。 str_new_members = "" for user in new_members: # アカウントの有無をメアドとログインIDでチェックし、未登録ならユーザ登録する has_account = False for _ in redmine.user.filter(name=user.login): print('checking for', _.login, user.login) # check by id # 'yagi' でuser.filterすると 'yagiABC''XYZyagi' などもヒットするので一致の検証 if _.login == user.login: has_account = True user.id = _.id break for _ in redmine.user.filter(name=user.email): print('checking for', _.mail, user.email) # check by email if _.mail == user.email: has_account = True break if has_account: print(f"{user.login}({user.id})は、すでにアカウントが存在します") else: print(f"新規登録します: {user.login}") # ユーザ登録 u = redmine.user.create( login = user.login, firstname = user.name, lastname = user.company, mail = user.email, password = INITIAL_PW, must_change_passwd = True, send_information = True) if u.id: user.id = u.id print(f"{user.login}({user.id})を、redmineに新規登録しました") else: print(f"{user.login}を、登録できませんでした(T_T)") continue # プロジェクトに、メンバー追加 # 現プロジェクトメンバー一覧と照合して、既にメンバーだったら処理をパスする print(f"{user.login}を、プロジェクト「{project_name}」メンバーに追加: ", end="") for membership in redmine.project_membership.filter(project_id=project_id): if user.id == membership.user.id: user.membership_id = membership.id print(f"既存メンバーでした") if user.membership_id == None: membership = redmine.project_membership.create( project_id=project_id, user_id=user.id, role_ids=[ ROLE_CLIENT ]) # dir(membership): ['id', 'project', 'roles', 'user'] # dir(membership.user): ['groups', 'id', 'issues', 'memberships', 'name', 'time_entries'] if membership.id: user.membership_id = membership.id str_new_members += f"{user.name} さん\n" print(f"成功(membership id: {membership.id})") else: user.membership_id = None print("失敗") continue issue.watcher.add(user.id) # 本人を登録チケットのウォッチャーに追加 print('-'*40)
登録依頼チケットを更新して本人に案内文を送る
- 今回は、Redmineの通知メールのしくみを利用して案内文を本人に送ることにしたので、準備として新メンバーをチケットのウォッチャーに追加する。
- 案内文を作る。RedmineのBASIC認証情報は毎回入力することにした。プロジェクトごとにBasic認証情報が違うのでconfig.iniに書くのは違うと思ったため。
- 案内文を作って、チケットにコメント投稿する。Redmineが更新内容付きの通知メールを送信する。
#================================================= # 登録依頼チケットを更新して本人に案内文を送る #================================================= # In[107]: print("新メンバーへの案内文を作成します") print('='*40) # redmineのBASIC認証(プロジェクトごとに発行している)を入力 basic_id_passwd = input("プロジェクトのBASIC認証ID / passwd を入力してください") issue = redmine.issue.get(ticket_id) issue.status_id = KAKUNIN_MACHI # チケットのステータスを確認待ちに変更 body = f""" {str_new_members} 株式会社xxxxのxxxです。お世話になっております。 Redmine(レッドマイン)のプロジェクト「{project_name}」に メンバー追加いたしました。 Redmineへのアクセス方法 URL: {REDMINE_URL}projects/{project_path}/issues BASIC認証ID/passwd: {basic_id_passwd} ↓ BASIC認証通過後、redmineへのログインを求められます。 新規ユーザの方には別メールにて redmineのID/passwd をご案内しています。 ご確認ください。 ↓ 弊社 Redmine が初めての方へ Redmineの使い方とローカル運用ルールを掲載しています。ご一読ください。 {REDMINE_URL} よろしくお願いいたします。 -- 株式会社xxxx xxx <xxxx@example.com> """ issue.notes = body issue.save() print(issue.notes) print('='*40)
うまくいかなかった所
BASIC認証を通す書き方
BASIC認証を通しつつ、redmineの認証はAPIキー方式で通したい時に、curlコマンドなら -uオプションを使って、
$ curl -u BASIC_ID:BASIC_PW -H 'X-Redmine-API-Key:REDMINE_API_KEY' https://redmine.example.com/issues.json?limit=1 | jq [.]
のように、BASIC認証のID/passwdを一緒に渡せばいい所を、Python-Redmineではどう書けばよいのかがわからなかった。
redmine API の 本人に接続情報をメールする機能が効かない
- 新規ユーザ登録時に、パスワードを自動発行し、発行されたID/passwdを本人宛にメール通知する指定にしたが、その指定がAPI経由では機能しなかった。
- パスワード自動発行(generate_password = True)
- 本人に接続情報をメールする(send_information = True)
- 手動登録時はこの機能は正常動作し、この運用でずっとやっていたので同じにしたかったのだが、動かないため やむなく初回ログイン時にパスワード強制変更するモードで代替し、ID/初期パスワードは 登録案内文でお知らせする方法に変えた。
- curlコマンドでAPIに直接アクセスしても同じ結果だったので Python-Redmineライブラリではなく Redmine APIの動作に不具合があるのかもしれない。あるいは弊社環境のRedmine固有の問題か。。。原因特定はできてない。
まとめ
- Redmineのプロジェクトにメンバー登録依頼があった時に、登録依頼チケットを起点として、Redmineのアカウント発行とプロジェクトメンバー追加をして本人に通知を送るまでを自動化するスクリプトを書いた。
- 利用者は自分だけなので、ローカルの Jupyter notebook上で動かす想定で作った。
- Redmine Rest API のハンドリングには Python-Redmineライブラリ を利用した。
- ConfigParser環境固有値を外部設定ファイル化して読み込む方法について学んだ。
参考リンク (2019.10.19追加)
- Redmine Rest API -- 公式ドキュメント
- Python-Redmineライブラリ -- 公式ドキュメント
- Redmineワンポイントチェック(9): REST APIを使ってユーザーを一括登録する | Redmine.JP Blog