From 6e44d3ff1012893d9e8f617d64a6aad3881139bf Mon Sep 17 00:00:00 2001 From: Lakshmi Kannan Date: Wed, 13 Aug 2014 17:34:40 -0700 Subject: [PATCH 1/7] Add sample JIRA sensor and action * Add JIRA sensor that would watch for new projects. * Add JIRA action that would create an issue. * Add JIRA action metadata. --- .../packages/jira/actions/create_issue.py | 86 +++++++++++++++++++ .../jira/actions/jira_create_issue.json | 33 +++++++ .../packages/jira/sensors/jira_sensor.py | 80 +++++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 contrib/sandbox/packages/jira/actions/create_issue.py create mode 100644 contrib/sandbox/packages/jira/actions/jira_create_issue.json create mode 100644 contrib/sandbox/packages/jira/sensors/jira_sensor.py diff --git a/contrib/sandbox/packages/jira/actions/create_issue.py b/contrib/sandbox/packages/jira/actions/create_issue.py new file mode 100644 index 0000000000..e2512ec569 --- /dev/null +++ b/contrib/sandbox/packages/jira/actions/create_issue.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python + +# Requirements +# pip install jira + +import os +import sys +from jira.client import JIRA + +RSA_CERT_FILE = '/home/vagrant/jira.pem' + + +class AuthedJiraClient(object): + def __init__(self, jira_server, oauth_creds): + self._client = JIRA(options={'server': jira_server}, + oauth=oauth_creds) + + def is_project_exists(self, project): + projs = self._client.projects() + project_names = [proj.key for proj in projs] + if project not in project_names: + return False + return True + + def create_issue(self, project=None, summary=None, desc=None, issuetype=None): + issue_dict = { + 'project': {'key': project}, + 'summary': summary, + 'description': desc, + 'issuetype': {'name': issuetype}, + } + new_issue = self._client.create_issue(fields=issue_dict) + return new_issue + + +def _read_cert(file_path): + with open(file_path) as f: + return f.read() + + +def _parse_args(args): + params = {} + params['jira_server'] = args[1] + params['oauth_token'] = args[2] + params['oauth_secret'] = args[3] + params['consumer_key'] = args[4] + params['project_name'] = args[5] + params['issue_summary'] = args[6] + params['issue_description'] = args[7] + params['issue_type'] = args[8] + return params + + +def _get_jira_client(params): + if not os.path.exists(RSA_CERT_FILE): + raise Exception('Cert file required for JIRA OAuth.') + rsa_key = _read_cert(RSA_CERT_FILE) + oauth_creds = { + 'access_token': params['oauth_token'], + 'access_token_secret': params['oauth_secret'], + 'consumer_key': params['consumer_key'], + 'key_cert': rsa_key + } + jira_client = AuthedJiraClient(params['jira_server'], oauth_creds) + return jira_client + + +def main(args): + params = _parse_args(args) + client = _get_jira_client(params) + proj = params['project_name'] + + try: + if not client.is_project_exists(proj): + raise Exception('Project ' + proj + ' does not exist.') + issue = client.create_issue(project=params['project_name'], + summary=params['issue_summary'], + desc=params['issue_description'], + issuetype=params['issue_type']) + except Exception as e: + sys.stderr.write(e.message + '\n') + else: + sys.stdout.write('Issue ' + issue + ' created.\n') + +if __name__ == '__main__': + main(sys.argv) diff --git a/contrib/sandbox/packages/jira/actions/jira_create_issue.json b/contrib/sandbox/packages/jira/actions/jira_create_issue.json new file mode 100644 index 0000000000..f71ac31396 --- /dev/null +++ b/contrib/sandbox/packages/jira/actions/jira_create_issue.json @@ -0,0 +1,33 @@ +{ + "name": "jira-create-issue", + "runner_type": "remote-exec-sysuser", + "description": "Create JIRA issue action.", + "enabled": true, + "entry_point": "jira/create_issue.py", + "parameters": { + "jira_server": { + "type": "string" + }, + "oauth_token": { + "type": "string" + }, + "oauth_token_secret": { + "type": "string" + }, + "consumer_key": { + "type": "string" + }, + "project_name": { + "type": "string" + }, + "issue_summary": { + "type": "string" + }, + "issue_description": { + "type": "string" + }, + "issue_type": { + "type": "string" + } + } +} diff --git a/contrib/sandbox/packages/jira/sensors/jira_sensor.py b/contrib/sandbox/packages/jira/sensors/jira_sensor.py new file mode 100644 index 0000000000..f4eb5d2c23 --- /dev/null +++ b/contrib/sandbox/packages/jira/sensors/jira_sensor.py @@ -0,0 +1,80 @@ +import os +import time + +from jira.client import JIRA + +RSA_CERT_FILE = '/home/vagrant/jira.pem' + + +class JIRASensor(object): + ''' + Sensor will monitor for any new projects created in JIRA and + emit trigger instance when one is created. + ''' + def __init__(self, container_service): + self._container_service = container_service + self._jira_server = 'https://stackstorm.atlassian.net' + # The Consumer Key created while setting up the "Incoming Authentication" in + # JIRA for the Application Link. + self._consumer_key = u'' + self._rsa_key = None + self._jira_client = None + self._access_token = u'' + self._access_secret = u'' + self._projects_available = None + self._sleep_time = 30 + + def _read_cert(self, file_path): + with open(file_path) as f: + return f.read() + + def setup(self): + global RSA_CERT_FILE + if not os.path.exists(RSA_CERT_FILE): + raise Exception('Cert file required for JIRA OAuth.') + + # The contents of the rsa.pem file generated (the private RSA key) + self._rsa_key = self._read_cert(RSA_CERT_FILE) + oauth_creds = { + 'access_token': self._access_token, + 'access_token_secret': self._access_secret, + 'consumer_key': self._consumer_key, + 'key_cert': self._rsa_key + } + + self._jira_client = JIRA(options={'server': self._jira_server}, + oauth=oauth_creds) + if self._projects_available is None: + self._projects_available = set() + for proj in self._jira_client.projects(): + self._projects_available.add(proj.key) + + def start(self): + while True: + for proj in self._jira_client.projects(): + if proj.key not in self._projects_available: + self._dispatch_trigger(proj) + self._projects_available.add(proj) + time.sleep(self._sleep_time) + + def stop(self): + pass + + def get_trigger_types(self): + return [ + { + 'name': 'st2.jira.project_tracker', + 'description': 'Stackstorm JIRA projects tracker', + 'payload_info': ['project_name', 'project_url'] + } + ] + + def _dispatch_trigger(self, proj): + trigger = {} + trigger['name'] = 'st2.jira.projects-tracker' + payload = {} + payload['project_name'] = proj.key + payload['project_url'] = proj.self + trigger['payload'] = payload + self._container_service.dispatch(trigger) + From c767f8cc9095c27cc53eaec3ab1eecc4b2e03b1b Mon Sep 17 00:00:00 2001 From: Lakshmi Kannan Date: Wed, 13 Aug 2014 17:59:41 -0700 Subject: [PATCH 2/7] Add: README.md for JIRA --- contrib/sandbox/packages/jira/README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 contrib/sandbox/packages/jira/README.md diff --git a/contrib/sandbox/packages/jira/README.md b/contrib/sandbox/packages/jira/README.md new file mode 100644 index 0000000000..a9ad86b34c --- /dev/null +++ b/contrib/sandbox/packages/jira/README.md @@ -0,0 +1,21 @@ +## Disclaimer +This documentation is written as of 06/17/2014. JIRA 6.3 implements OAuth1. Most of this doc would need to be revised when JIRA switches to OAuth2. + +## Steps +1. Generate RSA public/private key pair + ``` + # This will create a 2048 length RSA private key + $openssl genrsa -out mykey.pem 2048 + ``` + + ``` + # Now, create the public key associated with that private key + openssl rsa -in mykey.pem -pubout + ``` +2. Generate a consumer key. You can use python uuid.uuid4() to do this. +3. Configure JIRA for external access: + * Go to AppLinks section of your JIRA - https://JIRA_SERVER/plugins/servlet/applinks/listApplicationLinks + * Create a Generic Application with some fake URL + * Click Edit, hit IncomingAuthentication. Plug in the consumer key and RSA public key you generated. +4. Get access token using this [script](https://github.com/lakshmi-kannan/jira-oauth-access-token-generator/blob/master/generate_access_token.py) +5. Plug in the access token and access secret into the sensor or action. You are good to make JIRA calls. Note: OAuth token expires. You'll have to repeat the process based on the expiry date. From 0919749bd429f5909e308a5513ec804516f28ae6 Mon Sep 17 00:00:00 2001 From: Lakshmi Kannan Date: Thu, 14 Aug 2014 10:19:17 -0700 Subject: [PATCH 3/7] Fix: Better error message for cert file not found. --- contrib/sandbox/packages/jira/actions/create_issue.py | 2 +- contrib/sandbox/packages/jira/sensors/jira_sensor.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/contrib/sandbox/packages/jira/actions/create_issue.py b/contrib/sandbox/packages/jira/actions/create_issue.py index e2512ec569..d7360c9afc 100644 --- a/contrib/sandbox/packages/jira/actions/create_issue.py +++ b/contrib/sandbox/packages/jira/actions/create_issue.py @@ -53,7 +53,7 @@ def _parse_args(args): def _get_jira_client(params): if not os.path.exists(RSA_CERT_FILE): - raise Exception('Cert file required for JIRA OAuth.') + raise Exception('Cert file for JIRA OAuth not found at %s.' % RSA_CERT_FILE) rsa_key = _read_cert(RSA_CERT_FILE) oauth_creds = { 'access_token': params['oauth_token'], diff --git a/contrib/sandbox/packages/jira/sensors/jira_sensor.py b/contrib/sandbox/packages/jira/sensors/jira_sensor.py index f4eb5d2c23..2c4474f021 100644 --- a/contrib/sandbox/packages/jira/sensors/jira_sensor.py +++ b/contrib/sandbox/packages/jira/sensors/jira_sensor.py @@ -31,7 +31,7 @@ def _read_cert(self, file_path): def setup(self): global RSA_CERT_FILE if not os.path.exists(RSA_CERT_FILE): - raise Exception('Cert file required for JIRA OAuth.') + raise Exception('Cert file for JIRA OAuth not found at %s.' % RSA_CERT_FILE) # The contents of the rsa.pem file generated (the private RSA key) self._rsa_key = self._read_cert(RSA_CERT_FILE) @@ -77,4 +77,3 @@ def _dispatch_trigger(self, proj): payload['project_url'] = proj.self trigger['payload'] = payload self._container_service.dispatch(trigger) - From a210e446b3b915730e519756925c2e5478bf82c7 Mon Sep 17 00:00:00 2001 From: Lakshmi Kannan Date: Thu, 14 Aug 2014 10:21:29 -0700 Subject: [PATCH 4/7] Fix: Add proj.key to list of projects available --- contrib/sandbox/packages/jira/sensors/jira_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/sandbox/packages/jira/sensors/jira_sensor.py b/contrib/sandbox/packages/jira/sensors/jira_sensor.py index 2c4474f021..0b79eeba00 100644 --- a/contrib/sandbox/packages/jira/sensors/jira_sensor.py +++ b/contrib/sandbox/packages/jira/sensors/jira_sensor.py @@ -54,7 +54,7 @@ def start(self): for proj in self._jira_client.projects(): if proj.key not in self._projects_available: self._dispatch_trigger(proj) - self._projects_available.add(proj) + self._projects_available.add(proj.key) time.sleep(self._sleep_time) def stop(self): From 1da3acd23a04669a86778b75a8906c85c26c8b84 Mon Sep 17 00:00:00 2001 From: Lakshmi Kannan Date: Thu, 14 Aug 2014 10:58:40 -0700 Subject: [PATCH 5/7] Fix: Stub out config params into its own file --- .../packages/jira/actions/create_issue.py | 55 ++++++++++++------- .../packages/jira/actions/jira_config.json | 6 ++ 2 files changed, 41 insertions(+), 20 deletions(-) mode change 100644 => 100755 contrib/sandbox/packages/jira/actions/create_issue.py create mode 100644 contrib/sandbox/packages/jira/actions/jira_config.json diff --git a/contrib/sandbox/packages/jira/actions/create_issue.py b/contrib/sandbox/packages/jira/actions/create_issue.py old mode 100644 new mode 100755 index d7360c9afc..16e8e5a043 --- a/contrib/sandbox/packages/jira/actions/create_issue.py +++ b/contrib/sandbox/packages/jira/actions/create_issue.py @@ -3,11 +3,16 @@ # Requirements # pip install jira +try: + import simplejson as json +except ImportError: + import json import os import sys + from jira.client import JIRA -RSA_CERT_FILE = '/home/vagrant/jira.pem' +CONFIG_FILE = './jira_config.json' class AuthedJiraClient(object): @@ -40,36 +45,45 @@ def _read_cert(file_path): def _parse_args(args): params = {} - params['jira_server'] = args[1] - params['oauth_token'] = args[2] - params['oauth_secret'] = args[3] - params['consumer_key'] = args[4] - params['project_name'] = args[5] - params['issue_summary'] = args[6] - params['issue_description'] = args[7] - params['issue_type'] = args[8] + params['project_name'] = args[1] + params['issue_summary'] = args[2] + params['issue_description'] = args[3] + params['issue_type'] = args[4] return params -def _get_jira_client(params): - if not os.path.exists(RSA_CERT_FILE): - raise Exception('Cert file for JIRA OAuth not found at %s.' % RSA_CERT_FILE) - rsa_key = _read_cert(RSA_CERT_FILE) +def _get_jira_client(config): + rsa_cert_file = config['rsa_cert_file'] + if not os.path.exists(rsa_cert_file): + raise Exception('Cert file for JIRA OAuth not found at %s.' % rsa_cert_file) + rsa_key = _read_cert(rsa_cert_file) oauth_creds = { - 'access_token': params['oauth_token'], - 'access_token_secret': params['oauth_secret'], - 'consumer_key': params['consumer_key'], + 'access_token': config['oauth_token'], + 'access_token_secret': config['oauth_secret'], + 'consumer_key': config['consumer_key'], 'key_cert': rsa_key } - jira_client = AuthedJiraClient(params['jira_server'], oauth_creds) + jira_client = AuthedJiraClient(config['jira_server'], oauth_creds) return jira_client +def _get_config(): + global CONFIG_FILE + if not os.path.exists(CONFIG_FILE): + raise Exception('Config file not found at %s.' % CONFIG_FILE) + with open(CONFIG_FILE) as f: + return json.load(f) + + def main(args): + try: + client = _get_jira_client(_get_config()) + except Exception as e: + sys.stderr.write('Failed to create JIRA client: %s\n' % str(e)) + sys.exit(1) + params = _parse_args(args) - client = _get_jira_client(params) proj = params['project_name'] - try: if not client.is_project_exists(proj): raise Exception('Project ' + proj + ' does not exist.') @@ -78,7 +92,8 @@ def main(args): desc=params['issue_description'], issuetype=params['issue_type']) except Exception as e: - sys.stderr.write(e.message + '\n') + sys.stderr.write(str(e) + '\n') + sys.exit(2) else: sys.stdout.write('Issue ' + issue + ' created.\n') diff --git a/contrib/sandbox/packages/jira/actions/jira_config.json b/contrib/sandbox/packages/jira/actions/jira_config.json new file mode 100644 index 0000000000..b38f9a732d --- /dev/null +++ b/contrib/sandbox/packages/jira/actions/jira_config.json @@ -0,0 +1,6 @@ +{ + "rsa_cert_file": "/home/vagrant/jira.pem", + "oauth_token": "", + "oauth_secret": "", + "consumer_key": "" +} From db90e02d77c564158ac2e53f39f5cb81c78c3d6b Mon Sep 17 00:00:00 2001 From: Lakshmi Kannan Date: Thu, 14 Aug 2014 11:10:49 -0700 Subject: [PATCH 6/7] Fix: Stub out config params into a config file (sensor) --- .../packages/jira/sensors/jira_config.json | 6 ++++ .../packages/jira/sensors/jira_sensor.py | 34 +++++++++++++------ 2 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 contrib/sandbox/packages/jira/sensors/jira_config.json diff --git a/contrib/sandbox/packages/jira/sensors/jira_config.json b/contrib/sandbox/packages/jira/sensors/jira_config.json new file mode 100644 index 0000000000..b38f9a732d --- /dev/null +++ b/contrib/sandbox/packages/jira/sensors/jira_config.json @@ -0,0 +1,6 @@ +{ + "rsa_cert_file": "/home/vagrant/jira.pem", + "oauth_token": "", + "oauth_secret": "", + "consumer_key": "" +} diff --git a/contrib/sandbox/packages/jira/sensors/jira_sensor.py b/contrib/sandbox/packages/jira/sensors/jira_sensor.py index 0b79eeba00..00635b87b2 100644 --- a/contrib/sandbox/packages/jira/sensors/jira_sensor.py +++ b/contrib/sandbox/packages/jira/sensors/jira_sensor.py @@ -1,9 +1,16 @@ +# Requirements +# pip install jira + +try: + import simplejson as json +except ImportError: + import json import os import time from jira.client import JIRA -RSA_CERT_FILE = '/home/vagrant/jira.pem' +CONFIG_FILE = './jira_config.json' class JIRASensor(object): @@ -23,22 +30,29 @@ def __init__(self, container_service): self._access_secret = u'' self._projects_available = None self._sleep_time = 30 + self._config = None def _read_cert(self, file_path): with open(file_path) as f: return f.read() - def setup(self): - global RSA_CERT_FILE - if not os.path.exists(RSA_CERT_FILE): - raise Exception('Cert file for JIRA OAuth not found at %s.' % RSA_CERT_FILE) + def _parse_config(self): + global CONFIG_FILE + if not os.path.exists(CONFIG_FILE): + raise Exception('Config file %s not found.' % CONFIG_FILE) + with open(CONFIG_FILE) as f: + self._config = json.load(f) + rsa_cert_file = self._config['rsa_cert_file'] + if not os.path.exists(rsa_cert_file): + raise Exception('Cert file for JIRA OAuth not found at %s.' % rsa_cert_file) + self._rsa_key = self._read_cert(rsa_cert_file) - # The contents of the rsa.pem file generated (the private RSA key) - self._rsa_key = self._read_cert(RSA_CERT_FILE) + def setup(self): + self._parse_config() oauth_creds = { - 'access_token': self._access_token, - 'access_token_secret': self._access_secret, - 'consumer_key': self._consumer_key, + 'access_token': self._config['oauth_token'], + 'access_token_secret': self._config['oauth_secret'], + 'consumer_key': self._config['consumer_key'], 'key_cert': self._rsa_key } From 2835828a2acc62602954381b466773e94728cf9b Mon Sep 17 00:00:00 2001 From: Lakshmi Kannan Date: Thu, 14 Aug 2014 11:13:23 -0700 Subject: [PATCH 7/7] Fix: Improve README.md --- contrib/sandbox/packages/jira/README.md | 28 ++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/contrib/sandbox/packages/jira/README.md b/contrib/sandbox/packages/jira/README.md index a9ad86b34c..81c49b2c42 100644 --- a/contrib/sandbox/packages/jira/README.md +++ b/contrib/sandbox/packages/jira/README.md @@ -1,3 +1,29 @@ +# JIRA integration +This pack consists of a sample JIRA sensor and a JIRA action. + +## JIRA sensor +The sensor monitors for new projects and sends a trigger into the system whenever there is a new project. + +## JIRA action +The action script allows you to create a JIRA issue. + +## Requirements +To use either of the sensor or action, following are the dependencies: + +1. Python 2.7 or later. (Might work with 2.6. Not tested.) +2. pip install jira # installs python JIRA client + +## Configuration +Sensor and action come with a json configuration file (jira_config.json). You'll need to configure the following: + +1. JIRA server +2. OAuth token +3. OAuth secret +4. Consumer key + +To get these OAuth credentials, take a look at OAuth section. + +## OAuth ## Disclaimer This documentation is written as of 06/17/2014. JIRA 6.3 implements OAuth1. Most of this doc would need to be revised when JIRA switches to OAuth2. @@ -17,5 +43,5 @@ This documentation is written as of 06/17/2014. JIRA 6.3 implements OAuth1. Most * Go to AppLinks section of your JIRA - https://JIRA_SERVER/plugins/servlet/applinks/listApplicationLinks * Create a Generic Application with some fake URL * Click Edit, hit IncomingAuthentication. Plug in the consumer key and RSA public key you generated. -4. Get access token using this [script](https://github.com/lakshmi-kannan/jira-oauth-access-token-generator/blob/master/generate_access_token.py) +4. Get access token using this [script](https://github.com/lakshmi-kannan/jira-oauth-access-token-generator/blob/master/generate_access_token.py). These are the ones that are printed at the last. Save these keys somewhere safe. 5. Plug in the access token and access secret into the sensor or action. You are good to make JIRA calls. Note: OAuth token expires. You'll have to repeat the process based on the expiry date.