acokikoy's notes

最近気になる=[NoCode, Shopify], I am..=[Python, ウクレレ, マニュアル車, CMS] LoveなWebディレクター

Redmine APIで、プロジェクトにユーザ登録して本人に案内を送るところまでを自動化する

Redmine Rest API経由でアカウント発行とユーザのプロジェクトメンバー登録、本人に通知メールを送るところまでを自動化した話。

動機

自分の勤務先では、案件のプロジェクト進行から営業の引き合い、忘年会の計画に至るまでなんでもかんでもRedmineで管理している。

開発案件ではクライアントや外部スタッフに新しく参加してもらう場合があるが、都度、手動でアカウント発行して、プロジェクトへのメンバー追加をし、本人への案内メールを書くのが地味に面倒だった。

そこで、RedmineAPI経由で登録と本人通知までを自動化することにした。
今までも、メンバー登録 要請が 登録依頼チケットの形で来ることがあり、そうでない場合でも、まずチケットに登録者リストをとりまとめてから登録作業に入っていた。自動化するにあたっても、登録依頼チケットから登録に必要な情報を取得するフローとした。さらに、本人宛に案内を送る部分を、Redmine 自身のチケット更新通知機能に任せることにした。これ専用にメール送信機能を用意しないで済むし、さらに登録依頼チケット自身に作業履歴が残って好都合だ。

システム概要

処理の流れ

1. configファイルを読み込んで定数値をセットする

設定ファイルの中身

[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を渡す方式で実装した。
    1. 管理者自身のID/password を渡す方式
    2. APIキー方式
      1. keyパラメータで渡す
      2. BASIC認証で {APIキー}:{ランダムなパスワード} を渡す
      3. HTTPヘッダーで "X-Redmine-API-Key"として渡す
  • 当初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の通知メールのしくみを利用して案内文を本人に送ることにしたので、準備として新メンバーをチケットのウォッチャーに追加する。
  • 案内文を作る。RedmineBASIC認証情報は毎回入力することにした。プロジェクトごとにBasic認証情報が違うのでconfig.iniに書くのは違うと思ったため。
    • プロジェクトのBasic認証情報の入力
  • 案内文を作って、チケットにコメント投稿する。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認証を通す書き方

会社のRedmineBASIC認証配下にある。

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追加)