Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' of github.com:StackStorm-Exchange/stackstorm-jira
- Loading branch information
Showing
6 changed files
with
244 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
jira | ||
pyyaml | ||
cryptography | ||
pyjwt | ||
jira==2.0.0 | ||
pyyaml==3.13 | ||
cryptography==2.4.1 | ||
pyjwt==1.6.4 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
# See ./requirements.txt for requirements. | ||
import os | ||
|
||
from jira.client import JIRA | ||
|
||
from st2reactor.sensor.base import PollingSensor | ||
|
||
|
||
class JIRASensorForAPIv2(PollingSensor): | ||
''' | ||
Sensor will monitor for any new projects created in JIRA and | ||
emit trigger instance when one is created. | ||
''' | ||
def __init__(self, sensor_service, config=None, poll_interval=5): | ||
super(JIRASensorForAPIv2, self).__init__(sensor_service=sensor_service, | ||
config=config, | ||
poll_interval=poll_interval) | ||
|
||
self._jira_url = None | ||
# 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._poll_interval = 30 | ||
self._project = None | ||
self._issues_in_project = None | ||
self._jql_query = None | ||
self._trigger_name = 'issues_tracker_for_apiv2' | ||
self._trigger_pack = 'jira' | ||
self._trigger_ref = '.'.join([self._trigger_pack, self._trigger_name]) | ||
|
||
def _read_cert(self, file_path): | ||
with open(file_path) as f: | ||
return f.read() | ||
|
||
def setup(self): | ||
self._jira_url = self._config['url'] | ||
auth_method = self._config['auth_method'] | ||
|
||
if auth_method == 'oauth': | ||
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) | ||
self._poll_interval = self._config.get('poll_interval', self._poll_interval) | ||
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_url}, | ||
oauth=oauth_creds) | ||
elif auth_method == 'basic': | ||
basic_creds = (self._config['username'], self._config['password']) | ||
self._jira_client = JIRA(options={'server': self._jira_url}, | ||
basic_auth=basic_creds) | ||
|
||
else: | ||
msg = ('You must set auth_method to either "oauth"', | ||
'or "basic" your jira.yaml config file.') | ||
raise Exception(msg) | ||
|
||
if self._projects_available is None: | ||
self._projects_available = set() | ||
for proj in self._jira_client.projects(): | ||
self._projects_available.add(proj.key) | ||
self._project = self._config.get('project', None) | ||
if not self._project or self._project not in self._projects_available: | ||
raise Exception('Invalid project (%s) to track.' % self._project) | ||
self._jql_query = 'project=%s' % self._project | ||
all_issues = self._jira_client.search_issues(self._jql_query, maxResults=None) | ||
self._issues_in_project = {issue.key: issue for issue in all_issues} | ||
|
||
def poll(self): | ||
self._detect_new_issues() | ||
|
||
def cleanup(self): | ||
pass | ||
|
||
def add_trigger(self, trigger): | ||
pass | ||
|
||
def update_trigger(self, trigger): | ||
pass | ||
|
||
def remove_trigger(self, trigger): | ||
pass | ||
|
||
def _detect_new_issues(self): | ||
new_issues = self._jira_client.search_issues(self._jql_query, maxResults=50, startAt=0) | ||
|
||
for issue in new_issues: | ||
if issue.key not in self._issues_in_project: | ||
self._dispatch_issues_trigger(issue) | ||
self._issues_in_project[issue.key] = issue | ||
|
||
def _dispatch_issues_trigger(self, issue): | ||
trigger = self._trigger_ref | ||
payload = {} | ||
payload['project'] = self._project | ||
payload['id'] = issue.id | ||
payload['expand'] = issue.raw.get('expand', '') | ||
payload['issue_key'] = issue.key | ||
payload['issue_url'] = issue.self | ||
payload['issue_browse_url'] = self._jira_url + '/browse/' + issue.key | ||
payload['fields'] = issue.raw.get('fields', {}) | ||
self._sensor_service.dispatch(trigger, payload) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
--- | ||
class_name: "JIRASensorForAPIv2" | ||
entry_point: "jira_sensor_for_apiv2.py" | ||
description: "Sensor which monitors JIRA for new tickets" | ||
poll_interval: 30 | ||
trigger_types: | ||
- | ||
name: "issues_tracker_for_apiv2" | ||
description: "Trigger which indicates that a new issue has been created" | ||
payload_schema: | ||
type: "object" | ||
properties: | ||
project: | ||
type: "string" | ||
id: | ||
type: "string" | ||
expand: | ||
type: "string" | ||
issue_key: | ||
type: "string" | ||
issue_url: | ||
type: "string" | ||
issue_browse_url: | ||
type: "string" | ||
fields: | ||
type: "object" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import mock | ||
|
||
from st2tests.base import BaseSensorTestCase | ||
|
||
from jira_sensor_for_apiv2 import JIRASensorForAPIv2 | ||
|
||
JIRA_URL = "http://jira.hoge.com/" | ||
PROJECT_NAME = "PROJECT" | ||
ISSUE_ID = "112" | ||
ISSUE_SELF = "http://jira.hoge.com/rest/api/2/issue/" + ISSUE_ID | ||
ISSUE_KEY = "ISSUEKEY-1" | ||
|
||
ISSUE = { | ||
"project": PROJECT_NAME, | ||
"id": ISSUE_ID, | ||
"expand": "html,editmeta,changelog", | ||
"fields": { | ||
"assignee": None, | ||
"creator": { | ||
"displayName": "user01@test", | ||
"name": "user-name" | ||
}, | ||
"issuetype": { | ||
"name": "task" | ||
}, | ||
"components": [ | ||
{ | ||
"id": "421203", | ||
"name": "test_components" | ||
} | ||
] | ||
} | ||
} | ||
|
||
MOCK_ISSUE_RAW = ISSUE.copy() | ||
MOCK_ISSUE_RAW["key"] = ISSUE_KEY | ||
MOCK_ISSUE_RAW["self"] = ISSUE_SELF | ||
|
||
PAYLOAD = ISSUE.copy() | ||
PAYLOAD["issue_key"] = ISSUE_KEY | ||
PAYLOAD["issue_url"] = ISSUE_SELF | ||
PAYLOAD["issue_browse_url"] = JIRA_URL + '/browse/' + ISSUE_KEY | ||
|
||
TRIGGER = { | ||
"trace_context": None, | ||
"trigger": "jira.issues_tracker_for_apiv2", | ||
"payload": PAYLOAD | ||
} | ||
|
||
|
||
class JIRASensorForAPIv2TestCase(BaseSensorTestCase): | ||
maxDiff = None | ||
sensor_cls = JIRASensorForAPIv2 | ||
|
||
def test_poll(self): | ||
sensor = self.get_sensor_instance() | ||
sensor._jira_client = mock.Mock() | ||
sensor._jira_client.search_issues.return_value = [] | ||
sensor._issues_in_project = {} | ||
|
||
# no issues | ||
sensor.poll() | ||
self.assertEqual(self.get_dispatched_triggers(), []) | ||
|
||
# 1 new issue | ||
issue = mock.Mock() | ||
issue.raw = MOCK_ISSUE_RAW | ||
issue.id = ISSUE_ID | ||
issue.self = ISSUE_SELF | ||
issue.key = ISSUE_KEY | ||
|
||
sensor._project = PROJECT_NAME | ||
sensor._jira_url = JIRA_URL | ||
sensor._jira_client.search_issues.return_value = [issue] | ||
|
||
sensor.poll() | ||
payload = self.get_dispatched_triggers()[0]['payload'] | ||
self.assertEqual(payload, PAYLOAD) | ||
|
||
# 1 new issue | ||
issue = mock.Mock() | ||
issue.raw = MOCK_ISSUE_RAW | ||
issue.id = ISSUE_ID | ||
issue.self = ISSUE_SELF | ||
issue.key = ISSUE_KEY | ||
|
||
sensor._project = PROJECT_NAME | ||
sensor._jira_url = JIRA_URL | ||
sensor._jira_client.search_issues.return_value = [issue] | ||
|
||
sensor.poll() | ||
payload = self.get_dispatched_triggers()[0]['payload'] | ||
self.assertEqual(payload, PAYLOAD) |