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,65 @@
1
+
2
+ require 'json'
3
+
4
+ module Deadpool
5
+
6
+ module AdminServer
7
+
8
+ attr_accessor :deadpool_server
9
+
10
+ # data should be a JSON encoded hash. It must have a command key.
11
+ # {
12
+ # 'command' => 'command',
13
+ # 'pool' => 'pool_name',
14
+ # 'server' => 'server_label'
15
+ # }
16
+ def receive_data(data)
17
+ if data.to_s =~ /command/
18
+ deadpool_server.logger.debug "Received instruction: #{data}"
19
+ options = JSON.parse(data.to_s)
20
+
21
+ case options['command']
22
+ when 'full_report'
23
+ send_data full_report
24
+ when 'nagios_report'
25
+ send_data nagios_report
26
+ when 'promote_server'
27
+ send_data promote_server options
28
+ when 'stop'
29
+ send_data stop
30
+ else
31
+ send_data "Server did not understand the command."
32
+ end
33
+ end
34
+
35
+ close_connection_after_writing
36
+ end
37
+
38
+ protected
39
+
40
+ def full_report
41
+ return deadpool_server.system_check(true).full_report
42
+ end
43
+
44
+ def nagios_report
45
+ return deadpool_server.system_check.nagios_report
46
+ end
47
+
48
+ def promote_server(options)
49
+ if deadpool_server.promote_server(options['pool'], options['server'].to_sym)
50
+ return "Success.\n"
51
+ else
52
+ return "Failed!\n"
53
+ end
54
+ end
55
+
56
+ def stop
57
+ close_connection
58
+ deadpool_server.kill
59
+ end
60
+
61
+ end
62
+
63
+ end
64
+
65
+
@@ -0,0 +1,169 @@
1
+
2
+ require 'optparse'
3
+ require 'ostruct'
4
+ require 'strscan'
5
+ require 'socket'
6
+ require 'json'
7
+
8
+
9
+ module Deadpool
10
+
11
+ class CommandLineServer
12
+
13
+ def initialize(argv)
14
+ @argv = argv
15
+ end
16
+
17
+ def run
18
+ @options = self.parse_command_line
19
+ @config = Deadpool::Helper.configure @options
20
+
21
+ self.execute_command(@options)
22
+ end
23
+
24
+ def parse_command_line
25
+ options = Hash.new
26
+ options[:command_count] = 0
27
+ options[:config_path] = '/etc/deadpool'
28
+
29
+ @option_parser = OptionParser.new do |opts|
30
+ opts.banner = "Usage: deadpool_hosts {help|full_report|nagios_report} [options]"
31
+
32
+ opts.separator "Commands:"
33
+ opts.on("-h", "--help", "Print this help message.") do |help|
34
+ options[:command_count] += 1
35
+ options[:command] = :help
36
+ end
37
+ opts.on("--full_report", "Give the full system report.") do |full_report|
38
+ options[:command_count] += 1
39
+ options[:command] = :full_report
40
+ end
41
+ opts.on("--nagios_report", "Report system state in Nagios plugin format.") do |nagios_report|
42
+ options[:command_count] += 1
43
+ options[:command] = :nagios_report
44
+ end
45
+ opts.on("--promote_server", "Promote specified server to the master.") do |nagios_report|
46
+ options[:command_count] += 1
47
+ options[:command] = :promote_server
48
+ end
49
+ opts.on("--stop", "Stop the server.") do |stop|
50
+ options[:command_count] += 1
51
+ options[:command] = :stop
52
+ end
53
+
54
+ opts.separator "Options:"
55
+ opts.on("--server=SERVER_LABEL", String, "primary_host or secondary_host.") do |server|
56
+ options[:server] = server
57
+ end
58
+ opts.on("--pool=POOL_NAME", String, "Deadpool name to operate on.") do |pool|
59
+ options[:pool] = pool
60
+ end
61
+ opts.on("--config_path=PATH", String,
62
+ "Path to configs and custom plugins. #{options[:config_path]} by default.") do |config_path|
63
+ options[:config_path] = config_path
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 :help
83
+ help
84
+ when :full_report
85
+ full_report options
86
+ when :nagios_report
87
+ nagios_report options
88
+ when :promote_server
89
+ promote_server options
90
+ when :stop
91
+ stop options
92
+ else
93
+ help
94
+ end
95
+ end
96
+
97
+ def help(message=nil)
98
+ unless message.nil?
99
+ puts message
100
+ end
101
+ puts @option_parser.help
102
+ exit 4
103
+ end
104
+
105
+ def full_report(options)
106
+ puts send_command_to_deadpool_server :command => 'full_report'
107
+ end
108
+
109
+ def nagios_report(options)
110
+ response = send_command_to_deadpool_server :command => 'nagios_report'
111
+ puts response
112
+
113
+ if (response.to_s =~ /^OK/) != nil
114
+ exit 0
115
+ elsif (response.to_s =~ /^WARNING/) != nil
116
+ exit 1
117
+ elsif (response.to_s =~ /^CRITICAL/) != nil
118
+ exit 2
119
+ elsif (response.to_s =~ /^UNKNOWN/) != nil
120
+ exit 3
121
+ else
122
+ exit 4
123
+ end
124
+ end
125
+
126
+ def promote_server(options)
127
+ error_messages = []
128
+
129
+ if options[:pool].nil?
130
+ error_messages << "Promoting server requires --pool argument."
131
+ end
132
+
133
+ if options[:server].nil?
134
+ error_messages << "Promoting server requires --server argument."
135
+ end
136
+
137
+ unless error_messages.empty?
138
+ help error_messages.join "\n"
139
+ end
140
+
141
+ puts send_command_to_deadpool_server :command => 'promote_server', :pool => options[:pool], :server => options[:server]
142
+ end
143
+
144
+ def stop(options)
145
+ puts send_command_to_deadpool_server :command => 'stop'
146
+ end
147
+
148
+ def send_command_to_deadpool_server(options)
149
+ output = ''
150
+ socket = TCPSocket.open(@config[:admin_hostname], @config[:admin_port])
151
+
152
+ if socket
153
+ socket.puts JSON.dump(options)
154
+ while line = socket.gets
155
+ output += line
156
+ end
157
+ socket.close
158
+ else
159
+ puts "Couldn't connect to deadpool server."
160
+ exit 4
161
+ end
162
+
163
+ return output
164
+ end
165
+
166
+ end
167
+
168
+ end
169
+
@@ -0,0 +1,82 @@
1
+
2
+
3
+ # Blatanly stolen from Ben Marini
4
+ # https://gist.github.com/517442/9ed5c4bd66bd8afc6a379286d8be1d8fb8a33925
5
+
6
+
7
+ module Deadpool
8
+
9
+ module Daemonizer
10
+
11
+ def daemonize(opts={})
12
+ opts = { :log => '/dev/null', :pid => "/var/run/#{File.basename($0)}.pid" }.merge(opts)
13
+
14
+ $stdout.sync = $stderr.sync = true
15
+ $stdin.reopen("/dev/null")
16
+
17
+ exit if fork
18
+
19
+ Process.setsid
20
+
21
+ exit if fork
22
+
23
+ Dir.chdir("/") if opts[:chdir]
24
+ File.umask(0000) if opts[:umask]
25
+
26
+ if File.exist?(opts[:pid])
27
+ begin
28
+ existing_pid = File.read(opts[:pid]).to_i
29
+ Process.kill(0, existing_pid) # See if proc exists
30
+ abort "error: existing process #{existing_pid} using this pidfile, exiting"
31
+ rescue Errno::ESRCH
32
+ puts "warning: removing stale pidfile with pid #{existing_pid}"
33
+ end
34
+ end
35
+
36
+ File.open(opts[:pid], 'w') { |f| f.write($$) }
37
+
38
+ at_exit do
39
+ ( File.read(opts[:pid]).to_i == $$ and File.unlink(opts[:pid]) ) rescue nil
40
+ end
41
+
42
+ puts "forked process is #{$$}"
43
+ puts "output redirected to #{opts[:log]}"
44
+
45
+ $stdout.reopen(opts[:log], 'a')
46
+ $stderr.reopen(opts[:log], 'a')
47
+ $stdout.sync = $stderr.sync = true
48
+ end
49
+
50
+ def kill(opts={})
51
+ begin
52
+ opts = { :log => "/dev/null", :pid => "/var/run/#{File.basename($0)}.pid" }.merge(opts)
53
+ pid = File.read(opts[:pid]).to_i
54
+ sec = 60 # Seconds to wait before force killing
55
+ Process.kill("TERM", pid)
56
+
57
+ begin
58
+ SystemTimer.timeout(sec) do
59
+ loop do
60
+ puts "waiting #{sec} seconds for #{pid} before sending KILL"
61
+ Process.kill(0, pid) # See if proc exists
62
+
63
+ sec -= 1
64
+ sleep 1
65
+ end
66
+ end
67
+ rescue Errno::ESRCH
68
+ puts "killed process #{pid}"
69
+ rescue Timeout::Error
70
+ Process.kill("KILL", pid)
71
+ puts "force killed process #{pid}"
72
+ end
73
+
74
+ rescue Errno::ENOENT
75
+ puts "warning: pidfile #{opts[:pid]} does not exist"
76
+ rescue Errno::ESRCH
77
+ puts "warning: process #{pid} does not exist"
78
+ end
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,91 @@
1
+
2
+
3
+
4
+ module Deadpool
5
+
6
+ module FailoverProtocol
7
+
8
+ class Base
9
+
10
+ attr_accessor :logger
11
+ attr_reader :config
12
+
13
+ def initialize(config, failover_config, logger)
14
+ @name = failover_config[:name].nil? ? '' : failover_config[:name]
15
+ @state = Deadpool::State.new @name, self.class
16
+ @config = config
17
+ @logger = logger
18
+ @primary_host = @config[:primary_host]
19
+ @secondary_host = @config[:secondary_host]
20
+ @failover_config = failover_config
21
+ setup
22
+ end
23
+
24
+ # Implementation specific initialization should be placed here.
25
+ def setup
26
+ end
27
+
28
+ # Overwrite this if you need to.
29
+ # Update state to reflect that failover has been initiated.
30
+ # State must be updated to no less than WARNING.
31
+ # State must be CRITICAL if any step of the protocol fails.
32
+ # Lock the state at whatever stage the failover reached.
33
+ # return true or false on success or failure.
34
+ #
35
+ def initiate_failover_protocol!
36
+ logger.info "Performing Preflight Check"
37
+ @state.set_state WARNING, "Failover Protocol Initiated."
38
+
39
+ if preflight_check
40
+ logger.info "Preflight Check Passed."
41
+ @state.add_message "Preflight Check Passed."
42
+ else
43
+ logger.error "Preflight Check Failed! Aborting Failover Protocol."
44
+ @state.escalate_status_code CRITICAL
45
+ @state.add_error_message "Preflight Check Failed! Failover Protocol Aborted!"
46
+ @state.lock
47
+ return false
48
+ end
49
+
50
+ if promote_to_primary(@secondary_host)
51
+ logger.info "#{@secondary_host} successfully promoted to primary"
52
+ @state.add_message "Failover Protocol Successful."
53
+ @state.lock
54
+ return true
55
+ else
56
+ logger.info "#{@secondary_host} promotion failed."
57
+ @state.escalate_status_code CRITICAL
58
+ @state.add_error_message "Failover Protocol Failed!"
59
+ @state.lock
60
+ return false
61
+ end
62
+ end
63
+
64
+ # Return true or false
65
+ # Don't update system state.
66
+ # return true or false success or failure
67
+ def preflight_check
68
+ return false
69
+ end
70
+
71
+ # Promote the host to primary. This is used by initiate_failover_protocol!
72
+ # and for manual promotion by an administrator.
73
+ # new_primary is an IP address
74
+ # TODO: change new_primary to be a config label.
75
+ # return true or false success or failure
76
+ def promote_to_primary(new_primary)
77
+ return false
78
+ end
79
+
80
+ # Perform checks against anything that could cause a failover protocol to fail
81
+ # Perform checks on system state.
82
+ # return New Deadpool::StateSnapshot
83
+ def system_check
84
+ return Deadpool::StateSnapshot.new @state
85
+ end
86
+
87
+ end
88
+
89
+ end
90
+
91
+ end
@@ -0,0 +1,157 @@
1
+
2
+ require 'net/ssh'
3
+
4
+ module Deadpool
5
+
6
+ module FailoverProtocol
7
+
8
+ class EtcHosts < Base
9
+
10
+ def setup
11
+ @script_path = @failover_config[:script_path]
12
+ @service_host_name = @failover_config[:service_host_name]
13
+ @service_hosts_file = @failover_config[:service_hosts_file]
14
+ @client_hosts = @failover_config[:client_hosts]
15
+ @username = @failover_config[:username]
16
+ @password = @failover_config[:password]
17
+ @use_sudo = @failover_config[:use_sudo]
18
+ @sudo_path = @failover_config[:sudo_path].nil? ? 'sudo' : @failover_config[:sudo_path]
19
+ end
20
+
21
+ def preflight_check
22
+ @client_hosts.map { |h| test_client(h) && verify_client(h) }.all?
23
+ end
24
+
25
+ def test_client(client_host)
26
+ logger.debug "Testing Client #{client_host}"
27
+ output = run_script_command(client_host, '--test')
28
+ logger.debug "Output recieved From Client: #{output}"
29
+
30
+ if output.class == String
31
+ okay = (output =~ /^OK/) != nil
32
+ okay ? logger.info("#{client_host}: " + output.strip) : logger.error("#{client_host}: " + output.strip)
33
+ else
34
+ logger.error "Test Client had a critical failure on '#{client_host}'"
35
+ end
36
+
37
+ return okay
38
+ end
39
+
40
+ def verify_client(client_host, primary_host=nil)
41
+ logger.debug "Verifying Client #{client_host}"
42
+ primary_host = primary_host.nil? ? @primary_host : primary_host
43
+ command_arguments = "--verify --host_name='#{@service_host_name}' --ip_address='#{primary_host}'"
44
+ command_arguments += " --host_file='#{@service_hosts_file}'" if @service_hosts_file
45
+ output = run_script_command(client_host, command_arguments)
46
+ logger.debug "Output recieved From Client: #{output}"
47
+
48
+ if output.class == String
49
+ okay = (output =~ /^OK/) != nil
50
+ okay ? logger.info("#{client_host}: " + output.strip) : logger.error("#{client_host}: " + output.strip)
51
+ else
52
+ logger.error "Verify Client had a critical failure on '#{client_host}'"
53
+ end
54
+
55
+ return okay
56
+ end
57
+
58
+ def promote_to_primary(new_primary)
59
+ @client_hosts.map do |client_host|
60
+ # logger.debug "client_host: #{client_host}, New Primary: #{new_primary}"
61
+ promote_to_primary_on_client(client_host, new_primary)
62
+ end.all?
63
+ end
64
+
65
+ def promote_to_primary_on_client(client_host, new_primary)
66
+ # logger.debug "Assigning #{new_primary} as new primary on #{client_host}"
67
+ command_arguments = "--switch --host_name='#{@service_host_name}' --ip_address='#{new_primary}'"
68
+ command_arguments += " --host_file='#{@service_hosts_file}'" if @service_hosts_file
69
+ output = run_script_command(client_host, command_arguments)
70
+ logger.debug "Output received From Client: #{output}"
71
+
72
+ if output.class == String
73
+ okay = (output =~ /^OK/) != nil
74
+ okay ? logger.info("#{client_host}: " + output.strip) : logger.error("#{client_host}: " + output.strip)
75
+ else
76
+ logger.error "Promote to Primary on Client had a critical failure on '#{client_host}'"
77
+ end
78
+
79
+ return okay
80
+ end
81
+
82
+ def system_check
83
+ writable = []
84
+ not_writable = []
85
+ pointed_at_primary = []
86
+ pointed_at_secondary = []
87
+ pointed_at_neither = []
88
+
89
+ # Collect check data
90
+ @client_hosts.each do |client_host|
91
+ if test_client(client_host)
92
+ writable << client_host
93
+ else
94
+ not_writable << client_host
95
+ end
96
+
97
+ if verify_client(client_host, @primary_host)
98
+ pointed_at_primary << client_host
99
+ else
100
+ if verify_client(client_host, @secondary_host)
101
+ pointed_at_secondary << client_host
102
+ else
103
+ pointed_at_neither << client_host
104
+ end
105
+ end
106
+ end
107
+
108
+ # Compile write check data.
109
+ if !writable.empty? && not_writable.empty?
110
+ @state.set_state OK, "Write check passed all servers: #{writable.join(', ')}"
111
+ elsif !writable.empty? && !not_writable.empty?
112
+ @state.set_state WARNING, "Write check passed on: #{writable.join(', ')}"
113
+ @state.add_error_message "Write check failed on #{not_writable.join(', ')}"
114
+ elsif writable.empty?
115
+ @state.set_state WARNING, "Write check failed all servers: #{not_writable.join(', ')}"
116
+ end
117
+
118
+
119
+ # Compile verification data
120
+ if !pointed_at_primary.empty? && pointed_at_secondary.empty? && pointed_at_neither.empty?
121
+ @state.add_message "All client hosts are pointed at the primary."
122
+ elsif pointed_at_primary.empty? && !pointed_at_secondary.empty? && pointed_at_neither.empty?
123
+ @state.escalate_status_code WARNING
124
+ @state.add_error_message "All client hosts are pointed at the secondary."
125
+ else
126
+ @state.escalate_status_code CRITICAL
127
+ @state.add_error_message "Client hosts are pointing in different directions."
128
+ end
129
+
130
+ return Deadpool::StateSnapshot.new @state
131
+ end
132
+
133
+
134
+ protected
135
+
136
+ def run_script_command(host, command_arguments)
137
+ options = @password.nil? ? {} : {:password => @password}
138
+ command = "#{@script_path} #{command_arguments}"
139
+ command = "#{@sudo_path} #{command}" if @use_sudo
140
+
141
+ logger.debug "executing #{command} on #{host}"
142
+
143
+ begin
144
+ Net::SSH.start(host, @username, options) do |ssh|
145
+ return ssh.exec!(command)
146
+ end
147
+ rescue
148
+ logger.error "Couldn't execute #{command} on #{host}"
149
+ return false
150
+ end
151
+ end
152
+
153
+ end
154
+
155
+ end
156
+
157
+ end