awshucks 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/CHANGES +4 -0
  2. data/LICENSE +19 -0
  3. data/README +32 -0
  4. data/Rakefile +14 -0
  5. data/TODO +143 -0
  6. data/bin/awshucks +10 -0
  7. data/lib/awshucks.rb +29 -0
  8. data/lib/awshucks/cli.rb +76 -0
  9. data/lib/awshucks/command.rb +98 -0
  10. data/lib/awshucks/commands.rb +3 -0
  11. data/lib/awshucks/commands/backup.rb +59 -0
  12. data/lib/awshucks/commands/backups.rb +25 -0
  13. data/lib/awshucks/commands/help.rb +22 -0
  14. data/lib/awshucks/commands/list.rb +39 -0
  15. data/lib/awshucks/commands/new_config.rb +36 -0
  16. data/lib/awshucks/commands/reset_metadata_cache.rb +38 -0
  17. data/lib/awshucks/commands/restore.rb +54 -0
  18. data/lib/awshucks/config.rb +83 -0
  19. data/lib/awshucks/ext.rb +23 -0
  20. data/lib/awshucks/file_info.rb +23 -0
  21. data/lib/awshucks/file_store.rb +111 -0
  22. data/lib/awshucks/gemspec.rb +48 -0
  23. data/lib/awshucks/scanner.rb +58 -0
  24. data/lib/awshucks/specification.rb +128 -0
  25. data/lib/awshucks/version.rb +18 -0
  26. data/resources/awshucks.yml +21 -0
  27. data/spec/awshucks_spec.rb +7 -0
  28. data/spec/cli_spec.rb +130 -0
  29. data/spec/command_spec.rb +111 -0
  30. data/spec/commands/backup_spec.rb +164 -0
  31. data/spec/commands/backups_spec.rb +41 -0
  32. data/spec/commands/help_spec.rb +42 -0
  33. data/spec/commands/list_spec.rb +77 -0
  34. data/spec/commands/new_config_spec.rb +102 -0
  35. data/spec/commands/reset_metadata_cache_spec.rb +93 -0
  36. data/spec/commands/restore_spec.rb +219 -0
  37. data/spec/config_spec.rb +152 -0
  38. data/spec/ext_spec.rb +28 -0
  39. data/spec/file_info_spec.rb +45 -0
  40. data/spec/file_store_spec.rb +352 -0
  41. data/spec/scanner_spec.rb +106 -0
  42. data/spec/spec_helper.rb +36 -0
  43. data/spec/specification_spec.rb +41 -0
  44. metadata +121 -0
@@ -0,0 +1,25 @@
1
+ module Awshucks
2
+
3
+ class BackupsCommand < Command
4
+ self.command = 'backups'
5
+ self.usage = 'backups'
6
+ self.description = "Lists the available backups and their configurations"
7
+
8
+ def execute(args, config)
9
+
10
+ puts "configured backups:"
11
+ puts ""
12
+
13
+ config.backups.each do |backup|
14
+
15
+ puts " #{backup.name}:"
16
+ puts " location: #{backup.location}"
17
+ puts ""
18
+
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+
25
+ end
@@ -0,0 +1,22 @@
1
+ module Awshucks
2
+
3
+ class HelpCommand < Command
4
+ self.command = 'help'
5
+ self.usage = 'help [commandname]'
6
+ self.description = "displays this help message or lists help for the given command"
7
+
8
+ def execute(args, config)
9
+ if args.empty?
10
+ puts config.help_message
11
+ else
12
+ if cmd = Command.commands[args.first]
13
+ puts cmd.option_string
14
+ else
15
+ $stderr.puts "Could not find command #{args.first}"
16
+ end
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,39 @@
1
+ module Awshucks
2
+
3
+ class ListCommand < Command
4
+ self.command = 'list'
5
+ self.usage = 'list [config name]'
6
+ self.description = "lists the files stored on S3 for all configs or the specified config"
7
+
8
+ def execute(args, config)
9
+ @config = config
10
+ list = []
11
+ if args.empty?
12
+ list += config.backups
13
+ else
14
+ list << config.backup(args.first)
15
+ end
16
+
17
+ list.each {|b| list(b) }
18
+
19
+ rescue UnknownBackupError
20
+ $stderr.puts "Unknown backup: #{args.first}"
21
+ end
22
+
23
+ #######
24
+ private
25
+ #######
26
+
27
+ attr_reader :config
28
+
29
+ def list(backup_info)
30
+ puts "stored files from #{backup_info.location}:"
31
+ file_store = FileStore.new(config.connection, config.bucket, backup_info.name)
32
+ file_store.each_file do |filename|
33
+ puts " #{filename}"
34
+ end
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,36 @@
1
+ module Awshucks
2
+
3
+ class NewConfigCommand < Command
4
+ self.command = 'new_config'
5
+ self.usage = 'new_config [filename]'
6
+ self.description = "generates a new default config file. filename defaults to awshucks.yml"
7
+
8
+ # def custom_options(opts)
9
+ # opts.on('-f', '--force', "Overwrite the existing config file if it exists") do
10
+ # parsed_options.overwrite = true
11
+ # end
12
+ # end
13
+
14
+ def execute(args, config)
15
+ # option_parser.parse!(args)
16
+
17
+ dest_path = File.expand_path(args.empty? ? 'awshucks.yml' : args.first)
18
+ if File.exists?(dest_path)# && !parsed_options.overwrite
19
+ $stderr.puts "cannot create #{dest_path}, file already exists"
20
+ else
21
+ puts "creating new config file at #{dest_path}"
22
+ FileUtils.cp(awshucks_config, dest_path )
23
+ end
24
+ end
25
+
26
+ #######
27
+ private
28
+ #######
29
+
30
+ def awshucks_config
31
+ File.join(RESOURCE_DIR, 'awshucks.yml')
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,38 @@
1
+ module Awshucks
2
+
3
+ class ResetMetadataCacheCommand < Command
4
+ self.command = 'reset_metadata_cache'
5
+ self.usage = 'reset_metadata_cache [config name]'
6
+ self.description = "Resets (deletes) the metadata cache for all backups or the specified backup. " +
7
+ "The next backup will rely on the stored metadata for each file to restore the metadata during the next run, " +
8
+ "Be aware that this process will be significantly slower than using the metadata cache to begin with."
9
+
10
+ def execute(args, config)
11
+ @config = config
12
+ list = []
13
+ if args.empty?
14
+ list += config.backups
15
+ else
16
+ list << config.backup(args.first)
17
+ end
18
+
19
+ list.each {|b| reset(b) }
20
+
21
+ rescue UnknownBackupError
22
+ $stderr.puts "Unknown backup: #{args.first}"
23
+ end
24
+
25
+ #######
26
+ private
27
+ #######
28
+
29
+ attr_reader :config
30
+
31
+ def reset(backup_info)
32
+ puts "processing #{backup_info.name}:"
33
+ FileStore.new(config.connection, config.bucket, backup_info.name).reset_cache
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,54 @@
1
+ module Awshucks
2
+
3
+ class RestoreCommand < Command
4
+ self.command = 'restore'
5
+ self.usage = 'restore <config name> [file_or_dir_pattern [target dir]]'
6
+ self.description = <<-EOD
7
+ Restores from the specified config name.
8
+ Optionally, provide a file or directory name or pattern to specify what files to restore. This can be a wildcard, or if it's a specific path, use the relative path from the configured backup location. If it's a wildcard, don't forget to wrap it in quotes!
9
+ By default, awshucks will restore files to the location specified in the configuration, but can be directed elsewhere -- provide a target directory to set the destination.
10
+ EOD
11
+
12
+ def execute(args, config)
13
+ @config = config
14
+
15
+ if args.empty?
16
+ $stderr.puts "Please specify a backup configuration to restore from"
17
+ else
18
+ backup = config.backup(args[0])
19
+ pattern = args[1] || '*'
20
+ target_dir = args[2] || backup.location
21
+ restore(backup, pattern, target_dir)
22
+ end
23
+
24
+ rescue UnknownBackupError
25
+ $stderr.puts "Unknown backup: #{args.first}"
26
+ end
27
+
28
+ #######
29
+ private
30
+ #######
31
+
32
+ attr_reader :config
33
+
34
+ def restore(backup_info, pattern, target_dir)
35
+ target_dir = File.expand_path(target_dir)
36
+
37
+ puts "restoring backup from #{backup_info.location} to #{target_dir}"
38
+
39
+ file_store = FileStore.new(config.connection, config.bucket, backup_info.name)
40
+
41
+ file_store.each_file do |filename|
42
+ if File.fnmatch(pattern, filename)
43
+ puts " restoring #{filename}"
44
+ file_store.restore(filename, File.join(target_dir, filename))
45
+ else
46
+ puts " #{filename} doesn't match '#{pattern}', skipping"
47
+ end
48
+ end
49
+
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -0,0 +1,83 @@
1
+ module Awshucks
2
+
3
+ class RequiredConfigError < Exception
4
+ def initialize(name)
5
+ super "Missing required configuration setting: #{name}"
6
+ end
7
+ end
8
+
9
+ class UnknownBackupError < Exception;
10
+ def initialize(name)
11
+ super "Unknown backup configuration name #{name}"
12
+ end
13
+ end
14
+
15
+ class Config
16
+
17
+ # class << self
18
+ #
19
+ # def load(filename)
20
+ # new(open(filename))
21
+ # end
22
+ #
23
+ # end
24
+
25
+ REQUIRED_CONNECTION_KEYS = %w(access_key_id secret_access_key).freeze
26
+ REQUIRED_BACKUP_CONFIG_KEYS = %w(location).freeze
27
+ RESERVED_KEYS = (REQUIRED_CONNECTION_KEYS + %w(bucket)).freeze
28
+
29
+ attr_accessor :help_message # piggyback for the global help --> help command
30
+
31
+ def initialize(filename)
32
+ @filename = filename
33
+ end
34
+
35
+ # return the connection info
36
+ def connection
37
+ returning({:use_ssl => true, :persistent => false}) do |connection|
38
+ REQUIRED_CONNECTION_KEYS.each do |key|
39
+ raise RequiredConfigError, key unless config[key]
40
+ connection[key.intern] = config[key]
41
+ end
42
+ end
43
+ end
44
+
45
+ def bucket
46
+ returning(config['bucket']) do |bucket|
47
+ raise RequiredConfigError, 'bucket' unless bucket
48
+ end
49
+ end
50
+
51
+ # return the config for the specified backup, with validation
52
+ def backup(name)
53
+ raise UnknownBackupError, name if name.in? REQUIRED_CONNECTION_KEYS
54
+ returning OpenStruct.new(config[name]) do |backup|
55
+ REQUIRED_BACKUP_CONFIG_KEYS.each do |required|
56
+ raise RequiredConfigError, required unless backup.send(required)
57
+ end
58
+ backup.name = name
59
+ end
60
+ end
61
+
62
+ def backups
63
+ returning [] do |list|
64
+ (config.keys - RESERVED_KEYS).each do |name|
65
+ list << backup(name)
66
+ end
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ attr_reader :filename
73
+
74
+ def config
75
+ @config ||= YAML.load(File.open(File.expand_path(filename)))
76
+ rescue Errno::ENOENT # file not found
77
+ $stderr.puts "config file not found: #{filename}"
78
+ exit(-1)
79
+ end
80
+
81
+ end
82
+
83
+ end
@@ -0,0 +1,23 @@
1
+ # miscellaneous core extensions
2
+
3
+ class Object
4
+ def returning(obj)
5
+ yield obj
6
+ obj
7
+ end
8
+ end
9
+
10
+ # thanks to http://devel.touset.org/blog/articles/2007/09/02/enumerable-include
11
+ class Object
12
+ def in?(enum)
13
+ enum.include? self
14
+ end
15
+ end
16
+
17
+ class File
18
+ class << self
19
+ def md5sum(filename)
20
+ Digest::MD5.hexdigest(read(filename))
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Awshucks
2
+
3
+ class FileInfo
4
+
5
+ attr_reader :filename, :full_path, :mtime, :size
6
+
7
+ def initialize(filename, full_path, stat)
8
+ @filename, @full_path= filename, full_path
9
+ @mtime = stat.mtime.gmtime
10
+ @size = stat.size
11
+ end
12
+
13
+ def md5sum
14
+ @md5sum ||= File.md5sum(full_path)
15
+ end
16
+
17
+ def data
18
+ File.open(full_path)
19
+ end
20
+
21
+ end
22
+
23
+ end
@@ -0,0 +1,111 @@
1
+ module Awshucks
2
+
3
+ class UnknownFileError < Exception; end
4
+
5
+ class FileStore
6
+
7
+ def initialize(connection_info, bucket_name, prefix)
8
+ @bucket_name, @prefix = bucket_name, prefix
9
+ AWS::S3::Base.establish_connection!(connection_info) unless AWS::S3::Base.connected?
10
+ end
11
+
12
+ def each_file
13
+ bucket.objects(:prefix => prefix).each { |f| yield remove_prefix(f.key) unless f.key == metadata_key }
14
+ end
15
+
16
+ def different_from?(file_info)
17
+ return true unless info = metadata_for(file_info.filename)
18
+ if info['mtime'] == file_info.mtime.gmtime.to_s
19
+ false
20
+ else
21
+ if info['md5sum'] == file_info.md5sum
22
+ info['mtime'] = file_info.mtime.gmtime.to_s # update the metadata cache
23
+ false
24
+ else
25
+ true
26
+ end
27
+ end
28
+ end
29
+
30
+ def store(file_info)
31
+ metadata[file_info.filename] = { 'mtime' => file_info.mtime.to_s, 'md5sum' => file_info.md5sum }
32
+ s3_metadata = { 'x-amz-meta-mtime' => file_info.mtime.to_s, 'x-amz-meta-md5sum' => file_info.md5sum }
33
+ AWS::S3::S3Object.store(prefixed_filename(file_info.filename), file_info.data, bucket.name, s3_metadata).success?
34
+ end
35
+
36
+ # stores the value of the file stored on S3 to the target file
37
+ def restore(filename, target)
38
+ raise UnknownFileError, "unknown file #{filename}" unless file = bucket[prefixed_filename(filename)]
39
+ FileUtils.mkdir_p(File.dirname(target))
40
+ File.open(target, 'w') do |out|
41
+ file.value do |data|
42
+ out.write(data)
43
+ end
44
+ end
45
+ # warn if the file's md5 doesn't match after writing it out
46
+ $stderr.puts "warning: md5sums don't match" if metadata_for(filename)['md5sum'] != File.md5sum(target)
47
+ end
48
+
49
+ def save_cache
50
+ AWS::S3::S3Object.store(metadata_key, metadata.to_yaml, bucket.name)
51
+ end
52
+
53
+ def reset_cache
54
+ puts "deleting the metadata cache..."
55
+ AWS::S3::S3Object.delete(metadata_key, bucket.name)
56
+ @metadata = {}
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :bucket_name, :prefix
62
+
63
+ def bucket
64
+ @bucket ||= AWS::S3::Bucket.find(bucket_name)
65
+ rescue AWS::S3::ResponseError => e
66
+ # this is awkward, but the NoSuchBucket exception itself doesn't get defined until it's raised.
67
+ raise e unless e.message =~ /bucket does not exist/
68
+ puts "could not find bucket #{bucket_name}, creating it"
69
+ retry if AWS::S3::Bucket.create(bucket_name)
70
+ end
71
+
72
+ def metadata
73
+ @metadata ||= load_metadata_from_cache
74
+ end
75
+
76
+ # returns or caches the metadata for a file from the s3 object if it's not already cached
77
+ def metadata_for(filename)
78
+ return metadata[filename] if metadata[filename]
79
+ return nil unless file = find(filename)
80
+ metadata[filename] = returning({}) do |info|
81
+ info['mtime'] = file.metadata['mtime'].to_s
82
+ info['md5sum'] = file.metadata['md5sum']
83
+ end
84
+ end
85
+
86
+ def find(filename)
87
+ search = prefixed_filename(filename)
88
+ bucket.objects(:prefix => prefix).detect do |file|
89
+ file.key == search
90
+ end
91
+ end
92
+
93
+ def prefixed_filename(filename)
94
+ File.join(prefix, filename)
95
+ end
96
+
97
+ def remove_prefix(filename)
98
+ filename.sub(/\A#{prefix}\//, '')
99
+ end
100
+
101
+ def load_metadata_from_cache
102
+ bucket[metadata_key] ? YAML.load(bucket[metadata_key].value) : {}
103
+ end
104
+
105
+ def metadata_key
106
+ prefix + '_metadata.yml'
107
+ end
108
+
109
+ end
110
+
111
+ end