erlnixify 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,35 @@
1
+ Feature: Node
2
+ In order to manage a running erlang node
3
+ As a developer
4
+ I want to be able to bring it up and shut it down
5
+ and regularly check to see if its still running as expected
6
+
7
+ Scenario: Basic startup
8
+ Given a valid configuration
9
+ And the erlang node is started
10
+ When the node is brought up
11
+ Then no errors or problems occur
12
+
13
+ Scenario: Startup failure
14
+ Given a valid configuration with invalid command
15
+ And the erlang node is started
16
+ When the node fails
17
+ Then an exception occures
18
+
19
+ Scenario: Check failure
20
+ Given a valid configuration with invalid check command
21
+ And the erlang node is started
22
+ When the check fails
23
+ And the erlang node is halted
24
+
25
+ Scenario: Check Timeout
26
+ Given a valid configuration with a long running check comamnd
27
+ And the erlang node is started
28
+ When the check command times out
29
+ Then the erlang node is halted
30
+
31
+ Scenario: Signal TERM
32
+ Given a valid configuration
33
+ And the erlang node is started
34
+ When a term signal is recieved
35
+ Then the erlang node is halted
@@ -0,0 +1,20 @@
1
+ Feature: Settings
2
+ In order to provide a 'templatable' settings file
3
+ As a developer
4
+ I want to be able to provide a settings file
5
+ and have the system warn me when I get settings wrong
6
+
7
+ Scenario: A basic yaml settings file
8
+ Given a settings file
9
+ When I load that settings file
10
+ Then the settings should be available in the settings object
11
+
12
+ Scenario: Command line options override config
13
+ Given an options object that contains config values
14
+ When a new settings file is loaded up using that options
15
+ Then the command line options override the file options
16
+
17
+ Scenario: Reasonable Default Settings
18
+ Given a lack of a config file and command line options
19
+ When a settings are loaded
20
+ Then that settings object contains sane defaults
@@ -0,0 +1,108 @@
1
+
2
+
3
+ Given(/^a valid configuration$/) do
4
+ hostname = `hostname -f`.strip
5
+ @opts = Erlnixify::Opts.new(["--cookie", "fubachu",
6
+ "--startuptimeout", "10",
7
+ "--checkinterval", "10",
8
+ "--name", "foo",
9
+ "--command", "erl -noshell -setcookie fubachu -name foo@#{hostname}"
10
+ ])
11
+ @settings = Erlnixify::Settings.new(@opts)
12
+ end
13
+
14
+ Then(/^no errors or problems occur$/) do
15
+ assert @node.is_running?
16
+ @node.halt_nicely
17
+ sleep @settings[:startuptimeout]
18
+ assert (not @node.is_running?)
19
+ end
20
+
21
+ Given(/^the erlang node is started$/) do
22
+ @node = Erlnixify::Node.new @settings
23
+ Thread.new do
24
+ begin
25
+ @node.start
26
+ rescue Erlnixify::NodeError => node_error
27
+ @node_start_error = node_error
28
+ rescue Exception => e
29
+ assert_fail "Other exception should not occur #{e.message}"
30
+ end
31
+ end
32
+ sleep (@settings[:startuptimeout] + 30)
33
+ end
34
+
35
+ When(/^the node is brought up$/) do
36
+ assert @node.is_running?
37
+ end
38
+
39
+ Given(/^a valid configuration with invalid command$/) do
40
+ hostname = `hostname -f`.strip
41
+ @opts = Erlnixify::Opts.new(["--cookie", "fubachu",
42
+ "--startuptimeout", "10",
43
+ "--checkinterval", "10",
44
+ "--name", "foo",
45
+ "--command", "this should fail"
46
+ ])
47
+ @settings = Erlnixify::Settings.new(@opts)
48
+ end
49
+
50
+ When(/^the node fails$/) do
51
+ assert @node_start_error != nil, "An NodeError was expected to be thrown"
52
+ assert @node.is_running? == false, "Node is still running"
53
+ end
54
+
55
+ Then(/^an exception occures$/) do
56
+ assert @node_start_error != nil, "An NodeError was expected to be thrown"
57
+ end
58
+
59
+ Given(/^a valid configuration with invalid check command$/) do
60
+ hostname = `hostname -f`.strip
61
+ @opts = Erlnixify::Opts.new(["--cookie", "fubachu",
62
+ "--startuptimeout", "10",
63
+ "--checkinterval", "10",
64
+ "--name", "foo",
65
+ "--command", "erl -noshell -setcookie fubachu -name foo@#{hostname}",
66
+ "--check", "invalid check",
67
+
68
+ ])
69
+ @settings = Erlnixify::Settings.new(@opts)
70
+ end
71
+
72
+ When(/^the check fails$/) do
73
+ assert @node_start_error.message == "Node check failed"
74
+ end
75
+
76
+
77
+ When(/^the erlang node is halted$/) do
78
+ assert false == @node.is_running?, "The node is running when it should not be"
79
+ end
80
+
81
+ Given(/^a valid configuration with a long running check comamnd$/) do
82
+ check_time = 60 * 1000 # 60 seconds in milliseconds
83
+ hostname = `hostname -f`.strip
84
+ @opts = Erlnixify::Opts.new(["--cookie", "fubachu",
85
+ "--startuptimeout", "10",
86
+ "--checkinterval", "10",
87
+ "--name", "foo",
88
+ "--command", "erl -noshell -setcookie fubachu -name foo@#{hostname}",
89
+ "--check", "timer sleep [#{check_time}]",
90
+
91
+ ])
92
+ @settings = Erlnixify::Settings.new(@opts)
93
+ end
94
+
95
+ When(/^the check command times out$/) do
96
+ sleep 60 # should be a enough given the timeout
97
+
98
+ assert @node_start_error.message == "Check command timeout occurred", "Timeout did not occur"
99
+ end
100
+
101
+ When(/^a term signal is recieved$/) do
102
+ begin
103
+ Process.kill("TERM", $$)
104
+ assert_fail "Did not recieve error"
105
+ rescue Erlnixify::NodeError => e
106
+ assert e.message == "SIGTERM recieved, shutting down"
107
+ end
108
+ end
@@ -0,0 +1,63 @@
1
+ require "erlnixify"
2
+
3
+ Before do
4
+ end
5
+
6
+ After do
7
+ end
8
+
9
+ Given(/^a settings file$/) do
10
+ file = File.dirname(__FILE__) + '/../test_data/settings_config.yml'
11
+ @opts = Erlnixify::Opts.new(["--config", file])
12
+ end
13
+
14
+ When(/^I load that settings file$/) do
15
+ @settings = Erlnixify::Settings.new(@opts)
16
+ end
17
+
18
+ Then(/^the settings should be available in the settings object$/) do
19
+ assert_equal 60, @settings[:startuptimeout]
20
+ assert_equal "/some/place", @settings[:release]
21
+ assert_equal "/some/root/file", @settings[:erlang]
22
+ assert_equal "mynode", @settings[:name]
23
+ assert_equal "sleep 10", @settings[:command]
24
+ assert_equal "testcookie!", @settings[:cookie]
25
+ assert_equal 30, @settings[:checkinterval]
26
+ end
27
+
28
+ Given(/^an options object that contains config values$/) do
29
+ file = File.dirname(__FILE__) + '/../test_data/settings_config.yml'
30
+ @opts = Erlnixify::Opts.new(["--config", file,
31
+ "--checkinterval", "100",
32
+ "--startuptimeout", "500",
33
+ "--cookie", "fubachu"])
34
+ end
35
+
36
+ When(/^a new settings file is loaded up using that options$/) do
37
+ @settings = Erlnixify::Settings.new(@opts)
38
+ end
39
+
40
+ Then(/^the command line options override the file options$/) do
41
+ assert_equal 500, @settings[:startuptimeout]
42
+ assert_equal "/some/place", @settings[:release]
43
+ assert_equal "/some/root/file", @settings[:erlang]
44
+ assert_equal "mynode", @settings[:name]
45
+ assert_equal "sleep 10", @settings[:command]
46
+ assert_equal "fubachu", @settings[:cookie]
47
+ assert_equal 100, @settings[:checkinterval]
48
+ end
49
+
50
+
51
+ Given(/^a lack of a config file and command line options$/) do
52
+ @opts = Erlnixify::Opts.new([])
53
+ end
54
+
55
+ When(/^a settings are loaded$/) do
56
+ @settings = Erlnixify::Settings.new(@opts)
57
+ end
58
+
59
+ Then(/^that settings object contains sane defaults$/) do
60
+ assert_equal 60, @settings[:startuptimeout]
61
+ assert_equal ENV["HOME"], @settings[:home]
62
+ assert_equal 30, @settings[:checkinterval]
63
+ end
@@ -0,0 +1,7 @@
1
+ release: /some/place
2
+ erlang: /some/root/file
3
+ name: mynode
4
+ command: sleep 10
5
+ cookie: testcookie!
6
+ startuptimeout: 60
7
+ checkinterval: 30
@@ -0,0 +1,6 @@
1
+ module Erlnixify
2
+ # An Error thrown from the erlnixify system eventually this should
3
+ # by expanded to individual error types.
4
+ class NodeError < StandardError
5
+ end
6
+ end
@@ -0,0 +1,135 @@
1
+ require 'logger'
2
+ require 'erlnixify/exceptions'
3
+
4
+ module Erlnixify
5
+ COMMAND_WRAPPER = "%{erl_interface}/bin/erl_call -n %{fullnode} \
6
+ -c '%{cookie}' -a '%{cmd}'"
7
+
8
+ SHUTDOWN_COMMAND = "init stop"
9
+
10
+ BRUTAL_SHUTDOWN_COMMAND = "erlang halt 127"
11
+
12
+ # The process class owns the Running erlang process. It knows how to
13
+ # query it if its active, and kill it if something goes long. This
14
+ # class is the guts of erlnixify
15
+ class Node
16
+ def initialize(settings)
17
+ @settings = settings
18
+ @command = @settings[:command] % settings.settings
19
+ @check_command = self.interpolate_cmd(@settings[:check])
20
+ @halt_command = self.interpolate_cmd(SHUTDOWN_COMMAND)
21
+ @brutal_halt_command = self.interpolate_cmd(BRUTAL_SHUTDOWN_COMMAND)
22
+ @checkregex = Regexp.new @settings[:checkregex]
23
+
24
+ @log = Logger.new(STDOUT)
25
+ @log.level = Logger::DEBUG
26
+
27
+ Signal.trap("TERM") do
28
+ self.halt_nicely
29
+ raise NodeError, "SIGTERM recieved, shutting down"
30
+ end
31
+
32
+ Signal.trap("INT") do
33
+ self.halt_nicely
34
+ raise NodeError, "SIGINT recieved, shutting down"
35
+ end
36
+
37
+ at_exit { self.external_kill }
38
+ end
39
+
40
+ def start
41
+ @log.debug "starting process"
42
+ env = {}
43
+ env["HOME"] = @settings[:home] if @settings[:home]
44
+
45
+ begin
46
+ @log.debug "spawning command '#{@command}' with #{env}"
47
+ @pid = Process.spawn(env, @command)
48
+ rescue Errno::ENOENT
49
+ @log.debug "Invalid command provided, raising error"
50
+ raise NodeError, "Command does not exist"
51
+ end
52
+
53
+ @log.debug "waiting for #{@settings[:startuptimeout]} seconds for startup"
54
+ sleep @settings[:startuptimeout]
55
+ self.monitor
56
+ end
57
+
58
+ def monitor
59
+ @log.debug "starting monitor of Pid #{@pid}"
60
+ loop do
61
+ if is_running?
62
+ self.check
63
+ sleep @settings[:checkinterval]
64
+ else
65
+ raise NodeError, "Node not running"
66
+ end
67
+ break if @stop
68
+ @log.debug "Node responded correctly, continuing check"
69
+ end
70
+ end
71
+
72
+ def check
73
+ begin
74
+ Timeout.timeout(@settings[:checktimeout]) do
75
+ self.raw_check
76
+ end
77
+ rescue Timeout::Error
78
+ self.halt_nicely
79
+ raise NodeError, "Check command timeout occurred"
80
+ end
81
+ end
82
+
83
+ def raw_check
84
+ @log.debug "Checking the status of Pid #{@pid}"
85
+ @log.debug "#{@check_command} =~ #{@checkregex}"
86
+ result = `#{@check_command}`
87
+ @log.debug "got #{result}"
88
+ if not (result =~ @checkregex)
89
+ @log.debug "Check failed, halting system"
90
+ self.halt_nicely
91
+ raise NodeError, "Node check failed"
92
+ end
93
+ end
94
+
95
+ def is_running?
96
+ @log.debug "Checking if Pid (#{@pid}) is running"
97
+ if @pid
98
+ begin
99
+ Process.getpgid(@pid)
100
+ true
101
+ rescue Errno::ESRCH
102
+ false
103
+ end
104
+ else
105
+ false
106
+ end
107
+ end
108
+
109
+ def halt_nicely
110
+ `#{@halt_command}`
111
+ sleep @settings[:checkinterval]
112
+ if self.is_running?
113
+ self.halt_brutally
114
+ end
115
+ end
116
+
117
+ def halt_brutally
118
+ `#{@brutal_halt_command}`
119
+ sleep @settings[:checkinterval]
120
+ if self.is_running?
121
+ self.external_kill
122
+ end
123
+ end
124
+
125
+ def external_kill
126
+ Process.kill("KILL", @pid) if @pid
127
+ end
128
+
129
+ def interpolate_cmd(cmd)
130
+ local_settings = @settings.settings.clone
131
+ local_settings[:cmd] = cmd % local_settings
132
+ COMMAND_WRAPPER % local_settings
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,45 @@
1
+ require 'slop'
2
+
3
+ module Erlnixify
4
+
5
+ # The Opts class provides options parsing support for erlnixify. It
6
+ # is similar to settings with the exception that it is designed to
7
+ # get its values from the command line.
8
+ #
9
+ class Opts
10
+
11
+ attr_reader :options
12
+
13
+ def initialize(args)
14
+ opts = Slop.parse(args) do
15
+
16
+ banner = "Usage: erlnixify [options]"
17
+
18
+ on :b, :release=, 'Release Root Directory'
19
+ on :e, :erlang=, 'Erlang Root Directory'
20
+ on :o, :home=, "The home directory to explicitly set"
21
+ on :n, :name=, "The short name of the node to be managed"
22
+ on :fullnode=, "The fully qualified node name"
23
+ on :m, :command=, "The command to run to start the release"
24
+ on :k, :check=, "The command to check if the release is active"
25
+ on :r, :checkregex=, "The regex that must match to the output of check command"
26
+ on :x, :cookiefile=, "A file that contains the erlang cookie, not needed if cookie is set"
27
+ on :i, :cookie=, "The cookie itself, not needed if cookie-file is set"
28
+ on(:t, :startuptimeout=,
29
+ "The amount of time to let the system startup in seconds",
30
+ as: Integer)
31
+ on(:a, :checkinterval=,
32
+ "How often erlnixify should check to see if the system is still running",
33
+ as: Integer)
34
+ on(:w, :checktimeout=,
35
+ "The longest time a check can run, defaults to 30 seconds",
36
+ as: Integer)
37
+ on :c, :config=, "A file that contains the YAML based config for this system"
38
+ on :v, :version, "Show the Version"
39
+ end
40
+
41
+ @options = opts.to_hash
42
+ end
43
+ end
44
+
45
+ end
@@ -0,0 +1,127 @@
1
+ require 'yaml'
2
+
3
+ module Erlnixify
4
+
5
+ # Provides a library for settings to be accessed from accross the
6
+ # system
7
+ class Settings
8
+ attr_accessor :settings
9
+ attr_accessor :options
10
+
11
+ STARTUP_TIMEOUT = 60
12
+ CHECK_INTERVAL = 30
13
+ CHECK_TIMEOUT = 30
14
+ CHECK_COMMAND = "erlang statistics [reductions]"
15
+ CHECK_REGEX = "^{\\d+, \\d+}$"
16
+
17
+ def initialize(opts)
18
+ @options = opts
19
+ config = @options.options[:config]
20
+
21
+ @settings = self.default_settings
22
+
23
+ self.load! config if config
24
+ self.merge(@options.options)
25
+ self.post_settings_setup
26
+ end
27
+
28
+ def default_settings
29
+ defaults = {release: nil,
30
+ erlang: nil,
31
+ home: ENV["HOME"],
32
+ name: nil,
33
+ fullnode: nil,
34
+ command: nil,
35
+ check: CHECK_COMMAND,
36
+ checkregex: CHECK_REGEX,
37
+ cookiefile: nil,
38
+ cookie: nil,
39
+ startuptimeout: STARTUP_TIMEOUT,
40
+ checkinterval: CHECK_INTERVAL,
41
+ checktimeout: CHECK_TIMEOUT,
42
+ config: nil}
43
+ defaults
44
+ end
45
+
46
+ def load!(filename, options = {})
47
+ newsets = YAML::load_file(filename)
48
+
49
+ env = options[:env].to_sym if options[:env]
50
+
51
+ if env
52
+ newsets = newsets[env] if newsets[env]
53
+ end
54
+
55
+ self.merge(newsets)
56
+ end
57
+
58
+ def [](key)
59
+ return @settings[key]
60
+ end
61
+
62
+ def merge(data)
63
+ @settings = @settings.inject({}) do |newhash, (key, value)|
64
+ symkey = key.to_sym
65
+ data_value = data[symkey]
66
+ data_value = data[key.to_s] unless data_value
67
+ if data_value
68
+ newhash[symkey] = data_value
69
+ else
70
+ newhash[symkey] = value
71
+ end
72
+ newhash
73
+ end
74
+ end
75
+
76
+ def post_settings_setup
77
+ @settings[:erlang] = self.find_erlang_root
78
+ @settings[:cookie] = self.find_cookie
79
+ @settings[:erl_interface] = self.find_erl_interface
80
+ @settings[:fullnode] = self.find_full_node
81
+ end
82
+
83
+ def find_erlang_root
84
+ if @settings[:erlang]
85
+ @settings[:erlang]
86
+ elsif File.directory? "/usr/local/lib/erlang"
87
+ "/usr/local/lib/erlang"
88
+ elsif File.directory? "/usr/lib/erlang"
89
+ "/usr/lib/erlang"
90
+ end
91
+ end
92
+
93
+
94
+ def find_cookie
95
+ cookie_file = @settings[:cookiefile]
96
+ if @settings[:cookie]
97
+ @settings[:cookie]
98
+ elsif cookie_file
99
+ if File.exists? cookie_file
100
+ IO.read cookie_file
101
+ else
102
+ raise RuntimeError, "Cookie file does not exist"
103
+ end
104
+ end
105
+ end
106
+
107
+ def find_full_node
108
+ if @settings[:fullnode]
109
+ @settings[:fullnode]
110
+ else
111
+ hostname = `hostname -f`.strip
112
+ node = @settings[:name]
113
+ "#{node}@#{hostname}"
114
+ end
115
+ end
116
+
117
+ def find_erl_interface
118
+ erlang_root = @settings[:erlang]
119
+ release_root = @settings[:release]
120
+ if erlang_root && (File.directory? erlang_root)
121
+ Dir.glob("#{erlang_root}/lib/erl_interface-*").first
122
+ elsif release_root && (File.directory? release_root)
123
+ Dir.glob("#{release_root}/lib/erl_interface-*").first
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,3 @@
1
+ module Erlnixify
2
+ VERSION = "0.0.1"
3
+ end
data/lib/erlnixify.rb ADDED
@@ -0,0 +1,24 @@
1
+ require "erlnixify/version"
2
+ require "erlnixify/opts"
3
+ require "erlnixify/settings"
4
+ require "erlnixify/node"
5
+
6
+ module Erlnixify
7
+
8
+ def main(args)
9
+ @opts = Opts.new(args)
10
+
11
+ if opts.version?
12
+ puts Erlnixify::VERSION
13
+ exit 0
14
+ end
15
+
16
+ @settings = Settings.new(opts)
17
+ @process = Process.new(@settings)
18
+ begin
19
+ @process.start
20
+ rescue Erlnixify::NodeError
21
+ exit 127
22
+ end
23
+ end
24
+ end
data/reek.yml ADDED
@@ -0,0 +1,8 @@
1
+ Duplication:
2
+ max_calls: 2
3
+
4
+ LongMethod:
5
+ max_statements: 10
6
+ exclude:
7
+ - initialize
8
+ enabled: true
data/settings.feature ADDED
@@ -0,0 +1,20 @@
1
+ Feature: Settings
2
+ In order to provide a 'templatable' settings file
3
+ As a developer
4
+ I want to be able to provide a settings file
5
+ and have the system warn me when I get settings wrong
6
+
7
+ Scenario: A basic yaml settings file
8
+ Given a settings file
9
+ When I load that settings file
10
+ Then the settings should be available in the settings object
11
+
12
+ Scenario: Command line options override config
13
+ Given an options object that contains config values
14
+ When a new settings file is loaded up using that options
15
+ Then the command line options override the file options
16
+
17
+ Scenario: Reasonable Default Settings
18
+ Given a lack of a config file and command line options
19
+ When a settings are loaded
20
+ Then that settings object contains sane defaults