backups-cli 1.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3828b34e41c40252925145a874a3e7fd6532c840
4
+ data.tar.gz: 80c63eadf29e7635ae6f915e201778cd5d73a2a9
5
+ SHA512:
6
+ metadata.gz: 82e51cd0ca2024470f35921864970bbb356208bbf0589a4d4dcb1f931bffeb51aca6be019f4da551c75abb1628d651ad147a06814048377969b13a11d58fefd1
7
+ data.tar.gz: 3a373562b5c61e704b8146173cd595d51de6b6f3848a100075913323e1d4f43b06271fdb4e1b7bc362656f99dad021666157169361ca9e490b0fbb12b3fcd286
@@ -0,0 +1,2 @@
1
+ pkg/
2
+ Gemfile.lock
@@ -0,0 +1,5 @@
1
+ ---
2
+ language: ruby
3
+
4
+ script:
5
+ - echo "Date is $(date)"
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2016 Schibsted Products & Technology AS.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,256 @@
1
+ # backups-cli
2
+
3
+ [![Build Status](https://travis-ci.org/schibsted/backups-cli.svg?branch=master)](https://travis-ci.org/schibsted/backups-cli)
4
+
5
+ This tool backups different data sources to S3.
6
+
7
+
8
+ # Usage
9
+
10
+ To see all available commands run the tool with no options:
11
+
12
+ ```
13
+ % ./bin/backups
14
+ Commands:
15
+ backups crontab # Shows the crontab config
16
+ backups help [COMMAND] # Describe available commands or one specific command
17
+ backups install # Sets up the crontab for all jobs
18
+ backups ls # Lists all the configured jobs
19
+ backups show [JOB] # Shows the merged config (for a JOB or them all)
20
+ backups start [JOB] # Starts a backup JOB or all of them
21
+ backups verify JOB # Restores and verifies a backup JOB
22
+ backups version # Show current version
23
+
24
+ ```
25
+
26
+ ### Configuration
27
+
28
+ The look up directories for the configs are, in order:
29
+
30
+ - The value in the `BACKUPS_CONFIG_DIR` env variable
31
+ - `${HOME}/.backups-cli`
32
+ - `/etc/backups-cli`
33
+
34
+ The config directory will be the first it can find. All `*.yaml` files under
35
+ that directory will be loaded. You might find useful to organise backups into
36
+ subfolders. You can inline `BACKUPS_CONFIG_DIR` to override existing file system
37
+ directories:
38
+
39
+ BACKUPS_CONFIG_DIR="/opt/my-backups" ./bin/backups start local-mysql
40
+
41
+
42
+ ### List all jobs
43
+
44
+ To list all defined jobs call the `ls` command:
45
+
46
+ ```
47
+ $ ./bin/backups ls
48
+ JOB CRONTAB INSTALL ENABLED
49
+ local-mysql 0 4 * * * true true
50
+ minimal 0 4 * * * true true
51
+ ```
52
+
53
+ `INSTALL` refers if a job will be installed on the crontab and `ENABLED` if it
54
+ can be run. You can run manually not installed jobs but the script will refuse
55
+ to start disabled jobs.
56
+
57
+
58
+ ### Show one job details
59
+
60
+ Use the command `show` to print out json details about all the details for a
61
+ job. Note that the details presented here show the job as perceived from the
62
+ script, meaning when all the defaults have been merged. Leave the `job` argument
63
+ out to show the whole config after the app has processed all yaml files and
64
+ applied the defaults. The merge pattern is:
65
+
66
+ - Deep merge any local `default` settings into all jobs entries in the same file
67
+ - Finally deep merge the top `default` entry (in the `main.yaml` file)
68
+
69
+ These merges mean you can provide global and file fallbacks (local settings
70
+ prevail), which means you can organise the backup config files by env/adapter
71
+ type or any way you fancy.
72
+
73
+
74
+ ```json
75
+ $ ./bin/backups show local-mysql | jq .
76
+ {
77
+ "s3": {
78
+ "bucket": "acme-productions-backups",
79
+ "path": "local/mysql"
80
+ },
81
+ "encryption": {
82
+ "secret": "pass"
83
+ },
84
+ "tags": {
85
+ "env": "local",
86
+ "new": null
87
+ },
88
+ "type": "mysql",
89
+ "backup": {
90
+ "connection": {
91
+ "username": "root",
92
+ "password": "root"
93
+ }
94
+ },
95
+ "_name": "mysql-local",
96
+ "_file": "/Users/pedro/.backups-cli/local/mysql.yaml"
97
+ }
98
+ ```
99
+
100
+ ### Add a new job
101
+
102
+ Here's the minimal backup config needed:
103
+
104
+ ```yaml
105
+ ---
106
+ jobs:
107
+ my-fancy-backup:
108
+ type: mysql
109
+ ```
110
+
111
+ Here's a more reasonable example of job (they go under the tops `jobs` entry):
112
+
113
+ ```yaml
114
+ ---
115
+ jobs:
116
+ acme-productions:
117
+ type: mysql
118
+ tags:
119
+ env: local
120
+ type: test
121
+ s3:
122
+ bucket: acme-dumps
123
+ path: production/mysql
124
+ backup:
125
+ connection:
126
+ host: localhost
127
+ username: backup
128
+ password: password
129
+ crontab:
130
+ hour: "*/4"
131
+ ```
132
+
133
+ Note the lack of the leading and trailing slashes in the S3 path configs. The
134
+ only required parameter is the `type`. All others parameters depend either on
135
+ the adapter itself or are defaulted from the `backups.defaults` entry.
136
+
137
+ Tags are metadata associated with a job and are passed to listeners, like
138
+ Datadog or Slack. To remove a defaulted tag simply set it to `null`.
139
+
140
+
141
+ ### Run a job
142
+
143
+ Run `./bin/backup` with the command `start` and pass a job name:
144
+
145
+ ./bin/backups start <job>
146
+
147
+ The `<job>` parameter should exist somewhere on some yaml file within
148
+ config directory under the entry `jobs`. Look into `config/jobs-example.yaml`
149
+ for inspiration. For our example:
150
+
151
+ ./bin/backups start local-mysql
152
+
153
+ The backups comes with a dry-run mode that you can use to preview what it would
154
+ execute in a real run:
155
+
156
+ ./bin/backups start local-mysql --dry-run
157
+
158
+ Backups are encrypted if you set the `encryption.secret` setting.
159
+
160
+ Sending to S3 is done when the `s3.bucket` setting is set and the setting
161
+ `s3.active` is not explicitly set to `false`.
162
+
163
+
164
+ ### Install the crontab
165
+
166
+ Run the command `install` to loop through the configured job and set a crontab
167
+ job for each of them
168
+
169
+ ./bin/backups install
170
+
171
+ If you want to add a custom job but not install it you need to set the field
172
+ `backup.crontan.install` to `false` in the crontab settings for that backup job:
173
+
174
+ ```yaml
175
+ acme-productions:
176
+ type: mysql
177
+ enabled: false
178
+ backup:
179
+ crontab:
180
+ install: false
181
+ hour: 0
182
+ ```
183
+
184
+ This config shows also where to disable the job. It's the `enabled` setting
185
+ directly under the job.
186
+
187
+ ### Development
188
+
189
+ Create the file `/etc/backups-cli/main.yaml`
190
+
191
+ ```yaml
192
+ ---
193
+ backups:
194
+ paths:
195
+ backups: /tmp/backups
196
+ verify: /tmp/verify
197
+ listeners:
198
+ slack:
199
+ webhook: https://hooks.slack.com/services/X/Y/Z
200
+ channel: spt-payment-backups
201
+ _active: false
202
+ _events:
203
+ - _start
204
+ - error
205
+ datadog:
206
+ api_key: x
207
+ app_key: y
208
+ crontab:
209
+ header: "MAILTO=backups@spid.no\nPATH=/usr/bin:/bin:/usr/local/bin"
210
+
211
+ defaults:
212
+ # options:
213
+ # cleanup: false
214
+ # silent: false
215
+ s3:
216
+ bucket: 740872874188-database-backups
217
+ path: test/mysql
218
+ # active: false
219
+ encryption:
220
+ secret: x
221
+ backup:
222
+ crontab:
223
+ prefix: ". /etc/profile.d/proxy.sh &&"
224
+ postfix: "2>/dev/null"
225
+ ```
226
+
227
+
228
+ ### Release procedure
229
+
230
+ To release a new version of this gem you need to have the Ryubygems credentials
231
+ in place. Start with going into https://rubygems.org/profile/edit and run the
232
+ `curl -u schibsted https://rubygems.org/api/v1/api_key.yaml > ~/.gem/credentials`
233
+ command they recommend there. This is the *only* step required to release a new
234
+ gem version to rubygems.
235
+
236
+ On this repo, just update the file `lib/backups/version.rb` with a bumped version
237
+ and commit all changes to the git repo. Then run:
238
+
239
+ rake
240
+
241
+ If you need to revoke an older gem version just run:
242
+
243
+ gem yank -v 0.5.14 backups-cli
244
+
245
+ ### Known issues
246
+
247
+ The verify code runs two checks per table:
248
+
249
+ - A `CHECK TABLE <table>`
250
+ - A `SELECT COUNT(*) FROM <table>` on the table
251
+
252
+ The problem with the second is that the count is run after the dump and these
253
+ stats can be skewed in inserts are done after the dump. If you start to get
254
+ mismatches in this last one I suggest you use a float comparision, that is, one
255
+ that only reports if the stats are more than say 1% different.
256
+
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ task default: [
5
+ :release,
6
+ ]
@@ -0,0 +1,25 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+
3
+ require "backups/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "backups-cli"
7
+ spec.version = Backups::VERSION
8
+ spec.description = "This tool backups different data sources to S3."
9
+ spec.summary = "This tool backups different data sources to S3"
10
+ spec.authors = ["Schibsted"]
11
+ spec.email = ["spt@schibsted.com"]
12
+ spec.homepage = "https://github.com/schibsted/backups-cli"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split $/
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename f }
17
+
18
+ spec.add_dependency "thor"
19
+ spec.add_dependency "json"
20
+ spec.add_dependency "mysql2"
21
+ spec.add_dependency "dogapi"
22
+ spec.add_dependency "slack-notifier"
23
+ spec.add_dependency "tablelize"
24
+ spec.add_development_dependency "rake"
25
+ end
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+ # Copyright 2016 Schibsted Products & Technology AS.
3
+ # Licensed under the terms of the MIT license. See LICENSE in the project root.
4
+ $:.push File.expand_path("../../lib", __FILE__)
5
+
6
+ require "logger"
7
+ require "backups"
8
+
9
+ log_level = ENV.fetch("BACKUPS_LOG_LEVEL", "info").upcase
10
+ DRY_RUN = ENV.fetch("BACKUPS_DRY_RUN", "0") == "1"
11
+ log_file = ENV.fetch("BACKUPS_LOG_FILE", "/var/log/backups-cli.log")
12
+
13
+ # Accept --debug or -x flags for stdout debug logging
14
+ if ARGV.include? "--debug" or ARGV.include? "-x"
15
+ log_file = STDOUT
16
+ log_level = "DEBUG"
17
+ ARGV.delete("--debug")
18
+ ARGV.delete("-x")
19
+ end
20
+
21
+ $LOG_ACTIVE = 1
22
+ $LOGGER = Logger.new(log_file)
23
+ $LOGGER.level = Object.const_get("Logger::#{log_level}")
24
+ # $LOGGER.progname = File.basename(__FILE__)
25
+ $LOGGER.progname = "main"
26
+ $LOGGER.formatter = proc do |type, time, name, message|
27
+ "[#{time}] #{name} #{type.ljust(6)} #{message}\n"
28
+ end
29
+
30
+ $LOGGER.warn "Log level is #{log_level}"
31
+
32
+
33
+ begin
34
+ Backups::Cli.start ARGV
35
+ rescue => e
36
+ cmd = ARGV[0] || "undef"
37
+ db = ARGV[1] || "undef"
38
+ msg = e.to_s.encode!("UTF-8", {undef: :replace})
39
+ Backups::Events.fire :error, {
40
+ error: msg,
41
+ command: cmd,
42
+ db: db,
43
+ }
44
+ $LOGGER.error "Error: #{msg}."
45
+
46
+ raise e
47
+ end
@@ -0,0 +1,19 @@
1
+ # $:.push File.expand_path("../lib", __FILE__)
2
+
3
+ require "backups/cli"
4
+ require "backups/version"
5
+ require "backups/events"
6
+ require "backups/system"
7
+ require "backups/loader"
8
+ require "backups/runner"
9
+ require "backups/crontab"
10
+ require "backups/base"
11
+ require "backups/driver/mysql"
12
+ require "backups/stats/mysql"
13
+ require "backups/adapter/mysql"
14
+ require "backups/verify/mysql"
15
+ require "backups/listener"
16
+ require "backups/ext/hash"
17
+ require "backups/ext/string"
18
+ require "backups/ext/fixnum"
19
+ require "backups/ext/nil_class"
@@ -0,0 +1,202 @@
1
+ require "yaml"
2
+
3
+ module Backups
4
+ module Adapter
5
+ class Mysql < Base
6
+
7
+ include ::Backups::Stats::Mysql
8
+
9
+ def run
10
+ setup
11
+ create_dump
12
+ compress @job_dir, @job_zip, @secret
13
+
14
+ @job_size = File.size(@job_zip) if not $DRY_RUN
15
+ $LOGGER.info "File #{@job_zip} created with #{@job_size} bytes"
16
+
17
+ send_to_s3 @s3_bucket, @s3_prefix, @job_zip, @config if @s3_active
18
+ clean_up if @cleanup
19
+ report
20
+
21
+ return @report
22
+ end
23
+
24
+ def verify
25
+ s3 = @config.fetch("s3", {})
26
+ @name = @config.fetch('_name')
27
+ bucket = s3.fetch("bucket", "")
28
+ path = s3.fetch("path", "")
29
+ date = get_date_path()
30
+ folder = "s3://#{bucket}/#{path}/#{@name}/#{date}/"
31
+ $LOGGER.warn "Latest s3 path is: " + get_latest_s3(folder)
32
+ end
33
+
34
+ private
35
+ # If you are lost here's what the vars mean:
36
+ #
37
+ # Variable Value
38
+ # --------- ----------------------------------
39
+ # backup_dir /tmp/backups/
40
+ # source_dir <source>/
41
+ # job_zip <prefix>-<timestamp>.zip
42
+ # job_dir <prefix>-<timestamp>/
43
+ def setup
44
+ load_configs
45
+
46
+ backups = $GLOBAL.fetch('backups', {})
47
+ paths = backups.fetch('paths', {})
48
+ encryption = @config.fetch('encryption', {})
49
+ s3 = @config.fetch('s3', {})
50
+ prefix = get_prefix()
51
+ date_path = get_date_path()
52
+ timestamp = get_timestamp()
53
+ backup_dir = paths.fetch('backups', '/tmp/backups')
54
+
55
+ @backup = @config.fetch('backup', {})
56
+ @options = @config.fetch('options', {})
57
+ @connection = @backup.fetch('connection', {})
58
+ @secret = encryption.fetch('secret', nil)
59
+ @cleanup = @options.fetch('cleanup', true)
60
+ @name = @config.fetch('_name')
61
+ @group = @name.gsub(/[^0-9A-Za-z]/, '_')
62
+
63
+ job_name = "#{prefix}-#{timestamp}"
64
+ @source_dir = "#{backup_dir}/#{@name}"
65
+ @job_dir = "#{@source_dir}/#{job_name}"
66
+ @job_zip = "#{@source_dir}/#{job_name}.zip"
67
+ @job_size = nil
68
+
69
+ s3_path = s3.fetch('path', "")
70
+ @s3_region = s3.fetch('region', "eu-west-1")
71
+ @s3_bucket = s3.fetch('bucket', nil)
72
+ @s3_active = s3.fetch('active', @s3_bucket != nil)
73
+ @s3_prefix = "#{s3_path}/#{@name}/#{date_path}"
74
+
75
+ @report = {
76
+ started: Time.now,
77
+ completed: nil,
78
+ file: nil,
79
+ size: nil,
80
+ report: nil,
81
+ }
82
+ end
83
+
84
+ private
85
+ def get_dump_command database
86
+ host = @connection.fetch("host", "localhost")
87
+ username = @connection.fetch("username", nil)
88
+ password = @connection.fetch("password", nil)
89
+ silent = @options.fetch("silent", true)
90
+ master_data = @options.fetch("master-data", 0)
91
+ disable_keys = @options.fetch("disable-keys", true)
92
+ lock_tables = @options.fetch("lock-tables", false)
93
+ use_defaults = @group.size > 0 ? true : false
94
+ use_defaults = @options.fetch("use-defaults", use_defaults)
95
+ events = @options.fetch("events", false)
96
+
97
+ command = []
98
+ command << "mysqldump"
99
+ command << "--defaults-group-suffix=_#{@group}"
100
+ command << "--host=#{host}"
101
+
102
+ command << "--user='#{username}'" if username and !use_defaults
103
+ command << "--password='#{password}'" if password and !use_defaults
104
+ command << "--master-data=#{master_data}" if master_data
105
+ command << "--events" if events
106
+ command << "--skip-events" unless events
107
+ command << "--lock-tables" if lock_tables
108
+ command << "--single-transaction" unless lock_tables
109
+ command << "--disable-keys" if disable_keys
110
+ command << database
111
+ command << "> #{@job_dir}/#{database}.sql"
112
+ command << "2>/dev/null" if silent
113
+
114
+ command.join(" ")
115
+ end
116
+
117
+ def get_db_list
118
+ host = @connection.fetch("host", "localhost")
119
+ skip = [
120
+ "information_schema",
121
+ "performance_schema",
122
+ "sys",
123
+ ]
124
+
125
+ command = "mysql --defaults-group-suffix=_#{@group}"
126
+ command << " --host #{host}"
127
+ command << " -e 'show databases' | awk 'NR>1'"
128
+ $LOGGER.debug command
129
+
130
+ `#{command}`.split().select do |db|
131
+ not skip.include? db
132
+ end
133
+ end
134
+
135
+ def create_dump
136
+ $LOGGER.info "Creating #{@job_dir}"
137
+ mkdir @job_dir
138
+
139
+ database = @backup.fetch("database", nil)
140
+ databases = @backup.fetch("databases", [])
141
+ databases << database if database
142
+
143
+ # Are we dumping specific databases
144
+ if databases.size > 0
145
+ $LOGGER.info "Preparing databases dump"
146
+ databases.each do |db|
147
+ create_database_dump db
148
+ end
149
+ else
150
+ create_server_dump
151
+ end
152
+ end
153
+
154
+ def create_server_dump
155
+ $LOGGER.info "Preparing server dump"
156
+ get_db_list().each do |db|
157
+ create_database_dump db
158
+ end
159
+ end
160
+
161
+ def create_database_dump db
162
+ $LOGGER.info "Dumping database #{db}"
163
+ command = get_dump_command(db)
164
+ create_database_stats db
165
+ exec command
166
+ end
167
+
168
+ def create_database_stats db
169
+ $LOGGER.debug "Creating #{db} database stats"
170
+ file = "#{@job_dir}/#{db}-stats.yaml"
171
+ stats = get_database_stats(db)
172
+ write file, stats.to_yaml
173
+ end
174
+
175
+ def clean_up
176
+ $LOGGER.info "Cleaning #{@source_dir}"
177
+ delete @job_zip if File.exists? @job_zip
178
+ nuke_dir @job_dir
179
+
180
+ delete_dir @source_dir
181
+ end
182
+
183
+ def report
184
+ path = "#{@s3_bucket}/#{@s3_prefix}/#{File.basename(@job_zip)}"
185
+
186
+ @report[:completed] = Time.now
187
+ @report[:file] = @job_zip
188
+ @report[:size] = @job_size
189
+ @report[:url] = "s3://#{path}"
190
+ @report[:view] = "https://console.aws.amazon.com/s3/buckets" +
191
+ "#{path}?region=#{@s3_region}"
192
+ end
193
+
194
+ def get_latest_s3 folder
195
+ $LOGGER.info "Finding latest dump in folder #{folder}"
196
+ $LOGGER.info "aws s3 ls #{folder}/|awk '{ print $4 }'|tail -n 1"
197
+ exec "aws s3 ls #{folder}/|awk '{ print $4 }'|tail -n 1"
198
+ end
199
+
200
+ end
201
+ end
202
+ end