├── .gitignore
├── LICENSE
├── README.md
├── build.py
├── example_config.ini
├── setup.cfg
└── src
├── main
└── python
│ └── icinga2jira.py
└── unittest
└── python
├── CloseIssue_tests.py
├── IcingaEnvironment_tests.py
├── Issue_tests.py
├── OpenIssue_tests.py
└── icinga2jira_tests.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 |
3 | # C extensions
4 | *.so
5 |
6 | # Packages
7 | *.egg
8 | *.egg-info
9 | dist
10 | build
11 | eggs
12 | parts
13 | bin
14 | var
15 | sdist
16 | develop-eggs
17 | .installed.cfg
18 | lib
19 | lib64
20 | __pycache__
21 |
22 | # Installer logs
23 | pip-log.txt
24 |
25 | # Unit test / coverage reports
26 | .coverage
27 | .tox
28 | nosetests.xml
29 |
30 | # Translations
31 | *.mo
32 |
33 | # Mr Developer
34 | .mr.developer.cfg
35 | .project
36 | .pydevproject
37 |
38 | target
39 |
40 | # common virtualenv names
41 | venv
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 | {description}
294 | Copyright (C) {year} {fullname}
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | {signature of Ty Coon}, 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Plugin for Icinga to automatically report alerts to Jira
2 |
3 | # This project is DEPRECATED and not any longer supported
4 |
5 | Icinga offers the possibility to relay notifications via command scripts. This plugin is intended to forward these
6 | notifications automatically into a Jira ticket system.
7 |
8 | This plugin currently only handles the following notification types:
9 |
10 | * ``PROBLEM`` for service and host problems
11 | * ``RECOVERY`` for service and host problems
12 |
13 | This plugin is written in Python. It works on Python 2.6 and 2.7.
14 |
15 | For installation instructions and development issues please go into our wiki:
16 |
17 | https://github.com/ImmobilienScout24/python-icinga-jira-plugin/wiki
18 |
19 | ## Contributors
20 |
21 | * Oliver Hoogvliet
22 | * Maximilien Riehl
23 | * Valentin Haenel
24 |
--------------------------------------------------------------------------------
/build.py:
--------------------------------------------------------------------------------
1 | from pybuilder.core import use_plugin, init
2 |
3 | use_plugin("python.core")
4 | use_plugin("python.unittest")
5 | use_plugin("python.install_dependencies")
6 | use_plugin("python.pylint")
7 | use_plugin("python.distutils")
8 | use_plugin("python.pydev")
9 | use_plugin('copy_resources')
10 | use_plugin("python.coverage")
11 |
12 | name = 'python-icinga2jira-plugin'
13 | version = '0.1.0'
14 | summary = 'Icinga plugin for sending alarming tickets to Jira'
15 |
16 | default_task = ["analyze", "publish"]
17 |
18 |
19 | @init
20 | def set_properties(project):
21 | project.build_depends_on("pylint")
22 | project.build_depends_on("mock")
23 | project.build_depends_on("docopt")
24 | project.build_depends_on("jira-python")
25 | project.build_depends_on("jinja2")
26 | project.set_property('copy_resources_target', '$dir_dist')
27 | project.get_property('copy_resources_glob').extend(['setup.cfg'])
28 | project.install_file('/usr/lib64/icinga/plugins', 'icinga2jira.py')
29 |
30 |
31 | @init(environments='teamcity')
32 | def set_properties_for_teamcity_builds(project):
33 | import os
34 | project.version = '%s-%s' % (project.version, os.environ.get('BUILD_NUMBER', 0))
35 | project.default_task = ['install_dependencies', 'publish']
36 |
--------------------------------------------------------------------------------
/example_config.ini:
--------------------------------------------------------------------------------
1 | [settings]
2 | url = https://example.com
3 | username =
4 | password =
5 | jira_project_key = YPK
6 | jira_issue_type = Technical task
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_rpm]
2 | requires = python-jira python-docopt python-jinja2
3 |
--------------------------------------------------------------------------------
/src/main/python/icinga2jira.py:
--------------------------------------------------------------------------------
1 | """
2 | Usage:
3 | icinga2jira.py ( -c config )
4 |
5 | Options:
6 | -h --help Show this screen.
7 | -c, --config CONFIG config file for plugin
8 |
9 | """
10 | from __future__ import print_function
11 |
12 | import os
13 | import sys
14 | import ConfigParser
15 | import textwrap
16 | from abc import ABCMeta, abstractmethod
17 |
18 | from docopt import docopt
19 | from jira.client import JIRA
20 | from jira.exceptions import JIRAError
21 | from jinja2 import Template
22 |
23 |
24 | MANDATORY_CONFIG_ENTRIES = [
25 | 'url', 'username', 'password', 'jira_project_key', 'jira_issue_type']
26 |
27 |
28 | class CantCloseTicketException(Exception):
29 | pass
30 |
31 |
32 | class UnknownIssueException(Exception):
33 | pass
34 |
35 |
36 | class IcingaEnvironment(object):
37 | MAPPING = {'host_address': 'ICINGA_HOSTADDRESS',
38 | 'host_name': 'ICINGA_HOSTNAME',
39 | 'host_output': 'ICINGA_HOSTOUTPUT',
40 | 'host_problem_id': 'ICINGA_HOSTPROBLEMID',
41 | 'host_state': 'ICINGA_HOSTSTATE',
42 | 'last_service_problem_id': 'ICINGA_LASTSERVICEPROBLEMID',
43 | 'last_host_problem_id': 'ICINGA_LASTHOSTPROBLEMID',
44 | 'notification_author': 'ICINGA_NOTIFICATIONAUTHOR',
45 | 'notification_comment': 'ICINGA_NOTIFICATIONCOMMENT',
46 | 'notification_type': 'ICINGA_NOTIFICATIONTYPE',
47 | 'service_description': 'ICINGA_SERVICEDESC',
48 | 'service_output': 'ICINGA_SERVICEOUTPUT',
49 | 'service_priority_id': 'ICINGA_SERVICEJIRA_PRIORITY_ID',
50 | 'service_problem_id': 'ICINGA_SERVICEPROBLEMID',
51 | 'service_state': 'ICINGA_SERVICESTATE',
52 | 'short_date_time': 'ICINGA_SHORTDATETIME',
53 | }
54 |
55 | ICINGA_PREFIX = "ICI"
56 |
57 | def __init__(self, environment):
58 | for attribute_name, argument_name in self.MAPPING.iteritems():
59 | if argument_name in environment and environment[argument_name] not in [None, '']:
60 | setattr(self, attribute_name, environment[argument_name])
61 | else:
62 | setattr(self, attribute_name, None)
63 |
64 | self._validate()
65 |
66 | def _extract_missing_values(self, keys):
67 | missing = []
68 | for env_key in keys:
69 | if getattr(self, env_key) in [None, '']:
70 | missing.append(self.MAPPING[env_key])
71 | return missing
72 |
73 | def _validate_recovery_data(self):
74 | missing_service_recovery_data = self._extract_missing_values(
75 | ['last_service_problem_id'])
76 | missing_host_recovery_data = self._extract_missing_values(
77 | ['last_host_problem_id'])
78 |
79 | if self.is_service_issue():
80 | if len(missing_service_recovery_data) != 0:
81 | raise ValueError('Environment is missing values: %s' %
82 | ', '.join(set(missing_service_recovery_data)))
83 | elif self.is_host_issue():
84 | if len(missing_host_recovery_data) != 0:
85 | raise ValueError('Environment is missing values: %s' %
86 | ', '.join(set(missing_host_recovery_data)))
87 |
88 | def _validate_problem_data(self):
89 | missing_service_data = self._extract_missing_values(
90 | ['host_name', 'service_state', 'service_problem_id'])
91 | missing_host_data = self._extract_missing_values(
92 | ['host_name', 'host_state', 'host_problem_id'])
93 | if len(missing_service_data) != 0 and len(missing_host_data) != 0:
94 | raise ValueError('Environment is missing values: %s' %
95 | ', '.join(set(missing_service_data + missing_host_data)))
96 |
97 | def _validate(self):
98 | if not self.notification_type:
99 | raise ValueError('Environment is missing %s' %
100 | self.MAPPING['notification_type'])
101 |
102 | if (self.has_new_problem()):
103 | self._validate_problem_data()
104 | elif self.is_recovered():
105 | self._validate_recovery_data()
106 | else:
107 | pass
108 |
109 | def has_new_problem(self):
110 | return self.notification_type == 'PROBLEM'
111 |
112 | def is_recovered(self):
113 | return self.notification_type == 'RECOVERY'
114 |
115 | def is_service_issue(self):
116 | return self.service_problem_id or self.last_service_problem_id
117 |
118 | def is_host_issue(self):
119 | return not self.is_service_issue()
120 |
121 | def get_recovery_last_problem_id(self):
122 | if not self.is_recovered():
123 | raise TypeError(
124 | "Ticket does not act in recovery mode, but %s" % self.notification_type)
125 | if self.is_service_issue():
126 | return self.last_service_problem_id
127 | return self.last_host_problem_id
128 |
129 | def _create_icinga_label(self, icinga_id):
130 | return "%s#%s#%s" % (self.ICINGA_PREFIX, icinga_id, self.host_name)
131 |
132 | def get_jira_recovery_label(self):
133 | return self._create_icinga_label(self.get_recovery_last_problem_id())
134 |
135 | def create_labels_list(self):
136 | labels = []
137 | if self.is_service_issue():
138 | labels.append(self._create_icinga_label(self.service_problem_id))
139 | else:
140 | labels.append(self._create_icinga_label(self.host_problem_id))
141 | return labels
142 |
143 |
144 | def issue_factory(jira, icinga_environment, config):
145 | if icinga_environment.has_new_problem():
146 | return OpenIssue(jira, config, icinga_environment)
147 |
148 | elif icinga_environment.is_recovered():
149 | return CloseIssue(jira, icinga_environment)
150 |
151 | else:
152 | raise UnknownIssueException("Unknown icinga alert")
153 |
154 |
155 | class Issue(object):
156 | __metaclass__ = ABCMeta
157 |
158 | @abstractmethod
159 | def execute(self):
160 | pass
161 |
162 | def create_description(self):
163 | DESCRIPTION_TEMPLATE = textwrap.dedent("""
164 | {% if notification_type == "PROBLEM" %}
165 | {color:#3b0b0b}*Icinga Problem Alert*{color}
166 | {% elif notification_type == "RECOVERY" %}
167 | {color:#0b3b0b}*Icinga Recovery Alert*{color}
168 | {% elif notification_type == "ACKNOWLEDGEMENT" %}
169 | {color:#0f5d94}*Icinga Acknowledgement*{color}
170 | {% else %}
171 | {color:#585858}*Unknown Alert*{color}
172 | {% endif %}
173 |
174 | The following information was provided by Icinga:
175 | * Date & Time: {{short_date_time}}
176 | * Host Address: {{host_address}}
177 | {% if service_description %}
178 | * Status Information: {{service_output}}
179 | * Current Host State: {{host_state}}
180 | * Current Service State: {{service_state}}
181 | {% else %}
182 | * Status Information: {{host_output}}
183 | * Current Host State: {{host_state}}
184 | {% endif %}
185 | {% if notification_type == "ACKNOWLEDGEMENT" %}
186 | * Notification Author: {{ notification_author }}
187 | * Notification Comment: {{ notification_comment }}
188 | {% endif %}
189 |
190 | {% if notification_type == "RECOVERY" %}
191 | This ticket was closed automatically.
192 | {% endif %}
193 | """)
194 | template = Template(DESCRIPTION_TEMPLATE, trim_blocks=True)
195 | return str(template.render(self.icinga_environment.__dict__)).strip()
196 |
197 |
198 | class OpenIssue(Issue):
199 |
200 | def __init__(self, jira, config, icinga_environment):
201 | self.jira = jira
202 |
203 | self.project_key = config['jira_project_key']
204 | self.issue_type = config['jira_issue_type']
205 |
206 | self.icinga_environment = icinga_environment
207 |
208 | def execute(self):
209 | return [self.jira.create_issue(fields=self._create_issue_dict())]
210 |
211 | def _create_issue_dict(self):
212 | return {'project': {'key': self.project_key},
213 | 'summary': self._create_summary(),
214 | 'description': self.create_description(),
215 | 'issuetype': {'name': self.issue_type},
216 | 'labels': self.icinga_environment.create_labels_list()
217 | }
218 |
219 | def _create_summary(self):
220 | if self.icinga_environment.is_service_issue():
221 | return ("ICINGA: %s on %s is %s" %
222 | (self.icinga_environment.service_description,
223 | self.icinga_environment.host_name,
224 | self.icinga_environment.service_state))
225 | else:
226 | return "ICINGA: %s is %s" % (self.icinga_environment.host_name,
227 | self.icinga_environment.host_state)
228 |
229 |
230 | class CloseIssue(Issue):
231 |
232 | def __init__(self, jira, icinga_environment):
233 | self.jira = jira
234 | self.icinga_environment = icinga_environment
235 |
236 | def execute(self):
237 | issues = self._find_jira_issues_by_label()
238 | handled_issues = []
239 | for issue in issues:
240 | try:
241 | self._close(issue)
242 | self._set_comment(issue)
243 | handled_issues.append(issue)
244 | except CantCloseTicketException as e:
245 | print("WARNING: %s could not be closed, reason: %s" %
246 | (issue.key, str(e)))
247 | return handled_issues
248 |
249 | def _find_jira_issues_by_label(self):
250 | return self.jira.search_issues("labels='%s'" % self.icinga_environment.get_jira_recovery_label())
251 |
252 | def _set_comment(self, issue):
253 | self.jira.add_comment(issue, self.create_description())
254 |
255 | def _close(self, issue):
256 | try:
257 | close_transition_id = self._get_close_transition(issue)
258 | if close_transition_id:
259 | self.jira.transition_issue(issue, close_transition_id)
260 | except JIRAError as jira_error:
261 | raise CantCloseTicketException(jira_error)
262 |
263 | def _get_close_transition(self, issue):
264 | for transition in self.jira.transitions(issue):
265 | if transition['name'] == 'Close':
266 | return int(transition['id'])
267 | raise CantCloseTicketException(
268 | "Ticket does not have 'Close' transition; maybe it's already closed")
269 |
270 |
271 | def open_jira_session(server, username, password, verify=False):
272 | return JIRA(options={'server': server, 'verify': verify},
273 | basic_auth=(username, password))
274 |
275 |
276 | def parse_and_validate_config_file(file_pointer):
277 | config_parser = ConfigParser.ConfigParser()
278 | config_parser.readfp(file_pointer)
279 | config = dict(config_parser.items('settings'))
280 | for key in MANDATORY_CONFIG_ENTRIES:
281 | if key not in config:
282 | raise ValueError('config file is missing: %s' % key)
283 | return config
284 |
285 |
286 | def print_usage_and_exit(arguments):
287 | print(arguments)
288 | sys.exit(1)
289 |
290 |
291 | def parse_arguments(argv=None):
292 | arguments = docopt(__doc__, argv=argv)
293 | return arguments
294 |
295 |
296 | def read_configuration_file(args):
297 | with open(args['--config']) as file_pointer:
298 | return parse_and_validate_config_file(file_pointer)
299 |
300 |
301 | def create_ticket_list(config, issues):
302 | return ["%s/browse/%s" % (config['url'], issue.key) for issue in issues]
303 |
304 | if __name__ == '__main__':
305 | args = parse_arguments()
306 | try:
307 | config = read_configuration_file(args)
308 | icinga_environment = IcingaEnvironment(os.environ)
309 | except IOError as e:
310 | print("Could not find configuration file: %s" % e)
311 | print_usage_and_exit(args)
312 | except ValueError as e:
313 | print(e)
314 | print_usage_and_exit(args)
315 | except ConfigParser.NoSectionError as e:
316 | print("Configuration file is corrupt: %s" % e)
317 | print_usage_and_exit(args)
318 |
319 | jira = open_jira_session(config['url'],
320 | config['username'],
321 | config['password'])
322 |
323 | try:
324 | issues = issue_factory(jira, icinga_environment, config).execute()
325 | issue_url_list_as_string = ",".join(create_ticket_list(config, issues))
326 | print("Event %s has been successfully handled: %s" %
327 | (icinga_environment.notification_type, issue_url_list_as_string))
328 | except Exception as e:
329 | print("An error occurred while handling event %s: %s" %
330 | (icinga_environment.notification_type, e))
331 | sys.exit(1)
332 |
--------------------------------------------------------------------------------
/src/unittest/python/CloseIssue_tests.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from mock import Mock, patch
4 | from jira.exceptions import JIRAError
5 |
6 | from icinga2jira import CloseIssue, CantCloseTicketException
7 |
8 | ANY_ISSUE = {'id': 'any id'}
9 | ANY_TRANSITIONS = [{'name': 'Close', 'id': 45},
10 | {'name': 'Start Work', 'id': 11},
11 | ]
12 |
13 |
14 | def create_issue_mock(key):
15 | issue = Mock()
16 | issue.key.return_value = key
17 | return issue
18 |
19 |
20 | class TestCloseIssue(unittest.TestCase):
21 | def setUp(self):
22 | self.jira_mock = Mock()
23 | self.ticket = Mock()
24 | self.icinga_environment = Mock()
25 | self.icinga_environment.get_jira_recovery_label.return_value = 'ICI#123'
26 | self.close_issue = CloseIssue(self.jira_mock, self.icinga_environment)
27 | self.print_patcher = patch('__builtin__.print')
28 | self.print_patcher.start()
29 |
30 | def tearDown(self):
31 | self.print_patcher.stop()
32 |
33 | def test_find_jira_issue_by_label(self):
34 | self.jira_mock.search_issues.return_value = 'found issue'
35 |
36 | result = self.close_issue._find_jira_issues_by_label()
37 |
38 | self.assertEqual('found issue', result)
39 | self.ticket.get_jira_recovery_label.assert_called()
40 | self.jira_mock.search_issues.assert_called_with("labels='ICI#123'")
41 |
42 | def test_set_comment(self):
43 | with patch.object(CloseIssue, 'create_description', return_value='comment') as create_mock:
44 | close_issue = CloseIssue(self.jira_mock, self.icinga_environment)
45 |
46 | close_issue._set_comment(ANY_ISSUE)
47 |
48 | create_mock.create_description.assert_called()
49 | self.jira_mock.add_comment.assert_called_with(ANY_ISSUE, 'comment')
50 |
51 | def test_get_close_transition_returns_id_of_close_transition_in_issue(self):
52 | self.jira_mock.transitions.return_value = ANY_TRANSITIONS
53 |
54 | actual_transition_id = self.close_issue._get_close_transition({'id': 'any id'})
55 |
56 | self.assertEqual(45, actual_transition_id)
57 |
58 | def test_get_close_transition_with_unicode(self):
59 | self.jira_mock.transitions.return_value = ANY_TRANSITIONS
60 |
61 | actual_transition_id = self.close_issue._get_close_transition({'id': 'any id'})
62 |
63 | self.assertEqual(45, actual_transition_id)
64 |
65 | def test_get_close_transition_raises_exception_when_no_close_transition_has_been_found(self):
66 | transitions = [{'name': 'Start Work', 'id': 11}]
67 | self.jira_mock.transitions.return_value = transitions
68 |
69 | self.assertRaises(CantCloseTicketException,
70 | self.close_issue._get_close_transition,
71 | ANY_ISSUE)
72 |
73 | def test_get_close_transition_raises_exception_when_no_transitions_have_been_found(self):
74 | self.jira_mock.transitions.return_value = []
75 |
76 | self.assertRaises(CantCloseTicketException,
77 | self.close_issue._get_close_transition,
78 | ANY_ISSUE)
79 |
80 | def test_close_happy_trail(self):
81 | transitions = [{'name': 'Close', 'id': 45}]
82 | self.jira_mock.transitions.return_value = transitions
83 |
84 | self.close_issue._close(ANY_ISSUE)
85 |
86 | self.jira_mock.transition_issue.assert_called_with(ANY_ISSUE, 45)
87 |
88 | def test_close_on_already_closed_ticket_raises_exception(self):
89 | self.jira_mock.transitions.return_value = []
90 |
91 | self.assertRaises(CantCloseTicketException, self.close_issue._close, ANY_ISSUE)
92 |
93 | def test_close_reraises_exception_if_jira_fails(self):
94 | self.jira_mock.transitions.return_value = ANY_TRANSITIONS
95 | self.jira_mock.transition_issue.side_effect = JIRAError
96 |
97 | self.assertRaises(CantCloseTicketException,
98 | self.close_issue._close, ANY_ISSUE)
99 |
100 | def test_execute_is_called_properly_and_returns_list_of_handled_issues(self):
101 | find_mock = Mock(return_value=['issue1', 'issue2'])
102 | close_mock = Mock()
103 | comment_mock = Mock()
104 |
105 | with patch.multiple(CloseIssue,
106 | _find_jira_issues_by_label=find_mock,
107 | _close=close_mock,
108 | _set_comment=comment_mock
109 | ) as values:
110 | close_issue = CloseIssue(self.jira_mock, self.icinga_environment)
111 | result = close_issue.execute()
112 |
113 | self.assertEqual(find_mock.call_count, 1)
114 | self.assertEqual(close_mock.call_count, 2)
115 | comment_mock.assert_called_twice_with(comment_str='comment')
116 | self.assertEqual(['issue1', 'issue2'], result)
117 |
118 | def test_execute_skips_issues_causing_CantCloseTicketException(self):
119 | find_mock = Mock(return_value=[create_issue_mock('a'), create_issue_mock('b')])
120 | close_mock = Mock(side_effect=CantCloseTicketException())
121 | comment_mock = Mock()
122 |
123 | with patch.multiple(CloseIssue,
124 | _find_jira_issues_by_label=find_mock,
125 | _close=close_mock,
126 | _set_comment=comment_mock
127 | ) as values:
128 | close_issue = CloseIssue(self.jira_mock, self.icinga_environment)
129 | result = close_issue.execute()
130 |
131 | self.assertEqual(find_mock.call_count, 1)
132 | self.assertEqual(close_mock.call_count, 2)
133 | comment_mock.assert_not_called()
134 | self.assertEqual([], result)
135 |
--------------------------------------------------------------------------------
/src/unittest/python/IcingaEnvironment_tests.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from icinga2jira import IcingaEnvironment
4 |
5 | ANY_PROJECT_KEY = 'MON'
6 | ANY_ISSUE_TYPE = 'Technical task'
7 | ANY_LAST_SERVICE_PROBLEM_ID = "76540"
8 | ANY_PRIORITY_ID = "12"
9 | ANY_HOST_PROBLEM_ID = "76543"
10 | ANY_SERVICE_PROBLEM_ID = "12345"
11 | ANY_SERVICE_STATE = "any service state"
12 | ANY_HOSTNAME = "myserver1"
13 | ANY_SERVICE_DESCRIPTION = "foo application services"
14 | ANY_COMMENT = "any comment"
15 | ANY_NOTIFICATION_AUTHOR = "any notification author"
16 | ANY_HOST_OUTPUT = 'any host output'
17 | ANY_HOST_STATE = 'any host state'
18 | NOTIFICATION_TYPE_PROBLEM = "PROBLEM"
19 | NOTIFICATION_TYPE_RECOVERY = "RECOVERY"
20 | ANY_SERVICE_MESSAGE = "any service message"
21 | ANY_SHORT_DATE_TIME = "11-26-2013 15:42:05"
22 | ANY_LAST_HOST_PROBLEM_ID = "9999999999999"
23 | ANY_ICINGA_LABEL_STRING_FORMAT = "ICI#%s#%s"
24 |
25 |
26 | def create_valid_environment_dict_for_host_problem():
27 | return {
28 | 'ICINGA_HOSTNAME': ANY_HOSTNAME,
29 | 'ICINGA_HOSTOUTPUT': ANY_HOST_OUTPUT,
30 | 'ICINGA_HOSTPROBLEMID': ANY_HOST_PROBLEM_ID,
31 | 'ICINGA_HOSTSTATE': ANY_HOST_STATE,
32 | 'ICINGA_NOTIFICATIONAUTHOR': ANY_NOTIFICATION_AUTHOR,
33 | 'ICINGA_NOTIFICATIONCOMMENT': ANY_COMMENT,
34 | 'ICINGA_NOTIFICATIONTYPE': NOTIFICATION_TYPE_PROBLEM,
35 | 'ICINGA_SHORTDATETIME': ANY_SHORT_DATE_TIME,
36 | }
37 |
38 | def create_valid_environment_dict_for_host_recovery():
39 | return {
40 | 'ICINGA_HOSTNAME': ANY_HOSTNAME,
41 | 'ICINGA_HOSTOUTPUT': ANY_HOST_OUTPUT,
42 | 'ICINGA_LASTHOSTPROBLEMID': ANY_LAST_HOST_PROBLEM_ID,
43 | 'ICINGA_HOSTSTATE': ANY_HOST_STATE,
44 | 'ICINGA_NOTIFICATIONAUTHOR': ANY_NOTIFICATION_AUTHOR,
45 | 'ICINGA_NOTIFICATIONCOMMENT': ANY_COMMENT,
46 | 'ICINGA_NOTIFICATIONTYPE': NOTIFICATION_TYPE_RECOVERY,
47 | 'ICINGA_SHORTDATETIME': ANY_SHORT_DATE_TIME,
48 | }
49 |
50 | def create_valid_environment_dict_for_service_problem():
51 | return {
52 | 'ICINGA_HOSTNAME': ANY_HOSTNAME,
53 | 'ICINGA_HOSTOUTPUT': ANY_HOST_OUTPUT,
54 | 'ICINGA_HOSTSTATE': ANY_HOST_STATE,
55 | 'ICINGA_NOTIFICATIONAUTHOR': ANY_NOTIFICATION_AUTHOR,
56 | 'ICINGA_NOTIFICATIONCOMMENT': ANY_COMMENT,
57 | 'ICINGA_NOTIFICATIONTYPE': NOTIFICATION_TYPE_PROBLEM,
58 | 'ICINGA_SERVICEDESC': ANY_SERVICE_DESCRIPTION,
59 | 'ICINGA_SERVICEOUTPUT': ANY_SERVICE_MESSAGE,
60 | 'ICINGA_SERVICEPROBLEMID': ANY_SERVICE_PROBLEM_ID,
61 | 'ICINGA_SERVICESTATE': ANY_SERVICE_STATE,
62 | 'ICINGA_SHORTDATETIME': ANY_SHORT_DATE_TIME,
63 | }
64 |
65 | def create_valid_environment_dict_for_service_recovery():
66 | return {
67 | 'ICINGA_HOSTNAME': ANY_HOSTNAME,
68 | 'ICINGA_HOSTOUTPUT': ANY_HOST_OUTPUT,
69 | 'ICINGA_HOSTSTATE': ANY_HOST_STATE,
70 | 'ICINGA_LASTSERVICEPROBLEMID': ANY_LAST_SERVICE_PROBLEM_ID,
71 | 'ICINGA_NOTIFICATIONAUTHOR': ANY_NOTIFICATION_AUTHOR,
72 | 'ICINGA_NOTIFICATIONCOMMENT': ANY_COMMENT,
73 | 'ICINGA_NOTIFICATIONTYPE': NOTIFICATION_TYPE_RECOVERY,
74 | 'ICINGA_SERVICEDESC': ANY_SERVICE_DESCRIPTION,
75 | 'ICINGA_SERVICEOUTPUT': ANY_SERVICE_MESSAGE,
76 | 'ICINGA_SERVICESTATE': ANY_SERVICE_STATE,
77 | 'ICINGA_SERVICEJIRA_PRIORITY_ID': ANY_PRIORITY_ID,
78 | 'ICINGA_SHORTDATETIME': ANY_SHORT_DATE_TIME,
79 | }
80 |
81 |
82 | class IcingaEnvironmentTest(unittest.TestCase):
83 |
84 | def test_icinga_environment_should_skip_nonexisting_or_empty_environment_values(self):
85 | environment = create_valid_environment_dict_for_service_problem()
86 | environment['ICINGA_LASTHOSTPROBLEMID'] = None
87 | environment['ICINGA_HOSTPROBLEMID'] = ''
88 | environment.pop('ICINGA_NOTIFICATIONCOMMENT')
89 |
90 | icinga_environment = IcingaEnvironment(environment)
91 | self.assertEqual(icinga_environment.last_host_problem_id, None)
92 | self.assertEqual(icinga_environment.host_problem_id, None)
93 | self.assertEqual(icinga_environment.notification_comment, None)
94 |
95 |
96 | def test_validation_raises_value_error_with_empty_environment(self):
97 | self.assertRaises(ValueError, IcingaEnvironment, {})
98 |
99 | def test_validation_for_PROBLEM_raises_value_error_with_only_notification_type(self):
100 | environment = {'ICINGA_NOTIFICATIONTYPE': NOTIFICATION_TYPE_PROBLEM}
101 | self.assertRaises(ValueError, IcingaEnvironment, environment)
102 |
103 | def test_validation_for_PROBLEM_succeeds_with_minimal_service_state_environment(self):
104 | environment = {'ICINGA_NOTIFICATIONTYPE': NOTIFICATION_TYPE_PROBLEM,
105 | 'ICINGA_SERVICEDESC': ANY_SERVICE_DESCRIPTION,
106 | 'ICINGA_HOSTNAME': ANY_HOSTNAME,
107 | 'ICINGA_SERVICESTATE': ANY_SERVICE_STATE,
108 | 'ICINGA_SERVICEPROBLEMID': ANY_SERVICE_PROBLEM_ID,
109 | }
110 | IcingaEnvironment(environment)
111 |
112 | def test_validation_for_PROBLEM_raises_ValueError_when_service_problem_id_is_empty(self):
113 | environment = {'ICINGA_NOTIFICATIONTYPE': NOTIFICATION_TYPE_PROBLEM,
114 | 'ICINGA_SERVICEDESC': ANY_SERVICE_DESCRIPTION,
115 | 'ICINGA_HOSTNAME': ANY_HOSTNAME,
116 | 'ICINGA_SERVICESTATE': ANY_SERVICE_STATE,
117 | 'ICINGA_SERVICEPROBLEMID': '',
118 | }
119 | self.assertRaises(ValueError, IcingaEnvironment, environment)
120 |
121 |
122 | def test_validation_for_PROBLEM_succeeds_with_minimal_host_environment(self):
123 | environment = {'ICINGA_NOTIFICATIONTYPE': NOTIFICATION_TYPE_PROBLEM,
124 | 'ICINGA_HOSTNAME': ANY_HOSTNAME,
125 | 'ICINGA_HOSTSTATE': ANY_HOST_STATE,
126 | 'ICINGA_HOSTPROBLEMID': ANY_HOST_PROBLEM_ID,
127 | }
128 | IcingaEnvironment(environment)
129 |
130 | def test_validation_for_PROBLEM_raises_when_host_problem_id_is_empty(self):
131 | environment = {'ICINGA_NOTIFICATIONTYPE': NOTIFICATION_TYPE_PROBLEM,
132 | 'ICINGA_HOSTNAME': ANY_HOSTNAME,
133 | 'ICINGA_HOSTSTATE': ANY_HOST_STATE,
134 | 'ICINGA_HOSTPROBLEMID': '',
135 | }
136 | self.assertRaises(ValueError, IcingaEnvironment, environment)
137 |
138 | def test_validation_for_RECOVERY_succeeds_with_minimal_service_state_environment(self):
139 | environment = {'ICINGA_NOTIFICATIONTYPE': NOTIFICATION_TYPE_RECOVERY,
140 | 'ICINGA_SERVICEDESC': ANY_SERVICE_DESCRIPTION,
141 | 'ICINGA_LASTSERVICEPROBLEMID': ANY_LAST_SERVICE_PROBLEM_ID,
142 | }
143 | IcingaEnvironment(environment)
144 |
145 | def test_validation_for_RECOVERY_succeeds_with_minimal_host_state_environment(self):
146 | environment = {'ICINGA_NOTIFICATIONTYPE': NOTIFICATION_TYPE_RECOVERY,
147 | 'ICINGA_SERVICEDESC': None,
148 | 'ICINGA_LASTHOSTPROBLEMID': ANY_LAST_HOST_PROBLEM_ID,
149 | }
150 | IcingaEnvironment(environment)
151 |
152 | def test_validation_for_RECOVERY_raises_for_missing_service_and_host_problem_id(self):
153 | environment = {'ICINGA_NOTIFICATIONTYPE': NOTIFICATION_TYPE_RECOVERY}
154 | self.assertRaises(ValueError, IcingaEnvironment, environment)
155 |
156 | def test_has_new_problem_returns_true(self):
157 | environment = IcingaEnvironment(create_valid_environment_dict_for_service_problem())
158 | self.assertTrue(environment.has_new_problem())
159 |
160 | def test_service_is_recovered(self):
161 | test_dict = create_valid_environment_dict_for_service_recovery()
162 | environment = IcingaEnvironment(test_dict)
163 | self.assertTrue(environment.is_recovered())
164 |
165 | def test_host_is_recovered(self):
166 | test_dict = create_valid_environment_dict_for_host_recovery()
167 | environment = IcingaEnvironment(test_dict)
168 | self.assertTrue(environment.is_recovered())
169 |
170 | def test_when_is_service_issue_is_true_then_is_host_issue_is_false(self):
171 | environment = IcingaEnvironment(create_valid_environment_dict_for_service_problem())
172 | self.assertTrue(environment.is_service_issue())
173 | self.assertFalse(environment.is_host_issue())
174 |
175 | def test_get_recovery_last_problem_id_raises_type_error_when_not_in_recovery_state(self):
176 | environment = IcingaEnvironment(create_valid_environment_dict_for_host_problem())
177 | self.assertRaises(TypeError, environment.get_recovery_last_problem_id)
178 |
179 | def test_get_recovery_last_problem_id_returns_last_service_problem_id_for_service_recovery(self):
180 | environment = IcingaEnvironment(create_valid_environment_dict_for_service_recovery())
181 | problem_id = environment.get_recovery_last_problem_id()
182 | self.assertEqual(ANY_LAST_SERVICE_PROBLEM_ID, problem_id)
183 |
184 | def test_get_recovery_last_problem_id_returns_last_host_problem_id_for_host_recovery(self):
185 | test_dict = create_valid_environment_dict_for_host_recovery()
186 | environment = IcingaEnvironment(test_dict)
187 | problem_id = environment.get_recovery_last_problem_id()
188 | self.assertEqual(ANY_LAST_HOST_PROBLEM_ID, problem_id)
189 |
190 | def test_get_jira_recovery_label_returns_correct_value_for_service_recovery(self):
191 | test_dict = create_valid_environment_dict_for_service_recovery()
192 |
193 | environment = IcingaEnvironment(test_dict)
194 |
195 | self.assertEqual(ANY_ICINGA_LABEL_STRING_FORMAT % (ANY_LAST_SERVICE_PROBLEM_ID, ANY_HOSTNAME),
196 | environment.get_jira_recovery_label())
197 |
198 | def test_get_jira_recovery_label_returns_correct_value_for_host_recovery(self):
199 | test_dict = create_valid_environment_dict_for_host_recovery()
200 |
201 | environment = IcingaEnvironment(test_dict)
202 |
203 | self.assertEqual(ANY_ICINGA_LABEL_STRING_FORMAT % (ANY_LAST_HOST_PROBLEM_ID, ANY_HOSTNAME),
204 | environment.get_jira_recovery_label())
205 |
206 | def test_create_labels_list_contains_service_problem_id(self):
207 | test_dict = create_valid_environment_dict_for_service_problem()
208 | environment = IcingaEnvironment(test_dict)
209 | actual_labels = environment.create_labels_list()
210 |
211 | self.assertEqual(len(actual_labels), 1)
212 | self.assertEqual(actual_labels[0], ANY_ICINGA_LABEL_STRING_FORMAT % (ANY_SERVICE_PROBLEM_ID, ANY_HOSTNAME))
213 |
214 | def test_create_labels_list_contains_host_problem_id(self):
215 | test_dict = create_valid_environment_dict_for_host_problem()
216 | environment = IcingaEnvironment(test_dict)
217 | actual_labels = environment.create_labels_list()
218 |
219 | self.assertEqual(1, len(actual_labels))
220 | self.assertEqual(ANY_ICINGA_LABEL_STRING_FORMAT % (ANY_HOST_PROBLEM_ID, ANY_HOSTNAME), actual_labels[0])
221 |
222 | def test_create_icinga_label_with_prefix_and_hostname(self):
223 | test_dict = create_valid_environment_dict_for_host_problem()
224 | environment = IcingaEnvironment(test_dict)
225 | actual_label = environment._create_icinga_label(ANY_HOST_PROBLEM_ID)
226 |
227 | self.assertEqual(ANY_ICINGA_LABEL_STRING_FORMAT % (ANY_HOST_PROBLEM_ID, ANY_HOSTNAME), actual_label)
228 |
229 |
--------------------------------------------------------------------------------
/src/unittest/python/Issue_tests.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import textwrap
3 | from mock import Mock
4 |
5 | from icinga2jira import Issue
6 |
7 |
8 | NOTIFICATION_TYPE_PROBLEM = "PROBLEM"
9 |
10 |
11 | def create_icinga_environment_mock(notification_type=NOTIFICATION_TYPE_PROBLEM):
12 | ANY_SERVICE_PROBLEM_ID = "12345"
13 | ANY_SERVICE_STATE = "any service state"
14 | ANY_SERVICE_DESCRIPTION = "foo application services"
15 | ANY_HOST_ADDRESS = "myserv1.server.com"
16 | ANY_COMMENT = "any comment"
17 | ANY_NOTIFICATION_AUTHOR = "any notification author"
18 | ANY_HOST_OUTPUT = 'any host output'
19 | ANY_HOST_STATE = 'any host state'
20 | ANY_SERVICE_MESSAGE = "any service message"
21 | ANY_SHORT_DATE_TIME = "11-26-2013 15:42:05"
22 |
23 | environment = Mock()
24 | environment.service_description = ANY_SERVICE_DESCRIPTION
25 | environment.service_state = ANY_SERVICE_STATE
26 | environment.host_state = ANY_HOST_STATE
27 | environment.short_date_time = ANY_SHORT_DATE_TIME
28 | environment.host_address = ANY_HOST_ADDRESS
29 | environment.service_output = ANY_SERVICE_MESSAGE
30 | environment.host_output = ANY_HOST_OUTPUT
31 | environment.notification_type = notification_type
32 | environment.notification_author = ANY_NOTIFICATION_AUTHOR
33 | environment.notification_comment = ANY_COMMENT
34 | environment.service_problem_id = ANY_SERVICE_PROBLEM_ID
35 | return environment
36 |
37 |
38 | class IssueDummy(Issue):
39 |
40 | def __init__(self, icinga_environment):
41 | self.icinga_environment = icinga_environment
42 |
43 | def execute(self):
44 | pass
45 |
46 |
47 | class TestIssue(unittest.TestCase):
48 |
49 | EXPECTED_PROBLEM_DESCRIPTION_WITH_SERVICE = textwrap.dedent("""
50 | {color:#3b0b0b}*Icinga Problem Alert*{color}
51 |
52 | The following information was provided by Icinga:
53 | * Date & Time: 11-26-2013 15:42:05
54 | * Host Address: myserv1.server.com
55 | * Status Information: any service message
56 | * Current Host State: any host state
57 | * Current Service State: any service state""").strip()
58 |
59 | EXPECTED_ACKNOWLEDGEMENT_DESCRIPTION_WITH_SERVICE = textwrap.dedent("""
60 | {color:#0f5d94}*Icinga Acknowledgement*{color}
61 |
62 | The following information was provided by Icinga:
63 | * Date & Time: 11-26-2013 15:42:05
64 | * Host Address: myserv1.server.com
65 | * Status Information: any service message
66 | * Current Host State: any host state
67 | * Current Service State: any service state
68 | * Notification Author: any notification author
69 | * Notification Comment: any comment""").strip()
70 |
71 | EXPECTED_PROBLEM_DESCRIPTION_WITHOUT_SERVICE_DESCRIPTION = textwrap.dedent("""
72 | {color:#3b0b0b}*Icinga Problem Alert*{color}
73 |
74 | The following information was provided by Icinga:
75 | * Date & Time: 11-26-2013 15:42:05
76 | * Host Address: myserv1.server.com
77 | * Status Information: any host output
78 | * Current Host State: any host state""").strip()
79 |
80 | EXPECTED_RECOVERY_DESCRIPTION_WITH_SERVICE = textwrap.dedent("""
81 | {color:#0b3b0b}*Icinga Recovery Alert*{color}
82 |
83 | The following information was provided by Icinga:
84 | * Date & Time: 11-26-2013 15:42:05
85 | * Host Address: myserv1.server.com
86 | * Status Information: any service message
87 | * Current Host State: any host state
88 | * Current Service State: any service state
89 |
90 | This ticket was closed automatically.""").strip()
91 |
92 | def setUp(self):
93 | self.icinga_environment = create_icinga_environment_mock()
94 | self.issue = IssueDummy(self.icinga_environment)
95 |
96 | def test_description_for_problem_with_service_description(self):
97 | actual_description = self.issue.create_description()
98 |
99 | self.assertEqual(actual_description, self.EXPECTED_PROBLEM_DESCRIPTION_WITH_SERVICE)
100 |
101 | def test_description_for_problem_without_service_description(self):
102 | self.icinga_environment.service_description = None
103 |
104 | actual_description = self.issue.create_description()
105 |
106 | self.assertEqual(actual_description,
107 | self.EXPECTED_PROBLEM_DESCRIPTION_WITHOUT_SERVICE_DESCRIPTION)
108 |
109 | def test_description_for_acknowledgement_with_service_description(self):
110 | self.icinga_environment.notification_type = "ACKNOWLEDGEMENT"
111 |
112 | actual_description = self.issue.create_description()
113 |
114 | self.assertEqual(actual_description, self.EXPECTED_ACKNOWLEDGEMENT_DESCRIPTION_WITH_SERVICE)
115 |
116 | def test_description_for_recovery_with_service_description(self):
117 | self.icinga_environment.notification_type = "RECOVERY"
118 |
119 | actual_description = self.issue.create_description()
120 |
121 | self.assertEqual(actual_description, self.EXPECTED_RECOVERY_DESCRIPTION_WITH_SERVICE)
122 |
--------------------------------------------------------------------------------
/src/unittest/python/OpenIssue_tests.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from mock import Mock, patch
4 |
5 | from icinga2jira import OpenIssue
6 |
7 |
8 | ANY_PROJECT_KEY = 'MON'
9 | ANY_ISSUE_TYPE = 'Technical task'
10 | ANY_LAST_SERVICE_PROBLEM_ID = "76540"
11 | ANY_PRIORITY_ID = "12"
12 | ANY_HOST_PROBLEM_ID = "76543"
13 | ANY_SERVICE_PROBLEM_ID = "12345"
14 | ANY_SERVICE_STATE = "any servcie state"
15 | ANY_HOSTNAME = "myserver1"
16 | ANY_SERVICE_DESCRIPTION = "foo application services"
17 | ANY_COMMENT = "any comment"
18 | ANY_NOTIFICATION_AUTHOR = "any notification author"
19 | ANY_HOST_OUTPUT = 'any host output'
20 | ANY_HOST_STATE = 'any host state'
21 | ANY_SERVICE_MESSAGE = "any service message"
22 | ANY_SHORT_DATE_TIME = "11-26-2013 15:42:05"
23 | NOTIFICATION_TYPE_PROBLEM = "PROBLEM"
24 |
25 |
26 | class TestOpenIssue(unittest.TestCase):
27 |
28 | def setUp(self):
29 | self.jira_mock = Mock()
30 | self.ticket = Mock()
31 | self.icinga_environment = self.create_icinga_environment_mock()
32 | self.config = {'jira_project_key': ANY_PROJECT_KEY, 'jira_issue_type': ANY_ISSUE_TYPE}
33 | self.open_issue = OpenIssue(self.jira_mock, self.config, self.icinga_environment)
34 |
35 | def test_execute_is_called_properly_and_returns_list_of_handled_issues(self):
36 | self.jira_mock.create_issue.return_value = 'new issue'
37 |
38 | with patch.object(OpenIssue, '_create_issue_dict', return_value='issue_dict') as mock_create:
39 | open_issue = OpenIssue(self.jira_mock, self.config, self.icinga_environment)
40 | result = open_issue.execute()
41 |
42 | self.jira_mock.create_issue.assert_called_with(fields='issue_dict')
43 | mock_create.assert_called()
44 | self.assertEqual(['new issue'], result)
45 |
46 | def test_ticket_init(self):
47 | self.assertTrue(self.open_issue.jira)
48 | self.assertEqual(ANY_ISSUE_TYPE, self.open_issue.issue_type)
49 | self.assertEqual(ANY_PROJECT_KEY, self.open_issue.project_key)
50 | self.assertTrue(self.open_issue.icinga_environment)
51 |
52 | def test_create_issue_dict_should_return_valid_issue(self):
53 | self.icinga_environment.create_labels_list.return_value = \
54 | ['ICI#%s' % ANY_SERVICE_PROBLEM_ID]
55 |
56 | open_issue = OpenIssue(self.jira_mock, self.get_valid_configuration(), self.icinga_environment)
57 | actual_issue = open_issue._create_issue_dict()
58 |
59 | self.assertEqual(actual_issue['issuetype']['name'], ANY_ISSUE_TYPE)
60 | self.assertEqual(actual_issue['project']['key'], ANY_PROJECT_KEY)
61 | self.assertTrue(actual_issue['summary'])
62 | self.assertTrue(actual_issue['description'])
63 | self.assertEqual(actual_issue['labels'][0], "ICI#" + ANY_SERVICE_PROBLEM_ID)
64 |
65 | def test_summary_with_service_description(self):
66 | self.icinga_environment.is_service_issue.return_value = True
67 |
68 | open_issue = OpenIssue(self.jira_mock, self.get_valid_configuration(), self.icinga_environment)
69 | actual_summary = open_issue._create_summary()
70 |
71 | self.assertEquals("ICINGA: %s on %s is %s" % (ANY_SERVICE_DESCRIPTION, ANY_HOSTNAME, ANY_SERVICE_STATE),
72 | actual_summary)
73 |
74 | def test_summary_without_service_description(self):
75 | self.icinga_environment.is_service_issue.return_value = False
76 |
77 | open_issue = OpenIssue(self.jira_mock, self.get_valid_configuration(), self.icinga_environment)
78 | actual_summary = open_issue._create_summary()
79 |
80 | self.assertEquals(actual_summary, "ICINGA: %s is %s" % (ANY_HOSTNAME, ANY_HOST_STATE))
81 |
82 | def get_valid_configuration(self):
83 | return {'jira_project_key': ANY_PROJECT_KEY, 'jira_issue_type': ANY_ISSUE_TYPE}
84 |
85 | def create_icinga_environment_mock(self):
86 | environment = Mock()
87 | environment.service_description = ANY_SERVICE_DESCRIPTION
88 | environment.service_state = ANY_SERVICE_STATE
89 | environment.host_name = ANY_HOSTNAME
90 | environment.host_state = ANY_HOST_STATE
91 | environment.short_date_time = ANY_SHORT_DATE_TIME
92 | environment.service_output = ANY_SERVICE_MESSAGE
93 | environment.host_output = ANY_HOST_OUTPUT
94 | environment.notification_type = NOTIFICATION_TYPE_PROBLEM
95 | environment.notification_author = ANY_NOTIFICATION_AUTHOR
96 | environment.notification_comment = ANY_COMMENT
97 | environment.service_problem_id = ANY_SERVICE_PROBLEM_ID
98 | return environment
99 |
--------------------------------------------------------------------------------
/src/unittest/python/icinga2jira_tests.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from docopt import DocoptExit
3 | import icinga2jira as i2j
4 | import textwrap
5 | from StringIO import StringIO
6 | from ConfigParser import NoSectionError
7 | from mock import patch, Mock
8 | from jira.exceptions import JIRAError
9 |
10 | ANY_TEXT_PARAMETER = "any service output"
11 | ANY_CONFIG_PATH = "/tmp/config.ini"
12 | ANY_URL = 'http://www.example.com'
13 | ANY_USERNAME = 'god'
14 | ANY_PASSWORD = 'h3aven'
15 | ANY_PROJECT_KEY = 'PROJECT_KEY'
16 | ANY_ISSUE_TYPE = 'Issue type'
17 | NOTIFICATION_TYPE_PROBLEM = "PROBLEM"
18 |
19 | TEMPLATE = textwrap.dedent("""
20 | [settings]
21 | url = %s
22 | username = %s
23 | password = %s
24 | jira_project_key = %s
25 | jira_issue_type = %s
26 | """ % (ANY_URL, ANY_USERNAME, ANY_PASSWORD,
27 | ANY_PROJECT_KEY, ANY_ISSUE_TYPE))
28 |
29 |
30 | def create_icinga_environment_mock(notification_type=NOTIFICATION_TYPE_PROBLEM):
31 |
32 | environment = Mock()
33 | environment.notification_type = notification_type
34 | return environment
35 |
36 |
37 | def create_template_with_skipped_line(line_idx):
38 | lines = TEMPLATE.strip().split('\n')
39 | return '\n'.join(lines[0:line_idx] + lines[line_idx + 1:])
40 |
41 |
42 | class TestReadConfig(unittest.TestCase):
43 |
44 | def test_parse_and_validate_config_file_delivers_all_expected_fields(self):
45 | self.assert_that_configuration_correctly_read_file(i2j.parse_and_validate_config_file(StringIO(TEMPLATE)))
46 |
47 | def test_parse_and_validate_config_file_throws_error_when_config_does_not_contain_settings_section(self):
48 | config = StringIO(textwrap.dedent("""
49 | [somethingelse]
50 | url = fsdfsdfsdf
51 | """))
52 | self.assertRaises(NoSectionError, i2j.parse_and_validate_config_file, config)
53 |
54 | def test_parse_and_validate_config_file_throws_error_when_config_missing_value(self):
55 | for line_idx in range(1, 6):
56 | target = create_template_with_skipped_line(line_idx)
57 | self.assertRaises(ValueError, i2j.parse_and_validate_config_file, StringIO(target))
58 |
59 | def assert_that_configuration_correctly_read_file(self, config_dict):
60 | self.assertEqual(config_dict['url'], ANY_URL)
61 | self.assertEqual(config_dict['username'], ANY_USERNAME)
62 | self.assertEqual(config_dict['password'], ANY_PASSWORD)
63 | self.assertEqual(config_dict['jira_project_key'], ANY_PROJECT_KEY)
64 | self.assertEqual(config_dict['jira_issue_type'], ANY_ISSUE_TYPE)
65 |
66 |
67 | class TestParseOptions(unittest.TestCase):
68 |
69 | def test_parse_mandatory_parameter_config_and_c(self):
70 | argv = ['--config', ANY_CONFIG_PATH]
71 | actualOptions = i2j.parse_arguments(argv)
72 | self.assertEqual(actualOptions['--config'], ANY_CONFIG_PATH)
73 |
74 | argv = ['-c', ANY_CONFIG_PATH]
75 | actualOptions = i2j.parse_arguments(argv)
76 | self.assertEqual(actualOptions['--config'], ANY_CONFIG_PATH)
77 |
78 | def test_when_unallowed_options_are_set_an_error_is_thrown(self):
79 | self.assertRaises(DocoptExit, i2j.parse_arguments, ["--foo", "bar"])
80 | self.assertRaises(DocoptExit, i2j.parse_arguments, ["open", "--foo"])
81 |
82 |
83 | class TestJIRAUsage(unittest.TestCase):
84 |
85 | def setUp(self):
86 | self.jira_mock = Mock()
87 | self.ticket = Mock()
88 |
89 | def test_open_jira_session_called_properly(self):
90 | with patch('icinga2jira.JIRA') as JIRA:
91 | JIRA.return_value = 'jira'
92 | result = i2j.open_jira_session('spam', 'eggs', 'ham')
93 | self.assertEqual(result, 'jira')
94 | JIRA.assert_called_with(basic_auth=('eggs', 'ham'),
95 | options={'verify': False, 'server': 'spam'})
96 |
97 | def test_open_jira_session_raises_exception(self):
98 | with patch('icinga2jira.JIRA') as JIRA:
99 | JIRA.side_effect = JIRAError()
100 | self.assertRaises(JIRAError, i2j.open_jira_session,
101 | 'spam', 'eggs', 'ham')
102 |
--------------------------------------------------------------------------------