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.
- data/bin/deadpool_admin +8 -0
- data/bin/deadpool_generator +8 -0
- data/bin/deadpool_hosts +222 -0
- data/config/default_environment.yml +7 -0
- data/doc/init/deadpool.conf +17 -0
- data/lib/deadpool.rb +45 -0
- data/lib/deadpool/admin.rb +197 -0
- data/lib/deadpool/admin_server.rb +65 -0
- data/lib/deadpool/command_line_server.rb +169 -0
- data/lib/deadpool/daemonizer.rb +82 -0
- data/lib/deadpool/failover_protocol.rb +91 -0
- data/lib/deadpool/failover_protocol/etc_hosts.rb +157 -0
- data/lib/deadpool/failover_protocol/exec_remote_command.rb +106 -0
- data/lib/deadpool/generator.rb +220 -0
- data/lib/deadpool/handler.rb +100 -0
- data/lib/deadpool/helper.rb +39 -0
- data/lib/deadpool/monitor/base.rb +58 -0
- data/lib/deadpool/monitor/generic_nagios.rb +47 -0
- data/lib/deadpool/monitor/mysql.rb +62 -0
- data/lib/deadpool/monitor/redis.rb +21 -0
- data/lib/deadpool/options.rb +62 -0
- data/lib/deadpool/server.rb +101 -0
- data/lib/deadpool/state.rb +78 -0
- data/lib/deadpool/state_snapshot.rb +81 -0
- metadata +125 -0
@@ -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
|