1"""
2JIRA Execution module
3=====================
4
5.. versionadded:: 2019.2.0
6
7Execution module to manipulate JIRA tickets via Salt.
8
9This module requires the ``jira`` Python library to be installed.
10
11Configuration example:
12
13.. code-block:: yaml
14
15  jira:
16    server: https://jira.atlassian.org
17    username: salt
18    password: pass
19"""
20
21import logging
22
23import salt.utils.args
24
25try:
26    import jira
27
28    HAS_JIRA = True
29except ImportError:
30    HAS_JIRA = False
31
32log = logging.getLogger(__name__)
33
34__virtualname__ = "jira"
35__proxyenabled__ = ["*"]
36
37JIRA = None
38
39
40def __virtual__():
41    return (
42        __virtualname__
43        if HAS_JIRA
44        else (False, "Please install the jira Python libary from PyPI")
45    )
46
47
48def _get_credentials(server=None, username=None, password=None):
49    """
50    Returns the credentials merged with the config data (opts + pillar).
51    """
52    jira_cfg = __salt__["config.merge"]("jira", default={})
53    if not server:
54        server = jira_cfg.get("server")
55    if not username:
56        username = jira_cfg.get("username")
57    if not password:
58        password = jira_cfg.get("password")
59    return server, username, password
60
61
62def _get_jira(server=None, username=None, password=None):
63    global JIRA
64    if not JIRA:
65        server, username, password = _get_credentials(
66            server=server, username=username, password=password
67        )
68        JIRA = jira.JIRA(
69            basic_auth=(username, password), server=server, logging=True
70        )  # We want logging
71    return JIRA
72
73
74def create_issue(
75    project,
76    summary,
77    description,
78    template_engine="jinja",
79    context=None,
80    defaults=None,
81    saltenv="base",
82    issuetype="Bug",
83    priority="Normal",
84    labels=None,
85    assignee=None,
86    server=None,
87    username=None,
88    password=None,
89    **kwargs
90):
91    """
92    Create a JIRA issue using the named settings. Return the JIRA ticket ID.
93
94    project
95        The name of the project to attach the JIRA ticket to.
96
97    summary
98        The summary (title) of the JIRA ticket. When the ``template_engine``
99        argument is set to a proper value of an existing Salt template engine
100        (e.g., ``jinja``, ``mako``, etc.) it will render the ``summary`` before
101        creating the ticket.
102
103    description
104        The full body description of the JIRA ticket. When the ``template_engine``
105        argument is set to a proper value of an existing Salt template engine
106        (e.g., ``jinja``, ``mako``, etc.) it will render the ``description`` before
107        creating the ticket.
108
109    template_engine: ``jinja``
110        The name of the template engine to be used to render the values of the
111        ``summary`` and ``description`` arguments. Default: ``jinja``.
112
113    context: ``None``
114        The context to pass when rendering the ``summary`` and ``description``.
115        This argument is ignored when ``template_engine`` is set as ``None``
116
117    defaults: ``None``
118        Default values to pass to the Salt rendering pipeline for the
119        ``summary`` and ``description`` arguments.
120        This argument is ignored when ``template_engine`` is set as ``None``.
121
122    saltenv: ``base``
123        The Salt environment name (for the rendering system).
124
125    issuetype: ``Bug``
126        The type of the JIRA ticket. Default: ``Bug``.
127
128    priority: ``Normal``
129        The priority of the JIRA ticket. Default: ``Normal``.
130
131    labels: ``None``
132        A list of labels to add to the ticket.
133
134    assignee: ``None``
135        The name of the person to assign the ticket to.
136
137    CLI Examples:
138
139    .. code-block:: bash
140
141        salt '*' jira.create_issue NET 'Ticket title' 'Ticket description'
142        salt '*' jira.create_issue NET 'Issue on {{ opts.id }}' 'Error detected on {{ opts.id }}' template_engine=jinja
143    """
144    if template_engine:
145        summary = __salt__["file.apply_template_on_contents"](
146            summary,
147            template=template_engine,
148            context=context,
149            defaults=defaults,
150            saltenv=saltenv,
151        )
152        description = __salt__["file.apply_template_on_contents"](
153            description,
154            template=template_engine,
155            context=context,
156            defaults=defaults,
157            saltenv=saltenv,
158        )
159    jira_ = _get_jira(server=server, username=username, password=password)
160    if not labels:
161        labels = []
162    data = {
163        "project": {"key": project},
164        "summary": summary,
165        "description": description,
166        "issuetype": {"name": issuetype},
167        "priority": {"name": priority},
168        "labels": labels,
169    }
170    data.update(salt.utils.args.clean_kwargs(**kwargs))
171    issue = jira_.create_issue(data)
172    issue_key = str(issue)
173    if assignee:
174        assign_issue(issue_key, assignee)
175    return issue_key
176
177
178def assign_issue(issue_key, assignee, server=None, username=None, password=None):
179    """
180    Assign the issue to an existing user. Return ``True`` when the issue has
181    been properly assigned.
182
183    issue_key
184        The JIRA ID of the ticket to manipulate.
185
186    assignee
187        The name of the user to assign the ticket to.
188
189    CLI Example:
190
191    .. code-block:: bash
192
193        salt '*' jira.assign_issue NET-123 example_user
194    """
195    jira_ = _get_jira(server=server, username=username, password=password)
196    assigned = jira_.assign_issue(issue_key, assignee)
197    return assigned
198
199
200def add_comment(
201    issue_key,
202    comment,
203    visibility=None,
204    is_internal=False,
205    server=None,
206    username=None,
207    password=None,
208):
209    """
210    Add a comment to an existing ticket. Return ``True`` when it successfully
211    added the comment.
212
213    issue_key
214        The issue ID to add the comment to.
215
216    comment
217        The body of the comment to be added.
218
219    visibility: ``None``
220        A dictionary having two keys:
221
222        - ``type``: is ``role`` (or ``group`` if the JIRA server has configured
223          comment visibility for groups).
224        - ``value``: the name of the role (or group) to which viewing of this
225          comment will be restricted.
226
227    is_internal: ``False``
228        Whether a comment has to be marked as ``Internal`` in Jira Service Desk.
229
230    CLI Example:
231
232    .. code-block:: bash
233
234        salt '*' jira.add_comment NE-123 'This is a comment'
235    """
236    jira_ = _get_jira(server=server, username=username, password=password)
237    comm = jira_.add_comment(
238        issue_key, comment, visibility=visibility, is_internal=is_internal
239    )
240    return True
241
242
243def issue_closed(issue_key, server=None, username=None, password=None):
244    """
245    Check if the issue is closed.
246
247    issue_key
248        The JIRA iD of the ticket to close.
249
250    Returns:
251
252    - ``True``: the ticket exists and it is closed.
253    - ``False``: the ticket exists and it has not been closed.
254    - ``None``: the ticket does not exist.
255
256    CLI Example:
257
258    .. code-block:: bash
259
260        salt '*' jira.issue_closed NE-123
261    """
262    if not issue_key:
263        return None
264    jira_ = _get_jira(server=server, username=username, password=password)
265    try:
266        ticket = jira_.issue(issue_key)
267    except jira.exceptions.JIRAError:
268        # Ticket not found
269        return None
270    return ticket.fields().status.name == "Closed"
271