smith 0.5.7
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/agency +55 -0
- data/bin/smithctl +102 -0
- data/lib/smith.rb +237 -0
- data/lib/smith/acl_compiler.rb +74 -0
- data/lib/smith/agent.rb +207 -0
- data/lib/smith/agent_cache.rb +40 -0
- data/lib/smith/agent_config.rb +22 -0
- data/lib/smith/agent_monitoring.rb +52 -0
- data/lib/smith/agent_process.rb +181 -0
- data/lib/smith/application/agency.rb +126 -0
- data/lib/smith/bootstrap.rb +153 -0
- data/lib/smith/cache.rb +61 -0
- data/lib/smith/command.rb +128 -0
- data/lib/smith/commands/agency/agents.rb +28 -0
- data/lib/smith/commands/agency/common.rb +18 -0
- data/lib/smith/commands/agency/kill.rb +13 -0
- data/lib/smith/commands/agency/list.rb +65 -0
- data/lib/smith/commands/agency/logger.rb +56 -0
- data/lib/smith/commands/agency/metadata.rb +14 -0
- data/lib/smith/commands/agency/restart.rb +39 -0
- data/lib/smith/commands/agency/start.rb +62 -0
- data/lib/smith/commands/agency/state.rb +14 -0
- data/lib/smith/commands/agency/stop.rb +70 -0
- data/lib/smith/commands/agency/version.rb +23 -0
- data/lib/smith/commands/smithctl/cat.rb +70 -0
- data/lib/smith/commands/smithctl/pop.rb +76 -0
- data/lib/smith/commands/smithctl/rm.rb +36 -0
- data/lib/smith/commands/smithctl/smithctl_version.rb +23 -0
- data/lib/smith/commands/smithctl/top.rb +42 -0
- data/lib/smith/commands/template.rb +9 -0
- data/lib/smith/config.rb +32 -0
- data/lib/smith/logger.rb +91 -0
- data/lib/smith/messaging/acl/agency_command.proto +5 -0
- data/lib/smith/messaging/acl/agent_command.proto +5 -0
- data/lib/smith/messaging/acl/agent_config_request.proto +4 -0
- data/lib/smith/messaging/acl/agent_config_update.proto +5 -0
- data/lib/smith/messaging/acl/agent_keepalive.proto +6 -0
- data/lib/smith/messaging/acl/agent_lifecycle.proto +12 -0
- data/lib/smith/messaging/acl/agent_stats.proto +14 -0
- data/lib/smith/messaging/acl/default.rb +51 -0
- data/lib/smith/messaging/acl/search.proto +9 -0
- data/lib/smith/messaging/amqp_options.rb +55 -0
- data/lib/smith/messaging/endpoint.rb +116 -0
- data/lib/smith/messaging/exceptions.rb +7 -0
- data/lib/smith/messaging/payload.rb +102 -0
- data/lib/smith/messaging/queue_factory.rb +67 -0
- data/lib/smith/messaging/receiver.rb +237 -0
- data/lib/smith/messaging/responder.rb +15 -0
- data/lib/smith/messaging/sender.rb +61 -0
- metadata +239 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require 'dm-core'
|
3
|
+
|
4
|
+
module Smith
|
5
|
+
class Agency
|
6
|
+
|
7
|
+
include Logger
|
8
|
+
|
9
|
+
attr_reader :agents, :agent_processes
|
10
|
+
|
11
|
+
def initialize(opts={})
|
12
|
+
DataMapper.setup(:default, "yaml:///#{Smith.config.agency.cache_path}")
|
13
|
+
|
14
|
+
@agent_processes = AgentCache.new(:paths => opts.delete(:paths))
|
15
|
+
end
|
16
|
+
|
17
|
+
def setup_queues
|
18
|
+
Messaging::Receiver.new('agency.control', :auto_delete => true, :durable => false, :strict => true).ready do |receiver|
|
19
|
+
receiver.subscribe do |r|
|
20
|
+
r.reply do |responder|
|
21
|
+
# Add a logger proc to the responder chain.
|
22
|
+
responder.callback { |ret| logger.debug { ret } if ret && !ret.empty? }
|
23
|
+
|
24
|
+
begin
|
25
|
+
Command.run(r.payload.command, r.payload.args, :agency => self, :agents => @agent_processes, :responder => responder)
|
26
|
+
rescue Command::UnkownCommandError => e
|
27
|
+
responder.value("Unknown command: #{r.payload.command}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
Messaging::Receiver.new('agent.lifecycle', :auto_delete => true, :durable => false).ready do |receiver|
|
34
|
+
receiver.subscribe do |r|
|
35
|
+
case r.payload.state
|
36
|
+
when 'dead'
|
37
|
+
dead(r.payload)
|
38
|
+
when 'acknowledge_start'
|
39
|
+
acknowledge_start(r.payload)
|
40
|
+
when 'acknowledge_stop'
|
41
|
+
acknowledge_stop(r.payload)
|
42
|
+
else
|
43
|
+
logger.warn { "Unkown command received on agent.lifecycle queue: #{r.payload.state}" }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
Messaging::Receiver.new('agent.keepalive', :auto_delete => true, :durable => false).ready do |receiver|
|
49
|
+
receiver.subscribe do |r|
|
50
|
+
keep_alive(r.payload)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def start_monitoring
|
56
|
+
@agent_monitor = AgentMonitoring.new(@agent_processes)
|
57
|
+
@agent_monitor.start_monitoring
|
58
|
+
end
|
59
|
+
|
60
|
+
# Stop the agency. This will wait for one second to ensure
|
61
|
+
# that any messages are flushed.
|
62
|
+
def stop
|
63
|
+
Smith.stop(true)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def acknowledge_start(agent_data)
|
69
|
+
@agent_processes[agent_data.name].tap do |agent_process|
|
70
|
+
if agent_data.pid == agent_process.pid
|
71
|
+
agent_process.monitor = agent_data.monitor
|
72
|
+
agent_process.singleton = agent_data.singleton
|
73
|
+
agent_process.metadata = agent_data.metadata
|
74
|
+
agent_process.acknowledge_start
|
75
|
+
else
|
76
|
+
logger.error { "Agent reports different pid during acknowledge_start: #{agent_data.name}" }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def acknowledge_stop(agent_data)
|
82
|
+
@agent_processes[agent_data.name].tap do |agent_process|
|
83
|
+
if agent_data.pid == agent_process.pid
|
84
|
+
agent_process.pid = nil
|
85
|
+
agent_process.monitor = nil
|
86
|
+
agent_process.singleton = nil
|
87
|
+
agent_process.started_at = nil
|
88
|
+
agent_process.last_keep_alive = nil
|
89
|
+
agent_process.acknowledge_stop
|
90
|
+
else
|
91
|
+
if agent_process.pid
|
92
|
+
logger.error { "Agent reports different pid during acknowledge_stop: #{agent_data.name}" }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def dead(agent_data)
|
99
|
+
@agent_processes[agent_data.name].no_process_running
|
100
|
+
logger.fatal { "Agent is dead: #{agent_data.name}" }
|
101
|
+
end
|
102
|
+
|
103
|
+
def keep_alive(agent_data)
|
104
|
+
@agent_processes[agent_data.name].tap do |agent_process|
|
105
|
+
if agent_data.pid == agent_process.pid
|
106
|
+
agent_process.last_keep_alive = agent_data.time
|
107
|
+
logger.verbose { "Agent keep alive: #{agent_data.name}: #{agent_data.time}" }
|
108
|
+
|
109
|
+
# We need to call save explicitly here as the keep alive is not part of
|
110
|
+
# the state_machine which is the thing that writes the state to disc.
|
111
|
+
agent_process.save
|
112
|
+
else
|
113
|
+
if agent_process.pid
|
114
|
+
logger.error { "Agent reports different pid during acknowledge_stop: #{agent_data.name}" }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# FIXME this doesn't work.
|
121
|
+
def delete_agent_process(agent_pid)
|
122
|
+
@agent_processes.invalidate(agent_pid)
|
123
|
+
AgentProcess.first('pid' => agent_pid).destroy!
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# This should never be run directly it should only ever be run by
|
4
|
+
# the agency.
|
5
|
+
|
6
|
+
$: << File.dirname(__FILE__) + '/..'
|
7
|
+
|
8
|
+
require 'smith'
|
9
|
+
|
10
|
+
module Smith
|
11
|
+
class AgentBootstrap
|
12
|
+
|
13
|
+
attr_reader :agent
|
14
|
+
|
15
|
+
include Logger
|
16
|
+
|
17
|
+
def initialize(path, agent_name)
|
18
|
+
# FIXME
|
19
|
+
# This doesn't do what I think it should. If an exception is
|
20
|
+
# thrown in setup_control_queue, for example, it just kills
|
21
|
+
# the agent without it actually raising the exception.
|
22
|
+
Thread.abort_on_exception = true
|
23
|
+
@agent_name = agent_name
|
24
|
+
@agent_filename = Pathname.new(path).join("#{agent_name.snake_case}.rb").expand_path
|
25
|
+
end
|
26
|
+
|
27
|
+
def signal_handlers
|
28
|
+
logger.debug { "Installing default signal handlers" }
|
29
|
+
%w{TERM INT QUIT}.each do |sig|
|
30
|
+
@agent.install_signal_handler(sig) do |sig|
|
31
|
+
logger.error { "Agent received: signal #{sig}: #{agent.name}" }
|
32
|
+
terminate!
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def load_agent
|
38
|
+
logger.debug { "Loading #{@agent_name} from: #{@agent_filename.dirname}" }
|
39
|
+
add_agent_load_path
|
40
|
+
load @agent_filename
|
41
|
+
@agent = Kernel.const_get(@agent_name).new
|
42
|
+
end
|
43
|
+
|
44
|
+
def start!
|
45
|
+
write_pid_file
|
46
|
+
@agent.run
|
47
|
+
@agent.started
|
48
|
+
end
|
49
|
+
|
50
|
+
# Exceptional shutdown of the agent. Note. Whenever this is
|
51
|
+
# called it almost certain that the reactor is not going to
|
52
|
+
# be running. So it must be restarted and then shutdown again
|
53
|
+
# See the note at the in main.
|
54
|
+
def terminate!(exception=nil)
|
55
|
+
logger.error { format_exception(exception) } if exception
|
56
|
+
logger.error { "Terminating: #{@agent_name}." }
|
57
|
+
|
58
|
+
if Smith.running?
|
59
|
+
send_dead_message
|
60
|
+
unlink_pid_file
|
61
|
+
Smith.stop
|
62
|
+
else
|
63
|
+
logger.debug { "Reconnecting to AMQP Broker." }
|
64
|
+
Smith.start do
|
65
|
+
send_dead_message
|
66
|
+
unlink_pid_file
|
67
|
+
Smith.stop
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Clean shutdown of the agent.
|
73
|
+
def shutdown
|
74
|
+
unlink_pid_file
|
75
|
+
Smith.stop if Smith.running?
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def write_pid_file
|
81
|
+
@pid = Daemons::PidFile.new(Daemons::Pid.dir(:normal, Dir::tmpdir, nil), ".rubymas-#{@agent_name.snake_case}", true)
|
82
|
+
@pid.pid = Process.pid
|
83
|
+
end
|
84
|
+
|
85
|
+
def send_dead_message
|
86
|
+
logger.debug { "Sending dead message to agency: #{@agent_name}" }
|
87
|
+
Messaging::Sender.new('agent.lifecycle', :auto_delete => true, :durable => false).ready do |sender|
|
88
|
+
sender.publish(ACL::Payload.new(:agent_lifecycle).content(:state => 'dead', :name => @agent_name))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def unlink_pid_file
|
93
|
+
if @pid && @pid.exist?
|
94
|
+
logger.debug { "Cleaning up pid file: #{@pid.filename}" }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def format_exception(exception)
|
99
|
+
str = "#{exception.class.to_s}: #{exception.message}\n\t"
|
100
|
+
if exception.backtrace
|
101
|
+
str << exception.backtrace[0..-1].join("\n\t")
|
102
|
+
end
|
103
|
+
str
|
104
|
+
end
|
105
|
+
|
106
|
+
# Add the ../lib to the load path. This assumes the directory
|
107
|
+
# structure is:
|
108
|
+
#
|
109
|
+
# $ROOT_PATH/agents
|
110
|
+
# .../lib
|
111
|
+
#
|
112
|
+
# where $ROOT_PATH can be anywhere.
|
113
|
+
#
|
114
|
+
# This needs to be better thought out.
|
115
|
+
# TODO think this through some more.
|
116
|
+
def add_agent_load_path
|
117
|
+
path = @agent_filename.dirname.dirname.join('lib')
|
118
|
+
# The load path may be a pathname or a string. Change to strings.
|
119
|
+
unless $:.detect { |p| p.to_s == path.to_s }
|
120
|
+
logger.debug { "Adding #{path} to load path" }
|
121
|
+
$: << path
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
path = ARGV[0]
|
128
|
+
agent_name = ARGV[1]
|
129
|
+
|
130
|
+
exit 1 if agent_name.nil? || path.nil?
|
131
|
+
|
132
|
+
# Set the running instance name to the name of the agent.
|
133
|
+
$0 = "#{agent_name}"
|
134
|
+
|
135
|
+
# load the acls
|
136
|
+
Smith.load_acls
|
137
|
+
|
138
|
+
bootstrapper = Smith::AgentBootstrap.new(path, agent_name)
|
139
|
+
|
140
|
+
# I've tried putting the exception handling in the main reactor loog
|
141
|
+
# but it doesn't do anything. I know theres a resaon for this but I
|
142
|
+
# don't what it is at the moment. Just beware that whenever there
|
143
|
+
# is an exception the recator is not going going to be running.
|
144
|
+
begin
|
145
|
+
Smith.start do
|
146
|
+
bootstrapper.load_agent
|
147
|
+
bootstrapper.signal_handlers
|
148
|
+
bootstrapper.start!
|
149
|
+
end
|
150
|
+
bootstrapper.shutdown
|
151
|
+
rescue Exception => e
|
152
|
+
bootstrapper.terminate!(e)
|
153
|
+
end
|
data/lib/smith/cache.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
module Smith
|
3
|
+
class Cache
|
4
|
+
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@cache = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def operator(operator)
|
12
|
+
@operator = operator
|
13
|
+
end
|
14
|
+
|
15
|
+
def entry(name, options=nil)
|
16
|
+
if @cache[name]
|
17
|
+
@cache[name]
|
18
|
+
else
|
19
|
+
if @operator.respond_to?(:call)
|
20
|
+
@cache[name] = @operator.call(name, options)
|
21
|
+
else
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
alias :[] :entry
|
28
|
+
|
29
|
+
def entries
|
30
|
+
@cache.keys.map(&:to_s)
|
31
|
+
end
|
32
|
+
|
33
|
+
def invalidate(name)
|
34
|
+
@cache.delete(name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def each
|
38
|
+
@cache.each_value { |v| yield v }
|
39
|
+
end
|
40
|
+
|
41
|
+
def empty?
|
42
|
+
@cache.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
def exist?(name)
|
46
|
+
!@cache[name].nil?
|
47
|
+
end
|
48
|
+
|
49
|
+
def size
|
50
|
+
@cache.size
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_s
|
54
|
+
@cache.to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
def update(name, entry)
|
58
|
+
@cache[name] = entry
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'trollop'
|
4
|
+
|
5
|
+
module Smith
|
6
|
+
class Command
|
7
|
+
class UnkownCommandError < RuntimeError; end
|
8
|
+
|
9
|
+
include Logger
|
10
|
+
|
11
|
+
# Load and run the command specified. This method takes:
|
12
|
+
# +target+ which is either a list of agents or the agency
|
13
|
+
# +vars+ variables to be passed in to the command. This takes the
|
14
|
+
# form of a hash and accessor methods are generated named after the
|
15
|
+
# key of the hash.
|
16
|
+
|
17
|
+
def self.run(command, args, vars)
|
18
|
+
# Change _ to - underscores look so ugly as a command name.
|
19
|
+
command = command.gsub(/-/, '_')
|
20
|
+
logger.debug { "Agency command: #{command}#{(args.empty?) ? '' : " #{args.join(', ')}"}." }
|
21
|
+
|
22
|
+
load_command(command)
|
23
|
+
|
24
|
+
clazz = Commands.const_get(Extlib::Inflection.camelize(command)).new
|
25
|
+
|
26
|
+
begin
|
27
|
+
options, target = parse_options(clazz, args)
|
28
|
+
|
29
|
+
vars.merge(:options => options, :target => target).each do |k,v|
|
30
|
+
clazz.instance_eval <<-EOM, __FILE__, __LINE__ + 1
|
31
|
+
instance_variable_set(:"@#{k}", v)
|
32
|
+
def #{k}=(z); @#{k} = z; end
|
33
|
+
def #{k}; @#{k}; end
|
34
|
+
EOM
|
35
|
+
end
|
36
|
+
|
37
|
+
clazz.execute
|
38
|
+
|
39
|
+
rescue Trollop::CommandlineError => e
|
40
|
+
vars[:responder].value(parser_help(clazz, :prefix => "Error: #{e.message}.\n"))
|
41
|
+
rescue Trollop::HelpNeeded
|
42
|
+
vars[:responder].value(parser_help(clazz))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Banner callback. This is called when the parser is called.
|
47
|
+
# I'm sure there is a better way of doing this.
|
48
|
+
def self.banner(command)
|
49
|
+
"smithctl #{command} OPTIONS [Agents]"
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Determine whether the command is an agency or smithctl command and load
|
55
|
+
# accordingly.
|
56
|
+
def self.load_command(cmd)
|
57
|
+
require command_path(cmd)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Check to see if the command is an agency or smithctl command.
|
61
|
+
def self.agency?
|
62
|
+
Smith.constants.include?(:Agency)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Return the full path of the ruby class.
|
66
|
+
def self.command_path(command)
|
67
|
+
send("#{command_type(command)}_path").join(command)
|
68
|
+
end
|
69
|
+
|
70
|
+
# What type of command is it?
|
71
|
+
def self.command_type(command)
|
72
|
+
case
|
73
|
+
when agency_command?(command)
|
74
|
+
:agency
|
75
|
+
when smithctl_command?(command)
|
76
|
+
:smithctl
|
77
|
+
else
|
78
|
+
raise UnkownCommandError, "Unknown command: #{command}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Is the command an agency command?
|
83
|
+
def self.agency_command?(cmd)
|
84
|
+
agency_path.join(cmd).sub_ext('.rb').exist?
|
85
|
+
end
|
86
|
+
|
87
|
+
# Is the command a smithctl command?
|
88
|
+
def self.smithctl_command?(cmd)
|
89
|
+
smithctl_path.join(cmd).sub_ext('.rb').exist?
|
90
|
+
end
|
91
|
+
|
92
|
+
# Return the agency command base path.
|
93
|
+
def self.agency_path
|
94
|
+
base_path.join('agency')
|
95
|
+
end
|
96
|
+
|
97
|
+
# Return the smithctl command base path.
|
98
|
+
def self.smithctl_path
|
99
|
+
base_path.join('smithctl')
|
100
|
+
end
|
101
|
+
|
102
|
+
# Return the command base path.
|
103
|
+
def self.base_path
|
104
|
+
@c64a6f4f ||= Smith.root_path.join('lib').join("smith").join('commands')
|
105
|
+
end
|
106
|
+
|
107
|
+
# Uses the options_parser method in the specific command class to procees
|
108
|
+
# any options associated with that command. If no options_parser method
|
109
|
+
# exits then an empty Array is returned. Any members of the args array
|
110
|
+
# that are not parsed by the options parser are return as the target, i.e.
|
111
|
+
# the agent(s) that the command is to operate on.
|
112
|
+
def self.parse_options(clazz, args)
|
113
|
+
if clazz.respond_to?(:options_parser)
|
114
|
+
[clazz.options_parser.parse(args), args]
|
115
|
+
else
|
116
|
+
[{}, args]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.parser_help(clazz, opts={})
|
121
|
+
StringIO.new.tap do |help|
|
122
|
+
help.puts opts[:prefix] if opts[:prefix]
|
123
|
+
clazz.options_parser.educate(help)
|
124
|
+
help.rewind
|
125
|
+
end.read
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|