squidward 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/squidward +13 -0
- data/lib/squidward.rb +27 -0
- data/lib/squidward/command.rb +3 -0
- data/lib/squidward/commands/base.rb +107 -0
- data/lib/squidward/commands/bucket.rb +13 -0
- data/lib/squidward/commands/credentials.rb +13 -0
- data/lib/squidward/commands/domain.rb +24 -0
- data/lib/squidward/commands/help.rb +79 -0
- data/lib/squidward/commands/info.rb +26 -0
- data/lib/squidward/commands/logs.rb +11 -0
- data/lib/squidward/commands/run.rb +86 -0
- data/lib/squidward/commands/version.rb +15 -0
- data/rakefile +51 -0
- data/spec/commands/base_test.rb +76 -0
- data/spec/commands/bucket_test.rb +20 -0
- data/spec/commands/credentials_test.rb +12 -0
- data/spec/commands/domain_test.rb +28 -0
- data/spec/commands/logs_test.rb +19 -0
- data/spec/commands/run_test.rb +133 -0
- data/spec/commands/version_test.rb +15 -0
- data/spec/mock_command.rb +7 -0
- data/spec/spec_config.rb +11 -0
- data/spec/squidward_test.rb +28 -0
- metadata +108 -0
data/bin/squidward
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
$LOAD_PATH.unshift(File.dirname(Pathname.new(__FILE__).realpath) + '/../')
|
5
|
+
|
6
|
+
require 'lib/squidward'
|
7
|
+
require 'lib/squidward/command'
|
8
|
+
|
9
|
+
args = ARGV.dup
|
10
|
+
ARGV.clear
|
11
|
+
command = args.shift.strip rescue 'help'
|
12
|
+
|
13
|
+
Squidward::Command.run!(command, args)
|
data/lib/squidward.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
%w{aws/s3 fileutils logger md5 base64 yaml sdbtools pstore}.each &method(:require)
|
3
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
4
|
+
require 'squidward/command'
|
5
|
+
|
6
|
+
# These are the default constants used along the code to refer to the squidward folders
|
7
|
+
CONF_PATH, SETTINGS_FILE, TEMP_PATH, LOG_FILE = "~/.squidward", ".settings", "temp", "squidward.log"
|
8
|
+
|
9
|
+
# Default module that contains the whole application
|
10
|
+
module Squidward
|
11
|
+
# Commands container, if you can't call this a command pattern implentation, there's no such thing
|
12
|
+
module Command
|
13
|
+
class << self
|
14
|
+
# Runs the given command and performs the proper invocation based on the user input.
|
15
|
+
# This method is the core dispatcher for the whole application.
|
16
|
+
def run!(command, args)
|
17
|
+
command, method = command.gsub(/-/,'_').split(/:/)
|
18
|
+
klass, method = eval("Squidward::Command::#{command.capitalize}"), (method or :index).to_sym
|
19
|
+
|
20
|
+
instance = klass.new
|
21
|
+
instance.send(method, args)
|
22
|
+
rescue
|
23
|
+
puts("Unknown command. Run 'squidward help' for usage information.")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Squidward
|
2
|
+
module Command
|
3
|
+
# Base command from where all the other commands inherit
|
4
|
+
# it contains the most used methods (stout, sterror, etc) and
|
5
|
+
# also handles the configuration, hence AWS credentials
|
6
|
+
class Base
|
7
|
+
# Creates a new instance and prompts the user for the
|
8
|
+
# credentials if they aren't stored
|
9
|
+
def initialize()
|
10
|
+
unless (@credentials = read_credentials)
|
11
|
+
@credentials = ask_for_credentials
|
12
|
+
store_credentials(@credentials)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Gets the AWS Credentials from the user
|
17
|
+
def ask_for_credentials
|
18
|
+
puts "Enter your AWS credentials."
|
19
|
+
|
20
|
+
print "Access Key ID: "
|
21
|
+
user = ask
|
22
|
+
|
23
|
+
print "Secret Access Key: "
|
24
|
+
password = ask_for_password
|
25
|
+
|
26
|
+
return [user, password]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Turns off the standard output while writting the password
|
30
|
+
def echo_off
|
31
|
+
system "stty -echo"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Turns on the standard output after writting the password
|
35
|
+
def echo_on
|
36
|
+
system "stty echo"
|
37
|
+
end
|
38
|
+
|
39
|
+
# Gets trimmed input from the user
|
40
|
+
def ask
|
41
|
+
gets.strip
|
42
|
+
end
|
43
|
+
|
44
|
+
# Gets trimmed input from the user but with echoing off
|
45
|
+
def ask_for_password
|
46
|
+
echo_off
|
47
|
+
password = ask
|
48
|
+
puts
|
49
|
+
echo_on
|
50
|
+
return password
|
51
|
+
end
|
52
|
+
|
53
|
+
# Reads the configuration from the profile, if the reload parameter is set to true, it will
|
54
|
+
# read it again, if not it will just return what it's already in-memory.
|
55
|
+
def configuration(reload = false)
|
56
|
+
# if the user asks not to reload we should send what we've got except we don't got anything
|
57
|
+
return @configuration if (@configuration and not reload)
|
58
|
+
path = File.expand_path File.join(CONF_PATH, SETTINGS_FILE)
|
59
|
+
# if the configuration file isn't created yet it should create it
|
60
|
+
return {} unless File.exists?(path)
|
61
|
+
@configuration = YAML.load_file(path)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Reads credentials from the configuration file
|
65
|
+
def read_credentials
|
66
|
+
conf = self.configuration
|
67
|
+
return nil unless conf[:credentials]
|
68
|
+
return [conf[:credentials][:access_key], conf[:credentials][:secret_key]]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Dumps configuration file contents to the user profile
|
72
|
+
def store_configuration(new_configuration)
|
73
|
+
FileUtils.mkpath(File.expand_path(CONF_PATH))
|
74
|
+
File.open(File.expand_path(File.join(CONF_PATH, SETTINGS_FILE)), "w+") do |io|
|
75
|
+
YAML::dump(new_configuration, io)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Stores the users credentials on the configuration.
|
80
|
+
def store_credentials(credentials)
|
81
|
+
conf = configuration
|
82
|
+
conf[:credentials] = {}
|
83
|
+
conf[:credentials][:access_key], conf[:credentials][:secret_key] = credentials[0], credentials[1]
|
84
|
+
store_configuration(conf)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Nice and clean way of echoing
|
88
|
+
def display(msg, newline=true)
|
89
|
+
if newline
|
90
|
+
puts(msg)
|
91
|
+
else
|
92
|
+
print(msg)
|
93
|
+
STDOUT.flush
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Logger singleton
|
98
|
+
def logger
|
99
|
+
return @logger if @logger
|
100
|
+
@logger = Logger.new(File.expand_path(File.join(CONF_PATH, LOG_FILE)))
|
101
|
+
@logger.level = Logger::INFO
|
102
|
+
@logger.formatter = Proc.new {|s, t, n, msg| "[#{t}] [#{s}] #{msg}\n"}
|
103
|
+
@logger
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Squidward
|
2
|
+
module Command
|
3
|
+
# Handles all the command related to AWS S3's buckets
|
4
|
+
class Bucket < Base
|
5
|
+
# Sets the destination bucket for the backups to be uploaded on S3
|
6
|
+
def set(args)
|
7
|
+
new_configuration = configuration
|
8
|
+
new_configuration[:default_bucket] = args[0]
|
9
|
+
store_configuration(new_configuration)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Squidward
|
2
|
+
module Command
|
3
|
+
# Handles all the commands related to credentials
|
4
|
+
class Credentials < Base
|
5
|
+
# Removes the stored credentials from the current configuration
|
6
|
+
def clear(args = nil)
|
7
|
+
new_configuration = configuration
|
8
|
+
new_configuration[:credentials] = nil
|
9
|
+
store_configuration(new_configuration)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Squidward
|
2
|
+
module Command
|
3
|
+
# Handles everything related to AWS SimpleDB Domains for Backup
|
4
|
+
class Domain < Base
|
5
|
+
# Configures a SimpleDB domain for backup. When given the second parameter is
|
6
|
+
# the virtual path inside the bucket to be stored.
|
7
|
+
def backup(args)
|
8
|
+
domain, vpath = args
|
9
|
+
conf = self.configuration
|
10
|
+
conf[:domains] = (conf[:domains] or []) + [{:domain => domain, :vpath => vpath}]
|
11
|
+
store_configuration(conf)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Removes a SimpleDB domain already being backed up from the current user profile
|
15
|
+
# so it doesn't run anymore.
|
16
|
+
def remove(args)
|
17
|
+
domain, vpath = args
|
18
|
+
conf = self.configuration
|
19
|
+
conf[:domains] = conf[:domains].reject{|d| d[:domain].downcase.strip == domain.downcase.strip}
|
20
|
+
store_configuration(conf)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Squidward
|
2
|
+
module Command
|
3
|
+
class Help < Base
|
4
|
+
class HelpGroup < Array
|
5
|
+
attr_reader :title
|
6
|
+
|
7
|
+
def initialize(title)
|
8
|
+
@title = title
|
9
|
+
end
|
10
|
+
|
11
|
+
def command(name, description)
|
12
|
+
self << [name, description]
|
13
|
+
end
|
14
|
+
|
15
|
+
def space
|
16
|
+
self << ['', '']
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.groups
|
21
|
+
@groups ||= []
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.group(title, &block)
|
25
|
+
groups << begin
|
26
|
+
group = HelpGroup.new(title)
|
27
|
+
yield group
|
28
|
+
group
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.create_default_groups!
|
33
|
+
group 'General Commands' do |group|
|
34
|
+
group.command 'help', 'show this usage'
|
35
|
+
group.command 'version', 'show gem version'
|
36
|
+
group.space
|
37
|
+
group.command 'bucket:set <name>', 'set the S3 bucket where the files will be uploaded (default: backup, created when missing)'
|
38
|
+
group.space
|
39
|
+
group.command 'credentials:clear', 'clear the credentials stored on your settings'
|
40
|
+
group.space
|
41
|
+
group.command 'domain:backup <domain> [vpath] ', 'add aws_simpledb domain for backup [optional vpath a virtual path inside the bucket]'
|
42
|
+
group.command 'domain:remove <domain>', 'remove aws_simpledb domain from backup list'
|
43
|
+
group.space
|
44
|
+
group.command 'run', 'run the process by taking one by one the domains configured on your settings'
|
45
|
+
group.space
|
46
|
+
group.command 'info', 'display your current settings'
|
47
|
+
group.command 'logs', 'display lastest information from the log file'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def index(args)
|
52
|
+
display usage
|
53
|
+
end
|
54
|
+
|
55
|
+
def usage
|
56
|
+
longest_command_length = self.class.groups.map do |group|
|
57
|
+
group.map { |g| g.first.length }
|
58
|
+
end.flatten.max
|
59
|
+
|
60
|
+
self.class.groups.inject(StringIO.new) do |output, group|
|
61
|
+
output.puts "=== %s" % group.title
|
62
|
+
output.puts
|
63
|
+
|
64
|
+
group.each do |command, description|
|
65
|
+
if command.empty?
|
66
|
+
output.puts
|
67
|
+
else
|
68
|
+
output.puts "%-*s # %s" % [longest_command_length, command, description]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
output.puts
|
72
|
+
output
|
73
|
+
end.string
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
Squidward::Command::Help.create_default_groups!
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Squidward
|
2
|
+
module Command
|
3
|
+
# Handles the echoing of the current configuration (pretty basic tough)
|
4
|
+
class Info < Base
|
5
|
+
# Goes thru the settings and echos their values.
|
6
|
+
def index(args = nil)
|
7
|
+
display("=== Current Settings")
|
8
|
+
display("Amazon Web Services Account: #{configuration[:credentials][:access_key]}")
|
9
|
+
display("Amazon S3 Bucket: #{configuration[:default_bucket]}")
|
10
|
+
display("")
|
11
|
+
if (configuration[:domains] and configuration[:domains].size > 0)
|
12
|
+
display("Configured Domains: ", false)
|
13
|
+
first = true
|
14
|
+
configuration[:domains].each do |domain|
|
15
|
+
spaces = (not first) ? " " * 30 : ""
|
16
|
+
display(spaces + "#{domain[:domain]} is being uploaded to #{domain[:vpath] or "<bucket_root>"}")
|
17
|
+
first = false
|
18
|
+
end
|
19
|
+
else
|
20
|
+
display("Configured Domains: You currently have no domains configuration for backup")
|
21
|
+
end
|
22
|
+
display("")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Squidward
|
2
|
+
module Command
|
3
|
+
# Handles the information related to the log being genereated by the application
|
4
|
+
class Logs < Base
|
5
|
+
# Echoes to the stdout the latest 25 lines of the log
|
6
|
+
def index(args = nil)
|
7
|
+
system "tail -n 25 #{File.expand_path(File.join(CONF_PATH, LOG_FILE))}"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Squidward
|
2
|
+
module Command
|
3
|
+
# Core class of the whole application. It simply runs everything once it has been configured.
|
4
|
+
class Run < Base
|
5
|
+
def initialize()
|
6
|
+
super
|
7
|
+
aws_config = {}
|
8
|
+
aws_config[:access_key_id], aws_config[:secret_access_key] = @credentials
|
9
|
+
AWS::S3::Base.establish_connection!(aws_config)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Runs the backup for each configured simpledb domain
|
13
|
+
def index(args = {})
|
14
|
+
ensure_bucket!(bucket)
|
15
|
+
domains.each do |domain|
|
16
|
+
internal_run(domain)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# If the configured bucket does not exists on AWS S3 it will create it,
|
21
|
+
# else it will just retrieve it.
|
22
|
+
def ensure_bucket!(bucket_name)
|
23
|
+
begin
|
24
|
+
AWS::S3::Bucket.find(bucket_name)
|
25
|
+
rescue
|
26
|
+
AWS::S3::Bucket.create(bucket_name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Performs a backup for a specific domain
|
31
|
+
def internal_run(pair = {})
|
32
|
+
domain_name, virtual_path = pair[:domain], (pair[:vpath] or "")
|
33
|
+
return unless database.domain_exists?(pair[:domain])
|
34
|
+
dump_file = dump_domain(domain_name)
|
35
|
+
dump_file_md5 = Base64.encode64(compute_md5(dump_file).digest)
|
36
|
+
upload_to = File.join(virtual_path, generate_dump_filename(domain_name))
|
37
|
+
AWS::S3::S3Object.store("#{upload_to}.tar.gz", open(dump_file), bucket, :content_md5 => dump_file_md5)
|
38
|
+
File.delete(dump_file)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Dumps the domain contents to YAML file
|
42
|
+
def dump_domain(domain_name)
|
43
|
+
temp_folder = File.expand_path(File.join(CONF_PATH, TEMP_PATH))
|
44
|
+
FileUtils.mkpath(temp_folder)
|
45
|
+
temp_file = File.join(temp_folder, generate_dump_filename(domain_name))
|
46
|
+
results = database.domain(domain_name).selection().results
|
47
|
+
File.open(temp_file, "w+") { |io| YAML::dump(results, io) }
|
48
|
+
`tar -czf "#{temp_file}.tar.gz" -C "#{File.dirname(temp_file)}" "#{File.basename(temp_file)}"`
|
49
|
+
File.delete(temp_file)
|
50
|
+
return "#{temp_file}.tar.gz"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Generates a unique filename for the current domain backup
|
54
|
+
def generate_dump_filename(domain_name)
|
55
|
+
"#{domain_name}-#{Time.now.utc.strftime("%Y%m%d%H%M%S%Z%Y")}"
|
56
|
+
end
|
57
|
+
|
58
|
+
# Singleton instance of the database
|
59
|
+
def database
|
60
|
+
@database ||= SDBTools::Database.new(@credentials[0], @credentials[1], :logger => self.logger)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns every configured domain for the current user
|
64
|
+
def domains
|
65
|
+
return self.configuration[:domains]
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns the bucket configured for the current user
|
69
|
+
def bucket
|
70
|
+
return (self.configuration[:default_bucket] or "backup")
|
71
|
+
end
|
72
|
+
|
73
|
+
# Computes the MD5 for the uploaded file, in-case you
|
74
|
+
# wanna do corruption detection
|
75
|
+
def compute_md5(filename)
|
76
|
+
md5_digest = Digest::MD5.new
|
77
|
+
File.open(filename, 'r') do |file|
|
78
|
+
file.each_line do |line|
|
79
|
+
md5_digest << line
|
80
|
+
end
|
81
|
+
end
|
82
|
+
return md5_digest
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Squidward
|
2
|
+
module Command
|
3
|
+
# Gem Versioning Helper and also displays current version to the user
|
4
|
+
class Version < Base
|
5
|
+
GEM_VERSION = "0.5"
|
6
|
+
|
7
|
+
# Displays current gem version to the user
|
8
|
+
def index(args = nil)
|
9
|
+
display("squidward-#{GEM_VERSION}")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
|
data/rakefile
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'lib/squidward'
|
3
|
+
require 'rake'
|
4
|
+
require 'spec'
|
5
|
+
require 'mocha'
|
6
|
+
require 'spec/rake/spectask'
|
7
|
+
require 'rake/gempackagetask'
|
8
|
+
require 'rake/rdoctask'
|
9
|
+
|
10
|
+
task :default => [:specs]
|
11
|
+
|
12
|
+
task :specs do
|
13
|
+
Spec::Rake::SpecTask.new('specs_run') do |t|
|
14
|
+
t.spec_files = FileList['spec/**/*.rb']
|
15
|
+
t.rcov = true
|
16
|
+
t.rcov_opts = ['--text-report', '--exclude', ".bundle,.gem,spec,Library,#{ENV['GEM_HOME']}", '--sort', 'coverage' ]
|
17
|
+
t.spec_opts = ['-cfn']
|
18
|
+
end
|
19
|
+
Rake::Task['specs_run'].invoke
|
20
|
+
end
|
21
|
+
|
22
|
+
namespace :dist do
|
23
|
+
spec = Gem::Specification.new do |s|
|
24
|
+
s.name = 'squidward'
|
25
|
+
s.version = Gem::Version.new(Squidward::Command::Version::GEM_VERSION)
|
26
|
+
s.summary = "Backup tool for taking SimpleDB domains to compressed files inside Amazon S3"
|
27
|
+
s.description = "A dead simple tool for doing backups of simpledb domains."
|
28
|
+
s.email = 'johnny.halife@me.com'
|
29
|
+
s.author = 'Johnny G. Halife'
|
30
|
+
s.homepage = 'http://squidward.heroku.com'
|
31
|
+
|
32
|
+
s.files = %w(rakefile) + Dir.glob("{bin,lib,spec}/**/*")
|
33
|
+
s.executables = "squidward"
|
34
|
+
s.default_executable = "squidward"
|
35
|
+
|
36
|
+
s.platform = Gem::Platform::RUBY
|
37
|
+
s.has_rdoc = true
|
38
|
+
s.rdoc_options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
|
39
|
+
|
40
|
+
s.require_path = "lib"
|
41
|
+
s.bindir = "bin"
|
42
|
+
|
43
|
+
# Dependencies
|
44
|
+
s.add_dependency 'aws-s3'
|
45
|
+
s.add_dependency 'sdbtools'
|
46
|
+
end
|
47
|
+
|
48
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
49
|
+
pkg.need_tar = true
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# enabling the load of files from root (on RSpec)
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../')
|
3
|
+
require 'spec_config'
|
4
|
+
|
5
|
+
describe "base commands functionality" do
|
6
|
+
|
7
|
+
it "should prompt for AWS Credentials when they do not exist" do
|
8
|
+
Squidward::Command::Base.any_instance.expects(:read_credentials).returns(false)
|
9
|
+
Squidward::Command::Base.any_instance.expects(:ask_for_credentials).returns(["access_key", "secret_key"])
|
10
|
+
Squidward::Command::Base.any_instance.expects(:store_credentials).with(["access_key", "secret_key"])
|
11
|
+
my_command = Squidward::Command::Base.new()
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should ask for credentials" do
|
15
|
+
Squidward::Command::Base.any_instance.expects(:read_credentials).returns(true)
|
16
|
+
my_command = Squidward::Command::Base.new()
|
17
|
+
my_command.expects(:puts).with("Enter your AWS credentials.")
|
18
|
+
my_command.expects(:print).with("Access Key ID: ")
|
19
|
+
my_command.expects(:print).with("Secret Access Key: ")
|
20
|
+
my_command.expects(:ask).returns("user")
|
21
|
+
my_command.expects(:ask_for_password).returns("password")
|
22
|
+
my_command.ask_for_credentials.should == ["user", "password"]
|
23
|
+
end
|
24
|
+
|
25
|
+
it "when asking for password should turn off echo and turn it on" do
|
26
|
+
Squidward::Command::Base.any_instance.expects(:read_credentials).returns(true)
|
27
|
+
my_command = Squidward::Command::Base.new()
|
28
|
+
my_command.expects(:echo_off)
|
29
|
+
my_command.expects(:echo_on)
|
30
|
+
my_command.expects(:puts)
|
31
|
+
my_command.expects(:ask).returns("password")
|
32
|
+
my_command.ask_for_password.should == "password"
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should read the configuration when its prompted for config (no relaod)" do
|
36
|
+
Squidward::Command::Base.any_instance.expects(:read_credentials).returns(true)
|
37
|
+
configuration_path = File.expand_path(File.join(CONF_PATH, SETTINGS_FILE))
|
38
|
+
File.expects(:exists?).with(configuration_path).returns(true)
|
39
|
+
YAML::expects(:load_file).with(configuration_path)
|
40
|
+
my_command = Squidward::Command::Base.new()
|
41
|
+
my_command.configuration
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should return the existing configuration when its prompted for config (no relaod)" do
|
45
|
+
Squidward::Command::Base.any_instance.expects(:read_credentials).returns(true)
|
46
|
+
configuration_path = File.expand_path(File.join(CONF_PATH, SETTINGS_FILE))
|
47
|
+
File.expects(:exists?).with(configuration_path).returns(true)
|
48
|
+
YAML::expects(:load_file).with(configuration_path).returns({:key => "value"})
|
49
|
+
my_command = Squidward::Command::Base.new()
|
50
|
+
my_command.configuration
|
51
|
+
# I'm doing it twice and it should work the second time (returns the cached copy)
|
52
|
+
my_command.configuration
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should store configuration" do
|
56
|
+
Squidward::Command::Base.any_instance.expects(:read_credentials).returns(true)
|
57
|
+
File.expects(:open).with(File.expand_path(File.join(CONF_PATH, SETTINGS_FILE)), "w+")
|
58
|
+
FileUtils.expects(:mkpath).with(File.expand_path(CONF_PATH))
|
59
|
+
my_command = Squidward::Command::Base.new()
|
60
|
+
my_command.store_configuration({})
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should read credentials when already there" do
|
64
|
+
Squidward::Command::Base.any_instance.expects(:configuration).returns({:credentials => {:secret_key => "secret", :access_key => "access"}}).twice
|
65
|
+
my_command = Squidward::Command::Base.new()
|
66
|
+
my_command.read_credentials.should == ["access", "secret"]
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should store credentials after getting them" do
|
70
|
+
Squidward::Command::Base.any_instance.expects(:read_credentials).returns(false)
|
71
|
+
Squidward::Command::Base.any_instance.expects(:ask_for_credentials).returns(["access_key", "secret_key"])
|
72
|
+
Squidward::Command::Base.any_instance.expects(:configuration).returns({:credentials => {}})
|
73
|
+
Squidward::Command::Base.any_instance.expects(:store_configuration).returns()
|
74
|
+
my_command = Squidward::Command::Base.new()
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# enabling the load of files from root (on RSpec)
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../')
|
3
|
+
require 'spec_config'
|
4
|
+
|
5
|
+
describe "runner command functionality" do
|
6
|
+
before do
|
7
|
+
Squidward::Command::Bucket.any_instance.expects(:read_credentials).returns(["access_key", "secret_key"])
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should inherit the Base functionality" do
|
11
|
+
my_command = Squidward::Command::Bucket.new()
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should set backup default bucket" do
|
15
|
+
my_command = Squidward::Command::Bucket.new()
|
16
|
+
my_command.expects(:configuration).returns({})
|
17
|
+
my_command.expects(:store_configuration).with({:default_bucket => "backup"})
|
18
|
+
my_command.set(["backup"])
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# enabling the load of files from root (on RSpec)
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../')
|
3
|
+
require 'spec_config'
|
4
|
+
|
5
|
+
describe "runner command functionality" do
|
6
|
+
it "should clear the stored credentials" do
|
7
|
+
my_command = Squidward::Command::Credentials.new()
|
8
|
+
my_command.expects(:configuration).returns({:credentials => ["foo", "bar"]})
|
9
|
+
my_command.expects(:store_configuration).with({:credentials => nil})
|
10
|
+
my_command.clear
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# enabling the load of files from root (on RSpec)
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../')
|
3
|
+
require 'spec_config'
|
4
|
+
|
5
|
+
describe "domain command functionality" do
|
6
|
+
it "should inherit the Base functionality" do
|
7
|
+
Squidward::Command::Domain.any_instance.expects(:read_credentials).returns(true)
|
8
|
+
my_command = Squidward::Command::Domain.new()
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should store backup for domain" do
|
12
|
+
existing_domains = [{:domain => "domain", :vpath => "vpath"}]
|
13
|
+
Squidward::Command::Domain.any_instance.expects(:read_credentials).returns(true)
|
14
|
+
Squidward::Command::Domain.any_instance.expects(:configuration).returns({:domains => existing_domains})
|
15
|
+
Squidward::Command::Domain.any_instance.expects(:store_configuration).with({:domains => existing_domains + [{:domain => "domain_name", :vpath => "virtual_path"}]})
|
16
|
+
my_command = Squidward::Command::Domain.new()
|
17
|
+
my_command.backup(["domain_name", "virtual_path"])
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should remove existing backup from the configuration and store new config" do
|
21
|
+
existing_domains = [{:domain => "domain", :vpath => "vpath"}]
|
22
|
+
Squidward::Command::Domain.any_instance.expects(:read_credentials).returns(true)
|
23
|
+
Squidward::Command::Domain.any_instance.expects(:configuration).returns({:domains => existing_domains})
|
24
|
+
Squidward::Command::Domain.any_instance.expects(:store_configuration).with({:domains => []})
|
25
|
+
my_command = Squidward::Command::Domain.new()
|
26
|
+
my_command.remove("domain")
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# enabling the load of files from root (on RSpec)
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../')
|
3
|
+
require 'spec_config'
|
4
|
+
|
5
|
+
describe "runner command functionality" do
|
6
|
+
before do
|
7
|
+
Squidward::Command::Logs.any_instance.expects(:read_credentials).returns(["access_key", "secret_key"])
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should inherit the Base functionality" do
|
11
|
+
my_command = Squidward::Command::Logs.new()
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should set backup default bucket" do
|
15
|
+
my_command = Squidward::Command::Logs.new()
|
16
|
+
my_command.expects(:system).with("tail -n 25 #{File.expand_path(File.join(CONF_PATH, LOG_FILE))}")
|
17
|
+
my_command.index
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# enabling the load of files from root (on RSpec)
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../')
|
3
|
+
require 'spec_config'
|
4
|
+
|
5
|
+
describe "runner command functionality" do
|
6
|
+
before do
|
7
|
+
Squidward::Command::Run.any_instance.expects(:read_credentials).returns(["access_key", "secret_key"])
|
8
|
+
AWS::S3::Base.expects(:establish_connection!).with(:access_key_id => "access_key", :secret_access_key => "secret_key")
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should inherit the Base functionality" do
|
12
|
+
my_command = Squidward::Command::Run.new()
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should return services to run from the configuration" do
|
16
|
+
configuration = {:domains => [{:domain => "domain1", :vpath => "path2"},
|
17
|
+
{:domain => "domain2", :vpath => "path2"}]}
|
18
|
+
|
19
|
+
my_command = Squidward::Command::Run.new()
|
20
|
+
my_command.expects(:configuration).returns(configuration)
|
21
|
+
my_command.domains.should == configuration[:domains]
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should return services to run from the configuration" do
|
25
|
+
configuration = {:default_bucket => "backup"}
|
26
|
+
my_command = Squidward::Command::Run.new()
|
27
|
+
my_command.expects(:configuration).returns(configuration)
|
28
|
+
my_command.bucket.should == configuration[:default_bucket]
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should return services to run from the configuration" do
|
32
|
+
my_command = Squidward::Command::Run.new()
|
33
|
+
my_command.expects(:configuration).returns({})
|
34
|
+
my_command.bucket.should == "backup"
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should run the backup for each domain (and ensure the destination bucket exists)" do
|
38
|
+
my_command = Squidward::Command::Run.new()
|
39
|
+
my_command.expects(:ensure_bucket!).returns("backup.squidward.full").once
|
40
|
+
my_command.expects(:domains).returns([{:domain => "domain1", :vpath => "bucket1"}])
|
41
|
+
my_command.expects(:internal_run).with({:domain => "domain1", :vpath => "bucket1"})
|
42
|
+
my_command.index
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should run the backup for each domain (and avoide creating the destination bucket when it exists)" do
|
46
|
+
my_command = Squidward::Command::Run.new()
|
47
|
+
my_command.expects(:bucket).returns("backup.squidward.full")
|
48
|
+
my_command.expects(:ensure_bucket!).with("backup.squidward.full")
|
49
|
+
my_command.expects(:domains).returns([{:domain => "domain1", :vpath => "bucket1"}])
|
50
|
+
my_command.expects(:internal_run).with({:domain => "domain1", :vpath => "bucket1"})
|
51
|
+
my_command.index
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should dump a domain and return the file path to the dump result" do
|
55
|
+
FileUtils.expects(:mkpath).with(File.expand_path(File.join(CONF_PATH, TEMP_PATH))).returns("temp-path")
|
56
|
+
dump_file = File.expand_path(File.join(CONF_PATH, TEMP_PATH, "dump"))
|
57
|
+
|
58
|
+
my_command = Squidward::Command::Run.new()
|
59
|
+
my_command.expects(:generate_dump_filename).returns("dump")
|
60
|
+
|
61
|
+
(mock_selection = mock).expects(:results).returns([])
|
62
|
+
(mock_domain = mock).expects(:selection).returns(mock_selection)
|
63
|
+
(mock_db = mock).expects(:domain).with("custom-domain").returns(mock_domain)
|
64
|
+
my_command.expects(:database).returns(mock_db)
|
65
|
+
|
66
|
+
File.expects(:open).with(dump_file, "w+")
|
67
|
+
File.expects(:delete).with(dump_file)
|
68
|
+
|
69
|
+
my_command.expects(:`).with("tar -czf \"#{dump_file}.tar.gz\" -C \"#{File.dirname(dump_file)}\" \"#{File.basename(dump_file)}\"")
|
70
|
+
my_command.dump_domain("custom-domain").should == "#{dump_file}.tar.gz"
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should generate the filename for the dumped file" do
|
74
|
+
now = Time.now
|
75
|
+
Time.expects(:now).returns(now).twice
|
76
|
+
my_command = Squidward::Command::Run.new()
|
77
|
+
my_command.generate_dump_filename("my-domain").should == "my-domain-#{Time.now.utc.strftime("%Y%m%d%H%M%S%Z%Y")}"
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should do anything if domain does not exist" do
|
81
|
+
my_command = Squidward::Command::Run.new()
|
82
|
+
(mock_db = mock).expects(:domain_exists?).with("domain1").returns(false)
|
83
|
+
my_command.expects(:database).returns(mock_db)
|
84
|
+
my_command.internal_run(:domain => "domain1", :vpath => "/sdb/")
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should do full blown backup of a single domain when it exists" do
|
88
|
+
my_command = Squidward::Command::Run.new()
|
89
|
+
my_command.expects(:bucket).returns("my_bucket")
|
90
|
+
|
91
|
+
my_command.expects(:generate_dump_filename).with("domain").returns("dump")
|
92
|
+
|
93
|
+
# the domain already exists
|
94
|
+
(mock_db = mock).expects(:domain_exists?).with("domain").returns(true)
|
95
|
+
my_command.expects(:database).returns(mock_db)
|
96
|
+
|
97
|
+
# ask the library to dump the files
|
98
|
+
my_command.expects(:dump_domain).with("domain").returns("mock/path/to/file.tar.gz")
|
99
|
+
|
100
|
+
# generate md5-base64 for the file
|
101
|
+
(mock_digest = mock).expects(:digest).returns("digest")
|
102
|
+
my_command.expects(:compute_md5).with("mock/path/to/file.tar.gz").returns(mock_digest)
|
103
|
+
Base64.expects(:encode64).with("digest").returns("base64digest")
|
104
|
+
|
105
|
+
# mock the file opening
|
106
|
+
my_command.expects(:open).with("mock/path/to/file.tar.gz").returns("contents")
|
107
|
+
|
108
|
+
AWS::S3::S3Object.expects(:store).with("/sdb/dump.tar.gz", "contents", "my_bucket", :content_md5 => "base64digest")
|
109
|
+
|
110
|
+
File.expects(:delete).with("mock/path/to/file.tar.gz")
|
111
|
+
my_command.internal_run(:domain => "domain", :vpath => "/sdb/")
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should hold a singleton instance of the database" do
|
115
|
+
(mock_logger = mock).expects(:info)
|
116
|
+
Squidward::Command::Run.any_instance.expects(:logger).returns(mock_logger)
|
117
|
+
my_command = Squidward::Command::Run.new()
|
118
|
+
my_command.database.nil?.should == false
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should look up for the bucket when calling ensure if it exists" do
|
122
|
+
my_command = Squidward::Command::Run.new()
|
123
|
+
AWS::S3::Bucket.expects(:find).with("my_bucket").returns(mock)
|
124
|
+
my_command.ensure_bucket!("my_bucket").nil?.should == false
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should create the bucket when it does not exist" do
|
128
|
+
my_command = Squidward::Command::Run.new()
|
129
|
+
AWS::S3::Bucket.expects(:find).with("my_bucket").raises(StandardError)
|
130
|
+
AWS::S3::Bucket.expects(:create).with("my_bucket").returns(mock)
|
131
|
+
my_command.ensure_bucket!("my_bucket").nil?.should == false
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# enabling the load of files from root (on RSpec)
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../')
|
3
|
+
require 'spec_config'
|
4
|
+
|
5
|
+
describe "runner command functionality" do
|
6
|
+
before do
|
7
|
+
Squidward::Command::Version.any_instance.expects(:read_credentials).returns(["access_key", "secret_key"])
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should show version string as {gem_name}-{version}" do
|
11
|
+
my_command = Squidward::Command::Version.new()
|
12
|
+
my_command.expects(:display).with("squidward-#{Squidward::Command::Version::GEM_VERSION}")
|
13
|
+
my_command.index
|
14
|
+
end
|
15
|
+
end
|
data/spec/spec_config.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# enabling the load of files from root (on RSpec)
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../')
|
3
|
+
require 'spec_config'
|
4
|
+
|
5
|
+
describe "launcher behavior" do
|
6
|
+
it "should invoke the proper command and method" do
|
7
|
+
(command = mock).expects(:run)
|
8
|
+
Squidward::Command::Mock.expects(:new).returns(command)
|
9
|
+
Squidward::Command.run!("mock:run", nil)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should invoke index method as the default method when not provided" do
|
13
|
+
(command = mock).expects(:index)
|
14
|
+
Squidward::Command::Mock.expects(:new).returns(command)
|
15
|
+
Squidward::Command.run!("mock", nil)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should raise missing command when the method is invalid" do
|
19
|
+
Squidward::Command.expects(:puts).with("Unknown command. Run 'squidward help' for usage information.")
|
20
|
+
Squidward::Command.run!("invalid", nil)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should raise missing command when the command action does not exist" do
|
24
|
+
Squidward::Command.expects(:puts).with("Unknown command. Run 'squidward help' for usage information.")
|
25
|
+
Squidward::Command::Mock.expects(:new).returns(mock)
|
26
|
+
Squidward::Command.run!("mock:non-existing", nil)
|
27
|
+
end
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: squidward
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 5
|
8
|
+
version: "0.5"
|
9
|
+
platform: ruby
|
10
|
+
authors:
|
11
|
+
- Johnny G. Halife
|
12
|
+
autorequire:
|
13
|
+
bindir: bin
|
14
|
+
cert_chain: []
|
15
|
+
|
16
|
+
date: 2010-04-26 00:00:00 -03:00
|
17
|
+
default_executable: squidward
|
18
|
+
dependencies:
|
19
|
+
- !ruby/object:Gem::Dependency
|
20
|
+
name: aws-s3
|
21
|
+
prerelease: false
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
segments:
|
27
|
+
- 0
|
28
|
+
version: "0"
|
29
|
+
type: :runtime
|
30
|
+
version_requirements: *id001
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: sdbtools
|
33
|
+
prerelease: false
|
34
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
segments:
|
39
|
+
- 0
|
40
|
+
version: "0"
|
41
|
+
type: :runtime
|
42
|
+
version_requirements: *id002
|
43
|
+
description: A dead simple tool for doing backups of simpledb domains.
|
44
|
+
email: johnny.halife@me.com
|
45
|
+
executables:
|
46
|
+
- squidward
|
47
|
+
extensions: []
|
48
|
+
|
49
|
+
extra_rdoc_files: []
|
50
|
+
|
51
|
+
files:
|
52
|
+
- rakefile
|
53
|
+
- bin/squidward
|
54
|
+
- lib/squidward/command.rb
|
55
|
+
- lib/squidward/commands/base.rb
|
56
|
+
- lib/squidward/commands/bucket.rb
|
57
|
+
- lib/squidward/commands/credentials.rb
|
58
|
+
- lib/squidward/commands/domain.rb
|
59
|
+
- lib/squidward/commands/help.rb
|
60
|
+
- lib/squidward/commands/info.rb
|
61
|
+
- lib/squidward/commands/logs.rb
|
62
|
+
- lib/squidward/commands/run.rb
|
63
|
+
- lib/squidward/commands/version.rb
|
64
|
+
- lib/squidward.rb
|
65
|
+
- spec/commands/base_test.rb
|
66
|
+
- spec/commands/bucket_test.rb
|
67
|
+
- spec/commands/credentials_test.rb
|
68
|
+
- spec/commands/domain_test.rb
|
69
|
+
- spec/commands/logs_test.rb
|
70
|
+
- spec/commands/run_test.rb
|
71
|
+
- spec/commands/version_test.rb
|
72
|
+
- spec/mock_command.rb
|
73
|
+
- spec/spec_config.rb
|
74
|
+
- spec/squidward_test.rb
|
75
|
+
has_rdoc: true
|
76
|
+
homepage: http://squidward.heroku.com
|
77
|
+
licenses: []
|
78
|
+
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options:
|
81
|
+
- --line-numbers
|
82
|
+
- --inline-source
|
83
|
+
- -A cattr_accessor=object
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
segments:
|
91
|
+
- 0
|
92
|
+
version: "0"
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
segments:
|
98
|
+
- 0
|
99
|
+
version: "0"
|
100
|
+
requirements: []
|
101
|
+
|
102
|
+
rubyforge_project:
|
103
|
+
rubygems_version: 1.3.6
|
104
|
+
signing_key:
|
105
|
+
specification_version: 3
|
106
|
+
summary: Backup tool for taking SimpleDB domains to compressed files inside Amazon S3
|
107
|
+
test_files: []
|
108
|
+
|