maws 0.8.0

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.
Files changed (50) hide show
  1. data/bin/maws +10 -0
  2. data/lib/maws/chunk_source.rb +41 -0
  3. data/lib/maws/command.rb +62 -0
  4. data/lib/maws/command_loader.rb +28 -0
  5. data/lib/maws/command_options_parser.rb +37 -0
  6. data/lib/maws/commands/configure.rb +287 -0
  7. data/lib/maws/commands/console.rb +38 -0
  8. data/lib/maws/commands/create.rb +25 -0
  9. data/lib/maws/commands/describe.rb +15 -0
  10. data/lib/maws/commands/destroy.rb +11 -0
  11. data/lib/maws/commands/elb-add.rb +17 -0
  12. data/lib/maws/commands/elb-describe.rb +23 -0
  13. data/lib/maws/commands/elb-disable-zones.rb +17 -0
  14. data/lib/maws/commands/elb-enable-zones.rb +18 -0
  15. data/lib/maws/commands/elb-remove.rb +16 -0
  16. data/lib/maws/commands/set-prefix.rb +24 -0
  17. data/lib/maws/commands/set-security-groups.rb +442 -0
  18. data/lib/maws/commands/start.rb +11 -0
  19. data/lib/maws/commands/status.rb +25 -0
  20. data/lib/maws/commands/stop.rb +11 -0
  21. data/lib/maws/commands/teardown.rb +11 -0
  22. data/lib/maws/commands/volumes-cleanup.rb +22 -0
  23. data/lib/maws/commands/volumes-status.rb +43 -0
  24. data/lib/maws/commands/wait.rb +61 -0
  25. data/lib/maws/connection.rb +121 -0
  26. data/lib/maws/core_ext/object.rb +5 -0
  27. data/lib/maws/description/ebs.rb +40 -0
  28. data/lib/maws/description/ec2.rb +72 -0
  29. data/lib/maws/description/elb.rb +52 -0
  30. data/lib/maws/description/rds.rb +47 -0
  31. data/lib/maws/description.rb +78 -0
  32. data/lib/maws/instance/ebs.rb +45 -0
  33. data/lib/maws/instance/ec2.rb +144 -0
  34. data/lib/maws/instance/elb.rb +92 -0
  35. data/lib/maws/instance/rds.rb +84 -0
  36. data/lib/maws/instance.rb +167 -0
  37. data/lib/maws/instance_collection.rb +98 -0
  38. data/lib/maws/instance_display.rb +84 -0
  39. data/lib/maws/instance_matcher.rb +27 -0
  40. data/lib/maws/loader.rb +173 -0
  41. data/lib/maws/logger.rb +66 -0
  42. data/lib/maws/mash.rb +9 -0
  43. data/lib/maws/maws.rb +102 -0
  44. data/lib/maws/profile_loader.rb +92 -0
  45. data/lib/maws/specification.rb +127 -0
  46. data/lib/maws/ssh.rb +7 -0
  47. data/lib/maws/trollop.rb +782 -0
  48. data/lib/maws/volumes_command.rb +29 -0
  49. data/lib/maws.rb +25 -0
  50. metadata +115 -0
@@ -0,0 +1,84 @@
1
+ require 'maws/instance'
2
+
3
+ # for RDS instances name == aws_id
4
+ class Instance::RDS < Instance
5
+ def create
6
+ return if alive?
7
+
8
+ if config(:replica)
9
+ # READ REPLICA
10
+ info "creating RDS Read Replica #{name}..."
11
+ source_role_name = config(:source_role, true)
12
+
13
+ source_instance = instances.with_role(source_role_name).first
14
+
15
+ if source_instance.nil?
16
+ error "...can't create read replica - the source role '#{source_role_name}' doesn't exist"
17
+ return
18
+ end
19
+
20
+ unless source_instance.valid_read_replica_source?
21
+ error "...can't create read replica - source rds #{source_instance.name} is not valid (#{source_instance.status})!"
22
+ return
23
+ end
24
+
25
+ result = connection.rds.create_db_instance_read_replica(name, source_instance.name,
26
+ :instance_class => config(:instance_class, true),
27
+ :availability_zone => region_physical_zone)
28
+ else
29
+ info "creating RDS #{name}..."
30
+
31
+ # MASTER DB
32
+ create_opts = {}
33
+ create_opts[:engine] = config(:engine)
34
+ create_opts[:engine_version] = config(:engine_version)
35
+ create_opts[:instance_class] = config(:instance_class)
36
+ create_opts[:auto_minor_version_upgrade] = config(:auto_minor_version_upgrade)
37
+ create_opts[:allocated_storage] = config(:allocated_storage)
38
+ create_opts[:db_name] = config(:db_name)
39
+ create_opts[:db_parameter_group] = config(:db_parameter_group)
40
+ create_opts[:db_security_groups] = security_groups
41
+ create_opts[:backup_retention_period] = config(:backup_retention_period)
42
+ create_opts[:preferred_backup_window] = config(:preferred_backup_window)
43
+ create_opts[:preferred_maintenance_window] = config(:preferred_maintenance_window)
44
+
45
+ if config(:scope).eql?("region")
46
+ create_opts[:multi_az] = true
47
+ else
48
+ create_opts[:availability_zone] = @command_options.availability_zone
49
+ end
50
+
51
+ master_username = config(:master_username, true)
52
+ master_password = config(:master_password, true)
53
+
54
+ result = connection.rds.create_db_instance(name, master_username, master_password, create_opts)
55
+ end
56
+
57
+ sync_from_description(result)
58
+ info "...done (RDS #{name} is being created)\n\n"
59
+ end
60
+
61
+ def destroy
62
+ return unless alive?
63
+ stoppable_states = %w(available failed storage-full incompatible-parameters incompatible-restore)
64
+ unless stoppable_states.include? status
65
+ info "can't destroy RDS #{aws_id} while it is #{status}"
66
+ return
67
+ end
68
+ connection.rds.delete_db_instance(aws_id, :skip_final_snapshot => true)
69
+ info "destroying RDS #{aws_id}"
70
+ end
71
+
72
+ def valid_read_replica_source?
73
+ alive? && !config(:replica)
74
+ end
75
+
76
+ def service
77
+ :rds
78
+ end
79
+
80
+ def display_fields
81
+ super + [:endpoint_address, :endpoint_port]
82
+ end
83
+
84
+ end
@@ -0,0 +1,167 @@
1
+ require 'maws/mash'
2
+ require 'maws/instance_matcher'
3
+ require 'maws/instance_display'
4
+
5
+
6
+ class Instance
7
+ attr_accessor :name
8
+ attr_accessor :region, :zone, :role, :index, :groups, :prefix
9
+ attr_reader :description
10
+
11
+ NA_STATUS = 'n/a'
12
+
13
+ def self.create(maws, config, prefix, zone, role, index, options = {})
14
+ options = mash(options)
15
+
16
+ service = options.service || config.combined[role].service
17
+ region = options.region || config.region
18
+ name = options.name || name_for(config, prefix, zone, role, index)
19
+
20
+ klass = Instance.const_get("#{service.to_s.upcase}")
21
+
22
+ klass.new(maws, config, name, region, prefix, zone, role, index)
23
+ end
24
+
25
+ def self.name_for(config, prefix, zone, role, index)
26
+ add_prefix = prefix.empty? ? "" : prefix + "."
27
+ "#{add_prefix}#{config.profile.name}-#{role}-#{index}#{zone}"
28
+ end
29
+
30
+ def initialize(maws, config, name, region, prefix, zone, role, index)
31
+ @maws = maws
32
+ @config = config
33
+ @name = name
34
+ @region = region
35
+ @zone = zone
36
+ @role = role
37
+ @index = index
38
+
39
+ @prefix = prefix
40
+
41
+ @description = mash
42
+ @groups = %w(all)
43
+ end
44
+
45
+ def connection
46
+ @maws.connection
47
+ end
48
+
49
+ def instances
50
+ @maws.instances
51
+ end
52
+
53
+ def description=(description)
54
+ # never nil
55
+ @description = description || mash
56
+ end
57
+
58
+ def logical_zone
59
+ @zone
60
+ end
61
+
62
+ def physical_zone
63
+ @zone || @description.physical_zone || @config.specified_zones.first
64
+ end
65
+
66
+ def aws_id
67
+ description.aws_id
68
+ end
69
+
70
+ def status
71
+ description.status || 'n/a'
72
+ end
73
+
74
+ def region_zone
75
+ region + zone
76
+ end
77
+
78
+ def region_physical_zone
79
+ region + physical_zone
80
+ end
81
+
82
+
83
+ def terminated?
84
+ status == 'terminated'
85
+ end
86
+
87
+ def alive?
88
+ aws_id && !terminated?
89
+ end
90
+
91
+ def to_s
92
+ "#{name} #{status} #{aws_id}"
93
+ end
94
+
95
+ def inspect
96
+ "<#{self.class} #{to_s}>"
97
+ end
98
+
99
+ def has_approximate_status?(approximate_status)
100
+ if approximate_status == "n/a" or approximate_status == "terminated"
101
+ !alive?
102
+ elsif approximate_status == "ssh"
103
+ self.respond_to?(:ssh_available?) ? self.ssh_available? : has_approximate_status?("available")
104
+ elsif approximate_status == "running" || approximate_status == "available"
105
+ status == "running" || status == "available"
106
+ else
107
+ approximate_status == status
108
+ end
109
+ end
110
+
111
+ def method_missing(method_name, *args, &block)
112
+ config(method_name) ||
113
+ description[method_name] ||
114
+ @config.command_line[method_name]
115
+ end
116
+
117
+ def config(key, required=false)
118
+ if required && @config.combined[role][key].nil?
119
+ raise ArgumentError.new("Missing required config: #{key}")
120
+ end
121
+
122
+ @config.combined[role][key]
123
+ end
124
+
125
+ def security_groups
126
+ groups = config(:security_groups).to_a.dup
127
+ groups << "#{service}_default"
128
+
129
+ if @config.profile.security_rules and @config.profile.security_rules[role]
130
+ groups << "#{@profile.name}-#{role_name}"
131
+ end
132
+
133
+ groups
134
+ end
135
+
136
+ def service
137
+ raise ArgumentError, "No service for generic instance"
138
+ end
139
+
140
+ def display_fields
141
+ [:zone, :name, :status]
142
+ end
143
+
144
+ def display
145
+ InstanceDisplay.new(self, display_fields)
146
+ end
147
+
148
+ def matches?(filters={})
149
+ approximate_status = filters.delete(:approximate_status)
150
+ matched = InstanceMatcher.matches?(self, filters)
151
+
152
+ # approximate status is not a single state
153
+ # it might require an ssh connection
154
+ matched &&= has_approximate_status?(approximate_status) if approximate_status
155
+ matched
156
+ end
157
+
158
+ protected
159
+
160
+ end
161
+
162
+ # load all instance files
163
+ Dir.glob(File.dirname(__FILE__) + '/instance/*.rb') {|file| require file}
164
+
165
+
166
+
167
+
@@ -0,0 +1,98 @@
1
+ class InstanceCollection
2
+ include Enumerable
3
+
4
+ attr_accessor :members
5
+
6
+ def initialize(members = [])
7
+ @members = members.sort_by {|m| [m.region, m.zone.to_s, m.role.to_s, m.index || 1]}
8
+ end
9
+
10
+ def add(instance)
11
+ @members << instance
12
+ end
13
+
14
+ def each
15
+ @members.each {|m| yield m}
16
+ end
17
+
18
+ def empty?
19
+ @members.empty?
20
+ end
21
+
22
+ def first
23
+ @members.first
24
+ end
25
+
26
+ def *(x)
27
+ @members * x
28
+ end
29
+
30
+ def matching(filters = {})
31
+ InstanceCollection.new(self.find_all {|i| i.matches?(filters)})
32
+ end
33
+
34
+ def not_matching(filters)
35
+ InstanceCollection.new(self.find_all {|i| !i.matches?(filters)})
36
+ end
37
+
38
+ def services
39
+ self.map {|i| i.service}.uniq
40
+ end
41
+
42
+ def roles
43
+ map{|i| i.role}.uniq
44
+ end
45
+
46
+ def zones
47
+ map{|i| i.zone}.uniq
48
+ end
49
+
50
+ def roles_in_order_of(roles_list)
51
+ roles.sort_by {|r| roles_list.index(r)}
52
+ end
53
+
54
+ # scopes
55
+ def specified
56
+ self.matching(:groups => 'specified')
57
+ end
58
+
59
+ def not_specified
60
+ self.not_matching(:groups => 'specified')
61
+ end
62
+
63
+ def aws
64
+ self.matching(:groups => 'aws')
65
+ end
66
+
67
+ def alive
68
+ self.matching(:alive? => true)
69
+ end
70
+
71
+ def not_alive
72
+ self.matching(:alive? => false)
73
+ end
74
+
75
+ def with_service(service)
76
+ self.matching(:service => service.to_sym)
77
+ end
78
+
79
+ def with_role(role)
80
+ self.matching(:role => role)
81
+ end
82
+
83
+ def with_zone(zone)
84
+ self.matching(:zone => zone)
85
+ end
86
+
87
+ def without_role(role)
88
+ self.not_matching(:role => role)
89
+ end
90
+
91
+ def ebs
92
+ with_service(:ebs)
93
+ end
94
+
95
+ def with_approximate_status(status)
96
+ self.matching(:approximate_status => status)
97
+ end
98
+ end
@@ -0,0 +1,84 @@
1
+ class InstanceDisplay
2
+ def self.display_collection_for_role(role, instances)
3
+ headers = instances.first.display.headers
4
+ service_title = instances.first.service.to_s.downcase
5
+ info "\n\n**** " + role.upcase + " * #{service_title} ****************"
6
+
7
+ # separate by zone
8
+ previous_zone = instances.zones.first
9
+ rows = []
10
+ instances.map {|instance|
11
+ if previous_zone == instance.zone
12
+ rows << instance.display.values
13
+ else
14
+ previous_zone = instance.zone
15
+ rows << instance.display.blank_values
16
+ rows << instance.display.values
17
+ end
18
+ }
19
+
20
+ info table(headers, *rows)
21
+ end
22
+
23
+ def initialize(instance, fields)
24
+ @instance = instance
25
+ @fields = fields
26
+ end
27
+
28
+ def headers
29
+ @fields.map {|f| f.to_s.upcase.gsub('_', ' ')}
30
+ end
31
+
32
+ def values
33
+ @fields.collect do |field|
34
+ value = value(field, @instance.send(field))
35
+ end
36
+ end
37
+
38
+ def blank_values
39
+ @fields.map { "" }
40
+ end
41
+
42
+ def value(field, value)
43
+ if field.to_sym == :status
44
+ status(value)
45
+ else
46
+ value.to_s
47
+ end
48
+ end
49
+
50
+ def status(status)
51
+ case status
52
+ when 'unknown' : '?'
53
+ when 'non-existant' : 'n/a'
54
+ when 'terminated' : 'n/a (terminated)'
55
+ else status.to_s
56
+ end
57
+ end
58
+
59
+ def pretty_details
60
+ title = @instance.name.to_s.upcase
61
+ data = @instance.description.description
62
+
63
+ InstanceDisplay.pretty_describe(title, data)
64
+ end
65
+
66
+ def self.pretty_describe(title, data)
67
+ pretty_describe_heading(title)
68
+ if data.is_a? String
69
+ info data
70
+ else
71
+ ap data
72
+ end
73
+ pretty_describe_footer
74
+ end
75
+
76
+ def self.pretty_describe_heading(title)
77
+ title = title[0,62]
78
+ info "++++++++++ " + title + " " + ("+" * (75 - title.length))
79
+ end
80
+
81
+ def self.pretty_describe_footer
82
+ info "+-------------------------------------------------------------------------------------+\n\n\n"
83
+ end
84
+ end
@@ -0,0 +1,27 @@
1
+ class InstanceMatcher
2
+ def self.matches?(instance, filters)
3
+ filters.each {|filter, expected_value|
4
+ value = instance.send(filter)
5
+ if value.is_a? Array
6
+ return false if value.find_all {|v| value_matches?(v, expected_value)}.empty?
7
+ else
8
+ return false unless value_matches?(value, expected_value)
9
+ end
10
+ }
11
+ true
12
+ end
13
+
14
+ private
15
+ def self.value_matches?(value, expected_value)
16
+ if expected_value.is_a? Array
17
+ return false unless expected_value.include?(value)
18
+ else
19
+ if !expected_value # treat false/nil as the same
20
+ return !value
21
+ else
22
+ return false if expected_value != value
23
+ end
24
+ end
25
+ true
26
+ end
27
+ end
@@ -0,0 +1,173 @@
1
+ require 'yaml'
2
+
3
+ require 'maws/maws'
4
+
5
+ require 'maws/profile_loader'
6
+ require 'maws/command_loader'
7
+
8
+ require 'maws/command_options_parser'
9
+
10
+ class Loader
11
+
12
+ def initialize(base_path, config_config_path)
13
+ @base_path = base_path
14
+ @cc_path = config_config_path
15
+ @commands_path = File.expand_path("commands/", File.dirname(__FILE__))
16
+
17
+ # stores all config
18
+ @config = mash
19
+ @command = nil
20
+
21
+ Loader.config_file_must_exist!('main', @cc_path)
22
+
23
+ @command_options_parser = CommandOptionsParser.new(@config)
24
+ end
25
+
26
+ def load_and_run
27
+ load_config
28
+ load_command
29
+ initialize_command
30
+
31
+ parse_command_line_options
32
+ verify_command
33
+
34
+ @maws = Maws.new(@config, @command)
35
+ @command.maws = @maws
36
+
37
+ @maws.run!
38
+ end
39
+
40
+ private
41
+
42
+ def load_config
43
+ @config.config = mash(YAML.load_file(@cc_path))
44
+
45
+ @config.config.paths.commands = @commands_path
46
+ @config.config.paths.base = @base_path
47
+ @config.config.paths.config = @cc_path
48
+ @config.config.paths.template_output = 'tmp/'
49
+
50
+ expand_config_paths
51
+
52
+ load_aws_key
53
+ glob_available_profiles_and_commands
54
+ read_core_command_line_options
55
+ exit_with_basic_help_usage if needs_basic_help_usage?
56
+ exit_on_missing_profile
57
+ exit_on_missing_command
58
+
59
+ ProfileLoader.new(@config).load
60
+ end
61
+
62
+ def load_command
63
+ CommandLoader.new(@config).load
64
+ end
65
+
66
+ def initialize_command
67
+ @command = @config.command_class.new(@config)
68
+ end
69
+
70
+ def parse_command_line_options
71
+ @command_options_parser.process_command_options(@command)
72
+ @command.process_options
73
+ end
74
+
75
+ def verify_command
76
+ @command.verify_options
77
+ @command.verify_configs
78
+ end
79
+
80
+ def load_aws_key
81
+ Loader.config_file_must_exist!('aws_key', @config.config.paths.aws_key)
82
+
83
+ @config.aws_key = mash
84
+
85
+ key_id, secret_key = *File.read(@config.config.paths.aws_key).lines.map {|l| l.chomp}
86
+ @config.aws_key.key_id = key_id
87
+ @config.aws_key.secret_key = secret_key
88
+ end
89
+
90
+ def glob_available_profiles_and_commands
91
+ available_profiles = mash
92
+ available_commands = mash
93
+
94
+ Dir.glob(@config.config.paths.profiles + '/*.yml').each {|path|
95
+ name = File.basename(path,'.yml')
96
+ available_profiles[name] = path
97
+ }
98
+
99
+ Dir.glob(@config.config.paths.commands + '/*.rb').each {|path|
100
+ name = File.basename(path,'.rb')
101
+ available_commands[name] = path
102
+ }
103
+
104
+ @config.config.available_profiles = available_profiles
105
+ @config.config.available_commands = available_commands
106
+ end
107
+
108
+ def read_core_command_line_options
109
+ @config.command_line = mash
110
+ @config.command_line.profile_name = ARGV.shift
111
+ @config.command_line.command_name = ARGV.shift
112
+ end
113
+
114
+ def needs_basic_help_usage?
115
+ profile_name = @config.command_line.profile_name
116
+ command_name = @config.command_line.command_name
117
+
118
+ profile_name.blank? ||
119
+ command_name.blank? ||
120
+ profile_name == '-h' ||
121
+ profile_name == '--help' ||
122
+ command_name == '-h' ||
123
+ command_name == '--help'
124
+ end
125
+
126
+ def exit_with_basic_help_usage
127
+ profile_name = @config.command_line.profile_name
128
+ command_name = @config.command_line.command_name
129
+
130
+ profile_name = nil if profile_name == '-h' || profile_name == '--help'
131
+ command_name = nil if command_name == '-h' || command_name == '--help'
132
+
133
+ exit_with_basic_usage(profile_name, command_name)
134
+ end
135
+
136
+ def exit_on_missing_profile
137
+ profile_name = @config.command_line.profile_name
138
+
139
+ if @config.config.available_profiles[profile_name].blank?
140
+ puts "ERROR: no such profile: #{profile_name}"
141
+ exit_with_basic_usage(profile_name, @config.command_line.command_name)
142
+ end
143
+ end
144
+
145
+ def exit_on_missing_command
146
+ command_name = @config.command_line.command_name
147
+
148
+ if @config.config.available_commands[command_name].blank?
149
+ puts "ERROR: no such command: #{command_name}"
150
+ exit_with_basic_usage(@config.command_line.profile_name, command_name)
151
+ end
152
+ end
153
+
154
+ def exit_with_basic_usage(profile_name, command_name)
155
+ puts @command_options_parser.usage(profile_name, command_name)
156
+ exit(1)
157
+ end
158
+
159
+ def expand_config_paths
160
+ base_path = @config.config.paths.base
161
+ @config.config.paths.each { |path_name, path_location|
162
+ @config.config.paths[path_name] = File.expand_path(path_location, base_path)
163
+ }
164
+ end
165
+
166
+ def self.config_file_must_exist!(name, path)
167
+ unless File.exists? path
168
+ error "No #{name} config: #{path} found. Quiting!"
169
+ exit(1)
170
+ end
171
+ end
172
+
173
+ end
@@ -0,0 +1,66 @@
1
+ class NullLogger
2
+ def error str
3
+ end
4
+
5
+ def warn str
6
+ end
7
+
8
+ def info str
9
+ end
10
+
11
+ def debug str
12
+ end
13
+ end
14
+
15
+
16
+ class RightAWSLogger
17
+ def error str
18
+ $stderr.puts "[ERROR]: #{str}"
19
+ end
20
+
21
+ def warn str
22
+ puts "[warning]: #{str}"
23
+ end
24
+
25
+ def info str
26
+ puts "---- " + str.to_s
27
+ end
28
+
29
+ def debug str
30
+ puts str
31
+ end
32
+ end
33
+
34
+ class MawsLogger
35
+ def error str
36
+ $stderr.puts "[ERROR]: #{str}"
37
+ end
38
+
39
+ def warn str
40
+ puts "[warning]: #{str}"
41
+ end
42
+
43
+ def info str
44
+ puts str
45
+ end
46
+
47
+ def debug str
48
+ puts str
49
+ end
50
+ end
51
+
52
+ $logger ||= MawsLogger.new
53
+ $right_aws_logger ||= RightAWSLogger.new
54
+
55
+
56
+ def info str
57
+ $logger.info str
58
+ end
59
+
60
+ def error str
61
+ $logger.error str
62
+ end
63
+
64
+ def warn str
65
+ $logger.warn str
66
+ end
data/lib/maws/mash.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'hashie'
2
+
3
+ class Hashie::Mash
4
+ undef :count # usually count is the number of keys mash has
5
+ end
6
+
7
+ def mash(x = {})
8
+ Hashie::Mash.new x
9
+ end