diff --git a/conf/selfservice.yml b/conf/selfservice.yml index 7e85405c..d3da2fb9 100644 --- a/conf/selfservice.yml +++ b/conf/selfservice.yml @@ -6,3 +6,5 @@ ssm_host_limit: 10 ssm_default_lifetime: 5 # How many clouds (and auth tokens, one per cloud) a unique user ID can have ssm_user_cloud_limit: 2 +# Set to true to create a Jira ticket when a self-schedule is created +ssm_jira_create_ticket: false diff --git a/src/quads/server/blueprints/assignments.py b/src/quads/server/blueprints/assignments.py index b55950e6..f2a08e8f 100644 --- a/src/quads/server/blueprints/assignments.py +++ b/src/quads/server/blueprints/assignments.py @@ -1,7 +1,9 @@ +import asyncio import re from datetime import datetime from flask import Blueprint, Response, jsonify, make_response, request, g +from quads.tools.external.jira import Jira, JiraException from sqlalchemy import inspect from quads.config import Config @@ -301,7 +303,6 @@ def create_self_assignment() -> Response: kwargs = { "description": description, "owner": owner, - "ticket": ticket, "qinq": qinq, "wipe": wipe, "ccuser": cc_user, @@ -310,6 +311,53 @@ def create_self_assignment() -> Response: } if _vlan: kwargs["vlan_id"] = int(vlan) + + create_jira_ticket = Config.get("ssm_jira_create_ticket", False) + if create_jira_ticket: + loop = asyncio.get_event_loop() + try: + jira = Jira( + Config["jira_url"], + loop=loop, + ) + except JiraException as ex: # pragma: no cover + response = { + "status_code": 400, + "error": "Bad Request", + "message": f"Jira connection failed: {ex}", + } + return make_response(jsonify(response), 400) + description = "" + for key, value in kwargs.items(): + description += f"{key}: {value}\n" + + try: + response = loop.run_until_complete( + jira.create_ticket( + summary=f"[SSM] {description}", + description=description, + labels=["SELF-SCHEDULED"], + ) + ) + except JiraException as ex: + response = { + "status_code": 400, + "error": "Bad Request", + "message": f"Jira ticket creation failed: {ex}", + } + return make_response(jsonify(response), 400) + + ticket = response.get("key").split("-")[1] + kwargs["ticket"] = ticket + else: + if not ticket: + response = { + "status_code": 400, + "error": "Bad Request", + "message": "Missing Jira ticket number while automatic ticket creation is disabled", + } + return make_response(jsonify(response), 400) + _assignment_obj = AssignmentDao.create_assignment(**kwargs) return jsonify(_assignment_obj.as_dict()) diff --git a/src/quads/tools/external/jira.py b/src/quads/tools/external/jira.py index 02b42a92..088a4ebe 100755 --- a/src/quads/tools/external/jira.py +++ b/src/quads/tools/external/jira.py @@ -120,6 +120,48 @@ async def put_request(self, endpoint, payload): logger.error("Resource not found: %s" % self.url + endpoint) return False + async def create_ticket(self, summary, description, labels=None): + """Create a Jira ticket.""" + if labels is None: + labels = [] + endpoint = "/issue/" + logger.debug("POST new ticket") + short_summary = summary.split("\r") + title = f"{short_summary[0]}" + + data = { + "fields": { + "project": {"key": self.ticket_queue}, + "issuetype": {"name": "Task"}, + "summary": title, + "description": description, + } + } + if labels: + data["fields"].update({"labels": labels}) + + response = await self.post_request(endpoint, data) + return response + + async def create_subtask(self, parent_ticket, cloud, description, type_of_subtask): + """Create a Jira subtask for a specified parent ticket.""" + endpoint = "/issue/" + logger.debug("POST new subtask") + title = f"{cloud} {type_of_subtask}" + + data = { + "fields": { + "project": {"key": self.ticket_queue}, + "issuetype": {"id": "5"}, + "parent": {"key": f"{self.ticket_queue}-{parent_ticket}"}, + "summary": title, + "labels": [type_of_subtask.upper()], + "description": description, + } + } + response = await self.post_request(endpoint, data) + return response + async def add_watcher(self, ticket, watcher): issue_id = "%s-%s" % (Config["ticket_queue"], ticket) endpoint = "/issue/%s/watchers" % issue_id @@ -203,6 +245,19 @@ async def get_watchers(self, ticket): return None return result + async def get_user_by_email(self, email): + """Find a Jira user by email.""" + endpoint = f"/user/search?username={email}" + logger.debug("GET user: %s" % endpoint) + result = await self.get_request(endpoint) + if not result: + logger.error("User not found") + return None + for user in result: + if user.get("emailAddress") == email: + return user + return None + async def get_all_pending_tickets(self): transition_id = await self.get_transition_id("In Progress") query = {"status": transition_id} @@ -246,3 +301,17 @@ async def search_tickets(self, query=None): logger.error("Failed to get pending tickets") return None return result + + async def get_field_allowed_values(self, field_id, ticket_id=1): + """Get list of allowed values from JIRA API for a specified field.""" + endpoint = f"/issue/{self.ticket_queue}-{ticket_id}/editmeta" + result = await self.get_request(endpoint) + if not result: + logger.error("Failed to get allowed values") + return None + try: + result = result["fields"][f"customfield_{field_id}"]["allowedValues"] + result = [entry["value"] for entry in result if not entry["value"].startswith("One or more of the")] + except (ValueError, AttributeError): + logger.error("Failed to get allowed values") + return result