<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>