pylon 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/lib/pylon.rb ADDED
@@ -0,0 +1,19 @@
1
+ # Author:: AJ Christensen (<aj@junglist.gen.nz>)
2
+ # Copyright:: Copyright (c) 2011 AJ Christensen
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or#implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ class Pylon
18
+ VERSION = "0.1.1"
19
+ end
@@ -0,0 +1,161 @@
1
+ #
2
+ # Author:: AJ Christensen (<aj@junglist.gen.nz>)
3
+ # Copyright:: Copyright (c) 2011 AJ Christensen
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or#implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require "mixlib/cli"
20
+ require_relative "config"
21
+ require_relative "log"
22
+ require_relative "daemon"
23
+ require_relative "elector"
24
+
25
+ class Pylon
26
+ class Application
27
+ class << self
28
+ def fatal! message, error_code = -1
29
+ Pylon::Log.fatal message
30
+ Process.exit error_code
31
+ end
32
+
33
+ end
34
+
35
+ include Mixlib::CLI
36
+
37
+ def configure_logging
38
+ Pylon::Log.init(Pylon::Config[:log_location])
39
+ if ( Pylon::Config[:log_location] != STDOUT ) && STDOUT.tty? && (!Pylon::Config[:daemonize])
40
+ stdout_logger = Logger.new(STDOUT)
41
+ STDOUT.sync = true
42
+ stdout_logger.formatter = Pylon::Log.logger.formatter
43
+ Pylon::Log.loggers << stdout_logger
44
+ end
45
+ Pylon::Log.level = Pylon::Config[:log_level]
46
+ end
47
+
48
+ def run argv=ARGV
49
+ parse_options(argv)
50
+ Pylon::Config.merge!(config)
51
+ configure_logging
52
+ Pylon::Log.info "Pylon #{Pylon::VERSION} warming up"
53
+
54
+ Pylon::Daemon.change_privilege
55
+ Pylon::Daemon.daemonize "pylon" if Pylon::Config[:daemonize]
56
+
57
+ elector = Pylon::Elector.new
58
+ end
59
+
60
+ def initialize
61
+ super
62
+ trap("TERM") do
63
+ Pylon::Application.fatal!("SIGTERM received, stopping", 1)
64
+ end
65
+
66
+ trap("INT") do
67
+ Pylon::Application.fatal!("SIGINT received, stopping", 2)
68
+ end
69
+
70
+ unless RUBY_PLATFORM =~ /mswin|mingw32|windows/
71
+ trap("QUIT") do
72
+ Pylon::Log.info("SIGQUIT received, call stack:\n " + caller.join("\n "))
73
+ end
74
+
75
+ trap("HUP") do
76
+ Pylon::Log.info("SIGHUP received, reconfiguring")
77
+ # reconfigure
78
+ end
79
+ end
80
+ end
81
+
82
+ option :config_file,
83
+ :short => "-c CONFIG",
84
+ :long => "--config CONFIG",
85
+ :default => 'config.rb',
86
+ :description => "The configuration file to use"
87
+
88
+ option :log_level,
89
+ :short => "-l LEVEL",
90
+ :long => "--log_level LEVEL",
91
+ :description => "Set the log level (debug, info, warn, error, fatal)",
92
+ :required => true,
93
+ :proc => Proc.new { |l| l.to_sym }
94
+
95
+ option :help,
96
+ :short => "-h",
97
+ :long => "--help",
98
+ :description => "Show this message",
99
+ :on => :tail,
100
+ :boolean => true,
101
+ :show_options => true,
102
+ :exit => 0
103
+
104
+ option :daemonize,
105
+ :short => "-d",
106
+ :long => "--daemonize",
107
+ :description => "send pylon to the background",
108
+ :proc => lambda { |p| true }
109
+
110
+ option :user,
111
+ :short => "-u USER",
112
+ :long => "--user USER",
113
+ :description => "User to set privilege to",
114
+ :proc => nil
115
+
116
+ option :group,
117
+ :short => "-g GROUP",
118
+ :long => "--group GROUP",
119
+ :description => "Group to set privilege to",
120
+ :proc => nil
121
+
122
+ option :multicast_address,
123
+ :short => "-a ADDRESS",
124
+ :long => "--multicast-address ADDRESS",
125
+ :description => "Address to use for UDP multicast"
126
+
127
+ option :multicast_port,
128
+ :short => "-p PORT",
129
+ :long => "--multicast-port PORT",
130
+ :description => "Port to use for UDP multicast"
131
+
132
+ option :multicast_loopback,
133
+ :short => "-L",
134
+ :long => "--multicast-loopback",
135
+ :description => "Enable multicast over loopback interfaces",
136
+ :proc => lambda { |loop| true }
137
+
138
+ option :interface,
139
+ :short => "-i INTERFACE",
140
+ :long => "--interface INTERFACE",
141
+ :description => "Interface to use to send multicast over"
142
+
143
+ option :tcp_address,
144
+ :short => "-t TCPADDRESS",
145
+ :long => "--tcp-address TCPADDRESS",
146
+ :description => "Interface to use to bind request socket to"
147
+
148
+ option :tcp_port,
149
+ :short => "-P TCPPORT",
150
+ :long => "--tcp-port TCPPORT",
151
+ :description => "Port to bind request socket to"
152
+
153
+ option :minimum_master_nodes,
154
+ :short => "-m NODES",
155
+ :long => "--minimum-master-nodes NODES",
156
+ :description => "How many nodes to wait for before starting master election",
157
+ :proc => lambda { |nodes| nodes.to_i }
158
+
159
+ end
160
+ end
161
+
@@ -0,0 +1,61 @@
1
+ #
2
+ # Author:: AJ Christensen (<aj@junglist.gen.nz>)
3
+ # Copyright:: Copyright (c) 2011 AJ Christensen
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or#implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ require "mixlib/config"
19
+
20
+ class Pylon
21
+ class Config
22
+ extend Mixlib::Config
23
+
24
+ config_file "~/.pylon.rb"
25
+ log_level :info
26
+ log_location STDOUT
27
+ daemonize false
28
+ user nil
29
+ group nil
30
+ umask 0022
31
+
32
+ # Options for the multicast server
33
+ multicast true
34
+ multicast_address "225.4.5.6"
35
+ multicast_port "13336"
36
+ multicast_ttl 3
37
+ multicast_listen_address nil
38
+ multicast_loopback false
39
+ interface_address "127.0.0.1"
40
+
41
+ # TCP settings
42
+ tcp_address "*"
43
+ tcp_port "13335"
44
+ tcp_retries 10
45
+ tcp_timeout 5
46
+
47
+ # cluster settings
48
+ maximum_weight 1000
49
+ cluster_name "pylon"
50
+ Seed_tcp_endpoints []
51
+ master nil
52
+ minimum_master_nodes 1
53
+ sleep_after_announce 5
54
+
55
+ # TODO: not implemented
56
+ ping_interval 1
57
+ ping_timeout 30
58
+ ping_retries 3
59
+
60
+ end
61
+ end
@@ -0,0 +1,171 @@
1
+ #
2
+ # Author:: AJ Christensen (<aj@junglist.gen.nz>)
3
+ # Copyright:: Copyright (c) 2011 AJ Christensen
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ require "etc"
19
+ require_relative "application"
20
+ require_relative "config"
21
+
22
+ class Pylon
23
+ class Daemon
24
+ class << self
25
+ attr_accessor :name
26
+
27
+ # Daemonize the current process, managing pidfiles and process uid/gid
28
+ #
29
+ # === Parameters
30
+ # name<String>:: The name to be used for the pid file
31
+ #
32
+ def daemonize(name)
33
+ @name = name
34
+ pid = pid_from_file
35
+ unless running?
36
+ remove_pid_file()
37
+ Pylon::Log.info("Daemonizing..")
38
+ begin
39
+ exit if fork
40
+ Process.setsid
41
+ exit if fork
42
+ Pylon::Log.info("Forked, in #{Process.pid}. Priveleges: #{Process.euid} #{Process.egid}")
43
+ File.umask Pylon::Config[:umask]
44
+ $stdin.reopen("/dev/null")
45
+ $stdout.reopen("/dev/null", "a")
46
+ $stderr.reopen($stdout)
47
+ save_pid_file
48
+ at_exit { remove_pid_file }
49
+ rescue NotImplementedError => e
50
+ Pylon::Application.fatal!("There is no fork: #{e.message}")
51
+ end
52
+ else
53
+ Pylon::Application.fatal!("Pylon is already running pid #{pid}")
54
+ end
55
+ end
56
+
57
+ # Check if Pylon is running based on the pid_file
58
+ # ==== Returns
59
+ # Boolean::
60
+ # True if Pylon is running
61
+ # False if Pylon is not running
62
+ #
63
+ def running?
64
+ if pid_from_file.nil?
65
+ false
66
+ else
67
+ Process.kill(0, pid_from_file)
68
+ true
69
+ end
70
+ rescue Errno::ESRCH, Errno::ENOENT
71
+ false
72
+ rescue Errno::EACCES => e
73
+ Pylon::Application.fatal!("You don't have access to the PID file at #{pid_file}: #{e.message}")
74
+ end
75
+
76
+ # Gets the pid file for @name
77
+ # ==== Returns
78
+ # String::
79
+ # Location of the pid file for @name
80
+ def pid_file
81
+ Pylon::Config[:pid_file] or "/tmp/#{@name}.pid"
82
+ end
83
+
84
+ # Suck the pid out of pid_file
85
+ # ==== Returns
86
+ # Integer::
87
+ # The PID from pid_file
88
+ # nil::
89
+ # Returned if the pid_file does not exist.
90
+ #
91
+ def pid_from_file
92
+ File.read(pid_file).chomp.to_i
93
+ rescue Errno::ENOENT, Errno::EACCES
94
+ nil
95
+ end
96
+
97
+ # Store the PID on the filesystem
98
+ # This uses the Pylon::Config[:pid_file] option, or "/tmp/name.pid" otherwise
99
+ #
100
+ def save_pid_file
101
+ file = pid_file
102
+ begin
103
+ FileUtils.mkdir_p(File.dirname(file))
104
+ rescue Errno::EACCES => e
105
+ Pylon::Application.fatal!("Failed store pid in #{File.dirname(file)}, permission denied: #{e.message}")
106
+ end
107
+
108
+ begin
109
+ File.open(file, "w") { |f| f.write(Process.pid.to_s) }
110
+ rescue Errno::EACCES => e
111
+ Pylon::Application.fatal!("Couldn't write to pidfile #{file}, permission denied: #{e.message}")
112
+ end
113
+ end
114
+
115
+ # Delete the PID from the filesystem
116
+ def remove_pid_file
117
+ FileUtils.rm(pid_file) if File.exists?(pid_file)
118
+ end
119
+
120
+ # Change process user/group to those specified in Pylon::Config
121
+ #
122
+ def change_privilege
123
+ Dir.chdir("/")
124
+
125
+ if Pylon::Config[:user] and Pylon::Config[:group]
126
+ Pylon::Log.info("About to change privilege to #{Pylon::Config[:user]}:#{Pylon::Config[:group]}")
127
+ _change_privilege(Pylon::Config[:user], Pylon::Config[:group])
128
+ elsif Pylon::Config[:user]
129
+ Pylon::Log.info("About to change privilege to #{Pylon::Config[:user]}")
130
+ _change_privilege(Pylon::Config[:user])
131
+ end
132
+ end
133
+
134
+ # Change privileges of the process to be the specified user and group
135
+ #
136
+ # ==== Parameters
137
+ # user<String>:: The user to change the process to.
138
+ # group<String>:: The group to change the process to.
139
+ #
140
+ # ==== Alternatives
141
+ # If group is left out, the user will be used (changing to user:user)
142
+ #
143
+ def _change_privilege(user, group=user)
144
+ uid, gid = Process.euid, Process.egid
145
+
146
+ begin
147
+ target_uid = Etc.getpwnam(user).uid
148
+ rescue ArgumentError => e
149
+ Pylon::Application.fatal!("Failed to get UID for user #{user}, does it exist? #{e.message}")
150
+ return false
151
+ end
152
+
153
+ begin
154
+ target_gid = Etc.getgrnam(group).gid
155
+ rescue ArgumentError => e
156
+ Pylon::Application.fatal!("Failed to get GID for group #{group}, does it exist? #{e.message}")
157
+ return false
158
+ end
159
+
160
+ if (uid != target_uid) or (gid != target_gid)
161
+ Process.initgroups(user, target_gid)
162
+ Process::GID.change_privilege(target_gid)
163
+ Process::UID.change_privilege(target_uid)
164
+ end
165
+ true
166
+ rescue Errno::EPERM => e
167
+ Pylon::Application.fatal!("Permission denied when trying to change #{uid}:#{gid} to #{target_uid}:#{target_gid}. #{e.message}")
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,164 @@
1
+ #
2
+ # Author:: AJ Christensen (<aj@junglist.gen.nz>)
3
+ # Copyright:: Copyright (c) 2011 AJ Christensen
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or#implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ require "uuidtools"
19
+ require "ffi-rzmq"
20
+ require "thread"
21
+ require "json"
22
+ require_relative "node"
23
+
24
+ class Pylon
25
+ class Elector
26
+
27
+ attr_accessor :cluster_name, :context, :multicast_endpoint, :node, :nodes, :multicast_announcer_thread, :multicast_listener_thread, :tcp_listener_thread, :master
28
+
29
+ def initialize
30
+ @cluster_name = Pylon::Config[:cluster_name]
31
+ @context = ZMQ::Context.new(1)
32
+
33
+ @node = Pylon::Node.new
34
+ @nodes = Array(@node)
35
+ @master = Pylon::Config[:master]
36
+
37
+ Pylon::Log.info "elector[#{cluster_name}] initialized, starting pub/sub sockets on #{multicast_endpoint} and tcp listener socket on #{node.unicast_endpoint}"
38
+
39
+ Thread.abort_on_exception = true
40
+
41
+ scheduler = Thread.new do
42
+ @unicast_announcer_thread = node.unicast_announcer
43
+ @multicast_announcer_thread = node.multicast_announcer
44
+ @multicast_listener_thread = multicast_listener
45
+
46
+ @multicast_listener_thread.join
47
+ @unicast_announcer_thread.join
48
+ @multicast_announcer_thread.join
49
+ end
50
+ scheduler.join
51
+
52
+ # join listeners
53
+ # @unicast_announcer_thread.join
54
+ # @tcp_listener_thread.join
55
+ # sleep 5
56
+ # @multicast_announcer_thread.join
57
+
58
+ at_exit do
59
+ Log.debug "cleaning up zeromq context"
60
+ @context.terminate
61
+ end
62
+ end
63
+
64
+ def stop_announcer
65
+ @multicast_announcer_thread.stop
66
+ end
67
+
68
+ def run_announcer
69
+ @multicast_announcer.thread.run
70
+ end
71
+
72
+ def pause_listeners
73
+ @tcp_listener_thread.stop
74
+ @multicast_listener_thread.stop
75
+ end
76
+
77
+ def run_listeners
78
+ @tcp_listener_thread.run
79
+ @multicst_listener_thread.run
80
+ end
81
+
82
+ def unicast_endpoint
83
+ @unicast_endpoint ||= "tcp://#{Pylon::Config[:tcp_address]}:#{Pylon::Config[:tcp_port]}"
84
+ end
85
+
86
+ def multicast_endpoint
87
+ @multicast_endpoint ||= "epgm://#{Pylon::Config[:interface]};#{Pylon::Config[:multicast_address]}:#{Pylon::Config[:multicast_port]}"
88
+ end
89
+
90
+ def add_node node
91
+ @nodes << node unless @nodes.include? node
92
+ end
93
+
94
+ def multicast_listener
95
+ Thread.new do
96
+ Log.debug "multicast_listener: zeromq sub socket starting up on #{@multicast_endpoint}"
97
+ sub_socket = context.socket ZMQ::SUB
98
+ sub_socket.setsockopt ZMQ::IDENTITY, "node"
99
+ sub_socket.setsockopt ZMQ::SUBSCRIBE, ""
100
+ sub_socket.setsockopt ZMQ::RATE, 1000
101
+ sub_socket.setsockopt ZMQ::MCAST_LOOP, Pylon::Config[:multicast_loopback]
102
+ sub_socket.connect multicast_endpoint
103
+ loop do
104
+ uuid = sub_socket.recv_string
105
+ Log.debug "multicast_listener: handling announce from #{uuid}"
106
+ handle_announce sub_socket.recv_string if sub_socket.more_parts?
107
+ end
108
+ end
109
+ end
110
+
111
+ def allocate_master
112
+ if node.uuid == nodes.sort.first.uuid
113
+ @master = true
114
+ Log.info "allocate_master: master allocated"
115
+ nodes.each do |node|
116
+ connect_node node.uuid.to_s, node.unicast_endpoint
117
+ end
118
+ else
119
+ Log.info "allocate_master: someone else is the master, getting ready for work"
120
+ @master = false
121
+ end
122
+ end
123
+
124
+ def handle_announce recv_string
125
+ Log.info "handle_announce: got string #{recv_string}"
126
+ node = JSON.parse(recv_string)
127
+ Log.info "handle_anounce: got announce from #{node}"
128
+ if master
129
+ Log.info "handle_announce: I am the master: updating #{node} of leadership status"
130
+ connect_node(node.uuid.to_s, node.unicast_endpoint)
131
+ elsif nodes.length < Pylon::Config[:minimum_master_nodes]
132
+ if nodes.include? node
133
+ Log.info "handle_announce: skipping node #{node}, already known"
134
+ Log.debug "handle_announce: nodes: #{nodes}"
135
+ else
136
+ Log.info "handle_announce: subscribing for #{node} on endpoint: #{node.unicast_endpoint}"
137
+ connect_node(node.uuid.to_s, node.unicast_endpoint)
138
+ end
139
+ else
140
+ allocate_master
141
+ end
142
+ end
143
+
144
+ def connect_node uuid, endpoint
145
+ Log.debug "connect_node: subscribe socket connecting to #{endpoint}"
146
+ sub_socket = context.socket ZMQ::SUB
147
+ sub_socket.setsockopt ZMQ::IDENTITY, uuid
148
+ sub_socket.setsockopt ZMQ::SUBSCRIBE, uuid
149
+ sub_socket.connect endpoint
150
+ uuid = sub_socket.recv_string
151
+ Log.debug "connect_node: got uuid on sub socket, parsing node"
152
+ new_node = JSON.parse(sub_socket.recv_string) if sub_socket.more_parts?
153
+ Log.debug "connect_node: node: #{new_node}"
154
+ if nodes.include? new_node
155
+ Log.info "connect_node: skipping node #{new_node}, already in local list, sleeping for 60 secs"
156
+ Log.debug "connect_node: nodes: #{nodes}"
157
+ sleep 60
158
+ else
159
+ Log.info "connect_node: connected to node #{new_node}, adding to local list"
160
+ add_node new_node
161
+ end
162
+ end
163
+ end
164
+ end