bibliotech 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/bin/bibliotech +5 -0
  3. data/doc/example_config_file.yml +58 -0
  4. data/doc/todo.txt +19 -0
  5. data/lib/bibliotech/application.rb +95 -0
  6. data/lib/bibliotech/backups/file_record.rb +16 -0
  7. data/lib/bibliotech/backups/prune_list.rb +58 -0
  8. data/lib/bibliotech/backups/pruner.rb +71 -0
  9. data/lib/bibliotech/backups/scheduler.rb +49 -0
  10. data/lib/bibliotech/builders/database.rb +25 -0
  11. data/lib/bibliotech/builders/file.rb +75 -0
  12. data/lib/bibliotech/builders/gzip.rb +51 -0
  13. data/lib/bibliotech/builders/mysql.rb +35 -0
  14. data/lib/bibliotech/builders/postgres.rb +37 -0
  15. data/lib/bibliotech/builders.rb +43 -0
  16. data/lib/bibliotech/cli.rb +24 -0
  17. data/lib/bibliotech/command_generator.rb +86 -0
  18. data/lib/bibliotech/command_runner.rb +36 -0
  19. data/lib/bibliotech/compression/bzip2.rb +6 -0
  20. data/lib/bibliotech/compression/gzip.rb +6 -0
  21. data/lib/bibliotech/compression/sevenzip.rb +5 -0
  22. data/lib/bibliotech/compression.rb +35 -0
  23. data/lib/bibliotech/config.rb +269 -0
  24. data/lib/bibliotech/rake_lib.rb +82 -0
  25. data/lib/bibliotech.rb +7 -0
  26. data/spec/bibliotech/backup_pruner_spec.rb +58 -0
  27. data/spec/bibliotech/backup_scheduler_spec.rb +108 -0
  28. data/spec/bibliotech/command_generator/mysql_spec.rb +170 -0
  29. data/spec/bibliotech/command_generator/postgres_spec.rb +180 -0
  30. data/spec/bibliotech/command_generator_spec.rb +99 -0
  31. data/spec/bibliotech/command_runner_spec.rb +50 -0
  32. data/spec/bibliotech/compression/bunzip2_spec.rb +9 -0
  33. data/spec/bibliotech/compression/bzip2_spec.rb +9 -0
  34. data/spec/bibliotech/compression/gzip_spec.rb +9 -0
  35. data/spec/bibliotech/compression/sevenzip_spec.rb +9 -0
  36. data/spec/bibliotech/compression_spec.rb +28 -0
  37. data/spec/bibliotech/config_spec.rb +151 -0
  38. data/spec/gem_test_suite.rb +0 -0
  39. data/spec/spec_helper.rb +2 -0
  40. metadata +150 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fa336e5473f11901ab7dcddd786ad077ca6cc9d4
4
+ data.tar.gz: 7823f13b23335c838867c1c6d20dee7707924c39
5
+ SHA512:
6
+ metadata.gz: fb3d5921a720ab1dc4e2b682d8d6be78155a4f6c39a70554b1c83a164696624fbf7fc3188b865c2a8904e20cacb8a1240a1e407867f13bc6eba7da7a675e013f
7
+ data.tar.gz: a16f586a9e18301134a6a454a7cd3749b64994753f4b94a8076ca862bbcf82479f2dd83e508fa08c3a0d3264293fccb9d3d11a0e6b5d7ee1c98f3d7796b3f9fb
data/bin/bibliotech ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bibliotech/cli'
4
+
5
+ BiblioTech::CLI.start(ARGV)
@@ -0,0 +1,58 @@
1
+ # bibliotech.yml on production server
2
+ # used by commands like
3
+ # rake bibliotech:backups:create
4
+ # rake bibliotech:backups:cycle
5
+ backups:
6
+ dir: db_backups
7
+ compress: gzip # [ none, gzip, bzip2, 7zip ]
8
+ keep:
9
+ hourlies: 48
10
+ dailies: 14
11
+ weeklies: 8
12
+ monthlies: all
13
+
14
+ create: hourly
15
+ prune: daily
16
+ clone:
17
+ storage: s3 [ file, ftp, scp ]
18
+ target: ecliptic_db_backups # URL
19
+
20
+ database_config_env: production
21
+ database_config_file: 'config/database.yml' # this is the default
22
+ # -- or --
23
+ database_config:
24
+ hostname: xxxx
25
+ username: xxxxx
26
+ password: xxxxx
27
+ port: xxxx
28
+
29
+ #bibliotech.yml on staging server
30
+ #useful for:
31
+ # > rake bibliotech:restore_from[<remote name, e.g. production>]
32
+ # # (copies most recent database backup from this path or URL and restores it)
33
+ backups: none
34
+ database_config_env: staging
35
+ database_config_file: 'config/database.yml' # this is the default
36
+ # -- or --
37
+ database_config:
38
+ hostname: xxxx
39
+ username: xxxxx
40
+ password: xxxxx
41
+ port: xxxx
42
+
43
+ remotes:
44
+ production:
45
+ host: some.server.com
46
+ path: "/var/www/eclipticenterprises.com/current"
47
+ compressed: gzip
48
+
49
+
50
+ #bibliotech.yml on dev
51
+ # used by commands like
52
+ # > rake bibliotech:remote_sync:down[production] # production is default if ommitted
53
+ # > rake bibliotech:remote_sync:up
54
+ # > cap bibliotech:remote_sync:down[production] # uses Cap config, so URL below is unnecessary in config
55
+ #
56
+ remotes:
57
+ production: "root@appserver2.lrdesign.com:/var/www/eclipticenterprises.com/current"
58
+ staging: "root@appserver2.lrdesign.com:/var/www/staging.eclipticenterprises.com/current"
data/doc/todo.txt ADDED
@@ -0,0 +1,19 @@
1
+ * load DB configs from a database.yml
2
+ * export commands: mysql
3
+ * export commands: posgresql
4
+ * export commands: postgis
5
+ * import commands: mysql
6
+ * import commands: postgresql
7
+ * import commands: postgis
8
+ * rake task: backup databases
9
+ * rake task: filter dated DB backups
10
+ * configuration: how many backups to keep of what age
11
+ *
12
+ * rake task: import DB locally by filename
13
+ * rake task: import[&migrate] most recent DB (at default name) from production, locally
14
+ * rake task: import[&migrate] most recent DB (at default name) from production, remotely
15
+ * rake task: download and import remote sql backup
16
+ * rake task: upload and import local backup to remote:
17
+ *
18
+ * cap task: download and import remote sql backup
19
+ * cap task: upload and import local backup to remote:
@@ -0,0 +1,95 @@
1
+ require 'bibliotech'
2
+ require 'caliph'
3
+ require 'valise'
4
+ require 'bibliotech/backups/pruner'
5
+
6
+ module BiblioTech
7
+ class Application
8
+ attr_accessor :config_path, :config_hash
9
+ attr_writer :shell
10
+
11
+ def initialize
12
+ @memos = {}
13
+ @shell = Caliph.new
14
+ @config_path = %w{/etc/bibliotech /usr/share/bibliotech ~/.bibliotech ./.bibliotech ./config/bibliotech}
15
+ end
16
+
17
+ def valise
18
+ @memos[:valise] ||=
19
+ begin
20
+ dirs = config_path
21
+ Valise::define do
22
+ dirs.reverse.each do |dir|
23
+ rw dir
24
+ end
25
+ ro from_here(%w{.. default_configuration}, up_to("lib"))
26
+ handle "*.yaml", :yaml, :hash_merge
27
+ handle "*.yml", :yaml, :hash_merge
28
+ end
29
+ end
30
+ end
31
+
32
+ def config
33
+ @memos[:config] ||= Config.new(valise)
34
+ end
35
+
36
+ def commands
37
+ @memos[:command] ||= CommandGenerator.new(config)
38
+ end
39
+
40
+ def pruner(options)
41
+ Backups::Pruner.new(config.merge(options))
42
+ end
43
+
44
+ def prune_list(options)
45
+ pruner(options).list
46
+ end
47
+
48
+ def reset
49
+ @memos.clear
50
+ end
51
+
52
+ def import(options)
53
+ @shell.run(commands.import(options))
54
+ end
55
+
56
+ def export(options)
57
+ @shell.run(commands.export(options))
58
+ end
59
+
60
+ def create_backup(options)
61
+ time = Time.now.utc
62
+ pruner = pruner(options)
63
+ return unless pruner.backup_needed?(time)
64
+ options["backups"] ||= options[:backups] || {}
65
+ options["backups"]["filename"] = pruner.filename_for(time)
66
+ export(options)
67
+ end
68
+
69
+ #pull a dump from a remote
70
+ def get(options)
71
+ @shell.run(commands.fetch(options))
72
+ end
73
+
74
+ #push a dump to a remote
75
+ def send(options)
76
+ @shell.run(commands.push(options))
77
+ end
78
+
79
+ #clean up the DB dumps
80
+ def prune(options=nil)
81
+ pruner(option || {}).go
82
+ end
83
+
84
+ #return the latest dump of the DB
85
+ def latest(options = nil)
86
+ pruner(options || {}).most_recent.path
87
+ end
88
+
89
+ def remote_cli(remote, command, options)
90
+ @shell.run(commands.ssh_cli(remote, command, options))
91
+ end
92
+ end
93
+
94
+ App = Application
95
+ end
@@ -0,0 +1,16 @@
1
+ module BiblioTech
2
+ module Backups
3
+ class FileRecord
4
+ attr_accessor :path, :timestamp, :keep
5
+
6
+ def initialize(path, timestamp)
7
+ @path, @timestamp = path, timestamp
8
+ @keep = false
9
+ end
10
+
11
+ def keep?
12
+ !!@keep
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,58 @@
1
+ module BiblioTech
2
+ module Backups
3
+ class PruneList
4
+ TIMESTAMP_REGEX = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})_(?<hour>\d{2}):(?<minute>\d{2})/
5
+
6
+ attr_accessor :path, :prefix
7
+
8
+ def initialize(path, prefix)
9
+ @path, @prefix = path, prefix
10
+ end
11
+
12
+ def list
13
+ files = []
14
+ Dir.new(path).each do |file|
15
+ next if %w{. ..}.include?(file)
16
+ file_record = build_record(file)
17
+ if file_record.nil?
18
+ else
19
+ files << file_record
20
+ end
21
+ end
22
+ files
23
+ end
24
+
25
+ def prefix_timestamp_re
26
+ prefix_re(TIMESTAMP_REGEX)
27
+ end
28
+
29
+ def prefix_re(also)
30
+ /\A#{prefix}-#{also}\..*\z/
31
+ end
32
+
33
+ def self.filename_for(prefix, time)
34
+ time.strftime("#{prefix}-%Y-%m-%d_%H:%M.sql")
35
+ end
36
+
37
+ def build_record(file)
38
+ if file =~ prefix_re(/.*/)
39
+ if !(match = prefix_timestamp_re.match(file)).nil?
40
+ timespec = %w{year month day hour minute}.map do |part|
41
+ Integer(match[part], 10)
42
+ end
43
+ parsed_time = Time::utc(*timespec)
44
+ return FileRecord.new(File::join(path, file), parsed_time)
45
+ else
46
+ raise "File prefixed #{prefix} doesn't match #{prefix_timestamp_re.to_s}: #{File::join(path, file)}"
47
+ end
48
+ else
49
+ if file !~ TIMESTAMP_REGEX
50
+ warn "Stray file in backups directory: #{File::join(path, file)}"
51
+ return nil
52
+ end
53
+ end
54
+ return nil
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,71 @@
1
+ require 'bibliotech/backups/prune_list'
2
+ require 'bibliotech/backups/file_record'
3
+ require 'bibliotech/backups/scheduler'
4
+
5
+ module BiblioTech
6
+ module Backups
7
+ class Pruner
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+ attr_reader :config
12
+
13
+ def path
14
+ @path ||= config.backup_path
15
+ end
16
+
17
+ def name
18
+ @name ||= config.backup_name
19
+ end
20
+
21
+ def schedules
22
+ @schedules ||=
23
+ [].tap do |array|
24
+ config.each_prune_schedule do |frequency, limit|
25
+ array << Scheduler.new(frequency, limit)
26
+ end
27
+ end
28
+ end
29
+
30
+ def backup_needed?(time)
31
+ time - most_recent.timestamp < config.backup_frequency * 60
32
+ end
33
+
34
+ def list
35
+ @list ||=
36
+ begin
37
+ list = PruneList.new(path, name).list
38
+ schedules.each do |schedule|
39
+ schedule.mark(list)
40
+ end
41
+ list
42
+ end
43
+ end
44
+
45
+ def most_recent
46
+ list.max_by do |record|
47
+ record.timestamp
48
+ end
49
+ end
50
+
51
+ def filename_for(time)
52
+ PruneList.filename_for(time)
53
+ end
54
+
55
+ def pruneable
56
+ list.select do |record|
57
+ !record.keep?
58
+ end
59
+ end
60
+
61
+ def go
62
+ return if schedules.empty?
63
+ pruneable.each {|record| delete(record.path) }
64
+ end
65
+
66
+ def delete(path)
67
+ File.unlink(path)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,49 @@
1
+ require 'bibliotech/backups/file_record'
2
+
3
+ module BiblioTech
4
+ module Backups
5
+ class Scheduler
6
+ attr_accessor :frequency, :limit
7
+
8
+ def initialize(frequency, limit)
9
+ @frequency, @limit = frequency, limit
10
+ @limit = nil if limit == "all"
11
+ end
12
+
13
+ def end_time(file_list)
14
+ file_list.map{|record| record.timestamp}.max
15
+ end
16
+
17
+ def compute_start_time(file_list)
18
+ limit_time = Time.at(0)
19
+ unless limit.nil?
20
+ limit_time = end_time(file_list) - limit * freq_seconds
21
+ end
22
+ [limit_time, file_list.map{|record| record.timestamp}.min - range].max
23
+ end
24
+
25
+ def freq_seconds
26
+ frequency * 60
27
+ end
28
+
29
+ def range
30
+ freq_seconds / 2
31
+ end
32
+
33
+ def mark(file_list)
34
+ time = end_time(file_list)
35
+ start_time = compute_start_time(file_list)
36
+ while time > start_time do
37
+ closest = file_list.min_by do |record|
38
+ (record.timestamp - time).abs
39
+ end
40
+ if (closest.timestamp - time).abs < range
41
+ closest.keep = true
42
+ end
43
+ time -= freq_seconds
44
+ end
45
+ return file_list
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ require 'bibliotech/builders'
2
+
3
+ module BiblioTech
4
+ module Builders
5
+ class Database < Base
6
+ def self.find_class(config)
7
+ adapter_registry.fetch(config.adapter) do
8
+ raise "config.adapter is #{config.adapter.inspect} - supported adapters are #{supported_adapters.join(", ")}"
9
+ end
10
+ end
11
+ end
12
+
13
+ class Import < Database
14
+ def self.registry_host
15
+ Import
16
+ end
17
+ end
18
+
19
+ class Export < Database
20
+ def self.registry_host
21
+ Export
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,75 @@
1
+ require 'bibliotech/builders'
2
+
3
+ module BiblioTech
4
+ module Builders
5
+ class File < Base
6
+ def self.find_class(config)
7
+ file = config.backup_file
8
+
9
+ explicit = find_explicit(config)
10
+ return explicit unless explicit.nil?
11
+
12
+ _, klass = adapter_registry.find{ |pattern, klass|
13
+ next if pattern.is_a? Symbol
14
+ file =~ pattern
15
+ }
16
+ klass || identity_adapter
17
+ rescue Config::MissingConfig
18
+ return NullAdapter
19
+ end
20
+
21
+ def file
22
+ config.backup_file
23
+ end
24
+ end
25
+
26
+ class FileInput < File
27
+ def self.identity_adapter
28
+ IdentityFileInput
29
+ end
30
+
31
+ def self.find_explicit(config)
32
+ return adapter_registry.fetch(config.expander) do
33
+ raise "config.expander is #{config.expander.inspect} - supported expanders are #{supported_adapters.select{|ad| ad.is_a? Symbol}.join(", ")}"
34
+ end
35
+ rescue Config::MissingConfig
36
+ nil
37
+ end
38
+
39
+ def self.registry_host
40
+ FileInput
41
+ end
42
+ end
43
+
44
+ class FileOutput < File
45
+ def self.identity_adapter
46
+ IdentityFileOutput
47
+ end
48
+
49
+ def self.find_explicit(config)
50
+ return adapter_registry.fetch(config.compressor) do
51
+ raise "config.compressor is #{config.compressor.inspect} - supported compressors are #{supported_adapters.select{|ad| ad.is_a? Symbol}.join(", ")}"
52
+ end
53
+ rescue KeyError
54
+ end
55
+
56
+ def self.registry_host
57
+ FileOutput
58
+ end
59
+ end
60
+
61
+ class IdentityFileInput < FileInput
62
+ def go(cmd)
63
+ cmd.redirect_stdin(file)
64
+ cmd
65
+ end
66
+ end
67
+
68
+ class IdentityFileOutput < FileOutput
69
+ def go(cmd)
70
+ cmd.redirect_stdout(file)
71
+ cmd
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,51 @@
1
+ require 'bibliotech/builders/file'
2
+
3
+ module BiblioTech
4
+ module Builders
5
+ class GzipExpander < FileInput
6
+ register(/.*\.gz\z/)
7
+ register(/.*\.gzip\z/)
8
+
9
+ def go(command)
10
+ command = cmd("gunzip", file) | command
11
+ end
12
+ end
13
+
14
+ class ExplicitGzipExpander < GzipExpander
15
+ register :gzip
16
+
17
+ def file
18
+ file = super
19
+ unless PATTERNS.any?{|pattern| pattern =~ file}
20
+ return file + ".gz"
21
+ end
22
+ file
23
+ end
24
+ end
25
+
26
+ class GzipCompressor < FileOutput
27
+ PATTERNS = [ /.*\.gz\z/, /.*\.gzip\z/ ]
28
+ PATTERNS.each do |pattern|
29
+ register pattern
30
+ end
31
+
32
+ def go(cmd)
33
+ cmd |= %w{gzip}
34
+ cmd.redirect_stdout(file)
35
+ cmd
36
+ end
37
+ end
38
+
39
+ class ExplicitGzipCompressor < GzipCompressor
40
+ register :gzip
41
+
42
+ def file
43
+ file = super
44
+ unless PATTERNS.any?{|pattern| pattern =~ file}
45
+ return file + ".gz"
46
+ end
47
+ file
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,35 @@
1
+ require 'bibliotech/builders/database'
2
+
3
+ module BiblioTech
4
+ module Builders
5
+ module MySql
6
+ class Export < Builders::Export
7
+ register :mysql
8
+
9
+ def go(command)
10
+ command.from('mysqldump')
11
+ config.optional{ command.options << "-h #{config.host}" }
12
+ config.optional{ command.options << "-u #{config.username}" }
13
+ config.optional{ command.options << "-P #{config.port}" } #ok
14
+ config.optional{ command.options << "--password='#{config.password}'" }
15
+ command.options << "#{config.database}"
16
+ command
17
+ end
18
+ end
19
+
20
+ class Import < Builders::Import
21
+ register :mysql
22
+
23
+ def go(command)
24
+ command.from('mysql')
25
+ config.optional{ command.options << "-h #{config.host}" }
26
+ config.optional{ command.options << "-u #{config.username}" }
27
+ config.optional{ command.options << "-P #{config.port}" } #ok
28
+ config.optional{ command.options << "--password='#{config.password}'" }
29
+ command.options << "#{config.database}"
30
+ command
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ require 'bibliotech/builders/database'
2
+
3
+ module BiblioTech
4
+ module Builders
5
+ module Postgres
6
+ class Export < Builders::Export
7
+ register :postgres
8
+
9
+ def go(command)
10
+ command.from('pg_dump', '-Fc')
11
+ config.optional{ command.options << "-h #{config.host}" }
12
+ config.optional{ command.env["PGPASSWORD"] = config.password }
13
+ config.optional{ command.options << "-p #{config.port}" } #ok
14
+
15
+ command.options << "-U #{config.username}"
16
+ command.options << "#{config.database}"
17
+ command
18
+ end
19
+ end
20
+
21
+ class Import < Builders::Import
22
+ register :postgres
23
+
24
+ def go(command)
25
+ command.from('pg_restore')
26
+ config.optional{ command.options << "-h #{config.host}" }
27
+ config.optional{ command.env["PGPASSWORD"] = config.password }
28
+ config.optional{ command.options << "-p #{config.port}" } #ok
29
+
30
+ command.options << "-U #{config.username}"
31
+ command.options << "-d #{config.database}"
32
+ command
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,43 @@
1
+ module BiblioTech
2
+ module Builders
3
+ class Base
4
+ include Caliph::CommandLineDSL
5
+ class << self
6
+ def register(adapter_name)
7
+ adapter_registry[adapter_name] = self
8
+ end
9
+
10
+ def adapter_registry
11
+ registry_host.registry
12
+ end
13
+
14
+ def registry
15
+ @registry ||={}
16
+ end
17
+
18
+ def supported_adapters
19
+ adapter_registry.keys
20
+ end
21
+
22
+ def for(config)
23
+ find_class(config).new(config)
24
+ end
25
+
26
+ def null_adapter
27
+ NullAdapter
28
+ end
29
+ end
30
+
31
+ def initialize(config)
32
+ @config = config
33
+ end
34
+ attr_reader :config
35
+ end
36
+
37
+ class NullAdapter < Base
38
+ def go(cmd)
39
+ cmd
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,24 @@
1
+ require 'thor'
2
+ require 'bibliotech/application'
3
+
4
+ module BiblioTech
5
+ class CLI < Thor
6
+ desc "latest", "Outputs the latest DB dump available locally"
7
+ def latest
8
+ app = App.new
9
+ app.latest
10
+ end
11
+
12
+ desc "dump FILENAME", "Create a new database dump into FILE"
13
+ def dump(file)
14
+ app = App.new
15
+ app.export(:backups => { :filename => file })
16
+ end
17
+
18
+ desc "load FILENAME", "Load a database file from FILE"
19
+ def load(file)
20
+ app = App.new
21
+ app.import(:backups => { :filename => file })
22
+ end
23
+ end
24
+ end