-
-
Notifications
You must be signed in to change notification settings - Fork 782
RFR: Add sample JIRA sensor and action (Pass #1) #379
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6e44d3f
c767f8c
0919749
a210e44
1da3acd
db90e02
2835828
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| # 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. | ||
|
|
||
| ## 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). 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. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| #!/usr/bin/env python | ||
|
|
||
| # Requirements | ||
| # pip install jira | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do we handle dependencies for now? Capture in pack README? Add to requirements.txt? Create extra pack-level requirement.txt? Create pack setup.py which will take care of all requirements? README is fine by me for alpha. Keep on capturing for post-alpha. |
||
|
|
||
| try: | ||
| import simplejson as json | ||
| except ImportError: | ||
| import json | ||
| import os | ||
| import sys | ||
|
|
||
| from jira.client import JIRA | ||
|
|
||
| CONFIG_FILE = './jira_config.json' | ||
|
|
||
|
|
||
| class AuthedJiraClient(object): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. authed! (No suggestion)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Picked up from previous work :). |
||
| 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['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(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': config['oauth_token'], | ||
| 'access_token_secret': config['oauth_secret'], | ||
| 'consumer_key': config['consumer_key'], | ||
| 'key_cert': rsa_key | ||
| } | ||
| 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) | ||
| 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(str(e) + '\n') | ||
| sys.exit(2) | ||
| else: | ||
| sys.stdout.write('Issue ' + issue + ' created.\n') | ||
|
|
||
| if __name__ == '__main__': | ||
| main(sys.argv) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "rsa_cert_file": "/home/vagrant/jira.pem", | ||
| "oauth_token": "", | ||
| "oauth_secret": "", | ||
| "consumer_key": "" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @m4dcoder Do we recognize jsonschema type that this is a host name so it can be string or ip and the validation happens?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's whatever jsonschema supports. So i believe you can do a oneof multiple type string with different regex patterns. |
||
| }, | ||
| "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" | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @m4dcoder : How do I specify an enum?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. { |
||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "rsa_cert_file": "/home/vagrant/jira.pem", | ||
| "oauth_token": "", | ||
| "oauth_secret": "", | ||
| "consumer_key": "" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| # Requirements | ||
| # pip install jira | ||
|
|
||
| try: | ||
| import simplejson as json | ||
| except ImportError: | ||
| import json | ||
| import os | ||
| import time | ||
|
|
||
| from jira.client import JIRA | ||
|
|
||
| CONFIG_FILE = './jira_config.json' | ||
|
|
||
|
|
||
| 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' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another hardcoded configuration. Need a temp solution for alpha, and longer term solution for later. |
||
| # 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 | ||
| self._config = None | ||
|
|
||
| def _read_cert(self, file_path): | ||
| with open(file_path) as f: | ||
| return f.read() | ||
|
|
||
| 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) | ||
|
|
||
| def setup(self): | ||
| self._parse_config() | ||
| oauth_creds = { | ||
| 'access_token': self._config['oauth_token'], | ||
| 'access_token_secret': self._config['oauth_secret'], | ||
| 'consumer_key': self._config['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(): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can also track removal of a project. Perhaps a new trigger or a delete property on the trigger. I prefer a new trigger to identify a deleted project.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just some random sensor so people can look at it and build their own. I can add a sensor that looks for deleted triggers. |
||
| if proj.key not in self._projects_available: | ||
| self._dispatch_trigger(proj) | ||
| self._projects_available.add(proj.key) | ||
| 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) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @enykeev: Let's about this later tonight. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add Header, a sentence of "what is this" - a JIRA pack with one action and one sensor - at the beginning.
Add some details of how to configure sensor and action.