summaryrefslogtreecommitdiffstats
path: root/files/redmine/reminder/redmine-remind
blob: 0f1e1e56ab4651a0cbca7cf116aca911722df1f4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/usr/bin/python3
import argparse
import logging
import sys
from email.mime.text import MIMEText
from socket import getfqdn
from urllib.parse import urljoin, urlencode
import requests
import smtplib
import yaml

log = logging.getLogger("redmine-remind")

DEFAULT_REDMINE_URL = "https://redmine.tails.boum.org/code/"
DEFAULT_QUERY_IDS = "318,319"
DEFAULT_EMAIL_FROM = "root@{fqdn}".format(fqdn=getfqdn())


class Fail(Exception):
    pass


class Redmine:
    def __init__(self, server, api_key):
        self.server = server
        self.api_key = api_key

    def query(self, api, **kw):
        url = urljoin(self.server, api)
        if kw:
            url += "?" + urlencode(kw)
        log.info("Querying %s", url)
        res = requests.get(url, headers={
            "X-Redmine-API-Key": self.api_key
        })
        res.raise_for_status()
        return res.json()

    def whoami(self):
        return self.query("users/current.json")

    def get_user(self, user_id):
        return self.query("users/{user_id}.json".format(user_id=user_id))


def reminder_message(body, sender, recipient):
    msg = MIMEText(body)
    msg['Subject'] = '[Tails] Please triage stalled tickets assigned to you'
    msg['From'] = sender
    msg['To'] = recipient
    msg['Bcc'] = sender
    return msg


def main():
    parser = argparse.ArgumentParser(description="Fetch reminder information from open Redmine issues")
    parser.add_argument("--verbose", "-v", action="store_true", help="verbose output")
    parser.add_argument("--debug", action="store_true", help="debug output")
    parser.add_argument("--api-key", action="store", required=True,
                        help="Redmine API key; use @file to read it from a file (required)")
    # We don't retrieve this information automatically from Redmine because:
    # 1. This information is not always public (every Redmine user can decide
    #    whether their email address is exposed or not) so to pull this info
    #    from Redmine, we would need to run the machinery with full
    #    administrative access to Redmine (or close to it), which feels
    #    a bit scary.
    # 2. We'll migrate away from Redmine soon, so let's invest as little
    #    as we possibly can into Redmine-specific code.
    # 3. Until then, maintaining this hard-coded info in Hiera should be
    #    very cheap.
    parser.add_argument("--users-file", action="store",
                        help="YAML file that contains a mapping from Redmine login name to email address")
    parser.add_argument("--server", action="store", default=DEFAULT_REDMINE_URL,
                        help="Redmine URL. Default: " + DEFAULT_REDMINE_URL)
    parser.add_argument("--query-ids", action="store", default=DEFAULT_QUERY_IDS,
                        help="IDs of queries to run. Default: " + DEFAULT_QUERY_IDS)
    parser.add_argument("--print0", action="store_true",
                        help="Print output names with the null character instead of newline")
    parser.add_argument("--print-email", action="store_true",
                        help="Print the generated reminder email")
    parser.add_argument("--send-email", action="store_true",
                        help="Send a reminder email")
    parser.add_argument("--email-body-file", action="store",
                        help="File that contains the email reminder body")
    parser.add_argument("--email-from", action="store", default=DEFAULT_EMAIL_FROM,
                        help="Sender email address. Default: " + DEFAULT_EMAIL_FROM)

    args = parser.parse_args()

    log_format = "%(levelname)s %(message)s"
    level = logging.WARN
    if args.debug:
        level = logging.DEBUG
    elif args.verbose:
        level = logging.INFO
    logging.basicConfig(level=level, stream=sys.stderr, format=log_format)

    # Check command line consistency
    if (args.print_email or args.send_email):
        if not args.users_file:
            sys.exit("You need to pass --users-file=FILE in order to generate email")
        if not args.email_body_file:
            sys.exit("You need to pass --email-body-file=FILE in order to generate email")

    # Read API key
    api_key = args.api_key
    if api_key.startswith("@"):
        with open(api_key[1:], "rt") as fd:
            api_key = fd.read().strip()

    query_ids = args.query_ids.split(",")

    # Fetch issues
    redmine = Redmine(args.server, api_key)
    names = set()
    for query_id in query_ids:
        res = redmine.query("issues.json", query_id=query_id, project_id="tails", limit=100)
        for issue in res["issues"]:
            assigned_to = issue.get("assigned_to")
            if assigned_to is None:
                continue
            names.add(assigned_to["name"])

    if args.print0:
        for name in sorted(names):
            print(name, end='\0')
    elif args.print_email or args.send_email:
        # Read users file
        with open(args.users_file, "rt") as fd:
            users_email = yaml.safe_load(fd.read())

        # Read email body file
        with open(args.email_body_file, "rt") as fd:
            email_body = fd.read()

        recipients = set()
        for name in names:
            try:
                recipients.add(users_email[name])
            except KeyError:
                log.error(
                    "No email address for {name}: add them to tails::redmine::reminder::users in Hiera"
                    .format(name=name))
        log.debug("Recipients: {recipients}".format(recipients=recipients))

        for recipient in recipients:
            msg = reminder_message(email_body, args.email_from, recipient)

            if args.print_email:
                print(msg)
            if args.send_email:
                s = smtplib.SMTP('localhost')
                s.send_message(msg)
                s.quit()
    else:
        for name in sorted(names):
            print(name)


if __name__ == "__main__":
    try:
        main()
    except Fail as e:
        print(e, file=sys.stderr)
        sys.exit(1)
    except Exception:
        log.exception("uncaught exception")