Skip to content

Commit 41d0a86

Browse files
authored
Use poetry. Build project as module (#74)
1 parent e070154 commit 41d0a86

File tree

9 files changed

+1548
-70
lines changed

9 files changed

+1548
-70
lines changed

.github/workloads/release.yml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: CI
2+
on: pull_request
3+
4+
jobs:
5+
test:
6+
runs-on: ubuntu-latest
7+
steps:
8+
- uses: actions/checkout@v2
9+
- uses: actions/setup-python@v2
10+
- name: Install Poetry
11+
run: curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
12+
- name: Add Poetry to path
13+
run: echo "${HOME}/.poetry/bin" >> $GITHUB_PATH
14+
- name: Install venv
15+
run: poetry install

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ venv
55
config.yml
66
/.pytest_cache/
77
/src/zabbix_cachet.egg-info/
8+
/dist/
9+
/database.sqlite
10+
/config_zbx_old.yml

.pre-commit-config.yaml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
repos:
2+
- repo: https://github.com/python-poetry/poetry
3+
rev: 1.8.0
4+
hooks:
5+
- id: poetry-check
6+
- id: poetry-lock
7+
- id: poetry-export
8+
args: ["--without-hashes", "--format", "requirements.txt", "-o", "requirements.txt"]

README.md

+7-4
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,14 @@ items of services that is exported to Cachet and Cachet`s API key.
4646
```
4747
3. Drink a cup of tea (optional)
4848
49-
## Git
50-
1. Clone this repository
49+
## Python package
50+
1. Install python package via pip
51+
```bash
52+
pip install zabbix-cachet
53+
```
5154
2. Rename `config-example.yml` to `config.yml` and fill a file with your settings.
52-
3. Install python libs from `requirements.txt`
53-
4. Launch `zabbix-cachet.py`
55+
3. Define `CONFIG_FILE` environment variable which point to your `config.yml` or change current work directory to folder with config
56+
4. Launch `zabbix-cachet`
5457

5558
## Apt
5659
1. Add official Zabbix-Cachet [PPA](https://launchpad.net/~reg-tem4uk/+archive/ubuntu/zabbix-cachet):

poetry.lock

+1,387
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[tool.poetry]
2+
name = "zabbix-cachet"
3+
version = "2.1.1"
4+
description = "Python daemon which provides synchronisation between Zabbix IT Services and Cachet"
5+
authors = ["Artem Alexandrov <[email protected]>"]
6+
license = "MIT License"
7+
readme = "README.md"
8+
9+
[tool.poetry.dependencies]
10+
python = ">3.8,<4.0"
11+
requests = ">2.21.0"
12+
pyyaml = ">=5.4"
13+
pyzabbix = "1.3.1"
14+
pytz = ">=2024.1"
15+
16+
[tool.poetry.group.dev.dependencies]
17+
pytest = "8.2.0"
18+
pytest-env = "1.1.3"
19+
pylint = "^3.2.1"
20+
poetry-plugin-export = "^1.8.0"
21+
22+
[build-system]
23+
requires = ["poetry-core"]
24+
build-backend = "poetry.core.masonry.api"
25+
26+
[tool.poetry.scripts]
27+
zabbix-cachet = 'zabbix_cachet.main:main'

requirements.txt

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
PyYAML>=5.4
2-
pyzabbix==1.3.1
3-
requests>=2.21.0
4-
pytz==2024.1
5-
# Dev only
6-
#pytest==8.2.0
7-
#pytest-env==1.1.3
1+
certifi==2024.2.2 ; python_full_version > "3.8.0" and python_version < "4.0"
2+
charset-normalizer==3.3.2 ; python_full_version > "3.8.0" and python_version < "4.0"
3+
idna==3.7 ; python_full_version > "3.8.0" and python_version < "4.0"
4+
packaging==24.0 ; python_full_version > "3.8.0" and python_version < "4.0"
5+
pytz==2024.1 ; python_full_version > "3.8.0" and python_version < "4.0"
6+
pyyaml==6.0.1 ; python_full_version > "3.8.0" and python_version < "4.0"
7+
pyzabbix==1.3.1 ; python_full_version > "3.8.0" and python_version < "4.0"
8+
requests==2.32.2 ; python_full_version > "3.8.0" and python_version < "4.0"
9+
urllib3==2.2.1 ; python_full_version > "3.8.0" and python_version < "4.0"

src/zabbix_cachet/main.py

+89-56
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44
"""
55
import sys
66
import os
7+
import pathlib
78
import datetime
89
from dataclasses import dataclass
910
from typing import List, Union
1011

1112
import time
1213
import threading
1314
import logging
15+
16+
import requests
1417
import yaml
1518
import pytz
1619

@@ -20,7 +23,51 @@
2023

2124
__author__ = 'Artem Aleksandrov <qk4l()tem4uk.ru>'
2225
__license__ = """The MIT License (MIT)"""
23-
__version__ = '2.0.0'
26+
__version__ = '2.1.1'
27+
28+
29+
class Config:
30+
_instance = None
31+
32+
def __new__(cls, *args, **kwargs):
33+
if cls._instance is None:
34+
cls._instance = super(Config, cls).__new__(cls, *args, **kwargs)
35+
return cls._instance
36+
37+
def __init__(self):
38+
if not hasattr(self, 'initialized'):
39+
if os.getenv('CONFIG_FILE') is not None:
40+
self.config_file = pathlib.Path(os.environ['CONFIG_FILE'])
41+
else:
42+
self.config_file = pathlib.Path.cwd() / 'config.yml'
43+
if not self.config_file.is_file():
44+
logging.error(
45+
f"Config file {self.config_file} is absent. Set CONFIG_FILE to change path or create it there.")
46+
sys.exit(1)
47+
config = read_config(self.config_file)
48+
if not config:
49+
sys.exit(1)
50+
self.zabbix_config = config['zabbix']
51+
self.cachet_config = config['cachet']
52+
self.app_settings = config['settings']
53+
54+
if self.app_settings.get('time_zone'):
55+
self.tz = pytz.timezone(self.app_settings['time_zone'])
56+
else:
57+
self.tz = None
58+
59+
incident_templates_defaults = {
60+
'acknowledgement': "{message}\n\n###### {ack_time} by {author}\n\n______\n",
61+
'investigation': '',
62+
'resolving': '',
63+
}
64+
65+
self.templates = config.get('templates')
66+
for template_name, default_value in incident_templates_defaults.items():
67+
if self.templates.get(template_name, None) is None:
68+
self.templates[template_name] = default_value
69+
70+
self.initialized = True
2471

2572

2673
@dataclass
@@ -39,7 +86,7 @@ def __str__(self):
3986
return f"{self.cachet_group_name}/{self.cachet_component_name} - {self.zbx_serviceid}"
4087

4188

42-
def triggers_watcher(service_map: List[ZabbixCachetMap]) -> bool:
89+
def triggers_watcher(service_map: List[ZabbixCachetMap], zapi: Zabbix, cachet: Cachet) -> bool:
4390
"""
4491
Check zabbix triggers and update Cachet components
4592
Zabbix Priority:
@@ -57,6 +104,7 @@ def triggers_watcher(service_map: List[ZabbixCachetMap]) -> bool:
57104
4 - Fixed
58105
@return: boolean
59106
"""
107+
config = Config()
60108
for i in service_map: # type: ZabbixCachetMap
61109
# inc_status = 1
62110
# comp_status = 1
@@ -79,9 +127,9 @@ def triggers_watcher(service_map: List[ZabbixCachetMap]) -> bool:
79127
# component not operational mode. Resolve it.
80128
last_inc = cachet.get_incident(i.cachet_component_id)
81129
if str(last_inc['id']) != '0':
82-
if resolving_tmpl:
83-
inc_msg = resolving_tmpl.format(
84-
time=datetime.datetime.now(tz=tz).strftime('%b %d, %H:%M'),
130+
if config.templates['resolving']:
131+
inc_msg = config.templates['resolving'].format(
132+
time=datetime.datetime.now(tz=config.tz).strftime('%b %d, %H:%M'),
85133
) + cachet.get_incident(i.cachet_component_id)['message']
86134
else:
87135
inc_msg = cachet.get_incident(i.cachet_component_id)['message']
@@ -126,9 +174,9 @@ def triggers_watcher(service_map: List[ZabbixCachetMap]) -> bool:
126174
# TODO: Add timezone?
127175
# Move format to config file
128176
author = msg.get('name', '') + ' ' + msg.get('surname', '')
129-
ack_time = datetime.datetime.fromtimestamp(int(msg['clock']), tz=tz).strftime(
177+
ack_time = datetime.datetime.fromtimestamp(int(msg['clock']), tz=config.tz).strftime(
130178
'%b %d, %H:%M')
131-
ack_msg = acknowledgement_tmpl.format(
179+
ack_msg = config.templates['acknowledgement'].format(
132180
message=msg['message'],
133181
ack_time=ack_time,
134182
author=author
@@ -146,14 +194,14 @@ def triggers_watcher(service_map: List[ZabbixCachetMap]) -> bool:
146194
else:
147195
comp_status = 2
148196

149-
if not inc_msg and investigating_tmpl:
197+
if not inc_msg and config.templates['']:
150198
if zbx_event:
151199
zbx_event_clock = int(zbx_event.get('clock'))
152-
zbx_event_time = datetime.datetime.fromtimestamp(zbx_event_clock, tz=tz).strftime(
200+
zbx_event_time = datetime.datetime.fromtimestamp(zbx_event_clock, tz=config.tz).strftime(
153201
'%b %d, %H:%M')
154202
else:
155203
zbx_event_time = ''
156-
inc_msg = investigating_tmpl.format(
204+
inc_msg = config.templates['investigation'].format(
157205
group=i.cachet_group_name,
158206
component=i.cachet_component_name,
159207
time=zbx_event_time,
@@ -184,12 +232,14 @@ def triggers_watcher(service_map: List[ZabbixCachetMap]) -> bool:
184232
return True
185233

186234

187-
def triggers_watcher_worker(service_map, interval, tr_event):
235+
def triggers_watcher_worker(service_map, interval, tr_event: threading.Event, zapi: Zabbix, cachet: Cachet):
188236
"""
189237
Worker for triggers_watcher. Run it continuously with specific interval
190238
@param service_map: list of tuples
191239
@param interval: interval in seconds
192240
@param tr_event: treading.Event object
241+
@param zapi: Zabbix object
242+
@param cachet: Cachet object
193243
@return:
194244
"""
195245
logging.info('start trigger watcher')
@@ -198,7 +248,7 @@ def triggers_watcher_worker(service_map, interval, tr_event):
198248
# Do not run if Zabbix is not available
199249
if zapi.get_version():
200250
try:
201-
triggers_watcher(service_map)
251+
triggers_watcher(service_map, zapi=zapi, cachet=cachet)
202252
except Exception as e:
203253
logging.error('triggers_watcher() raised an Exception. Something gone wrong')
204254
logging.error(e, exc_info=True)
@@ -208,11 +258,13 @@ def triggers_watcher_worker(service_map, interval, tr_event):
208258
logging.info('end trigger watcher')
209259

210260

211-
def init_cachet(services: List[ZabbixService]) -> List[ZabbixCachetMap]:
261+
def init_cachet(services: List[ZabbixService], zapi: Zabbix, cachet: Cachet) -> List[ZabbixCachetMap]:
212262
"""
213263
Init Cachet by syncing Zabbix service to it
214264
Also func create mapping batten Cachet components and Zabbix IT services
215-
@param services: list of ZabbixService
265+
:param services: list of ZabbixService
266+
:param cachet: Cachet object
267+
:param zapi: Zabbix object
216268
@return: list of tuples
217269
"""
218270
# Zabbix Triggers to Cachet components id map
@@ -291,65 +343,39 @@ def read_config(config_f):
291343
return None
292344

293345

294-
if __name__ == '__main__':
295-
296-
if os.getenv('CONFIG_FILE') is not None:
297-
config_file = os.environ['CONFIG_FILE']
298-
else:
299-
config_file = os.path.dirname(os.path.realpath(__file__)) + '/config.yml'
300-
if not os.path.isfile(config_file):
301-
logging.error(f"Config file {config_file} is absent. Set CONFIG_FILE to change path or create it there.")
302-
sys.exit(1)
303-
config = read_config(config_file)
304-
if not config:
305-
sys.exit(1)
306-
ZABBIX = config['zabbix']
307-
CACHET = config['cachet']
308-
SETTINGS = config['settings']
309-
310-
if SETTINGS.get('time_zone'):
311-
tz = pytz.timezone(SETTINGS['time_zone'])
312-
else:
313-
tz = None
314-
315-
# Templates for incident displaying
316-
acknowledgement_tmpl_default = "{message}\n\n###### {ack_time} by {author}\n\n______\n"
317-
templates = config.get('templates')
318-
if templates:
319-
acknowledgement_tmpl = templates.get('acknowledgement', acknowledgement_tmpl_default)
320-
investigating_tmpl = templates.get('investigating', '')
321-
resolving_tmpl = templates.get('resolving', '')
322-
else:
323-
acknowledgement_tmpl = acknowledgement_tmpl_default
324-
346+
def main():
325347
exit_status = 0
348+
config = Config()
349+
326350
# Set Logging
327-
log_level = logging.getLevelName(SETTINGS['log_level'])
328-
log_level_requests = logging.getLevelName(SETTINGS['log_level_requests'])
351+
log_level = logging.getLevelName(config.app_settings['log_level'])
352+
log_level_requests = logging.getLevelName(config.app_settings['log_level_requests'])
329353
logging.basicConfig(
330354
level=log_level,
331355
format='%(asctime)s %(levelname)s: (%(threadName)s) %(message)s',
332356
datefmt='%Y-%m-%d %H:%M:%S %Z'
333357
)
334358
logging.getLogger("requests").setLevel(log_level_requests)
335-
logging.info(f'Zabbix Cachet v.{__version__} started (config: {config_file})')
359+
logging.info(f'Zabbix Cachet v.{__version__} started (config: {config.config_file})')
336360
inc_update_t = threading.Thread()
337361
event = threading.Event()
338362
try:
339-
zapi = Zabbix(ZABBIX['server'], ZABBIX['user'], ZABBIX['pass'], ZABBIX['https-verify'])
340-
cachet = Cachet(CACHET['server'], CACHET['token'], CACHET['https-verify'])
363+
zapi = Zabbix(config.zabbix_config['server'], config.zabbix_config['user'], config.zabbix_config['pass'],
364+
config.zabbix_config['https-verify'])
365+
cachet = Cachet(config.cachet_config['server'], config.cachet_config['token'],
366+
config.cachet_config['https-verify'])
341367
logging.info('Zabbix ver: {}. Cachet ver: {}'.format(zapi.version, cachet.version))
342368
zbxtr2cachet = ''
343369
while True:
344370
try:
345371
logging.debug('Getting list of Zabbix IT Services ...')
346-
it_services = zapi.get_itservices(SETTINGS['root_service'])
372+
it_services = zapi.get_itservices(config.app_settings['root_service'])
347373
logging.debug('Zabbix IT Services: {}'.format(it_services))
348374
# Create Cachet components and components groups
349375
logging.debug('Syncing Zabbix with Cachet...')
350-
zbxtr2cachet_new = init_cachet(it_services)
376+
zbxtr2cachet_new = init_cachet(it_services, zapi, cachet)
351377
except ZabbixNotAvailable:
352-
time.sleep(SETTINGS['update_comp_interval'])
378+
time.sleep(config.app_settings['update_comp_interval'])
353379
continue
354380
except ZabbixCachetException:
355381
zbxtr2cachet_new = False
@@ -375,15 +401,22 @@ def read_config(config_f):
375401
event.clear()
376402
inc_update_t = threading.Thread(name='Trigger Watcher',
377403
target=triggers_watcher_worker,
378-
args=(zbxtr2cachet, SETTINGS['update_inc_interval'], event))
404+
args=(zbxtr2cachet, config.app_settings['update_inc_interval'], event,
405+
zapi, cachet))
379406
inc_update_t.daemon = True
380407
inc_update_t.start()
381-
time.sleep(SETTINGS['update_comp_interval'])
382-
408+
time.sleep(config.app_settings['update_comp_interval'])
409+
except requests.exceptions.ConnectionError as err:
410+
logging.error(f"Failed to connect: {err}")
411+
exit_status = 1
383412
except KeyboardInterrupt:
384413
event.set()
385414
logging.info('Shutdown requested. See you.')
386415
except Exception as error:
387416
logging.exception(error)
388417
exit_status = 1
389418
sys.exit(exit_status)
419+
420+
421+
if __name__ == '__main__':
422+
main()

0 commit comments

Comments
 (0)