<p>Kevin Harwell has uploaded this change for <strong>review</strong>.</p><p><a href="https://gerrit.asterisk.org/c/repotools/+/18252">View Change</a></p><pre style="font-family: monospace,monospace; white-space: pre-wrap;">digium_jira: allow for greater flexibility in retrieving a JIRA client<br><br>Previously retrieving a JIRA client relied on hard-coded values, and<br>really only one server could be connected to. Getting authorization was<br>also limited to reading it from a a file (.jira_login) that contained<br>only the username and password, or the command line. Also clients were<br>not cached so subsequent calls to get the client meant renegotiating<br>the connection.<br><br>This patch maintains the old way of getting authorization, but adds a<br>new way to configure settings and authorization for a JIRA client. If<br>specified configuration is read from a .jira_config.yaml file. Client<br>configuration is looked up by a given key name, and options and auth<br>are specified as sub-structures.<br><br>Clients are also now cached, so subsequent calls for a named (note<br>'None' is allowed as a valid name) connection returned the already<br>established connection.<br><br>Change-Id: I5b4d439259434f576b98c39d4a49ff7414a6f17d<br>---<br>M digium_jira.py<br>1 file changed, 185 insertions(+), 21 deletions(-)<br><br></pre><pre style="font-family: monospace,monospace; white-space: pre-wrap;">git pull ssh://gerrit.asterisk.org:29418/repotools refs/changes/52/18252/1</pre><pre style="font-family: monospace,monospace; white-space: pre-wrap;"><span>diff --git a/digium_jira.py b/digium_jira.py</span><br><span>index 31aa301..2a0e64a 100644</span><br><span>--- a/digium_jira.py</span><br><span>+++ b/digium_jira.py</span><br><span>@@ -8,6 +8,7 @@</span><br><span> import logging</span><br><span> import os</span><br><span> import getpass</span><br><span style="color: hsl(120, 100%, 40%);">+import yaml</span><br><span> </span><br><span> from jira.client import JIRA</span><br><span> from version_parser import AsteriskVersion</span><br><span>@@ -17,37 +18,200 @@</span><br><span> </span><br><span> DEFAULT_PROJECT = 'asterisk'</span><br><span> </span><br><span style="color: hsl(0, 100%, 40%);">-def _get_jira_auth():</span><br><span style="color: hsl(0, 100%, 40%);">-    """Get JIRA credentials"""</span><br><span style="color: hsl(120, 100%, 40%);">+DEFAULT_CLIENT_CONFIG = {</span><br><span style="color: hsl(120, 100%, 40%);">+    "options": {</span><br><span style="color: hsl(120, 100%, 40%);">+        "server": "https://issues.asterisk.org/jira/",</span><br><span style="color: hsl(120, 100%, 40%);">+    },</span><br><span style="color: hsl(120, 100%, 40%);">+}</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+def _get_file(filename):</span><br><span style="color: hsl(120, 100%, 40%);">+    """Retrieve a file by filename</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    Opens, and returns the first file found of the given name checking in the</span><br><span style="color: hsl(120, 100%, 40%);">+    following directory order:</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+        1) current working directory</span><br><span style="color: hsl(120, 100%, 40%);">+        2) current user's home directory</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    Keyword Arguments:</span><br><span style="color: hsl(120, 100%, 40%);">+        filename - the name of the file to open</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    paths = [</span><br><span style="color: hsl(120, 100%, 40%);">+        ".",</span><br><span style="color: hsl(120, 100%, 40%);">+        os.path.expanduser("~"),</span><br><span style="color: hsl(120, 100%, 40%);">+    ]</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    for p in paths:</span><br><span style="color: hsl(120, 100%, 40%);">+        try:</span><br><span style="color: hsl(120, 100%, 40%);">+            f = open("{0}/{1}".format(p, filename), "r")</span><br><span style="color: hsl(120, 100%, 40%);">+            return f</span><br><span style="color: hsl(120, 100%, 40%);">+        except:</span><br><span style="color: hsl(120, 100%, 40%);">+            pass</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    raise IOError("File '{0}' not found in paths '{1}'".format(</span><br><span style="color: hsl(120, 100%, 40%);">+        filename, ", ".join(paths)))</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+def _auth_from_file(config):</span><br><span style="color: hsl(120, 100%, 40%);">+    """Get JIRA authentication information from a '.jira_login' file</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    A '.jira_login' file contains authentication information only, and should</span><br><span style="color: hsl(120, 100%, 40%);">+    contain only two lines. The first line should be the username and the</span><br><span style="color: hsl(120, 100%, 40%);">+    second line the password, e.g.</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+      <username></span><br><span style="color: hsl(120, 100%, 40%);">+      <password></span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    Leading and trailing spaces are stripped.</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    Keyword Arguments:</span><br><span style="color: hsl(120, 100%, 40%);">+        config - a working configuration</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span> </span><br><span>     try:</span><br><span style="color: hsl(0, 100%, 40%);">-        with open(os.path.expanduser('~') + "/.jira_login", "r") as jira_cache:</span><br><span style="color: hsl(0, 100%, 40%);">-            jira_user = jira_cache.readline().strip()</span><br><span style="color: hsl(0, 100%, 40%);">-            jira_pw = jira_cache.readline().strip()</span><br><span style="color: hsl(0, 100%, 40%);">-            return (jira_user, jira_pw)</span><br><span style="color: hsl(0, 100%, 40%);">-    except IOError:</span><br><span style="color: hsl(0, 100%, 40%);">-        pass</span><br><span style="color: hsl(120, 100%, 40%);">+        with _get_file(".jira_login") as f:</span><br><span style="color: hsl(120, 100%, 40%);">+            username = f.readline().strip()</span><br><span style="color: hsl(120, 100%, 40%);">+            if not username:</span><br><span style="color: hsl(120, 100%, 40%);">+                return False</span><br><span> </span><br><span style="color: hsl(0, 100%, 40%);">-    # Didn't get auth details from file, try interactive instead.</span><br><span style="color: hsl(0, 100%, 40%);">-    print "Please enter your username and pw for JIRA."</span><br><span style="color: hsl(0, 100%, 40%);">-    jira_user = raw_input("Username: ")</span><br><span style="color: hsl(0, 100%, 40%);">-    jira_pw = getpass.getpass("Password: ")</span><br><span style="color: hsl(120, 100%, 40%);">+            password = f.readline().strip()</span><br><span style="color: hsl(120, 100%, 40%);">+            if not password:</span><br><span style="color: hsl(120, 100%, 40%);">+                LOGGER.warn("Password not specified for {0} in {1}".format(</span><br><span style="color: hsl(120, 100%, 40%);">+                    username, f.name))</span><br><span> </span><br><span style="color: hsl(0, 100%, 40%);">-    return (jira_user, jira_pw)</span><br><span style="color: hsl(120, 100%, 40%);">+            config.update({</span><br><span style="color: hsl(120, 100%, 40%);">+                "basic_auth": (username, password),</span><br><span style="color: hsl(120, 100%, 40%);">+            })</span><br><span style="color: hsl(120, 100%, 40%);">+    except:</span><br><span style="color: hsl(120, 100%, 40%);">+        LOGGER.debug(".jira_login not found")</span><br><span style="color: hsl(120, 100%, 40%);">+        return False</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    return True</span><br><span> </span><br><span> </span><br><span style="color: hsl(0, 100%, 40%);">-def get_jira_client():</span><br><span style="color: hsl(0, 100%, 40%);">-    """Return a connected JIRA client"""</span><br><span style="color: hsl(120, 100%, 40%);">+def _auth_from_input(config):</span><br><span style="color: hsl(120, 100%, 40%);">+    """Get JIRA authentication information from interactive user input</span><br><span> </span><br><span style="color: hsl(0, 100%, 40%);">-    jira_user, jira_password = _get_jira_auth()</span><br><span style="color: hsl(120, 100%, 40%);">+    Keyword Arguments:</span><br><span style="color: hsl(120, 100%, 40%);">+        config - a working configuration</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span> </span><br><span style="color: hsl(0, 100%, 40%);">-    jira_options = {</span><br><span style="color: hsl(0, 100%, 40%);">-        'server': 'https://issues.asterisk.org/jira/'</span><br><span style="color: hsl(0, 100%, 40%);">-    }</span><br><span style="color: hsl(120, 100%, 40%);">+    print "Please enter your username and password for JIRA."</span><br><span> </span><br><span style="color: hsl(0, 100%, 40%);">-    jira = JIRA(options=jira_options, basic_auth=(jira_user, jira_password))</span><br><span style="color: hsl(120, 100%, 40%);">+    config.update({</span><br><span style="color: hsl(120, 100%, 40%);">+        "basic_auth": (</span><br><span style="color: hsl(120, 100%, 40%);">+            raw_input("Username: "),</span><br><span style="color: hsl(120, 100%, 40%);">+            getpass.getpass("Password: ")</span><br><span style="color: hsl(120, 100%, 40%);">+        ),</span><br><span style="color: hsl(120, 100%, 40%);">+    })</span><br><span> </span><br><span style="color: hsl(0, 100%, 40%);">-    return jira</span><br><span style="color: hsl(120, 100%, 40%);">+    return True</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+def _config_from_file(name, config):</span><br><span style="color: hsl(120, 100%, 40%);">+    """Get JIRA configuration (connection and authentication) information from</span><br><span style="color: hsl(120, 100%, 40%);">+    a '.jira_config.yaml' file</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    Retrieve the appropriate client configuration based on the given name. A</span><br><span style="color: hsl(120, 100%, 40%);">+    client section should be in the following YAML based format:</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+      <client name>:</span><br><span style="color: hsl(120, 100%, 40%);">+        options: <jira client options dictionary></span><br><span style="color: hsl(120, 100%, 40%);">+        <auth type> : <auth type options></span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    For more information on the JIRA client and authentication options see:</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+      https://jira.readthedocs.io/</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    Keyword Arguments:</span><br><span style="color: hsl(120, 100%, 40%);">+        name - a client section name</span><br><span style="color: hsl(120, 100%, 40%);">+        config - a working configuration</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    try:</span><br><span style="color: hsl(120, 100%, 40%);">+        with _get_file(".jira_config.yaml") as f:</span><br><span style="color: hsl(120, 100%, 40%);">+            c = yaml.safe_load(f)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+            section = c.get(name)</span><br><span style="color: hsl(120, 100%, 40%);">+            if section:</span><br><span style="color: hsl(120, 100%, 40%);">+                config.update(c[name])</span><br><span style="color: hsl(120, 100%, 40%);">+            elif name:</span><br><span style="color: hsl(120, 100%, 40%);">+                # If a name was specified and not found then don't try getting</span><br><span style="color: hsl(120, 100%, 40%);">+                # config/auth using other ways, but force a connection failure</span><br><span style="color: hsl(120, 100%, 40%);">+                config["options"]["server"] = "None"</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    except yaml.YAMLError as e:</span><br><span style="color: hsl(120, 100%, 40%);">+        LOGGER.error(e)</span><br><span style="color: hsl(120, 100%, 40%);">+        return False</span><br><span style="color: hsl(120, 100%, 40%);">+    except:</span><br><span style="color: hsl(120, 100%, 40%);">+        LOGGER.debug(".jira_config not found")</span><br><span style="color: hsl(120, 100%, 40%);">+        return False</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    return True</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+def _get_jira_config(config, name=None):</span><br><span style="color: hsl(120, 100%, 40%);">+    """Get JIRA client configuration and/or login credentials</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    There are currently three different ways configuration (connection and</span><br><span style="color: hsl(120, 100%, 40%);">+    authentication) can be retrieved (in order of attempted retrieval):</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    1) From a ~/.jira_config.yaml file (see _config_from_file)</span><br><span style="color: hsl(120, 100%, 40%);">+    2) From a ~/.jira_login file (see _auth_from_file)</span><br><span style="color: hsl(120, 100%, 40%);">+    3) From interactive user input (see _auth_from_input)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    Keyword Arguments:</span><br><span style="color: hsl(120, 100%, 40%);">+        name - a client name used to look up configuration</span><br><span style="color: hsl(120, 100%, 40%);">+        config - current working configuration</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    return _config_from_file(name, config) or _auth_from_file(config) \</span><br><span style="color: hsl(120, 100%, 40%);">+        or _auth_from_input(config)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+# Typically it is advised to not default a parameter with a dictionary as it</span><br><span style="color: hsl(120, 100%, 40%);">+# can lead to unexpected side effects, but those are the exact effects we want</span><br><span style="color: hsl(120, 100%, 40%);">+# with a cache. By initializing the cache this way a caller can supply their</span><br><span style="color: hsl(120, 100%, 40%);">+# own cache if they so desire, and if not then python takes care of keeping the</span><br><span style="color: hsl(120, 100%, 40%);">+# internal cache between calls.</span><br><span style="color: hsl(120, 100%, 40%);">+def get_jira_client(name=None, config=None, cache={}):</span><br><span style="color: hsl(120, 100%, 40%);">+    """Return a connected JIRA client</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    This stores connected clients by 'name' in 'cache'. Subsequent calls using</span><br><span style="color: hsl(120, 100%, 40%);">+    the same name will return the cached client object.</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    Keyword Arguments:</span><br><span style="color: hsl(120, 100%, 40%);">+        name - The client name used for lookup</span><br><span style="color: hsl(120, 100%, 40%);">+        config - JIRA client configuration</span><br><span style="color: hsl(120, 100%, 40%);">+        cache - Stores connected clients by name</span><br><span style="color: hsl(120, 100%, 40%);">+    """</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    # If the client already exists in the cache then use that one</span><br><span style="color: hsl(120, 100%, 40%);">+    client = cache.get(name)</span><br><span style="color: hsl(120, 100%, 40%);">+    if client:</span><br><span style="color: hsl(120, 100%, 40%);">+        return client</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    # Use defaults for any unspecified configuration options</span><br><span style="color: hsl(120, 100%, 40%);">+    working_config = DEFAULT_CLIENT_CONFIG.copy()</span><br><span style="color: hsl(120, 100%, 40%);">+    if config:</span><br><span style="color: hsl(120, 100%, 40%);">+        working_config.update(config)</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    success = _get_jira_config(working_config, name)</span><br><span style="color: hsl(120, 100%, 40%);">+    if success:</span><br><span style="color: hsl(120, 100%, 40%);">+        try:</span><br><span style="color: hsl(120, 100%, 40%);">+            LOGGER.debug("Attempting to connect to JIRA server '{0}".format(</span><br><span style="color: hsl(120, 100%, 40%);">+                working_config["options"]["server"]))</span><br><span style="color: hsl(120, 100%, 40%);">+            client = JIRA(**working_config)</span><br><span style="color: hsl(120, 100%, 40%);">+            cache[name] = client</span><br><span style="color: hsl(120, 100%, 40%);">+        except:</span><br><span style="color: hsl(120, 100%, 40%);">+            success = False</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    if not success:</span><br><span style="color: hsl(120, 100%, 40%);">+        LOGGER.error("Unable to connect to '{0}'".format(</span><br><span style="color: hsl(120, 100%, 40%);">+            working_config["options"]["server"]))</span><br><span style="color: hsl(120, 100%, 40%);">+</span><br><span style="color: hsl(120, 100%, 40%);">+    return client</span><br><span> </span><br><span> </span><br><span> class DigiumJira(object):</span><br><span></span><br></pre><p>To view, visit <a href="https://gerrit.asterisk.org/c/repotools/+/18252">change 18252</a>. To unsubscribe, or for help writing mail filters, visit <a href="https://gerrit.asterisk.org/settings">settings</a>.</p><div itemscope itemtype="http://schema.org/EmailMessage"><div itemscope itemprop="action" itemtype="http://schema.org/ViewAction"><link itemprop="url" href="https://gerrit.asterisk.org/c/repotools/+/18252"/><meta itemprop="name" content="View Change"/></div></div>

<div style="display:none"> Gerrit-Project: repotools </div>
<div style="display:none"> Gerrit-Branch: master </div>
<div style="display:none"> Gerrit-Change-Id: I5b4d439259434f576b98c39d4a49ff7414a6f17d </div>
<div style="display:none"> Gerrit-Change-Number: 18252 </div>
<div style="display:none"> Gerrit-PatchSet: 1 </div>
<div style="display:none"> Gerrit-Owner: Kevin Harwell <kharwell@digium.com> </div>
<div style="display:none"> Gerrit-MessageType: newchange </div>