├── VERSION ├── .gitignore ├── Dockerfile ├── LICENSE ├── setup.py ├── .github └── workflows │ └── build_and_upload_to_pypi.yml ├── README.md └── zbx-import.py /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | # virtual env 3 | /venv 4 | # pycharm 5 | .idea 6 | # setuptools 7 | build/ 8 | dist/ 9 | *.egg-info/ 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-slim 2 | 3 | COPY *.py *.md VERSION LICENSE /opt/ 4 | 5 | WORKDIR /opt 6 | 7 | RUN python setup.py install 8 | 9 | ENTRYPOINT ["/usr/local/bin/zbx-import.py"] 10 | 11 | CMD ["--help"] 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | Copyright (C) 2004 Sam Hocevar 4 | 14 rue de Plaisance, 75014 Paris, France 5 | Everyone is permitted to copy and distribute verbatim or modified 6 | copies of this license document, and changing it is allowed as long 7 | as the name is changed. 8 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 9 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 10 | 0. You just DO WHAT THE FUCK YOU WANT TO. 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from setuptools import setup 3 | 4 | setup( 5 | name='zabbix-import', 6 | version=open('VERSION', 'r').read().strip(), 7 | url='https://github.com/selivan/zabbix-import', 8 | license='WTFPL', 9 | license_files = [ 'LICENSE' ], 10 | description='Utility to import Zabbix XML configuration: templates, hosts, host groups', 11 | long_description=open("README.md", 'r').read(), 12 | long_description_content_type='text/markdown', 13 | # twine requires author_email if author is set, but I don't like spam so homepage is enough 14 | author='Pavel Selivanov github.com/selivan', 15 | author_email='selivan.at.github@gmail-REMOVE-ANTI-SPAM.com', 16 | python_requires='>=2.5, >=3.0', 17 | install_requires=[], 18 | scripts=['zbx-import.py'], 19 | data_files=[('', ['LICENSE','VERSION','README.md'])], 20 | ) 21 | -------------------------------------------------------------------------------- /.github/workflows/build_and_upload_to_pypi.yml: -------------------------------------------------------------------------------- 1 | name: build and upload to pypi if VERSION changed 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | 9 | jobs: 10 | build_and_upload: 11 | runs-on: ubuntu-latest 12 | # strategy: 13 | # matrix: 14 | # python-version: [3.8] 15 | steps: 16 | - id: file_changes 17 | uses: trilom/file-changes-action@v1.2.3 18 | with: 19 | output: 'json' 20 | - uses: actions/checkout@v2 21 | if: 22 | contains(steps.file_changes.outputs.files_modified, 'VERSION') 23 | - uses: actions/setup-python@v2 24 | if: 25 | contains(steps.file_changes.outputs.files_modified, 'VERSION') 26 | with: 27 | python-version: 3.8 28 | - name: Create a source package and upload it to pypi 29 | if: 30 | contains(steps.file_changes.outputs.files_modified, 'VERSION') 31 | run: | 32 | pip install --upgrade setuptools wheel twine 33 | python setup.py sdist 34 | python setup.py bdist_wheel 35 | python -m twine upload -u "__token__" -p "${{ secrets.pypi_token }}" dist/* 36 | # Possible actions to use instead of manual 37 | # - name: Publish a source package to PyPI 38 | # if: 39 | # contains(steps.file_changes.outputs.files_modified, 'VERSION') 40 | # uses: pypa/gh-action-pypi-publish@master 41 | # uses: ortega2247/pypi-upload-action@master 42 | # with: 43 | # user: __token__ 44 | # password: ${{ secrets.pypi_token }} 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pypi package](https://img.shields.io/pypi/v/zabbix-import?color=%233fb911&label=pypi%20package)](https://pypi.org/project/zabbix-import/) 2 | 3 | Utility to import exported XML configuration(templates, hosts, ...) into Zabbix using it's [API](https://www.zabbix.com/documentation/3.4/manual/api). 4 | 5 | ``` 6 | $ zbx-import.py -u Admin -p *** --url https://zabbix.local/api_jsonrpc.php exported_templates.xml 7 | SUCCESS: configuration import 8 | ``` 9 | 10 | Tested with Zabbix 3.4 and 4.0, probably will work with older versions up to 2.0. Written in pure python, no additional libraries are required. Works with both python 3 and python 2. 11 | 12 | Allows to control import options: 13 | 14 | * create new - add new elements from the import file. Default: True 15 | * update existing - update existing elements from the import file. Default: True 16 | * delete missing - remove existing elements not present in the import file. Default: False. *NOTE*: without this option importing existing template with changed triggers will create new triggers, but old ones with the same name and different value will remain. 17 | 18 | You can set this options for all elements or precisely select list of elements for the option: `--delete-missing 'triggers graphs'`. Check `--help` for available elements. 19 | 20 | ``` 21 | $ zbx-import.py -u Admin -p *** --url https://zabbix.local/api_jsonrpc.php --delete-missing exported_templates.xml 22 | SUCCESS: configuration import 23 | ``` 24 | 25 | ### Installation 26 | 27 | Simplest option - just use `zbx-import.py` directly, it does not have any dependencies. 28 | 29 | From [pypi.org](https://pypi.org): 30 | 31 | `pip install zabbix-import` 32 | 33 | Or create a Docker image and use it: 34 | 35 | ```bash 36 | docker build -t zabbix-import . 37 | # No options to get help on usage 38 | docker run -it --rm zabbix-import [options] 39 | ``` 40 | 41 | **P.S.** If this code is useful for you - don't forget to put a star on it's [github repo](https://github.com/selivan/zabbix-import). 42 | -------------------------------------------------------------------------------- /zbx-import.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | """ 5 | Import XML configuration files using Zabbix API. 6 | Detailed information about zabbix templates import/export using the 7 | Zabbix Web-UI and Zabbix API usage for import configurations, 8 | available at: 9 | https://www.zabbix.com/documentation/4.0/manual/xml_export_import/templates#importing 10 | https://www.zabbix.com/documentation/4.0/manual/api/reference/configuration/import 11 | """ 12 | from argparse import ArgumentParser, RawTextHelpFormatter 13 | import json 14 | import os 15 | import io # for handling file encoding in python2 16 | from pprint import pformat 17 | import sys 18 | 19 | try: # python3 20 | from urllib.request import Request, urlopen 21 | except: # python2 22 | from urllib2 import Request, urlopen 23 | import traceback 24 | 25 | DEFAULT_ZABBIX_API_URL = 'http://127.0.0.1:80/api_jsonrpc.php' 26 | ELEMENTS_OPTIONS_DICT = { 27 | 'createMissing': ['applications', 'discoveryRules', 'graphs', 'groups', 28 | 'hosts', 'httptests', 'images', 'items', 'maps', 29 | 'screens', 'templateLinkage', 'templates', 30 | 'templateScreens', 'triggers', 'valueMaps'], 31 | 'updateExisting': ['discoveryRules', 'graphs', 32 | 'hosts', 'httptests', 'images', 'items', 'maps', 33 | 'screens', 'templates', 34 | 'templateScreens', 'triggers', 'valueMaps'], 35 | 'deleteMissing': ['applications', 'discoveryRules', 'graphs', 36 | 'httptests', 'items', 'templateScreens', 'triggers'], 37 | } 38 | 39 | 40 | def __create_parser(): 41 | cm_list = ELEMENTS_OPTIONS_DICT['createMissing'] 42 | ue_list = ELEMENTS_OPTIONS_DICT['updateExisting'] 43 | dm_list = ELEMENTS_OPTIONS_DICT['deleteMissing'] 44 | # Number of displayed element values per line (on help description) 45 | BRKLN_NUM_EL = 6 46 | # Parse command line arguments 47 | parser = ArgumentParser(description=__doc__, 48 | formatter_class=RawTextHelpFormatter) 49 | parser.add_argument('template_file', 50 | help='Zabbix exported template xml file\n') 51 | parser.add_argument( 52 | '-u', '--user', 53 | help='Use the --user flag to provide the Zabbix API user name.\n' 54 | 'Alternatively you can set the ZABBIX_API_USER environment ' 55 | 'variable.\nOne of the two methods is required. ' 56 | 'In case you are using both,\nthe flag value takes ' 57 | 'precedence over the environment variable\n' 58 | ) 59 | parser.add_argument( 60 | '-p', '--passwd', metavar='PASSWORD', 61 | help='Use the --passwd flag to provide the Zabbix API password.\n' 62 | 'Alternatively you can set the ZABBIX_API_PASSWD environment ' 63 | 'variable.\nOne of the two methods is required. ' 64 | 'In case you are using both,\nthe flag value takes ' 65 | 'precedence over the environment variable\n' 66 | ) 67 | parser.add_argument('-s', '--url', default=DEFAULT_ZABBIX_API_URL, 68 | help='Zabbix API URL\nDefault value is: {}\n' 69 | ''.format(DEFAULT_ZABBIX_API_URL)) 70 | parser.add_argument( 71 | '--no-create-missing', nargs='*', default=None, 72 | help='All the elements in the xml file that are missing in the zabbix' 73 | '\ndatabase are being created by default.\nTo unselect the ' 74 | 'createMissing option (i.e set false), use this flag\n followed' 75 | ' by a list of space separated values to be excluded.\nThe ' 76 | 'available element values are:\n\n{}\n\nIf not any value is ' 77 | 'provided, all of them will be excluded for the\ncreateMissing ' 78 | 'option\n'.format('\n'.join( 79 | [', '.join(cm_list[idx:idx + BRKLN_NUM_EL]) 80 | for idx in range(len(cm_list))[::BRKLN_NUM_EL]])) 81 | ) 82 | parser.add_argument( 83 | '--no-update-existing', nargs='*', default=None, 84 | help='All the elements in the xml file that already exists in the ' 85 | 'zabbix\ndatabase are being updated by default.\nTo unselect the ' 86 | 'updateExisting option (i.e set false), use this flag\n followed ' 87 | 'by a list of space separated values to be excluded.\nThe ' 88 | 'available element values are:\n\n{}\n\nIf not any value is ' 89 | 'provided, all of them will be excluded for the\nupdateExisting ' 90 | 'option\n'.format('\n'.join( 91 | [', '.join(ue_list[idx:idx + BRKLN_NUM_EL]) 92 | for idx in range(len(ue_list))[::BRKLN_NUM_EL]] 93 | )) 94 | ) 95 | parser.add_argument( 96 | '--delete-missing', nargs='*', default=None, 97 | help='All the elements that existes in the zabbix database that are ' 98 | 'not\npresent in the xml file are being preserved by default.\n' 99 | 'To select the deleteMissing option (i.e set true), use this flag' 100 | '\nfollowed by a list of space separated values to be included.\n' 101 | 'The available element values are:\n\n{}\n\nIf not any value is ' 102 | 'provided, all of them will be included for the\ndeleteMissing ' 103 | 'option\n'.format('\n'.join( 104 | [', '.join(dm_list[idx:idx + BRKLN_NUM_EL]) 105 | for idx in range(len(dm_list))[::BRKLN_NUM_EL]] 106 | )) 107 | ) 108 | return parser 109 | 110 | 111 | def __build_rules(no_create_missing, no_update_existing, delete_missing): 112 | # https://www.zabbix.com/documentation/3.4/manual/api/reference/configuration/import 113 | if no_create_missing is None: 114 | no_create_missing = [] 115 | elif not any(no_create_missing): 116 | no_create_missing = ELEMENTS_OPTIONS_DICT['createMissing'] 117 | 118 | if no_update_existing is None: 119 | no_update_existing = [] 120 | elif not any(no_update_existing): 121 | no_update_existing = ELEMENTS_OPTIONS_DICT['updateExisting'] 122 | 123 | if delete_missing is None: 124 | delete_missing = [] 125 | elif not any(delete_missing): 126 | delete_missing = ELEMENTS_OPTIONS_DICT['deleteMissing'] 127 | 128 | rules = {el: {'createMissing': el not in no_create_missing} 129 | for el in ELEMENTS_OPTIONS_DICT['createMissing']} 130 | for el in ELEMENTS_OPTIONS_DICT['updateExisting']: 131 | rules[el]['updateExisting'] = el not in no_update_existing 132 | for el in ELEMENTS_OPTIONS_DICT['deleteMissing']: 133 | rules[el]['deleteMissing'] = el in delete_missing 134 | 135 | return rules 136 | 137 | 138 | def zbxrequest(url, method, auth, params): 139 | if params is None: 140 | params = {} 141 | data = {"jsonrpc": "2.0", "id": 1, "method": method, 142 | "auth": auth, "params": params} 143 | headers = {'Content-Type': 'application/json'} 144 | # Convert to string and then to byte 145 | data = json.dumps(data).encode('utf-8') 146 | req = Request(args.url, headers=headers, data=data) 147 | resp = urlopen(req) 148 | # Get string 149 | resp = resp.read().decode('utf-8') 150 | # Convert to object 151 | resp = json.loads(resp, encoding='utf-8') 152 | return resp 153 | 154 | 155 | def import_zabbix_template(template_file, user, passwd, url, 156 | no_create_missing=None, 157 | no_update_existing=None, delete_missing=None): 158 | rules = __build_rules(no_create_missing, 159 | no_update_existing, delete_missing) 160 | 161 | # TODO: add API version check 162 | # r=zbxrequest(args.url, method="apiinfo.version", auth=None, params={}) 163 | # print(r) 164 | 165 | # Get authentication token 166 | # https://www.zabbix.com/documentation/3.4/manual/api/reference/user/login 167 | auth_result = zbxrequest(url, method="user.login", auth=None, 168 | params={"user": user, "password": passwd}) 169 | 170 | # If authentication was not OK 171 | if 'result' not in auth_result: 172 | raise ZbxImportError('auth failed\n{}' 173 | ''.format(pformat(auth_result))) 174 | 175 | global auth_token 176 | auth_token = auth_result['result'] 177 | 178 | # Read template file content 179 | with io.open(template_file, 'r', encoding='utf-8') as f: 180 | source = f.read() 181 | 182 | # Set import parameters, including template file content 183 | params = {'format': 'xml', 'rules': rules, 'source': source} 184 | 185 | import_result = zbxrequest(url, method="configuration.import", 186 | auth=auth_token, params=params) 187 | # Something like: {'id': 1, 'jsonrpc': '2.0', 'result': True} 188 | 189 | if 'result' in import_result and import_result['result']: 190 | print('SUCCESS: configuration import') 191 | else: 192 | raise ZbxImportError('configuration import failed\n{}' 193 | ''.format(pformat(import_result))) 194 | 195 | 196 | class ZbxImportError(Exception): 197 | def __init__(self, message, errors=1): 198 | traceback.print_exc() 199 | super(ZbxImportError, self).__init__(message) 200 | self.errors = errors 201 | 202 | 203 | if __name__ == '__main__': 204 | 205 | parser = __create_parser() 206 | args = parser.parse_args() 207 | auth_token = None 208 | 209 | # Get user/password values from the environment variable, 210 | # in case the respective argument is missing: 211 | if args.user is None: 212 | try: 213 | args.user = os.environ['ZABBIX_API_USER'] 214 | except KeyError as err: 215 | raise ZbxImportError('Missing zabbix API user name.\n{}' 216 | ''.format(parser.__dict__[ 217 | '_option_string_actions' 218 | ]['--user'].help)) 219 | if args.passwd is None: 220 | try: 221 | args.passwd = os.environ['ZABBIX_API_PASSWD'] 222 | except KeyError as err: 223 | raise ZbxImportError('Missing zabbix API password.\n{}' 224 | ''.format(parser.__dict__[ 225 | '_option_string_actions' 226 | ]['--passwd'].help)) 227 | try: 228 | import_zabbix_template(args.template_file, args.user, args.passwd, 229 | args.url, args.no_create_missing, 230 | args.no_update_existing, args.delete_missing) 231 | 232 | except Exception as e: 233 | raise ZbxImportError(str(e)) 234 | 235 | finally: 236 | # Logout to prevent generation of unnecessary open sessions 237 | # https://www.zabbix.com/documentation/3.4/manual/api/reference/user/logout 238 | if auth_token is not None: 239 | zbxrequest(args.url, method="user.logout", 240 | auth=auth_token, params={}) 241 | --------------------------------------------------------------------------------