-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathrt_runticket_manager.py
executable file
·345 lines (278 loc) · 14 KB
/
rt_runticket_manager.py
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
#!/usr/bin/env python3
import os, sys, re
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from contextlib import suppress
import configparser
from rt import Rt, AuthorizationError
import logging as L
# This was copied and modified from illuminatus/rt_runticket_manager.py to smrtino
# and then copied back to bring the two into line.
# I'm pretty sure that:
# 1) The RTUtils.py library is no use outside this script (so I just merged them).
# 2) This whole script is now generic enough to be shared (and thus moved to the toolbox). But
# I've not (quite) done that yet.
def resolve_msg(in_val):
"""Replies and comments can be a literal string or "@./file" or "@-"
This function resolves them.
"""
#Deal with None/""
if not in_val:
return None
if in_val == "@-":
return sys.stdin.read()
elif in_val.startswith("@"):
with open(in_val[1:]) as in_file:
return in_file.read()
else:
return in_val
def main(args):
L.basicConfig(level=L.INFO, stream=sys.stderr)
run_id = args.run_id # eg. "r54041_20180518_115257" or "180807_A00291_0056_AHCFLYDMXX"
subject_postfix = args.subject
ticket_status = args.status
# Load the messages
reply_message = resolve_msg(args.reply)
comment_message = resolve_msg(args.comment)
# If there is no action, we just want to check the ticket (never create it)
check_only = not any([reply_message, comment_message, ticket_status, subject_postfix])
# Determine subject for new ticket or else change of subject.
if subject_postfix:
subject = f"{args.prefix} {run_id} : {subject_postfix}"
else:
subject = f"{args.prefix} {run_id}"
rt_config_name = 'test-rt' if args.test else os.environ.get('RT_SYSTEM', 'production-rt')
with RTManager( config_name = rt_config_name,
queue_setting = args.queue ) as rtm:
if check_only:
ticket_id, ticket_dict = rtm.search_run_ticket(run_id)
if ticket_id:
exit(f"Ticket #{ticket_id} for '{run_id}' has subject: {ticket_dict.get('Subject')}")
else:
exit(f"No open ticket found for '{run_id}'")
# Or we can explicitly ask never to open a new ticket
if args.no_create:
ticket_id, ticket_dict = rtm.search_run_ticket(run_id)
if ticket_id:
created = False
else:
exit(f"No open ticket found for '{run_id}'")
else:
# if the ticket does not exist, create it with the supplied message, be
# that a commet or a reply, else if it does exits just get the ID
ticket_id, created = rtm.find_or_create_run_ticket( run_id , subject, (reply_message or comment_message) )
print(f"{'New' if created else 'Existing'} ticket_id is {ticket_id}")
# change Subject of ticket
# Note that it is valid to pass --subject "" to clear the postfix
if not ( created or subject_postfix is None ):
rtm.change_ticket_subject( ticket_id , subject )
# reply to the ticket.
# Not if the ticket was created, as the message will already be in the blurb.
if reply_message and not created:
rtm.reply_to_ticket( ticket_id , reply_message )
# comment on the ticket
# if the ticket was just created with only this comment then there is no
# need to add it again
if comment_message and not (created and not reply_message):
rtm.comment_on_ticket( ticket_id , comment_message )
# change status of a ticket
if ticket_status:
rtm.change_ticket_status( ticket_id , ticket_status )
# This part was modified from illuminatus/RTUtils.py.
class RTManager():
def __init__(self, config_name, queue_setting):
"""Communication with RT is managed via the RT module.
This wrapper picks up connection params from an .ini file,
which must exist before you can even instatiate the object.
To actually connect, either call connect() explicitly or say:
with RTManager('test-rt') as rt_conn:
...
to connect implicitly.
"""
self._config_name = config_name
self._queue_setting = queue_setting # eg. pbrun, run
if config_name.lower() == 'none':
# Special case for short-circuiting RT entirely, whatever the .ini
# file says.
self._config = None
else:
self._config = self._get_config_from_ini(config_name)
self.tracker = None
def connect(self, timeout=60):
if not self._config:
L.warning("Making dummy connection - all operations will be no-ops.")
return self
self.server_path = self._config['server']
self.username, self.password = self._config['user'], self._config['pass']
self._queue = self._config[self._queue_setting + '_queue']
self.tracker = Rt( '/'.join([self.server_path, 'REST', '1.0']),
self.username,
self.password,
default_queue = self._queue )
L.info("Starting to connect")
if not self.tracker.login():
raise AuthorizationError(f'login() failed on {self._config_name} ({self.tracker.url})')
L.info("connected")
# Here comes the big monkey-patch-o-doom!
# It will force a 60-second timeout on the Rt session, assuming the internal implementation
# of session is not changed in the requests library.
from types import MethodType
foo = self.tracker.session
foo._merge_environment_settings = foo.merge_environment_settings
foo.merge_environment_settings = MethodType(
lambda s, *a: dict([*s._merge_environment_settings(*a).items(), ('timeout', s.timeout)]),
foo )
foo.timeout = timeout
# End of monkey business
return self
# Allow us to use this in a 'with' clause.
def __enter__(self):
return self.connect()
def __exit__(self, *exc):
# Can you logout of RT? Do you want to?
pass
def _get_config_from_ini(self, section_name):
# Either read the config pointed to by RT_SETTINGS or else the default.
# Don't attempt to read both, even though ConfigParser supports it.
file_name = os.environ.get('RT_SETTINGS')
file_name = file_name or os.path.join(os.path.expanduser('~'), '.rt_settings')
cp = configparser.ConfigParser()
if not cp.read(file_name):
raise AuthorizationError(f'unable to read configuration file {file_name}')
# A little validation
if section_name not in cp:
raise AuthorizationError(f'file {file_name} contains no configuration section {section_name}')
conf_section = cp[section_name]
# A little more validation
for x in ['server', 'user', 'pass', self._queue_setting + '_queue']:
if not conf_section.get(x):
raise AuthorizationError(f'file {file_name} did not contain setting {x} needed for RT authentication')
return conf_section
# Added for illuminatus, adapted for SMRTino
def find_or_create_run_ticket(self, run_id, subject, text=None):
"""Create a ticket for run if it does not exist already.
If text is specified it will be used as the request blurb for
the new ticket but if the ticket already existed it will be
ignored.
Returns a pair (ticket_id, created?).
"""
c = self._config
ticket_id, _ = self.search_run_ticket(run_id)
if ticket_id:
return ticket_id, False
# Since dummy mode returns 999, if ticket_id was unset we can infer we have a real
# connection and proceed with real ops.
# Text munge
text = re.sub(r'\n', r'\n ', text.rstrip()) if text \
else ""
ticket_id = int(self.tracker.create_ticket(
Subject = subject,
Queue = self._queue,
Requestor = c['requestor'],
Cc = c.get(self._queue_setting + '_cc') or "",
Text = text or "" ))
# Open the ticket, or we'll not find it again.
self.tracker.edit_ticket(ticket_id, Status='open')
return ticket_id, True
def search_run_ticket(self, run_id):
"""Search for a ticket referencing this run, and return the ticket number,
as an integer, along with the ticket metadata as a dict,
or return (None, None) if there is no such ticket.
"""
c = self._config
if not c:
#In dummy mode, all tickets are 999
return (999, dict())
# Note - if the tickets aren't opened then 'new' tickets will just pile up in RT,
# but I don't think that should happen.
tickets = list(self.tracker.search( Queue = self._queue,
Subject__like = f'%{run_id}%',
Status = 'open'
))
# The above logic could break if one run has a name which is a substring of another
# run name. This is actually possible for Promethion, so we now do a further check
# to ensure we really really got the right ticket.
for t in tickets:
if not re.search(r'\b{}\b'.format(run_id), t.get('Subject', '')):
L.warning(f"Disregarding ticket with subject {t.get('Subject')}")
t['id'] = None
tickets = [ t for t in tickets if t.get('id') ]
if not tickets:
return (None, None)
# Order the tickets by tid and get the highest one
def get_id(t): return int(t['id'].strip('ticket/'))
tickets.sort(key=get_id, reverse=True)
tid = get_id(tickets[0])
if len(tickets) > 1:
L.warning(f"Warning: We have {len(tickets)} open tickets for run {run_id}! Using the latest, {tid}")
#Failing that...
return (tid, tickets[0]) if tid > 0 else (None, None)
def reply_to_ticket(self, ticket_id, message, subject=None):
"""Sends a reply to the ticket.
"""
if subject:
# The rest API does not support supplying a subject, but I can maybe
# hack around this? No, not easily.
raise NotImplementedError("RT REST API does not support setting subjects on replies.")
# Dummy connection mode...
if not self._config: return True
return self.tracker.reply(ticket_id, text=message)
def comment_on_ticket(self, ticket_id, message, subject=None):
if subject:
#The rest API does not support supplying a subject, but I can maybe
#hack around this? No, not easily.
raise NotImplementedError("RT REST API does not support setting subjects on replies.")
# Dummy connection mode...
if not self._config: return True
return self.tracker.comment(ticket_id, text=message)
def change_ticket_status(self, ticket_id, status):
# Dummy connection mode...
if not self._config: return
kwargs = dict( Status = status )
# Ignore IndexError raised when subject is already set
with suppress(IndexError):
self.tracker.edit_ticket(ticket_id, **kwargs)
def change_ticket_subject(self, ticket_id, subject):
"""You can reply to a ticket with a one-off subject, but not via the
REST interface, which fundamentally does not support this.
(see share/html/REST/1.0/Forms/ticket/comment in the RT source code)
This call permanently changes the ticket subject.
"""
# Dummy connection mode...
if not self._config: return
# why the extra space?? I'm not sure but it looks to have been added deliberately.
kwargs = dict( Subject = f"{subject} " )
# Ignore IndexError raised when subject is already set
with suppress(IndexError):
self.tracker.edit_ticket(ticket_id, **kwargs)
def parse_args(*args):
description = """This script allows you to manipulate a ticket for an instrument run.
You can reply, comment, open, stall, resolve tickets.
Replying or commenting on a closed or non-existent ticket will create a new one,
unless you specify --no_create.
"""
argparser = ArgumentParser( description=description,
formatter_class = ArgumentDefaultsHelpFormatter )
argparser.add_argument("-r", "--run_id", required=True,
help="The run id of the ticket.")
argparser.add_argument("-Q", "--queue", required=True,
help="The queue to use. A name defined in rt_settings.ini as FOO_queue,"
" not a literal queue name.")
argparser.add_argument("--reply",
help="Post reply message to the ticket. " +
"Use @foo.txt to read the message from file foo.txt.")
argparser.add_argument("--comment",
help="Post comment message to the ticket" +
"Use @foo.txt to read the message from file foo.txt.")
argparser.add_argument("--subject",
help="Change the ticket subject (postfix)")
argparser.add_argument("--status",
help="Change status of the ticket")
argparser.add_argument("-P", "--prefix", default="Run",
help="Change the prefix used when making or renaming tickets")
argparser.add_argument("--no_create", action="store_true",
help="Avoid creating new tickets.")
argparser.add_argument("--test", action="store_true",
help="Set the script to connect to test-rt (as defined in rt_settings.ini)")
return argparser.parse_args(*args)
if __name__ == "__main__":
main(parse_args())