diff --git a/ocflib/infra/rt.py b/ocflib/infra/rt.py index 778f0d20..93f2eb60 100644 --- a/ocflib/infra/rt.py +++ b/ocflib/infra/rt.py @@ -3,6 +3,8 @@ from urllib.parse import urlencode import requests +import json +import copy class RtTicket(namedtuple('RtTicket', ('number', 'owner', 'subject', 'queue', 'status'))): @@ -16,34 +18,57 @@ def __str__(self): ).format(self=self) @classmethod - def from_number(cls, connection, num): - resp = connection.get('https://rt.ocf.berkeley.edu/REST/1.0/ticket/{}/view'.format(num)) - assert resp.status_code == 200, resp.status_code + def from_number(cls, _, num, auth): + """ + Second argument left for compatibility purposes, alghough I think no programme + ever used this function when I wrote this, lol + """ + resp = requests.get("https://rt.ocf.berkeley.edu/REST/2.0/ticket/{0}".format(num), **auth) + assert resp.ok, resp.status_code assert '200 Ok' in resp.text - lines = resp.text.splitlines() + jsonified = resp.json() - def find(header): - for line in lines: - if line.startswith(header + ': '): - return line.split(': ', 1)[1] + return cls( + number=jsonified["id"], + owner=jsonified["Owner"], + subject=jsonified["Subject"], + queue=jsonified["Queue"], + status=jsonified["Status"], + ) + + @classmethod + def get_latest(cls, _, queue, auth): + """ + Second argument left for compatibility purposes, alghough I think no programme + ever used this function when I wrote this, lol + """ + resp = requests.get("https://rt.ocf.berkeley.edu/REST/2.0/tickets?query=Queue='{}'&orderby=Created&order=DESC".format(queue), **auth) + assert resp.ok, resp.status_code + assert '200 Ok' in resp.text + + + jsonified = resp.json() + assert "items" in jsonified + assert len(jsonified["items"]) > 0 + + jsonified = jsonified["items"][0] return cls( - number=num, - owner=find('Owner'), - subject=find('Subject'), - queue=find('Queue'), - status=find('Status'), + number=jsonified["id"], + owner=jsonified["Owner"], + subject=jsonified["Subject"], + queue=jsonified["Queue"], + status=jsonified["Status"], ) @classmethod - def create(cls, connection, queue, requestor, subject, text, **kwargs): + def create(cls, _, queue, requestor, subject, text, auth, **kwargs): """Create an RT ticket and returns an instance of the result""" # RT prefixes multiline strings by a blank space text = text.replace('\n', '\n ') data = { - 'id': 'ticket/new', 'Queue': queue, 'Requestor': requestor, 'Subject': subject, @@ -51,37 +76,41 @@ def create(cls, connection, queue, requestor, subject, text, **kwargs): **kwargs, } - # RT's incoming data format has the form key: value, but these aren't HTTP Headers - body = '' - for k, v in data.items(): - body += '{}: {}\n'.format(k, v) + # Look like crap but it works? + # There must be some more elegant way but idk + additional_kwargs = copy.deepcopy(auth) + if "headers" not in additional_kwargs: + additional_kwargs["headers"] = {} + additional_kwargs["headers"]["content-type"] = "application/json" - # RT requires the POST content to be sent with no filename hence 'content': (None, body) - resp = connection.post('https://rt.ocf.berkeley.edu/REST/1.0/ticket/new', files={'content': (None, body)}) + resp = requests.post('https://rt.ocf.berkeley.edu/REST/2.0/ticket', json=data, **additional_kwargs) resp.raise_for_status() assert '200 Ok' in resp.text - match = re.search(r'Ticket ([0-9]+) created.', resp.text) - assert match, '200 response but no ticket number found in RT response' + assert "id" in resp.json(), '200 response but no ticket number found in RT response' + + return resp.json()["id"] - ticket_number = int(match.group(1)) - return ticket_number +def rt_connection_auth(*args): + """Return the authentication required to access REST API. -def rt_connection(user, password): - """Return a requests Session object authenticated against RT. + Accepts either a RtCredentials, a RtAuthenticationToken, a string (as token), + or two strings (as username and password) Currently, we only enable access to the REST API from a few select IPs. """ - s = requests.Session() - resp = s.post( - 'https://rt.ocf.berkeley.edu/REST/1.0/', - data=urlencode({'user': user, 'pass': password}), - timeout=20, - ) - assert resp.status_code == 200, resp.status_code - assert '200 Ok' in resp.text - return s + if len(args) == 1: + if isinstance(args[0], RtCredentials): + return {"auth": (args[0]["username"], args[0]["password"])} + elif isinstance(args[0], RtAuthenticationToken): + return {"headers": {"Authorization": "token {}".format(args[0]["token"])}} + elif isinstance(args[0], str): + return {"headers": {"Authorization": "token {}".format(args[0])}} + elif len(args) == 2 and all(map(lambda e: isinstance(e, str), args)): + return {"auth": (args[0], args[1])} + raise ValueError("Auth credential provided is not valid") + class RtCredentials(namedtuple('RtCredentials', [ @@ -93,3 +122,11 @@ class RtCredentials(namedtuple('RtCredentials', [ :param username: str :param password: str """ + +class RtAuthenticationToken(namedtuple('RtAuthenticationToken', [ + 'token' +])): + """Token for programmatically accessing RT. + + :param token: str + """