├── .gitignore ├── LICENSE.txt ├── README.md ├── RELEASE.md ├── get-table-list.pl └── zabbix-dump /.gitignore: -------------------------------------------------------------------------------- 1 | /.project 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jens Berthold 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backup script for Zabbix configuration data (MySQL/PostgreSQL) 2 | 3 | This is a MySQL/PostgreSQL database backup script for the [Zabbix](http://www.zabbix.com/) monitoring software from version 1.3.1 up to 4.4. 4 | 5 | ## Download 6 | 7 | Download the latest (stable) release here: 8 | 9 | https://github.com/maxhq/zabbix-backup/releases/latest 10 | 11 | ## More informations 12 | 13 | Please see the [Project Wiki](https://github.com/maxhq/zabbix-backup/wiki). 14 | 15 | ## Version history 16 | 17 | **0.9.3 (2020-01-17)** 18 | 19 | - ENH: Check for unknown tables 20 | - ENH: Speed up MySQL backup by not calling mysqldump for every single table anymore 21 | - ENH: New option -S to specify PostgreSQL schema 22 | - FIX: Stabilize and enhance PostgreSQL dump 23 | - FIX: Skip IP reverse lookup for localhost, fix multiline dig answers 24 | 25 | **0.9.2 (2020-01-16)** 26 | 27 | - ENH: Support for Zabbix 4.4 28 | - ENH: Fix (non-critical) shellcheck issues (Mario Trangoni) 29 | - CHG: Fix and enhance helper script get-table-list.pl 30 | - FIX: Escape special characters while reading password (ironbishop) 31 | - FIX: Re-enable accidentally disabled cleanup of postgresql password file 32 | - FIX: Insert hostname into backup file also if database resides on localhost 33 | 34 | **0.9.1 (2019-03-21)** 35 | 36 | - FIX: Correctly process hostname option -H (Tiago Cruz) 37 | 38 | **0.9.0 (2019-03-14)** 39 | 40 | - NEW: Support for PostgreSQL databases (Sergey Galkin) 41 | - NEW: Option -P to specify database server port (Sergey Galkin) 42 | - NEW: Support for socket connections to MySQL and PostgreSQL server (Greg Cockburn) 43 | - NEW: Database connection parameters are read from Zabbix servern config by default (Greg Cockburn) 44 | - ENH: Support for Zabbix 4.0 (Wesley Schaft) 45 | - ENH: Options -h and --help to show help (hostname is now specified using -H) 46 | - ENH: Options -Z to skip reading the Zabbix server config file 47 | - CHG: Rename script to "zabbix-dump" 48 | - FIX: Support for whitespaces in database parameters (Greg Cockburn) 49 | - FIX: Support for backslashes in manually entered password (Greg Cockburn) 50 | 51 | **0.8.2 (2016-09-08)** 52 | 53 | - NEW: Option -x to use XZ instead of GZ for compression (Jonathan Wright) 54 | - NEW: Option -0 for "no compression" 55 | - FIX: Evil space was masking end of here-document (fixed in #8 by @msjmeyer) 56 | - FIX: Prevent "Warning: Using a password on the command line interface can be insecure." 57 | 58 | **0.8.1 (2016-07-11)** 59 | 60 | - ENH: Added Zabbix 3.0.x tables to list (added & tested by Ruslan Ohitin) 61 | 62 | **0.8.0 (2016-01-22)** 63 | 64 | - FIX: Only invoke `dig` if available 65 | - ENH: Option -c to use a MySQL config ("options") file (suggested by Daniel Schneller) 66 | - ENH: Option -r to rotate backup files (Daniel Schneller) 67 | - ENH: Add database version to filename if available 68 | - ENH: Add quiet mode. IP reverse lookup optional (Daniel Schneller) 69 | - ENH: Bash related fixes (Misu Moldovan) 70 | - CHG: Default output directory is now $PWD instead of script dir 71 | 72 | **0.7.1 (2015-01-27)** 73 | 74 | - NEW: Parsing of commandline arguments implemented 75 | - ENH: Try reverse lookup of IPs and include hostname/IP in filename 76 | - REV: Stop if database password is wrong 77 | 78 | **0.7.0 (2014-10-02)** 79 | 80 | - ENH: Complete overhaul to make script work with lots of Zabbix versions 81 | 82 | **0.6.0 (2014-09-15)** 83 | 84 | - REV: Updated the table list for use with zabbix v2.2.3 85 | 86 | **0.5.0 (2013-05-13)** 87 | 88 | - NEW: Added table list comparison between database and script 89 | 90 | **0.4.0 (2012-03-02)** 91 | 92 | - REV: Incorporated mysqldump options (suggested by Jonathan Bayer) 93 | 94 | **0.3.0 (2012-02-06)** 95 | 96 | - ENH: Backup of Zabbix 1.9.x / 2.0.0, removed unnecessary use of 97 | variables (DATEBIN etc) for commands that use to be in $PATH 98 | 99 | **0.2.0 (2011-11-05)** 100 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | ## Releasing a new version 2 | 3 | ### Before commit 4 | 5 | * Update README.md 6 | 7 | ### After commit 8 | 9 | * Tag the release commit: `git tag v0.9.2 && git push --tags` 10 | * Add release at https://github.com/maxhq/zabbix-backup/releases 11 | 12 | **Summary of main changes** 13 | 14 | ``` 15 | This release fixes ... 16 | 17 | ### Direct download 18 | 19 | [zabbix-mysql-dump](https://raw.githubusercontent.com/maxhq/zabbix-backup/v0.9.2/zabbix-dump) (v0.9.2) 20 | 21 | ### Changelog 22 | 23 | Please see the [version history in the README](https://github.com/maxhq/zabbix-backup/blob/v0.9.2/README.md#version-history). 24 | 25 | ### Thanks! 26 | - @username 27 | ``` 28 | 29 | * Update https://github.com/maxhq/zabbix-backup/wiki 30 | * Update https://zabbix.org/wiki/Docs/howto/database_backup_script 31 | * Announce release at https://www.linkedin.com/groups/161448 32 | 33 | > **New version x.x.x of zabbix-dump** 34 | > 35 | > zabbix-dump is a Linux bash script for backing up the Zabbix configuration by saving MySQL or PostgreSQL database tables into a compressed file. 36 | > Tables holding configuration data will be fully backed up. For mass data tables (events, history, trends, ...) only the table schema is stored without any data (to keep the backup small). 37 | > 38 | > Overiew: https://github.com/maxhq/zabbix-backup/blob/master/README.md 39 | > Latest release: https://github.com/maxhq/zabbix-backup/releases/latest 40 | 41 | * Announce release at https://www.xing.com/communities/forums/100845147 42 | 43 | > **Neue Version x.x.x von zabbix-dump** 44 | > 45 | > zabbix-dump ist ein Linux-Bash-Skript zum Backup der Zabbix-Konfiguration durch Sicherung der MySQL- bzw. PostgreSQL-Datenbanktabellen in eine komprimierte Datei. 46 | > Es sichert Konfigurationsdaten komplett, bei Tabellen mit Massendaten (Historie, Events, Trends etc.) jedoch nur das "leere" Datenbankschema (um das Backup zu minimieren). 47 | > 48 | > Übersicht: https://github.com/maxhq/zabbix-backup/blob/master/README.md 49 | > Neueste Version: https://github.com/maxhq/zabbix-backup/releases/latest 50 | -------------------------------------------------------------------------------- /get-table-list.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # NAME 3 | # get-table-list.pl - List current and historic Zabbix database tables 4 | # 5 | # SYNOPSIS 6 | # This is mainly a helper script for developing the backup script. 7 | # 8 | # It connects to svn://svn.zabbix.com (using Subversion client "svn") and 9 | # fetches the schema definitions of all tagged Zabbix versions beginning 10 | # from 1.3.1 (this takes a while). 11 | # 12 | # It then prints out a list of all tables together with the first and last 13 | # Zabbix version where they were used. 14 | # 15 | # HISTORY 16 | # v0.1 - 2014-09-19 First version 17 | # 18 | # AUTHOR 19 | # Jens Berthold (maxhq), 2020 20 | use strict; 21 | use warnings; 22 | 23 | use version; 24 | 25 | my $REPO = 'https://git.zabbix.com/scm/zbx/zabbix.git'; 26 | my $REPO_WEB = 'https://git.zabbix.com/projects/ZBX/repos/zabbix/raw'; 27 | 28 | sub stop { 29 | my ($msg) = @_; 30 | print "ERROR: $msg\n"; 31 | exit; 32 | } 33 | 34 | # sort version numbers correctly 35 | sub cmpver { 36 | my ($a, $b) = @_; 37 | 38 | # split version parts: 1.2.3rc1 --> 1 2 3rc1 39 | my @a_parts = split /\./, $a; 40 | my @b_parts = split /\./, $b; 41 | 42 | for (my $i=0; $i= scalar(@b_parts); 44 | # split number parts: 3rc1 --> 3 rc 1 45 | my ($a_num, $a_type, $a_idx) = $a_parts[$i] =~ m/^(\d+)(\D+)?(\d+)?$/; 46 | my ($b_num, $b_type, $b_idx) = $b_parts[$i] =~ m/^(\d+)(\D+)?(\d+)?$/; 47 | my $cmp; 48 | # 3 before 4 49 | $cmp = $a_num <=> $b_num; return $cmp unless $cmp == 0; 50 | # 3rc1 before 3 51 | return -1 if $a_type and not $b_type; 52 | return 1 if not $a_type and $b_type; 53 | # a1 before b1 54 | $cmp = ($a_type//"") cmp ($b_type//""); return $cmp unless $cmp == 0; 55 | # rc1 before rc2 56 | $cmp = ($a_idx//0) <=> ($b_idx//0); return $cmp unless $cmp == 0; 57 | } 58 | # 1.2 before 1.2.1 59 | return -1 if scalar(@a_parts) < scalar(@b_parts); 60 | # equal 61 | return 0; 62 | } 63 | 64 | # Get tag list from repo: 65 | sub get_taglist { 66 | print "Querying existing tags from $REPO...\n"; 67 | 68 | # Returned format: 69 | # 7f6b20903537b9bbf72fe2b75ab7fac557856aad refs/tags/1.0 70 | # 693709cc4a80777f7759856c853b38cbc920f068 refs/tags/1.1 71 | my @tags_raw = `git ls-remote -t $REPO`; 72 | # remove trailing newline 73 | chomp @tags_raw; 74 | # skip release candidates, betas and tags like "zabicom-xxx" 75 | @tags_raw = grep { m{ refs/tags/ \d+ \. \d+ ( \. \d+ )? $}x } @tags_raw; 76 | 77 | # Create HashRef: 78 | # 1.0 => 7f6b20903537b9bbf72fe2b75ab7fac557856aad 79 | # 1.1 => 693709cc4a80777f7759856c853b38cbc920f068 80 | return { 81 | map { m{^ (\w+) \s+ refs/tags/ (.*) $}x; $2 => $1 } @tags_raw 82 | }; 83 | } 84 | 85 | # Read old table informations from zabbix-dump 86 | sub get_old_tabinfo { 87 | my $tabinfo_old = {}; 88 | 89 | open my $fh, '<', './zabbix-dump' or stop("Couldn't find 'zabbix-dump': $!"); 90 | 91 | my $within_data_section = 0; 92 | while (<$fh>) { 93 | chomp; 94 | if (/^__DATA__/) { $within_data_section = 1; next } 95 | next unless $within_data_section; 96 | my ($table, $from, undef, $to, $mode) = split /\s+/; 97 | 98 | $tabinfo_old->{$table} = { 99 | from => $from, 100 | to => $to, 101 | schema_only => ($mode//"") eq "SCHEMAONLY" ? 1 : 0, 102 | }; 103 | } 104 | 105 | return $tabinfo_old; 106 | } 107 | 108 | 109 | 110 | `which git` or stop("No Git client found"); 111 | 112 | my $tabinfo_old = get_old_tabinfo(); # old data from zabbix_dump 113 | 114 | my $tags = get_taglist(); 115 | my $tabinfo = {}; # for each table, create a list of Zabbix versions that know it 116 | 117 | print "Reading table schemas...\n"; 118 | 119 | # Loop over tags and read table schema 120 | for my $tag (sort { cmpver($a,$b) } keys %$tags) { 121 | next if cmpver($tag, "1.3.1") < 0; # before Zabbix 1.3.1, schema was stored as pure SQL 122 | 123 | my ($schema, $subdir); 124 | 125 | printf " - %-8s %s", $tag, "Looking for schema..."; 126 | # search in subdir /schema (<= 1.9.8) and /src for schema.(sql|tmpl) 127 | for my $sub (qw(schema src)) { 128 | # file list: 129 | # 100644 blob 1f0a05eb826dfcdb26f9429ad30c720454374ca1 data.tmpl 130 | # 100644 blob b98b5eecc62731508c09d9e76d7aed9d4eb201f2 schema.tmpl 131 | my @files_raw = `curl -s $REPO_WEB/create/$sub?at=refs%2Ftags%2F$tag`; 132 | next unless @files_raw; # directory not found? 133 | chomp @files_raw; # remove trailing newline 134 | my @files = map { /^ \d+ \s+ \w+ \s+ \w+ \s+ (.*) $/x; $1 } @files_raw; 135 | 136 | ($schema) = grep /^schema\.(sql|tmpl)/, @files; 137 | $subdir = $sub; 138 | last; 139 | } 140 | if (!$schema) { 141 | print "\nNo schema found in tag $tag\n"; 142 | next; 143 | } 144 | print " Processing ($schema)... "; 145 | my @table = `curl -s $REPO_WEB/create/$subdir/$schema?at=refs%2Ftags%2F$tag`; 146 | for (@table) { 147 | chomp; 148 | next unless m/^TABLE/; 149 | my (undef, $table) = split /\|/; 150 | $tabinfo->{$table} //= []; 151 | push @{$tabinfo->{$table}}, $tag; 152 | } 153 | print " Done\n"; 154 | } 155 | 156 | # 157 | # Print out results 158 | # 159 | print "\n\n"; 160 | print "TABLE FIRST USE LAST USE MODE\n"; 161 | print "----------------------------------------------------\n"; 162 | for my $tab (sort keys %$tabinfo) { 163 | my $mode = $tabinfo_old->{$tab} 164 | ? ($tabinfo_old->{$tab}->{schema_only} ? ' SCHEMAONLY' : '') 165 | : ' <-- NEW TABLE! Only store schema?'; 166 | printf "%-26s %-8s - %-8s%s\n", $tab, $tabinfo->{$tab}->[0], $tabinfo->{$tab}->[-1], $mode; 167 | } 168 | -------------------------------------------------------------------------------- /zabbix-dump: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | VERSION=0.9.3 3 | # 4 | # NAME 5 | # zabbix-dump - Configuration Backup for Zabbix' MySQL or PostgreSQL data 6 | # 7 | # SYNOPSIS 8 | # This is a MySQL configuration backup script for Zabbix 1.x, 2.x, 3.x and 4.x. 9 | # It does a full backup of all configuration tables, but only a schema 10 | # backup of large data tables. 11 | # 12 | # The script is based on a script by Ricardo Santos 13 | # (http://zabbixzone.com/zabbix/backuping-only-the-zabbix-configuration/) 14 | # 15 | # CONTRIBUTORS 16 | # - Ricardo Santos 17 | # - Jens Berthold (maxhq) 18 | # - Oleksiy Zagorskyi (zalex) 19 | # - Petr Jendrejovsky 20 | # - Jonathan Bayer 21 | # - Andreas Niedermann (dre-) 22 | # - Mișu Moldovan (dumol) 23 | # - Daniel Schneller (dschneller) 24 | # - Ruslan Ohitin (ruslan-ohitin) 25 | # - Jonathan Wright (neonardo1) 26 | # - msjmeyer 27 | # - Sergey Galkin (sergeygalkin) 28 | # - Greg Cockburn (gergnz) 29 | # - yangqi 30 | # - Johannes Petz (PetzJohannes) 31 | # - Wesley Schaft (wschaft) 32 | # - Tiago Cruz (tiago-cruz-movile) 33 | # - Mario Trangoni (mjtrangoni) 34 | # - ironbishop 35 | # 36 | # AUTHOR 37 | # Jens Berthold (maxhq), 2020 38 | # 39 | # LICENSE 40 | # This script is released under the MIT License (see LICENSE.txt) 41 | 42 | 43 | # 44 | # DEFAULT VALUES 45 | # 46 | # DO NOT EDIT THESE VALUES! 47 | # Instead, use command line parameters or a config file to specify options. 48 | # 49 | DUMPDIR="$PWD" 50 | DBTYPE="mysql" 51 | DEFAULT_DBHOST="127.0.0.1" 52 | DEFAULT_DBSCHEMA="public" 53 | DEFAULT_DBNAME="zabbix" 54 | DEFAULT_DBUSER="zabbix" 55 | DEFAULT_DBPASS="" 56 | COMPRESSION="gz" 57 | QUIET="no" 58 | REVERSELOOKUP="yes" 59 | GENERATIONSTOKEEP=0 60 | ZBX_CONFIG="/etc/zabbix/zabbix_server.conf" 61 | READ_ZBX_CONFIG="yes" 62 | HANDLE_UNKNOWN="fail" 63 | 64 | # 65 | # Show version 66 | # 67 | show_version() { 68 | echo "zabbix-dump version $VERSION" 69 | exit 70 | } 71 | 72 | if [[ "$1" == "--version" ]]; then show_version; fi 73 | 74 | # 75 | # Show help 76 | # 77 | if [[ "$1" == "--help" || "$1" == "-h" ]]; then 78 | cat <&2; exit 1 ;; 220 | :) echo "Option -$OPTARG requires an argument" >&2; exit 1 ;; 221 | esac 222 | done 223 | 224 | [ -n "$MYSQL_CONFIG" ] && READ_ZBX_CONFIG="no" 225 | 226 | # (Try) reading database config from zabbix_server.conf 227 | if [[ "${READ_ZBX_CONFIG}" == "yes" && -f "${ZBX_CONFIG}" && -r "${ZBX_CONFIG}" ]]; then 228 | [ "$QUIET" == "no" ] && echo "Reading database options from ${ZBX_CONFIG}..." 229 | 230 | # Reading config with awk (instead of source'ing the file) to avoid shell special characters execution 231 | DBHOST="$(/usr/bin/awk -F'=' '/^DBHost/{ print $2 }' "${ZBX_CONFIG}")" 232 | DBPORT="$(/usr/bin/awk -F'=' '/^DBPort/{ print $2 }' "${ZBX_CONFIG}")" 233 | DBNAME="$(/usr/bin/awk -F'=' '/^DBName/{ print $2 }' "${ZBX_CONFIG}")" 234 | DBSCHEMA="$(/usr/bin/awk -F'=' '/^DBSchema/{ print $2 }' "${ZBX_CONFIG}")" 235 | DBUSER="$(/usr/bin/awk -F'=' '/^DBUser/{ print $2 }' "${ZBX_CONFIG}")" 236 | DBPASS="$(/usr/bin/awk -F'=' '/^DBPassword/{ print $2 }' "${ZBX_CONFIG}")" 237 | 238 | # set non-existing variables to their Zabbix defaults (if they are non-empty string) 239 | [ -z ${DBHOST+x} ] && DBHOST="localhost" 240 | 241 | # Zabbix config has a special treatment for DBHost: 242 | # > If set to localhost, socket is used for MySQL. 243 | # > If set to empty string, socket is used for PostgreSQL. 244 | if [[ ( "$DBTYPE" == "mysql" && "$DBHOST" == "localhost" ) || ( "$DBTYPE" == "psql" && "$DBHOST" == "" ) ]]; then 245 | [ "$DBTYPE" == "mysql" ] && searchstr="mysql" 246 | [ "$DBTYPE" == "psql" ] && searchstr="postgres" 247 | sock=$(netstat -axn | grep -m1 "$searchstr" | sed -r 's/^.*\s+([^ ]+)$/\1/') 248 | if [[ -n "$sock" && -S $sock ]]; then DBSOCKET="$sock"; DBHOST=""; fi 249 | else 250 | DBSOCKET="$(/usr/bin/awk -F'=' '/^DBSocket/{ print $2 }' "${ZBX_CONFIG}")" 251 | fi 252 | 253 | # Otherwise: set default values 254 | else 255 | # if a MySQL config file is specified we assume it contains all connection parameters 256 | if [ -z "$MYSQL_CONFIG" ]; then 257 | DBHOST="$DEFAULT_DBHOST" 258 | DBNAME="$DEFAULT_DBNAME" 259 | DBUSER="$DEFAULT_DBUSER" 260 | DBPASS="$DEFAULT_DBPASS" 261 | fi 262 | fi 263 | 264 | # Always set default ports, even if we read other parameters from zabbix_server.conf 265 | [[ -z "$DBPORT" && "$DBTYPE" == "mysql" ]] && DBPORT="3306" 266 | [[ -z "$DBPORT" && "$DBTYPE" == "psql" ]] && DBPORT="5432" 267 | 268 | # Options specified via command line override defaults or those from zabbix_server.conf (if any) 269 | [ -n "$ODBHOST" ] && DBHOST=$ODBHOST 270 | [ -n "$ODBPORT" ] && DBPORT=$ODBPORT 271 | [ -n "$ODBSOCKET" ] && DBSOCKET=$ODBSOCKET && DBHOST="" 272 | [ -n "$ODBSCHEMA" ] && DBSCHEMA=$ODBSCHEMA 273 | [ -n "$ODBNAME" ] && DBNAME=$ODBNAME 274 | [ -n "$ODBUSER" ] && DBUSER=$ODBUSER 275 | [ -n "$ODBPASS" ] && DBPASS=$ODBPASS 276 | 277 | # Password prompt 278 | if [ "$DBPASS" = "-" ]; then 279 | read -r -s -p "Enter database password for user '$DBUSER' (input will be hidden): " DBPASS 280 | echo "" 281 | fi 282 | 283 | # MySQL config file validations 284 | if [ -n "$MYSQL_CONFIG" ]; then 285 | if [ ! -r "$MYSQL_CONFIG" ]; then 286 | echo "ERROR: Cannot read configuration file $MYSQL_CONFIG" >&2 287 | exit 1 288 | fi 289 | # Database name needs special treatment: 290 | # For mysqldump it has to be specified on the command line! 291 | # Therefore we need to get it from the config file 292 | if [ $DB_GIVEN -eq 0 ]; then 293 | DBNAME=$(grep -m 1 ^database= "$MYSQL_CONFIG" | cut -d= -f2) 294 | fi 295 | fi 296 | 297 | if [ -z "$DBNAME" ]; then 298 | echo "ERROR: Please specify a database name (option -d)" 299 | exit 1 300 | fi 301 | 302 | # 303 | # CONSTANTS 304 | # 305 | SUFFIX=""; test ! -z $COMPRESSION && SUFFIX=".${COMPRESSION}" 306 | 307 | DB_OPTS=() 308 | case $DBTYPE in 309 | mysql) 310 | [ -n "$MYSQL_CONFIG" ] && DB_OPTS=("${DB_OPTS[@]}" --defaults-extra-file="$MYSQL_CONFIG") 311 | [ -n "$DBSOCKET" ] && DB_OPTS=("${DB_OPTS[@]}" -S $DBSOCKET) 312 | [ -n "$DBHOST" ] && DB_OPTS=("${DB_OPTS[@]}" -h $DBHOST) 313 | [ -n "$DBUSER" ] && DB_OPTS=("${DB_OPTS[@]}" -u $DBUSER) 314 | [ -n "$DBPASS" ] && DB_OPTS=("${DB_OPTS[@]}" -p"$DBPASS") 315 | DB_OPTS=("${DB_OPTS[@]}" -P"$DBPORT") 316 | DB_OPTS_BATCH=("${DB_OPTS[@]}" --batch --silent) 317 | [ -n "$DBNAME" ] && DB_OPTS_BATCH=("${DB_OPTS_BATCH[@]}" -D $DBNAME) 318 | ;; 319 | psql) 320 | [ -n "$DBSOCKET" ] && DB_OPTS=("${DB_OPTS[@]}" -h $DBSOCKET) 321 | [ -n "$DBHOST" ] && DB_OPTS=("${DB_OPTS[@]}" -h $DBHOST) 322 | [ -n "$DBUSER" ] && DB_OPTS=("${DB_OPTS[@]}" -U $DBUSER) 323 | DB_OPTS=("${DB_OPTS[@]}" -p"$DBPORT") 324 | if [ -n "$DBPASS" ]; then 325 | export PGPASSFILE=$(mktemp -u) 326 | echo "$DBHOST:$DBPORT:$DBNAME:$DBUSER:$DBPASS" > $PGPASSFILE 327 | chmod 600 $PGPASSFILE 328 | fi 329 | DB_OPTS_BATCH=("${DB_OPTS[@]}" -Atw) 330 | [ -n "$DBNAME" ] && DB_OPTS_BATCH=("${DB_OPTS_BATCH[@]}" -d $DBNAME) 331 | ;; 332 | esac 333 | 334 | # Log file for errors 335 | ERRORLOG=$(mktemp) 336 | 337 | # Host name 338 | if [[ -z "$DBHOST" || "$DBHOST" == "127.0.0.1" || "$DBHOST" == "127.0.0.1" ]]; then 339 | DBHOSTNAME="$(uname -n)" 340 | else 341 | DBHOSTNAME="$DBHOST" 342 | 343 | # Try reverse lookup if IP is given 344 | command -v dig >/dev/null 2>&1 345 | FIND_DIG=$? 346 | if [[ "$REVERSELOOKUP" == "yes" && $FIND_DIG -eq 0 && -n "$DBHOST" ]]; then 347 | # Try resolving a given host ip 348 | newHostname=$(dig +noall +answer -x "${DBHOST}" | head -n1 | sed -r 's/((\S+)\s+)+([^\.]+)\..*/\3/') 349 | test \! -z "$newHostname" && DBHOSTNAME="$newHostname" 350 | fi 351 | fi 352 | 353 | # 354 | # CONFIG DUMP 355 | # 356 | if [ "$QUIET" == "no" ]; then 357 | cat <<-EOF 358 | Configuration: 359 | - type: $DBTYPE 360 | EOF 361 | [ -n "$MYSQL_CONFIG" ] && echo " - cfg file: $MYSQL_CONFIG" 362 | [ -n "$DBHOST" ] && echo " - host: $DBHOST ($DBHOSTNAME)" && echo " - port: $DBPORT" 363 | [ -n "$DBSOCKET" ] && echo " - socket: $DBSOCKET" 364 | [ -n "$DBSCHEMA" ] && echo " - schema: $DBSCHEMA" 365 | [ -n "$DBNAME" ] && echo " - database: $DBNAME" 366 | [ -n "$DBUSER" ] && echo " - user: $DBUSER" 367 | [ -n "$DUMPDIR" ] && echo " - output: $DUMPDIR" 368 | fi 369 | 370 | # 371 | # FUNCTIONS 372 | # 373 | 374 | # Returns TRUE if argument 1 is part of the given array (remaining arguments) 375 | elementIn () { 376 | local e 377 | for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done 378 | return 1 379 | } 380 | check_binary() { 381 | if ! which $1 > /dev/null; then 382 | echo "Executable '$1' not found." >&2 383 | case $1 in 384 | mysql) 385 | echo "(with Debian try \"apt-get install mysql-client\")" >&2 ;; 386 | psql) 387 | echo "(with Debian try \"apt-get install postgresql-client\")" >&2 ;; 388 | esac 389 | exit 1 390 | fi 391 | } 392 | clean_psql_pass() { 393 | if [ $DBTYPE = "psql" -a -n "$PGPASSFILE" ]; then 394 | rm -f $PGPASSFILE 395 | fi 396 | } 397 | 398 | # 399 | # CHECKS 400 | # 401 | case $DBTYPE in 402 | mysql) 403 | check_binary mysqldump ;; 404 | psql) 405 | check_binary pg_dump ;; 406 | *) 407 | echo "Sorry, database type '$DBTYPE' is not supported." 408 | echo "Please specify either 'mysql' or 'psql'." 409 | exit 1 ;; 410 | esac 411 | 412 | # 413 | # READ TABLE LIST from __DATA__ section at the end of this script 414 | # (http://stackoverflow.com/a/3477269/2983301) 415 | # 416 | SCHEMAONLY_TABLES=() 417 | KNOWN_TABLES=() 418 | while read -r line; do 419 | table=$(echo "$line" | cut -d" " -f1) 420 | echo "$line" | cut -d" " -f5 | grep -qi "SCHEMAONLY" 421 | test $? -eq 0 && SCHEMAONLY_TABLES+=($table) 422 | KNOWN_TABLES+=($table) 423 | done < <(sed '0,/^__DATA__$/d' "${BASH_SOURCE[*]}" | tr -s " ") 424 | 425 | # paranoid check 426 | if [ ${#SCHEMAONLY_TABLES[@]} -lt 5 ]; then 427 | echo "ERROR: The number of large data tables configured in this script is less than 5." >&2 428 | exit 1 429 | fi 430 | 431 | # 432 | # BACKUP 433 | # 434 | # Read table list from database 435 | [ "$QUIET" == "no" ] && echo "Fetching list of existing tables..." 436 | case $DBTYPE in 437 | mysql) 438 | DB_TABLES=$(mysql "${DB_OPTS_BATCH[@]}" -e "SELECT table_name FROM information_schema.tables WHERE table_schema = '$DBNAME'" 2>$ERRORLOG) 439 | ;; 440 | psql) 441 | DB_TABLES=$(psql "${DB_OPTS_BATCH[@]}" -c "SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_catalog='$DBNAME' AND table_type='BASE TABLE'" 2>$ERRORLOG) 442 | ;; 443 | esac 444 | if [ $? -ne 0 ]; then 445 | echo "ERROR while trying to access database:" 2>&1; 446 | cat $ERRORLOG 2>&1; 447 | clean_psql_pass 448 | exit 1; 449 | fi 450 | 451 | DB_TABLES=$(echo "$DB_TABLES" | sort) 452 | DB_TABLE_NUM=$(echo "$DB_TABLES" | wc -l) 453 | 454 | # Check if existing tables are known 455 | UNKNOWN_TABLES=() 456 | while read -r table; do 457 | elementIn "$table" "${KNOWN_TABLES[@]}" || UNKNOWN_TABLES+=($table) 458 | done <<<"$DB_TABLES" 459 | if [ ${#UNKNOWN_TABLES[@]} -gt 0 ]; then 460 | if [[ "$QUIET" == "no" || $HANDLE_UNKNOWN == "fail" ]]; then 461 | echo "" 462 | [ $HANDLE_UNKNOWN == "fail" ] && echo "ERROR" 463 | echo "Unknown tables found in database:" 464 | for tab in "${UNKNOWN_TABLES[@]}"; do echo " - $tab"; done 465 | [ $HANDLE_UNKNOWN == "backup" ] && echo "They will be included (full data backup) as -f was specified" 466 | [ $HANDLE_UNKNOWN == "ignore" ] && echo "They will be ignored as -i was specified" 467 | [ $HANDLE_UNKNOWN == "fail" ] && echo "To include them (full data backup) specify -f, to ignore them use -i" 468 | echo "" 469 | fi 470 | if [ $HANDLE_UNKNOWN == "fail" ]; then 471 | clean_psql_pass 472 | exit 1; 473 | fi 474 | fi 475 | 476 | # Query Zabbix database version 477 | VERSION="" 478 | case $DBTYPE in 479 | mysql) 480 | DB_VER=$(mysql "${DB_OPTS_BATCH[@]}" -N -e "select optional from dbversion;" 2>/dev/null) 481 | ;; 482 | psql) 483 | DB_VER=$(psql "${DB_OPTS_BATCH[@]}" -c "select optional from dbversion;" 2>/dev/null) 484 | ;; 485 | esac 486 | if [ $? -eq 0 ]; then 487 | # version string is like: 02030015 488 | re='(.*)([0-9]{2})([0-9]{4})' 489 | if [[ $DB_VER =~ $re ]]; then 490 | VERSION="_db-${DBTYPE}-${BASH_REMATCH[1]}.$(( ${BASH_REMATCH[2]} + 0 )).$(( ${BASH_REMATCH[3]} + 0 ))" 491 | fi 492 | fi 493 | 494 | # Assemble file name 495 | DUMPFILENAME_PREFIX="zabbix_cfg_${DBHOSTNAME}" 496 | DUMPFILEBASE="${DUMPFILENAME_PREFIX}_$(date +%Y%m%d-%H%M)${VERSION}.sql" 497 | DUMPFILE="$DUMPDIR/$DUMPFILEBASE" 498 | 499 | PROCESSED_SCHEMAONLY_TABLES=() 500 | i=0 501 | 502 | mkdir -p "${DUMPDIR}" 503 | 504 | [ "$QUIET" == "no" ] && echo "Starting table backups..." 505 | case $DBTYPE in 506 | mysql) 507 | # dump schemas 508 | DUMP_OPTS=(--opt --single-transaction --skip-lock-tables --no-data --routines) 509 | if [ $HANDLE_UNKNOWN == "ignore" ]; then 510 | while read -r table; do 511 | if elementIn "$table" "${UNKNOWN_TABLES[@]}"; then 512 | DUMP_OPTS+=(--ignore-table=$DBNAME.$table) 513 | fi 514 | done <<<"$DB_TABLES" 515 | fi 516 | 517 | mysqldump "${DB_OPTS[@]}" "${DUMP_OPTS[@]}" $DBNAME > "$DUMPFILE" 2>$ERRORLOG 518 | 519 | if [ $? -ne 0 ]; then echo $'\nERROR: Could not backup table schemas.\n' >&2; cat $ERRORLOG >&2; exit 1; fi 520 | 521 | # dump data 522 | DUMP_OPTS=(--opt --single-transaction --skip-lock-tables --no-create-info --skip-extended-insert) 523 | while read -r table; do 524 | if elementIn "$table" "${SCHEMAONLY_TABLES[@]}"; then 525 | DUMP_OPTS+=(--ignore-table=$DBNAME.$table) 526 | PROCESSED_SCHEMAONLY_TABLES+=($table) 527 | fi 528 | if [ $HANDLE_UNKNOWN == "ignore" ]; then 529 | if elementIn "$table" "${UNKNOWN_TABLES[@]}"; then 530 | DUMP_OPTS+=(--ignore-table=$DBNAME.$table) 531 | fi 532 | fi 533 | done <<<"$DB_TABLES" 534 | 535 | mysqldump "${DB_OPTS[@]}" "${DUMP_OPTS[@]}" $DBNAME >> "$DUMPFILE" 2>$ERRORLOG 536 | 537 | if [ $? -ne 0 ]; then echo $'\nERROR: Could not backup table data.\n' >&2; cat $ERRORLOG >&2; exit 1; fi 538 | ;; 539 | psql) 540 | DUMP_OPTS=() 541 | while read -r table; do 542 | if [ $HANDLE_UNKNOWN == "ignore" ]; then 543 | if elementIn "$table" "${UNKNOWN_TABLES[@]}"; then 544 | DUMP_OPTS+=(--exclude-table=$table) 545 | fi 546 | fi 547 | if elementIn "$table" "${SCHEMAONLY_TABLES[@]}"; then 548 | DUMP_OPTS+=(--exclude-table-data=$table) 549 | PROCESSED_SCHEMAONLY_TABLES+=($table) 550 | fi 551 | done <<<"$DB_TABLES" 552 | 553 | [ -n "$DBSCHEMA" ] && DUMP_OPTS=("${DUMP_OPTS[@]}" -n $DBSCHEMA) 554 | 555 | pg_dump "${DB_OPTS[@]}" "${DUMP_OPTS[@]}" -d $DBNAME > "$DUMPFILE" 2>$ERRORLOG 556 | 557 | if [ $? -ne 0 ]; then 558 | echo $'\nERROR: Could not backup database.\n' >&2 559 | cat $ERRORLOG >&2 560 | clean_psql_pass 561 | exit 1 562 | fi 563 | ;; 564 | esac 565 | 566 | rm $ERRORLOG 567 | 568 | # 569 | # COMPRESS BACKUP 570 | # 571 | if [ "$QUIET" == "no" ]; then 572 | echo $'\nFor the following large tables only the schema (without data) was stored:' 573 | for table in "${PROCESSED_SCHEMAONLY_TABLES[@]}"; do echo " - $table"; done 574 | 575 | echo $'\nCompressing backup file...' 576 | fi 577 | 578 | EXITCODE=0 579 | if [ "$COMPRESSION" == "gz" ]; then gzip -f "$DUMPFILE"; EXITCODE=$?; fi 580 | if [ "$COMPRESSION" == "xz" ]; then xz -f "$DUMPFILE"; EXITCODE=$?; fi 581 | if [ $EXITCODE -ne 0 ]; then 582 | echo $'\nERROR: Could not compress backup file, see previous messages' >&2 583 | clean_psql_pass 584 | exit 1 585 | fi 586 | 587 | [ "$QUIET" == "no" ] && echo "Backup Completed" && echo "${DUMPFILE}${SUFFIX}" 588 | 589 | # 590 | # ROTATE OLD BACKUPS 591 | # 592 | if [ $GENERATIONSTOKEEP -gt 0 ]; then 593 | [ "$QUIET" == "no" ] && echo "Removing old backups, keeping up to $GENERATIONSTOKEEP" 594 | REMOVE_OLD_CMD="cd \"$DUMPDIR\" && ls -t \"${DUMPFILENAME_PREFIX}\"* | /usr/bin/awk \"NR>${GENERATIONSTOKEEP}\" | xargs rm -f " 595 | eval ${REMOVE_OLD_CMD} 596 | if [ $? -ne 0 ]; then 597 | echo "ERROR: Could not rotate old backups" >&2 598 | clean_psql_pass 599 | exit 1 600 | fi 601 | fi 602 | 603 | clean_psql_pass 604 | exit 0 605 | 606 | ################################################################################ 607 | # List of all known table names. 608 | # The flag SCHEMAONLY marks tables that contain monitoring data (as opposed to 609 | # config data), so only their database schema will be backed up. 610 | # 611 | 612 | __DATA__ 613 | acknowledges 1.3.1 - 4.4.4 SCHEMAONLY 614 | actions 1.3.1 - 4.4.4 615 | alerts 1.3.1 - 4.4.4 SCHEMAONLY 616 | application_discovery 2.5.0 - 4.4.4 617 | application_prototype 2.5.0 - 4.4.4 618 | application_template 2.1.0 - 4.4.4 619 | applications 1.3.1 - 4.4.4 620 | auditlog 1.3.1 - 4.4.4 SCHEMAONLY 621 | auditlog_details 1.7 - 4.4.4 SCHEMAONLY 622 | autoreg 1.3.1 - 1.3.4 623 | autoreg_host 1.7 - 4.4.4 624 | conditions 1.3.1 - 4.4.4 625 | config 1.3.1 - 4.4.4 626 | config_autoreg_tls 4.4.0 - 4.4.4 627 | corr_condition 3.2.0 - 4.4.4 628 | corr_condition_group 3.2.0 - 4.4.4 629 | corr_condition_tag 3.2.0 - 4.4.4 630 | corr_condition_tagpair 3.2.0 - 4.4.4 631 | corr_condition_tagvalue 3.2.0 - 4.4.4 632 | corr_operation 3.2.0 - 4.4.4 633 | correlation 3.2.0 - 4.4.4 634 | dashboard 3.4.0 - 4.4.4 635 | dashboard_user 3.4.0 - 4.4.4 636 | dashboard_usrgrp 3.4.0 - 4.4.4 637 | dbversion 2.1.0 - 4.4.4 638 | dchecks 1.3.4 - 4.4.4 639 | dhosts 1.3.4 - 4.4.4 640 | drules 1.3.4 - 4.4.4 641 | dservices 1.3.4 - 4.4.4 642 | escalations 1.5.3 - 4.4.4 643 | event_recovery 3.2.0 - 4.4.4 SCHEMAONLY 644 | event_suppress 4.0.0 - 4.4.4 SCHEMAONLY 645 | event_tag 3.2.0 - 4.4.4 SCHEMAONLY 646 | events 1.3.1 - 4.4.4 SCHEMAONLY 647 | expressions 1.7 - 4.4.4 648 | functions 1.3.1 - 4.4.4 649 | globalmacro 1.7 - 4.4.4 650 | globalvars 1.9.6 - 4.4.4 651 | graph_discovery 1.9.0 - 4.4.4 652 | graph_theme 1.7 - 4.4.4 653 | graphs 1.3.1 - 4.4.4 654 | graphs_items 1.3.1 - 4.4.4 655 | group_discovery 2.1.4 - 4.4.4 656 | group_prototype 2.1.4 - 4.4.4 657 | groups 1.3.1 - 3.4.15 658 | help_items 1.3.1 - 2.1.8 659 | history 1.3.1 - 4.4.4 SCHEMAONLY 660 | history_log 1.3.1 - 4.4.4 SCHEMAONLY 661 | history_str 1.3.1 - 4.4.4 SCHEMAONLY 662 | history_str_sync 1.3.1 - 2.2.23 SCHEMAONLY 663 | history_sync 1.3.1 - 2.2.23 SCHEMAONLY 664 | history_text 1.3.1 - 4.4.4 SCHEMAONLY 665 | history_uint 1.3.1 - 4.4.4 SCHEMAONLY 666 | history_uint_sync 1.3.1 - 2.2.23 SCHEMAONLY 667 | host_discovery 2.1.4 - 4.4.4 668 | host_inventory 1.9.6 - 4.4.4 669 | host_profile 1.9.3 - 1.9.5 670 | host_tag 4.2.0 - 4.4.4 671 | hostmacro 1.7 - 4.4.4 672 | hosts 1.3.1 - 4.4.4 673 | hosts_groups 1.3.1 - 4.4.4 674 | hosts_profiles 1.3.1 - 1.9.2 675 | hosts_profiles_ext 1.6 - 1.9.2 676 | hosts_templates 1.3.1 - 4.4.4 677 | housekeeper 1.3.1 - 4.4.4 678 | hstgrp 4.0.0 - 4.4.4 679 | httpstep 1.3.3 - 4.4.4 680 | httpstep_field 3.4.0 - 4.4.4 681 | httpstepitem 1.3.3 - 4.4.4 682 | httptest 1.3.3 - 4.4.4 683 | httptest_field 3.4.0 - 4.4.4 684 | httptestitem 1.3.3 - 4.4.4 685 | icon_map 1.9.6 - 4.4.4 686 | icon_mapping 1.9.6 - 4.4.4 687 | ids 1.3.3 - 4.4.4 688 | images 1.3.1 - 4.4.4 689 | interface 1.9.1 - 4.4.4 690 | interface_discovery 2.1.4 - 4.4.4 691 | item_application_prototype 2.5.0 - 4.4.4 692 | item_condition 2.3.0 - 4.4.4 693 | item_discovery 1.9.0 - 4.4.4 694 | item_preproc 3.4.0 - 4.4.4 695 | item_rtdata 4.4.0 - 4.4.4 SCHEMAONLY 696 | items 1.3.1 - 4.4.4 697 | items_applications 1.3.1 - 4.4.4 698 | lld_macro_path 4.2.0 - 4.4.4 699 | maintenance_tag 4.0.0 - 4.4.4 700 | maintenances 1.7 - 4.4.4 701 | maintenances_groups 1.7 - 4.4.4 702 | maintenances_hosts 1.7 - 4.4.4 703 | maintenances_windows 1.7 - 4.4.4 704 | mappings 1.3.1 - 4.4.4 705 | media 1.3.1 - 4.4.4 706 | media_type 1.3.1 - 4.4.4 707 | media_type_param 4.4.0 - 4.4.4 708 | node_cksum 1.3.1 - 2.2.23 709 | node_configlog 1.3.1 - 1.4.7 710 | nodes 1.3.1 - 2.2.23 711 | opcommand 1.9.4 - 4.4.4 712 | opcommand_grp 1.9.2 - 4.4.4 713 | opcommand_hst 1.9.2 - 4.4.4 714 | opconditions 1.5.3 - 4.4.4 715 | operations 1.3.4 - 4.4.4 716 | opgroup 1.9.2 - 4.4.4 717 | opinventory 3.0.0 - 4.4.4 718 | opmediatypes 1.7 - 1.8.22 719 | opmessage 1.9.2 - 4.4.4 720 | opmessage_grp 1.9.2 - 4.4.4 721 | opmessage_usr 1.9.2 - 4.4.4 722 | optemplate 1.9.2 - 4.4.4 723 | problem 3.2.0 - 4.4.4 SCHEMAONLY 724 | problem_tag 3.2.0 - 4.4.4 SCHEMAONLY 725 | profiles 1.3.1 - 4.4.4 726 | proxy_autoreg_host 1.7 - 4.4.4 727 | proxy_dhistory 1.5 - 4.4.4 728 | proxy_history 1.5.1 - 4.4.4 729 | regexps 1.7 - 4.4.4 730 | rights 1.3.1 - 4.4.4 731 | screen_user 3.0.0 - 4.4.4 732 | screen_usrgrp 3.0.0 - 4.4.4 733 | screens 1.3.1 - 4.4.4 734 | screens_items 1.3.1 - 4.4.4 735 | scripts 1.5 - 4.4.4 736 | service_alarms 1.3.1 - 4.4.4 737 | services 1.3.1 - 4.4.4 738 | services_links 1.3.1 - 4.4.4 739 | services_times 1.3.1 - 4.4.4 740 | sessions 1.3.1 - 4.4.4 741 | slides 1.3.4 - 4.4.4 742 | slideshow_user 3.0.0 - 4.4.4 743 | slideshow_usrgrp 3.0.0 - 4.4.4 744 | slideshows 1.3.4 - 4.4.4 745 | sysmap_element_trigger 3.4.0 - 4.4.4 746 | sysmap_element_url 1.9.0 - 4.4.4 747 | sysmap_shape 3.4.0 - 4.4.4 748 | sysmap_url 1.9.0 - 4.4.4 749 | sysmap_user 3.0.0 - 4.4.4 750 | sysmap_usrgrp 3.0.0 - 4.4.4 751 | sysmaps 1.3.1 - 4.4.4 752 | sysmaps_elements 1.3.1 - 4.4.4 753 | sysmaps_link_triggers 1.5 - 4.4.4 754 | sysmaps_links 1.3.1 - 4.4.4 755 | tag_filter 4.0.0 - 4.4.4 756 | task 3.2.0 - 4.4.4 SCHEMAONLY 757 | task_acknowledge 3.4.0 - 4.4.4 SCHEMAONLY 758 | task_check_now 4.0.0 - 4.4.4 SCHEMAONLY 759 | task_close_problem 3.2.0 - 4.4.4 SCHEMAONLY 760 | task_remote_command 3.4.0 - 4.4.4 SCHEMAONLY 761 | task_remote_command_result 3.4.0 - 4.4.4 SCHEMAONLY 762 | timeperiods 1.7 - 4.4.4 763 | trends 1.3.1 - 4.4.4 SCHEMAONLY 764 | trends_uint 1.5 - 4.4.4 SCHEMAONLY 765 | trigger_depends 1.3.1 - 4.4.4 766 | trigger_discovery 1.9.0 - 4.4.4 767 | trigger_tag 3.2.0 - 4.4.4 768 | triggers 1.3.1 - 4.4.4 769 | user_history 1.7 - 2.4.8 770 | users 1.3.1 - 4.4.4 771 | users_groups 1.3.1 - 4.4.4 772 | usrgrp 1.3.1 - 4.4.4 773 | valuemaps 1.3.1 - 4.4.4 774 | widget 3.4.0 - 4.4.4 775 | widget_field 3.4.0 - 4.4.4 776 | --------------------------------------------------------------------------------