deadpool 0.1.1

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.
@@ -0,0 +1,106 @@
1
+ require 'net/ssh'
2
+
3
+ module Deadpool
4
+
5
+ module FailoverProtocol
6
+
7
+ class ExecRemoteCommand < Base
8
+
9
+ def setup
10
+ @test_command = @failover_config[:test_command]
11
+ @exec_command = @failover_config[:exec_command]
12
+ @client_hosts = @failover_config[:client_hosts]
13
+ @username = @failover_config[:username]
14
+ @password = @failover_config[:password]
15
+ @use_sudo = @failover_config[:use_sudo]
16
+ @sudo_path = @failover_config[:sudo_path].nil? ? 'sudo' : @failover_config[:sudo_path]
17
+ end
18
+
19
+ # Return true or false
20
+ # Don't update system state.
21
+ # return true or false success or failure
22
+ def preflight_check
23
+ @client_hosts.all? { |client_host| test_client(client_host) }
24
+ end
25
+
26
+ def test_client(client_host)
27
+ logger.debug "Testing Client #{client_host}"
28
+ return exec_remote_command(@test_command, client_host)
29
+ end
30
+
31
+ # Promote the host to primary. This is used by initiate_failover_protocol!
32
+ # and for manual promotion by an administrator.
33
+ # new_primary is an IP address
34
+ # TODO: change new_primary to be a config label.
35
+ # return true or false success or failure
36
+ def promote_to_primary(new_primary)
37
+ success = true
38
+
39
+ @client_hosts.each do |client_host|
40
+ if exec_remote_command(@exec_command, client_host)
41
+ exec_remote_command(@exec_command, client_host)
42
+ logger.info "Promotion exec command succeeded on #{client_host}"
43
+ else
44
+ logger.error "Promotion exec command failed on #{client_host}"
45
+ end
46
+ end
47
+
48
+ return success
49
+ end
50
+
51
+ # Perform checks against anything that could cause a failover protocol to fail
52
+ # Perform checks on system state.
53
+ # return New Deadpool::StateSnapshot
54
+ def system_check
55
+ failed = []
56
+ succeeded = []
57
+
58
+ # Collect check data
59
+ @client_hosts.each do |client_host|
60
+ if test_client(client_host)
61
+ succeeded << client_host
62
+ else
63
+ failed << client_host
64
+ end
65
+ end
66
+
67
+ # Compile write check data.
68
+ if !succeeded.empty? && failed.empty?
69
+ @state.set_state OK, "Exec test passed all servers: #{succeeded.join(', ')}"
70
+ elsif !succeeded.empty? && !failed.empty?
71
+ @state.set_state WARNING, "Exec test passed on: #{succeeded.join(', ')}"
72
+ @state.add_error_message "Exec test failed on #{failed.join(', ')}"
73
+ elsif succeeded.empty?
74
+ @state.set_state WARNING, "Exec test failed all servers: #{failed.join(', ')}"
75
+ end
76
+
77
+ return Deadpool::StateSnapshot.new @state
78
+ end
79
+
80
+ protected
81
+
82
+ def exec_remote_command(command, host)
83
+ options = @password.nil? ? {} : {:password => @password}
84
+ command = "#{command} ; echo 'ExecRemoteCommand.success: '$?"
85
+ command = "#{@sudo_path} #{command}" if @use_sudo
86
+
87
+ logger.debug "executing #{command} on #{host}"
88
+
89
+ begin
90
+ output = 0
91
+ Net::SSH.start(host, @username, options) do |ssh|
92
+ output = ssh.exec!(command)
93
+ end
94
+
95
+ return (output =~ /ExecRemoteCommand.success: 0/)
96
+ rescue
97
+ logger.error "Couldn't execute #{command} on #{host} with Username: #{@username} and options: #{options.inspect}"
98
+ return false
99
+ end
100
+ end
101
+
102
+ end
103
+
104
+ end
105
+
106
+ end
@@ -0,0 +1,220 @@
1
+ require 'optparse'
2
+ require 'ostruct'
3
+ require 'strscan'
4
+ require 'fileutils'
5
+
6
+ module Deadpool
7
+ class Generator
8
+
9
+ # include FileUtils
10
+
11
+ def initialize(argv)
12
+ @argv = argv
13
+ end
14
+
15
+ def run
16
+ @options = self.parse_command_line
17
+ @config = Deadpool::Helper.configure @options
18
+
19
+ self.execute_command(@options)
20
+ end
21
+
22
+ def parse_command_line
23
+ options = Hash.new
24
+ options[:command_count] = 0
25
+ options[:config_path] = '/etc/deadpool'
26
+ options[:upstart_config_path] = '/etc/init/deadpool.conf'
27
+ options[:upstart_init_path] = '/etc/init.d/deadpool'
28
+ options[:upstart_script_path] = '/lib/init/upstart-job'
29
+
30
+ @option_parser = OptionParser.new do |opts|
31
+ opts.banner = "Usage: deadpool_generator command [options]"
32
+
33
+ opts.separator "Commands:"
34
+ opts.on("-h", "--help", "Print this help message.") do |help|
35
+ options[:command_count] += 1
36
+ options[:command] = :help
37
+ end
38
+ opts.on("--upstart_init", "Generate and upstart config.") do |upstart|
39
+ options[:command_count] += 1
40
+ options[:command] = :upstart_init
41
+ end
42
+ opts.on("--configuration", "Generate a config directory structure and example files.") do |configuration|
43
+ options[:command_count] += 1
44
+ options[:command] = :configuration
45
+ end
46
+
47
+ opts.separator "Configuration Options:"
48
+ opts.on("--config_path=PATH", String, "path to create the config dir at (#{options[:config_path]})") do |config_path|
49
+ options[:config_path] = config_path
50
+ end
51
+
52
+ opts.separator "Upstart Options:"
53
+ opts.on("--upstart_config_path=PATH", String, "path to create the config dir at (#{options[:upstart_config_path]})") do |upstart_config_path|
54
+ options[:upstart_config_path] = upstart_config_path
55
+ end
56
+ opts.on("--upstart_init_path=PATH", String, "path to create the config dir at (#{options[:upstart_init_path]})") do |upstart_init_path|
57
+ options[:upstart_init_path] = upstart_init_path
58
+ end
59
+ opts.on("--upstart_script_path=PATH", String, "path to create the config dir at (#{options[:upstart_script_path]})") do |upstart_script_path|
60
+ options[:upstart_script_path] = upstart_script_path
61
+ unless File.exists? upstart_script_path
62
+ help "The upstart script is not at #{upstart_script_path}."
63
+ end
64
+ end
65
+ end
66
+
67
+ remaining_arguments = @option_parser.parse! @argv
68
+
69
+ unless remaining_arguments.empty?
70
+ help "[#{remaining_arguments.join(' ')}] is not understood."
71
+ end
72
+
73
+ if options[:command_count] == 0
74
+ help "You must specify a command."
75
+ end
76
+
77
+ return options
78
+ end
79
+
80
+ def execute_command(options)
81
+ case options[:command]
82
+ when :upstart_init
83
+ generate_upstart_init options
84
+ when :configuration
85
+ generate_configuration options
86
+ else
87
+ help
88
+ end
89
+ end
90
+
91
+ def help(message=nil)
92
+ unless message.nil?
93
+ puts message
94
+ end
95
+ puts @option_parser.help
96
+ exit 4
97
+ end
98
+
99
+ def generate_upstart_init(options)
100
+ config_path = options[:config_path]
101
+ upstart_config_path = options[:upstart_config_path]
102
+ upstart_init_path = options[:upstart_init_path]
103
+ upstart_script_path = options[:upstart_script_path]
104
+ config_params = config_path.nil? ? '' : "--config_path=#{config_path}"
105
+ ruby_path = `which ruby`.strip
106
+ deadpool_admin_path = `which deadpool_admin`.strip
107
+
108
+ upstart_conf =<<-EOF
109
+ description "Deadpool Service"
110
+ author "Kirt Fitzpatrick <kirt.fitzpatrick@akqa.com>"
111
+
112
+ umask 007
113
+ start on (net-device-up and local-filesystems)
114
+ stop on runlevel [016]
115
+ respawn
116
+ exec #{ruby_path} #{deadpool_admin_path} --foreground #{config_params}
117
+
118
+ EOF
119
+
120
+ if upstart_config_path
121
+ File.open File.join(upstart_config_path), 'w' do |file|
122
+ file.write upstart_conf
123
+ end
124
+
125
+ if upstart_init_path
126
+ if File.exists? upstart_init_path
127
+ puts "#{upstart_config_path} has been (over)written."
128
+ puts "#{upstart_init_path} already exists. It should be a symbolic link that points to #{upstart_script_path}"
129
+ ls_command = "ls -l #{upstart_init_path}"
130
+ # puts ls_command
131
+ # puts `#{ls_command}`
132
+ # puts "ln -s #{upstart_script_path} #{upstart_init_path}"
133
+ else
134
+ `ln -s #{upstart_script_path} #{upstart_init_path}`
135
+ end
136
+ end
137
+ else
138
+ puts upstart_conf
139
+ end
140
+ end
141
+
142
+ # mkdir path/config/pools
143
+ # path/config/environment.yml
144
+ # path/config/pools/example.yml
145
+ # mkdir path/lib/deadpool/monitor
146
+ # path/lib/deadpool/monitor
147
+ # mkdir path/lib/deadpool/failover_protocol
148
+ # path/lib/deadpool/failover_protocol
149
+ def generate_configuration(options)
150
+ path = config_path
151
+ FileUtils.mkdir_p(File.join(path, 'config/pools'))
152
+ FileUtils.mkdir_p(File.join(path, 'lib/deadpool/monitor'))
153
+ FileUtils.mkdir_p(File.join(path, 'lib/deadpool/failover_protocol'))
154
+ File.open File.join(path, 'config/pools/example.yml'), 'w' do |file|
155
+ file.write <<-EOF
156
+ pool_name: 'example_database'
157
+ primary_host: '10.1.2.3'
158
+ secondary_host: '10.2.3.4'
159
+ check_interval: 1
160
+ max_failed_checks: 10
161
+
162
+ # There can be only one monitor per pool at this time. The deadpool system
163
+ # defines no rules for the monitor configuration except that it is called
164
+ # monitor_config: and has monitor_class: defined at the base level.
165
+ # All other configuration variables are plugin specific.
166
+ monitor_config:
167
+ monitor_class: Mysql
168
+ nagios_plugin_path: '/usr/lib/nagios/plugins'
169
+
170
+ # There can be as many Failover Protocols as you want and you can use
171
+ # the same plugin multiple times. The deadpool defines no riles for the
172
+ # failover protocol config except that it be an array element of
173
+ # failover_protocol_configs and defines protocol_class at it's base. The rest
174
+ # of the configuration is specific to the failover protocol.
175
+ failover_protocol_configs:
176
+ - protocol_class: EtcHosts
177
+ script_path: '/usr/local/bin/deadpool_line_modifier'
178
+ service_host_name: 'master.mysql.example.project.client'
179
+ username: 'deadpool'
180
+ password: 'p4ssw0rd'
181
+ use_sudo: 1
182
+ client_hosts:
183
+ - '10.3.4.5' # app server 1 (web server)
184
+ - '10.4.5.6' # app server 2 (web server)
185
+
186
+ - protocol_class: ExecRemoteCommand
187
+ test_command: '/etc/init.d/nginx status'
188
+ exec_command: '/etc/init.d/nginx restart'
189
+ username: 'deadpool'
190
+ password: 'p4ssw0rd'
191
+ use_sudo: 1
192
+ client_hosts:
193
+ - '10.3.4.5' # app server 1 (web server)
194
+ - '10.4.5.6' # app server 2 (web server)
195
+ EOF
196
+ end
197
+
198
+ environment_config_path = File.join(path, 'config/environment.yml')
199
+ environment_conf = <<-EOF
200
+ # log_path: '/var/log/deadpool.log'
201
+ # log_level: INFO
202
+ # system_check_interval: 30
203
+ # admin_hostname: 'localhost'
204
+ # admin_port: 5507
205
+
206
+ EOF
207
+ if File.exists? enironment_config_path
208
+ puts "#{environment_config_path} already exists. Here's what we would have copied there."
209
+ puts environment_conf
210
+ else
211
+ File.open environment_config_path, 'w' do |file|
212
+ file.write environment_conf
213
+ end
214
+ end
215
+
216
+ end
217
+
218
+ end
219
+
220
+ end
@@ -0,0 +1,100 @@
1
+
2
+
3
+ module Deadpool
4
+
5
+ class Handler
6
+
7
+ attr_accessor :logger
8
+ attr_reader :state,
9
+ :failure_count,
10
+ :check_interval,
11
+ :max_failed_checks,
12
+ :pool_name,
13
+ :primary_host,
14
+ :secondary_host
15
+
16
+ def initialize(config, logger)
17
+ @state = Deadpool::State.new config[:pool_name], self.class
18
+ # @state = Deadpool::State.new "Deadpool::Handler - #{config[:pool_name]}"
19
+ @config = config
20
+ @logger = logger
21
+ @pool_name = config[:pool_name]
22
+ @check_interval = config[:check_interval]
23
+ @max_failed_checks = config[:max_failed_checks]
24
+ @primary_host = config[:primary_host]
25
+ @secondary_host = config[:secondary_host]
26
+ @failure_count = 0
27
+ instantiate_monitor
28
+ instantiate_failover_protocols
29
+ @state.set_state(OK, "Handler initialized.")
30
+ end
31
+
32
+ def monitor_pool(timer)
33
+ if @monitor.primary_ok?
34
+ @failure_count = 0
35
+ @state.set_state(OK, "Primary Check OK.")
36
+ logger.info "#{@pool_name} Primary Check Okay. Failure Count set to 0."
37
+ else
38
+ @failure_count += 1
39
+ @state.set_state(WARNING, "Primary Check failed #{@failure_count} times")
40
+ logger.warn "#{@pool_name} Primary Check Failed. Failure Count at #{@failure_count}"
41
+ end
42
+
43
+ if @failure_count >= @max_failed_checks
44
+ timer.cancel
45
+ @state.set_state(WARNING, "Failure threshold exceeded. Failover Protocol Initiated.")
46
+ logger.error "#{@pool_name} primary is dead. Initiating Failover Protocol."
47
+
48
+ success = true
49
+ @failover_protocols.each do |failover_protocol|
50
+ success = success && failover_protocol.initiate_failover_protocol!
51
+ end
52
+
53
+ if success
54
+ logger.warn "Failover Protocol Finished."
55
+ @state.set_state(WARNING, "Failover Protocol in place.")
56
+ @state.lock
57
+ else
58
+ logger.error "Failover Protocol Failed!"
59
+ @state.set_state(CRITICAL, "Failover Protocol Failed!")
60
+ @state.lock
61
+ end
62
+ end
63
+ end
64
+
65
+ def system_check
66
+ snapshot = Deadpool::StateSnapshot.new @state
67
+ snapshot.add_child @monitor.system_check
68
+ @failover_protocols.each do |failover_protocol|
69
+ # logger.debug failover_protocol.inspect
70
+ snapshot.add_child failover_protocol.system_check
71
+ end
72
+
73
+ return snapshot
74
+ end
75
+
76
+ def promote_server(server)
77
+ # This will stop at the first failure
78
+ @config[server] && @failover_protocols.all? do |failover_protocol|
79
+ failover_protocol.promote_to_primary @config[server]
80
+ end
81
+ end
82
+
83
+ protected
84
+
85
+ def instantiate_monitor
86
+ monitor_class = Deadpool::Monitor.const_get(@config[:monitor_config][:monitor_class])
87
+ @monitor = monitor_class.new(@config, @config[:monitor_config], logger)
88
+ end
89
+
90
+ def instantiate_failover_protocols
91
+ @failover_protocols = []
92
+ @config[:failover_protocol_configs].each do |failover_config|
93
+ failover_protocol_class = Deadpool::FailoverProtocol.const_get(failover_config[:protocol_class])
94
+ @failover_protocols << failover_protocol_class.new(@config, failover_config, logger)
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ end
@@ -0,0 +1,39 @@
1
+
2
+ module Deadpool
3
+
4
+ class Helper
5
+
6
+ def self.symbolize_keys(arg)
7
+ case arg
8
+ when Array
9
+ arg.map { |elem| symbolize_keys elem }
10
+ when Hash
11
+ Hash[
12
+ arg.map { |key, value|
13
+ k = key.is_a?(String) ? key.to_sym : key
14
+ v = symbolize_keys value
15
+ [k,v]
16
+ }]
17
+ else
18
+ arg
19
+ end
20
+ end
21
+
22
+ def self.configure(options)
23
+ default_config = YAML.load(File.read(File.join(File.dirname(__FILE__), '../../config/default_environment.yml')))
24
+ user_config = YAML.load(File.read(File.join(options[:config_path], 'config/environment.yml')))
25
+ config = Deadpool::Helper.symbolize_keys default_config.merge(user_config).merge(options)
26
+
27
+ return config
28
+ end
29
+
30
+ def self.setup_logger(config)
31
+ logger = Logger.new(config[:log_path])
32
+ logger.level = Logger.const_get(config[:log_level].upcase)
33
+
34
+ return logger
35
+ end
36
+
37
+ end
38
+
39
+ end