erlnixify 0.0.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,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