├── docs ├── images │ └── cgit_example.png └── cgit_integration.md ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── Dockerfile ├── README.md ├── zbx2git.json └── zbx2git.rb /docs/images/cgit_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syepes/zbx2git/HEAD/docs/images/cgit_example.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | build/ 3 | logs/ 4 | temp/ 5 | *~ 6 | *.swp 7 | .classpath 8 | .project 9 | .idea 10 | .settings 11 | gradle.properties 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #Change Log 2 | This project adheres to [Semantic Versioning](http://semver.org/). 3 | 4 | This CHANGELOG follows the format listed at [Keep A Changelog](http://keepachangelog.com/) 5 | 6 | ## [Unreleased] 7 | 8 | ## 0.1.1 - 2016-09-06 9 | ### Fixed 10 | - Correctly handle deleted files 11 | - Fix logging variable scope and logging levels 12 | 13 | ## 0.1.0 - 2016-09-02 14 | ### Added 15 | - Initial release 16 | 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2016 Sebastian YEPES FERNANDEZ 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you 4 | may not use this file except in compliance with the License. You may 5 | obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | implied. See the License for the specific language governing 13 | permissions and limitations under the License. 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Usage: 2 | # 3 | # docker build --force-rm -t zbx2git . 4 | # docker run -it --rm -h zbx2git -v /opt/zbx2git/zbx2git.json:/opt/zbx2git/zbx2git.json -v /opt/zbx2git/repository:/opt/zbx2git/repository -v /opt/zbx2git/logs:/opt/zbx2git/logs zbx2git 5 | # 6 | FROM ruby:alpine 7 | MAINTAINER Sebastian YEPES 8 | 9 | ARG APK_FLAGS_COMMON="-q" 10 | ARG APK_FLAGS_PERSISTANT="${APK_FLAGS_COMMON} --clean-protected --no-cache" 11 | ARG APK_FLAGS_DEV="${APK_FLAGS_COMMON} --no-cache" 12 | 13 | ENV LANG=en_US.UTF-8 \ 14 | TERM=xterm 15 | 16 | RUN apk update && apk upgrade \ 17 | && apk add ${APK_FLAGS_PERSISTANT} git \ 18 | && apk add ${APK_FLAGS_DEV} --virtual build-deps build-base curl libffi-dev \ 19 | && mkdir -p /opt/zbx2git/ \ 20 | && curl -#SL "https://raw.githubusercontent.com/syepes/zbx2git/master/zbx2git.rb" > /opt/zbx2git/zbx2git.rb \ 21 | && chmod 755 /opt/zbx2git/zbx2git.rb \ 22 | && gem install parallel zabbixapi git \ 23 | && git config --global user.email "zbx2git@example.com" \ 24 | && git config --global user.name "zbx2git" \ 25 | && sed -i '/.*raise ApiError.new("Zabbix API version:.*/d' /usr/local/bundle/gems/zabbixapi-*/lib/zabbixapi/client.rb \ 26 | && apk del ${APK_FLAGS_COMMON} --purge build-deps \ 27 | && rm -rf /var/cache/apk/* /tmp/* 28 | 29 | WORKDIR /opt/zbx2git/ 30 | CMD ["ruby", "zbx2git.rb"] 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | zbx2git - Zabbix Configuration Versioning Manager 2 | ================ 3 | --- 4 | zbx2git - Exports your Zabbix configuration and uses Git to store any changes made from the previous runs 5 | 6 | A brief overview of how zbx2git works: 7 | 8 | - Reads your Zabbix instance configuration from zbx2git.json 9 | - Exports all the available configuration objects through the API ([configuration.export](https://www.zabbix.com/documentation/3.2/manual/api/reference/configuration/export)) taking advantage of parallelism 10 | - Individually saves the retrieved configurations locally 11 | - A Git Repository is created and maintained for each of the exported configurations objects 12 | 13 | 14 | ## Pre-build Docker images 15 | - https://hub.docker.com/r/syepes/zbx2git/ 16 | - https://hub.docker.com/r/syepes/zbx2git-web/ 17 | 18 | 19 | ## Build Requirements 20 | - Ruby + Gems: parallel zabbixapi git 21 | - Git 22 | 23 | ## CGit integration 24 | Take a look at [cgit_integration](https://github.com/syepes/zbx2git/blob/master/docs/cgit_integration.md) 25 | 26 | ![cgit_integration](https://raw.githubusercontent.com/syepes/zbx2git/master/docs/images/cgit_example.png) 27 | 28 | ## Contribute 29 | If you have any idea for an improvement or find a bug do not hesitate in opening an issue. 30 | 31 | ## License 32 | zbx2git is distributed under the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0) 33 | 34 | Copyright © 2016, [Sebastian YEPES F.](mailto:syepes@gmail.com) 35 | 36 | ## Used open source projects 37 | [Ruby](http://ruby-lang.org) | 38 | [Parallel](https://github.com/grosser/parallel) | 39 | [Zabbixapi](https://github.com/express42/zabbixapi) | 40 | [Git](https://git-scm.com/) 41 | 42 | -------------------------------------------------------------------------------- /zbx2git.json: -------------------------------------------------------------------------------- 1 | { 2 | "export_cfg": [ 3 | { 4 | "type": "hosts", 5 | "method": "host.get", 6 | "output": [ "hostid", "name" ], 7 | "sortorder": "hostid", 8 | "id": "hostid" 9 | }, 10 | { 11 | "type": "groups", 12 | "method": "hostgroup.get", 13 | "output": [ "groupid", "name" ], 14 | "sortorder": "groupid", 15 | "id": "groupid" 16 | }, 17 | { 18 | "type": "valueMaps", 19 | "method": "valuemap.get", 20 | "output": [ "valuemapid", "name" ], 21 | "sortorder": "valuemapid", 22 | "id": "valuemapid" 23 | }, 24 | { 25 | "type": "templates", 26 | "method": "template.get", 27 | "output": [ "templateid", "name" ], 28 | "sortorder": "templateid", 29 | "id": "templateid" 30 | }, 31 | { 32 | "type": "images", 33 | "method": "image.get", 34 | "output": [ "imageid", "name" ], 35 | "sortorder": "imageid", 36 | "id": "imageid" 37 | }, 38 | { 39 | "type": "maps", 40 | "method": "map.get", 41 | "output": [ "sysmapid", "name" ], 42 | "sortorder": "sysmapid", 43 | "id": "sysmapid" 44 | }, 45 | { 46 | "type": "screens", 47 | "method": "screen.get", 48 | "output": [ "screenid", "name" ], 49 | "sortorder": "screenid", 50 | "id": "screenid" 51 | } 52 | ], 53 | "zabbix_cfg": [ 54 | { 55 | "inst": "ZBX-LAB1", 56 | "url": "http://localhost/api_jsonrpc.php", 57 | "user": "Admin", 58 | "password": "zabbix" 59 | }, 60 | { 61 | "inst": "ZBX-LAB2", 62 | "url": "http://localhost/api_jsonrpc.php", 63 | "user": "Admin", 64 | "password": "zabbix" 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /docs/cgit_integration.md: -------------------------------------------------------------------------------- 1 | zbx2git + cgit + uwsgi + nginx 2 | ================ 3 | These instructions are specific to CentOS 7 but should be a guideline for other distros 4 | --- 5 | 6 | #### OS Deps 7 | yum -y install git nginx unzip openssl-devel 8 | 9 | 10 | #### Install cgit 11 | cd /tmp/ 12 | curl -#SL https://git.zx2c4.com/cgit/snapshot/cgit-1.0.zip | unzip 13 | cd cgit-1.0 14 | make get-git 15 | make NO_LUA=1 16 | cp cgit /usr/sbin/ 17 | mkdir -p /usr/share/cgit/ 18 | cp cgit.css /usr/share/cgit/ 19 | curl -#SL http://www.zabbix.com/favicon.ico -o /usr/share/cgit/favicon.ico 20 | curl -#SL http://www.zabbix.com/img/zabbix_logo.png -o /usr/share/cgit/cgit.png 21 | mkdir -p /var/cache/cgit 22 | chown uwsgi:uwsgi /var/cache/cgit 23 | 24 | 25 | #### Install+Setup uwsgi 26 | cd /tmp/ 27 | curl http://uwsgi.it/install | bash -s cgi /usr/sbin/uwsgi 28 | 29 | cat >/etc/uwsgi.ini </etc/systemd/system/uwsgi.service < 9 | # Released under the Apache License, see LICENSE for details. 10 | # 11 | require 'logger' 12 | require 'json' 13 | require 'parallel' 14 | require 'fileutils' 15 | require "zabbixapi" 16 | require 'git' 17 | require 'openssl' 18 | 19 | PATH_CURRENT = Dir.pwd() 20 | 21 | 22 | def secs2human(secs) 23 | [[60, :seconds], [60, :minutes], [24, :hours], [1000, :days]].map{|count, name| 24 | if secs > 0 25 | secs, n = secs.divmod(count) 26 | "#{n.to_i} #{name}" 27 | end 28 | }.compact.reverse.join(' ') 29 | end 30 | 31 | def exportConfig(logger, inst, zbx, cfg) 32 | begin 33 | ts_s = Time.now.to_i 34 | path = "#{PATH_CURRENT}/repository/#{inst}/#{cfg[:type]}" 35 | logger.info "Start Exporting: #{cfg[:type]} (#{inst})" 36 | 37 | results = zbx.query( 38 | :method => cfg[:method], 39 | :params => {:output => cfg[:output], :sortorder => cfg[:sortorder]} 40 | ) 41 | 42 | # Clean previously exported files 43 | FileUtils.rm_f(Dir.glob("#{path}/*")) 44 | 45 | for result in results do 46 | begin 47 | # File name normalization 48 | file = "#{result["name"].gsub(/[^a-zA-Z0-9\-_\s\.\(\)]/,'')}.json" 49 | logger.debug "#{path}/#{file}" 50 | 51 | json = JSON.parse(zbx.query( 52 | :method => "configuration.export", 53 | :params => { 54 | :options => { 55 | cfg[:type] => [ result[cfg[:id]] ], 56 | }, 57 | :format => 'json' 58 | } 59 | )) 60 | 61 | # Clear export date to prevent unnecessary git changes 62 | json["zabbix_export"]["date"] = "" 63 | json_pretty = JSON.pretty_generate(json) 64 | 65 | FileUtils.mkdir_p(path) unless File.exists?(path) 66 | File.open("#{path}/#{file}","w"){|f| f.puts json_pretty} 67 | rescue Exception => e 68 | logger.error "Exporting: #{cfg[:type]} (#{inst}) : #{e.message}" 69 | logger.debug "Trace: #{e.backtrace.inspect}" 70 | end 71 | end 72 | 73 | begin 74 | if !File.exists?("#{path}/.git") 75 | g = Git.init(path, :log => logger) 76 | g.add(:all=>true) 77 | m = g.commit_all('initial') 78 | logger.debug "Git (Init): #{m}" if m != nil 79 | File.open("#{path}/.git/description","w"){|f| f.puts "#{inst} - #{cfg[:type]}"} 80 | else 81 | g = Git.open(path, :log => $logger) 82 | g.add(:all=>true) 83 | 84 | any_change = ["changed","added","deleted"].map{|x| x if !g.status.send(x.to_sym).empty?}.compact.delete_if(&:empty?) 85 | if !any_change.empty? 86 | logger.warn "Detected changes: (#{any_change.join(', ')}) in #{cfg[:type]} (#{inst})" 87 | end 88 | 89 | m = g.commit_all("#{any_change.join(', ')}") if !any_change.empty? 90 | logger.debug "Git (#{any_change.join(', ')}): #{m}" if m != nil 91 | end 92 | rescue Exception => e 93 | logger.error "Saving changes to git: #{cfg[:type]} (#{inst}) : #{e.message}" 94 | end 95 | 96 | logger.info "Finished Exporting: #{cfg[:type]} (#{inst}) in #{secs2human(Time.now.to_i - ts_s)}" 97 | rescue Exception => e 98 | logger.error "Exporting: #{cfg[:type]} (#{inst}) : #{e.message}" 99 | logger.debug "Trace: #{e.backtrace.inspect}" 100 | end 101 | end 102 | 103 | 104 | 105 | FileUtils.mkdir_p("#{PATH_CURRENT}/logs") unless File.exists?("#{PATH_CURRENT}/logs") 106 | log = Logger.new("#{PATH_CURRENT}/logs/zbx2git.log", 'monthly') 107 | log.level = Logger::INFO 108 | 109 | begin 110 | cfg = JSON.parse(File.read('zbx2git.json'), :symbolize_names => true) 111 | rescue Exception => e 112 | log.error "Error Loading config file: zbx2git.json : #{e.message}" 113 | log.debug "Trace: #{e.backtrace.inspect}" 114 | exit 1 115 | end 116 | 117 | 118 | log.info "Start Collecting of instances: #{cfg[:zabbix_cfg].map{|i| i[:inst] }.join(', ')}" 119 | 120 | ts_s = Time.now.to_i 121 | Parallel.each(cfg[:zabbix_cfg], in_processes: 8) { |zab_cfg| 122 | logger = Logger.new("#{PATH_CURRENT}/logs/zbx2git_#{zab_cfg[:inst]}.log", 'monthly') 123 | logger.level = Logger::INFO 124 | 125 | begin 126 | ts_s = Time.now.to_i 127 | logger.info "Start Collecting: #{zab_cfg[:inst]} - #{zab_cfg[:url]}" 128 | zbx = ZabbixApi.connect(:url => zab_cfg[:url], :user => zab_cfg[:user], :password => zab_cfg[:password], :timeout => 15) 129 | 130 | for exp_cfg in cfg[:export_cfg] do 131 | exportConfig(logger, zab_cfg[:inst], zbx, exp_cfg) 132 | end 133 | logger.info "Finished Collecting: #{zab_cfg[:inst]} in #{secs2human(Time.now.to_i - ts_s)}" 134 | 135 | rescue Exception => e 136 | logger.error "Connecting to Zabbix: #{zab_cfg[:inst]} : #{e.message}" 137 | logger.debug "Trace: #{e.backtrace.inspect}" 138 | end 139 | } 140 | log.info "Completed in #{secs2human(Time.now.to_i - ts_s)}" 141 | 142 | --------------------------------------------------------------------------------