squidward 0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|