pantry 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +9 -0
- data/.ruby-version +1 -0
- data/.travis.yml +19 -0
- data/Gemfile +15 -0
- data/Guardfile +16 -0
- data/LICENSE +20 -0
- data/README.md +53 -0
- data/Rakefile +18 -0
- data/Vagrantfile +86 -0
- data/bin/pantry +11 -0
- data/bin/pantry-client +38 -0
- data/bin/pantry-server +33 -0
- data/dist/client.yml +79 -0
- data/dist/server.yml +56 -0
- data/dist/upstart/pantry-client.conf +12 -0
- data/dist/upstart/pantry-server.conf +12 -0
- data/doc/message_packet.dot +19 -0
- data/doc/message_packet.dot.png +0 -0
- data/doc/network_topology.dot +42 -0
- data/doc/network_topology.dot.png +0 -0
- data/lib/celluloid_zmq_patches.rb +16 -0
- data/lib/opt_parse_plus.rb +184 -0
- data/lib/pantry.rb +197 -0
- data/lib/pantry/cli.rb +154 -0
- data/lib/pantry/client.rb +131 -0
- data/lib/pantry/client_info.rb +34 -0
- data/lib/pantry/client_registry.rb +104 -0
- data/lib/pantry/command.rb +194 -0
- data/lib/pantry/command_handler.rb +53 -0
- data/lib/pantry/command_line.rb +115 -0
- data/lib/pantry/commands/create_client.rb +30 -0
- data/lib/pantry/commands/download_directory.rb +35 -0
- data/lib/pantry/commands/echo.rb +32 -0
- data/lib/pantry/commands/edit_application.rb +60 -0
- data/lib/pantry/commands/register_client.rb +38 -0
- data/lib/pantry/commands/status.rb +78 -0
- data/lib/pantry/commands/sync_directory.rb +50 -0
- data/lib/pantry/commands/update_application.rb +45 -0
- data/lib/pantry/commands/upload_file.rb +68 -0
- data/lib/pantry/communication.rb +20 -0
- data/lib/pantry/communication/client.rb +75 -0
- data/lib/pantry/communication/client_filter.rb +117 -0
- data/lib/pantry/communication/file_service.rb +125 -0
- data/lib/pantry/communication/file_service/file_progress.rb +164 -0
- data/lib/pantry/communication/file_service/receive_file.rb +97 -0
- data/lib/pantry/communication/file_service/send_file.rb +74 -0
- data/lib/pantry/communication/publish_socket.rb +20 -0
- data/lib/pantry/communication/reading_socket.rb +89 -0
- data/lib/pantry/communication/receive_socket.rb +23 -0
- data/lib/pantry/communication/security.rb +44 -0
- data/lib/pantry/communication/security/authentication.rb +98 -0
- data/lib/pantry/communication/security/curve_key_store.rb +120 -0
- data/lib/pantry/communication/security/curve_security.rb +70 -0
- data/lib/pantry/communication/security/null_security.rb +32 -0
- data/lib/pantry/communication/send_socket.rb +19 -0
- data/lib/pantry/communication/serialize_message.rb +84 -0
- data/lib/pantry/communication/server.rb +97 -0
- data/lib/pantry/communication/subscribe_socket.rb +33 -0
- data/lib/pantry/communication/wait_list.rb +45 -0
- data/lib/pantry/communication/writing_socket.rb +46 -0
- data/lib/pantry/config.rb +182 -0
- data/lib/pantry/file_editor.rb +67 -0
- data/lib/pantry/logger.rb +78 -0
- data/lib/pantry/message.rb +134 -0
- data/lib/pantry/multi_command.rb +36 -0
- data/lib/pantry/server.rb +132 -0
- data/lib/pantry/test/acceptance.rb +83 -0
- data/lib/pantry/test/support/fake_fs.rb +31 -0
- data/lib/pantry/test/support/matchers.rb +13 -0
- data/lib/pantry/test/support/minitest.rb +13 -0
- data/lib/pantry/test/support/mock_ui.rb +23 -0
- data/lib/pantry/test/unit.rb +13 -0
- data/lib/pantry/ui.rb +68 -0
- data/lib/pantry/version.rb +3 -0
- data/pantry.gemspec +40 -0
- data/test/acceptance/cli/error_handling_test.rb +7 -0
- data/test/acceptance/cli/execute_command_on_clients_test.rb +32 -0
- data/test/acceptance/cli/request_info_from_server_test.rb +44 -0
- data/test/acceptance/communication/client_requests_info_from_server_test.rb +28 -0
- data/test/acceptance/communication/heartbeat_test.rb +19 -0
- data/test/acceptance/communication/pub_sub_communication_test.rb +53 -0
- data/test/acceptance/communication/security_test.rb +117 -0
- data/test/acceptance/communication/server_requests_info_from_client_test.rb +41 -0
- data/test/acceptance/test_helper.rb +25 -0
- data/test/fixtures/config.yml +22 -0
- data/test/fixtures/empty.yml +2 -0
- data/test/fixtures/file_to_upload +3 -0
- data/test/root_dir/.gitkeep +0 -0
- data/test/unit/cli_test.rb +173 -0
- data/test/unit/client_registry_test.rb +61 -0
- data/test/unit/client_test.rb +128 -0
- data/test/unit/command_handler_test.rb +79 -0
- data/test/unit/command_line_test.rb +5 -0
- data/test/unit/command_test.rb +206 -0
- data/test/unit/commands/create_client_test.rb +25 -0
- data/test/unit/commands/download_directory_test.rb +58 -0
- data/test/unit/commands/echo_test.rb +22 -0
- data/test/unit/commands/edit_application_test.rb +84 -0
- data/test/unit/commands/register_client_test.rb +41 -0
- data/test/unit/commands/status_test.rb +81 -0
- data/test/unit/commands/sync_directory_test.rb +75 -0
- data/test/unit/commands/update_application_test.rb +35 -0
- data/test/unit/commands/upload_file_test.rb +51 -0
- data/test/unit/communication/client_filter_test.rb +262 -0
- data/test/unit/communication/client_test.rb +99 -0
- data/test/unit/communication/file_service/receive_file_test.rb +214 -0
- data/test/unit/communication/file_service/send_file_test.rb +110 -0
- data/test/unit/communication/file_service_test.rb +56 -0
- data/test/unit/communication/publish_socket_test.rb +19 -0
- data/test/unit/communication/reading_socket_test.rb +110 -0
- data/test/unit/communication/receive_socket_test.rb +20 -0
- data/test/unit/communication/security/authentication_test.rb +97 -0
- data/test/unit/communication/security/curve_key_store_test.rb +110 -0
- data/test/unit/communication/security/curve_security_test.rb +44 -0
- data/test/unit/communication/security/null_security_test.rb +15 -0
- data/test/unit/communication/security_test.rb +49 -0
- data/test/unit/communication/send_socket_test.rb +19 -0
- data/test/unit/communication/serialize_message_test.rb +128 -0
- data/test/unit/communication/server_test.rb +106 -0
- data/test/unit/communication/subscribe_socket_test.rb +46 -0
- data/test/unit/communication/wait_list_test.rb +60 -0
- data/test/unit/communication/writing_socket_test.rb +46 -0
- data/test/unit/config_test.rb +150 -0
- data/test/unit/logger_test.rb +79 -0
- data/test/unit/message_test.rb +179 -0
- data/test/unit/multi_command_test.rb +45 -0
- data/test/unit/opt_parse_plus_test.rb +218 -0
- data/test/unit/pantry_test.rb +82 -0
- data/test/unit/server_test.rb +166 -0
- data/test/unit/test_helper.rb +25 -0
- data/test/unit/ui_test.rb +58 -0
- metadata +389 -13
data/lib/pantry/cli.rb
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
module Pantry
|
2
|
+
|
3
|
+
# Pantry's Command Line Interface.
|
4
|
+
class CLI < Client
|
5
|
+
|
6
|
+
def initialize(command_line, **args)
|
7
|
+
@command_line = Pantry::CommandLine.new(command_line)
|
8
|
+
|
9
|
+
args[:identity] ||= ENV["USER"]
|
10
|
+
super(**args)
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
prepare_local_pantry_root
|
15
|
+
|
16
|
+
options, arguments = @command_line.parse!
|
17
|
+
if options && process_global_options(options)
|
18
|
+
super
|
19
|
+
perform(options, arguments)
|
20
|
+
end
|
21
|
+
|
22
|
+
terminate
|
23
|
+
end
|
24
|
+
|
25
|
+
def prepare_local_pantry_root
|
26
|
+
if Pantry.config.root_dir.nil?
|
27
|
+
# TODO Find a .pantry up the chain vs building one
|
28
|
+
Pantry.config.root_dir = File.join(Dir.pwd, ".pantry")
|
29
|
+
FileUtils.mkdir_p(Pantry.root)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def process_global_options(options)
|
34
|
+
if options["verbose"]
|
35
|
+
Pantry.config.log_level = :info
|
36
|
+
Pantry.config.refresh
|
37
|
+
end
|
38
|
+
|
39
|
+
if options["debug"]
|
40
|
+
Pantry.config.log_level = :debug
|
41
|
+
Pantry.config.refresh
|
42
|
+
end
|
43
|
+
|
44
|
+
if server_host = options["host"]
|
45
|
+
Pantry.config.server_host = server_host
|
46
|
+
end
|
47
|
+
|
48
|
+
if curve_key_file = options["curve-key-file"]
|
49
|
+
Pantry.config.security = "curve"
|
50
|
+
copy_keys_file_into_pantry_root(curve_key_file)
|
51
|
+
end
|
52
|
+
|
53
|
+
if options["version"]
|
54
|
+
Pantry.ui.say Pantry::VERSION
|
55
|
+
terminate
|
56
|
+
return false
|
57
|
+
end
|
58
|
+
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
# Given the parsed options and the arguments left over,
|
63
|
+
# figure out what Command was requested, build up the appropriate structures
|
64
|
+
# and start the communication process.
|
65
|
+
def perform(options, arguments)
|
66
|
+
command_info, command_options = @command_line.triggered_command
|
67
|
+
|
68
|
+
if command_info
|
69
|
+
client_filter = Pantry::Communication::ClientFilter.new(
|
70
|
+
application: options['application'],
|
71
|
+
environment: options['environment'],
|
72
|
+
roles: options['roles']
|
73
|
+
)
|
74
|
+
|
75
|
+
command = command_info[:class].new(*arguments)
|
76
|
+
command_options = command_options.merge(options)
|
77
|
+
|
78
|
+
request(client_filter, command, command_options)
|
79
|
+
else
|
80
|
+
$stderr.puts "I don't know the #{arguments.first} command"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Fire off the requested Command.
|
85
|
+
def request(filter, command, options)
|
86
|
+
@command = command
|
87
|
+
@command.client = self
|
88
|
+
|
89
|
+
# We don't use send_request here because we don't want to deal with the
|
90
|
+
# wait-list future system. This lets command objects handle responses
|
91
|
+
# as they come back to the CLI as the command sees fit.
|
92
|
+
# If the command isn't meant directly for the Server, the Server will always
|
93
|
+
# respond first with the list of clients who will be executing the command
|
94
|
+
# and responding with the results. See Pantry::Commands::Echo for an example of how
|
95
|
+
# to work with this flow.
|
96
|
+
begin
|
97
|
+
message = @command.prepare_message(options)
|
98
|
+
message.to = filter.stream
|
99
|
+
message.requires_response!
|
100
|
+
|
101
|
+
send_message(message)
|
102
|
+
|
103
|
+
@command.wait_for_finish
|
104
|
+
|
105
|
+
copy_full_keys_back_to_curve_key_file
|
106
|
+
rescue Exception => ex
|
107
|
+
Pantry.ui.say("Error: #{ex.message}")
|
108
|
+
Pantry.logger.debug(ex.backtrace.join("\n"))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# All messages received by this client are assumed to be responses
|
113
|
+
# from previous commands.
|
114
|
+
def receive_message(message)
|
115
|
+
if @command
|
116
|
+
@command.receive_response(message)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
protected
|
121
|
+
|
122
|
+
#
|
123
|
+
# For the sake of being a Client, the file mentioned in --curve-key-file is
|
124
|
+
# copied into security/curve/client_keys.yml so that we don't have to do anything
|
125
|
+
# special to turn on Curve encryption for the Client. We do this for every run to ensure
|
126
|
+
# that if the keys or options change, the user isn't confused when the client is unable to
|
127
|
+
# connect or auth.
|
128
|
+
#
|
129
|
+
# To facilitate the first-time-connect situation, where a new Pantry Server has been spun up
|
130
|
+
# and the only known key is the server's public key, this will also copy the keys file back
|
131
|
+
# up to the file named in --curve-key-file because it will fill that file with a set of
|
132
|
+
# generated public and private keys.
|
133
|
+
#
|
134
|
+
|
135
|
+
def copy_keys_file_into_pantry_root(curve_key_file)
|
136
|
+
@curve_key_file = curve_key_file
|
137
|
+
FileUtils.mkdir_p(Pantry.root.join("security", "curve"))
|
138
|
+
FileUtils.cp(
|
139
|
+
Pantry.root.join(@curve_key_file),
|
140
|
+
Pantry.root.join("security", "curve", "client_keys.yml")
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
def copy_full_keys_back_to_curve_key_file
|
145
|
+
return unless @curve_key_file
|
146
|
+
FileUtils.cp(
|
147
|
+
Pantry.root.join("security", "curve", "client_keys.yml"),
|
148
|
+
Pantry.root.join(@curve_key_file)
|
149
|
+
)
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Pantry
|
2
|
+
|
3
|
+
# The Pantry Client.
|
4
|
+
# The Client runs on any server that needs provisioning,
|
5
|
+
# and communicates to the Server through various channels. Clients can
|
6
|
+
# be further configured to manage an application, for a given environment,
|
7
|
+
# and across any number of roles.
|
8
|
+
class Client
|
9
|
+
extend Forwardable
|
10
|
+
include Celluloid
|
11
|
+
finalizer :shutdown
|
12
|
+
|
13
|
+
# See Pantry::ClientInfo
|
14
|
+
def_delegators :@info, :identity, :application, :environment, :roles, :filter
|
15
|
+
|
16
|
+
# For testing / debugging purposes, keep hold of the last message this client received
|
17
|
+
attr_reader :last_received_message
|
18
|
+
|
19
|
+
def initialize(application: nil, environment: nil, roles: [], identity: nil, network_stack_class: Communication::Client)
|
20
|
+
@info = Pantry::ClientInfo.new(
|
21
|
+
application: application,
|
22
|
+
environment: environment,
|
23
|
+
roles: roles || [],
|
24
|
+
identity: identity || current_hostname
|
25
|
+
)
|
26
|
+
|
27
|
+
@commands = CommandHandler.new(self, Pantry.client_commands)
|
28
|
+
@networking = network_stack_class.new_link(self)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Start up the Client.
|
32
|
+
# This sets up the appropriate communication channels to the
|
33
|
+
# server, sends a registration message so the Server knows who
|
34
|
+
# just connected, and then waits for information to come.
|
35
|
+
def run
|
36
|
+
Pantry.set_proc_title("pantry client #{Pantry::VERSION} :: #{identity}")
|
37
|
+
@networking.run
|
38
|
+
send_registration_message
|
39
|
+
Pantry.logger.info("[#{identity}] Client registered and waiting for commands")
|
40
|
+
end
|
41
|
+
|
42
|
+
def shutdown
|
43
|
+
Pantry.logger.info("[#{identity}] Client Shutting down")
|
44
|
+
@registration_timer.cancel if @registration_timer
|
45
|
+
end
|
46
|
+
|
47
|
+
# Callback from Networking when a message is received
|
48
|
+
def receive_message(message)
|
49
|
+
Pantry.logger.debug("[#{identity}] Received message #{message.inspect}")
|
50
|
+
|
51
|
+
if message_meant_for_us?(message)
|
52
|
+
@last_received_message = message
|
53
|
+
results = @commands.process(message)
|
54
|
+
|
55
|
+
if message.requires_response?
|
56
|
+
Pantry.logger.debug("[#{identity}] Responding with #{results.inspect}")
|
57
|
+
send_results_back_to_requester(message, results)
|
58
|
+
end
|
59
|
+
else
|
60
|
+
Pantry.logger.debug("[#{identity}] Message discarded, not for us")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Send a Pantry::Message directly to its intended recipient.
|
65
|
+
# For a Client this is almost always the Server.
|
66
|
+
def send_message(message)
|
67
|
+
@networking.send_message(message)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Send a Pantry::Message but mark it as requiring a response.
|
71
|
+
# This will set up and return a Celluloid::Future that will contain the
|
72
|
+
# response once it is available.
|
73
|
+
def send_request(message)
|
74
|
+
message.requires_response!
|
75
|
+
|
76
|
+
Pantry.logger.debug("[#{identity}] Sending request #{message.inspect}")
|
77
|
+
|
78
|
+
@networking.send_request(message)
|
79
|
+
end
|
80
|
+
|
81
|
+
# See Pantry::Server#receive_file
|
82
|
+
def receive_file(file_size, file_checksum)
|
83
|
+
@networking.receive_file(file_size, file_checksum)
|
84
|
+
end
|
85
|
+
|
86
|
+
# See Pantry::Server#send_file
|
87
|
+
def send_file(file_path, receiver_uuid, file_uuid)
|
88
|
+
@networking.send_file(file_path, receiver_uuid, file_uuid)
|
89
|
+
end
|
90
|
+
|
91
|
+
protected
|
92
|
+
|
93
|
+
def current_hostname
|
94
|
+
Socket.gethostname
|
95
|
+
end
|
96
|
+
|
97
|
+
def send_registration_message
|
98
|
+
@networking.send_message(
|
99
|
+
Pantry::Commands::RegisterClient.new(self).to_message
|
100
|
+
)
|
101
|
+
@registration_timer =
|
102
|
+
after(Pantry.config.client_heartbeat_interval) { send_registration_message }
|
103
|
+
end
|
104
|
+
|
105
|
+
def send_results_back_to_requester(message, results)
|
106
|
+
response_message = message.build_response
|
107
|
+
|
108
|
+
[results].flatten(1).each do |entry|
|
109
|
+
response_message << entry
|
110
|
+
end
|
111
|
+
|
112
|
+
@networking.send_message(response_message)
|
113
|
+
end
|
114
|
+
|
115
|
+
# ZeroMQ's Pub/Sub topic matching is too simplistic to catch all the cases we
|
116
|
+
# need to handle. Given that if *any* topic matches the incoming message, we get
|
117
|
+
# the message even if it wasn't exactly meant for us. For example, if this client
|
118
|
+
# subscribes to the following topics:
|
119
|
+
#
|
120
|
+
# * pantry
|
121
|
+
# * pantry.test
|
122
|
+
# * pantry.test.app
|
123
|
+
#
|
124
|
+
# This client will receive messages sent to "pantry.test.web" because "pantry" and
|
125
|
+
# "pantry.test" both match (string start_with? check) the message. Thus, we add our
|
126
|
+
# own handling to the message check as a protective stop gap.
|
127
|
+
def message_meant_for_us?(message)
|
128
|
+
filter.matches?(message.to)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Pantry
|
2
|
+
|
3
|
+
# Simple class to keep track of a given client's identifying information
|
4
|
+
class ClientInfo
|
5
|
+
attr_reader :application
|
6
|
+
|
7
|
+
attr_reader :environment
|
8
|
+
|
9
|
+
attr_reader :roles
|
10
|
+
|
11
|
+
# The above gets packaged into a ClientFilter for use elsewhere
|
12
|
+
attr_reader :filter
|
13
|
+
|
14
|
+
# This client's current identity. By default a client's identity is it's `hostname`,
|
15
|
+
# but a specific one can be given. These identities should be unique across the set
|
16
|
+
# of clients connecting to a single Pantry Server, behavior of multiple clients with
|
17
|
+
# the same identity is currently undefined.
|
18
|
+
attr_reader :identity
|
19
|
+
|
20
|
+
def initialize(application: nil, environment: nil, roles: [], identity: nil)
|
21
|
+
@application = application
|
22
|
+
@environment = environment
|
23
|
+
@roles = roles
|
24
|
+
@identity = identity
|
25
|
+
|
26
|
+
@filter = Pantry::Communication::ClientFilter.new(
|
27
|
+
application: @application,
|
28
|
+
environment: @environment,
|
29
|
+
roles: @roles,
|
30
|
+
identity: @identity
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Pantry
|
2
|
+
|
3
|
+
# The ClientRegistry keeps track of clients who've checked in and supports
|
4
|
+
# various querying requests against the list of known clients.
|
5
|
+
class ClientRegistry
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
clear!
|
9
|
+
end
|
10
|
+
|
11
|
+
# Return all known clients
|
12
|
+
def all
|
13
|
+
@clients.map {|identity, record| record.client }
|
14
|
+
end
|
15
|
+
|
16
|
+
# Clear out the registry entirely
|
17
|
+
def clear!
|
18
|
+
@clients = Hash.new {|hash, key| hash[key] = ClientRecord.new }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Check in a client
|
22
|
+
def check_in(client)
|
23
|
+
@clients[client.identity].check_in(client)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Has the given client checked in?
|
27
|
+
def include?(client)
|
28
|
+
@clients[client.identity].checked_in?
|
29
|
+
end
|
30
|
+
|
31
|
+
# Find info for Client that matches the given identity
|
32
|
+
def find(identity)
|
33
|
+
if found = @clients[identity]
|
34
|
+
found.client
|
35
|
+
else
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Find and return all clients who will receive messages
|
41
|
+
# on the given stream or ClientFilter.
|
42
|
+
#
|
43
|
+
# If this method is given a block, the block will be processed as
|
44
|
+
# a #map of the list of clients and records. Block expected to be
|
45
|
+
# of the form:
|
46
|
+
#
|
47
|
+
# all_matching(filter) do |client, record|
|
48
|
+
# ...
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# The `record` contains internal knowledge of the Client's activity.
|
52
|
+
# See ClientRecord for what's contained.
|
53
|
+
def all_matching(stream_or_filter)
|
54
|
+
found_client_records =
|
55
|
+
case stream_or_filter
|
56
|
+
when String
|
57
|
+
select_records_matching do |record|
|
58
|
+
record.client.filter.matches?(stream_or_filter)
|
59
|
+
end
|
60
|
+
else
|
61
|
+
select_records_matching do |record|
|
62
|
+
stream_or_filter.includes?(record.client.filter)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
if block_given?
|
67
|
+
found_client_records.map do |record|
|
68
|
+
yield(record.client, record)
|
69
|
+
end
|
70
|
+
else
|
71
|
+
found_client_records.map(&:client)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
def select_records_matching
|
78
|
+
selected_records = @clients.clone.select do |identity, record|
|
79
|
+
yield(record)
|
80
|
+
end
|
81
|
+
|
82
|
+
selected_records.values
|
83
|
+
end
|
84
|
+
|
85
|
+
class ClientRecord
|
86
|
+
attr_reader :client, :last_checked_in_at
|
87
|
+
|
88
|
+
def initialize
|
89
|
+
@client = nil
|
90
|
+
@last_checked_in_at = nil
|
91
|
+
end
|
92
|
+
|
93
|
+
def check_in(client)
|
94
|
+
@client = client
|
95
|
+
@last_checked_in_at = Time.now
|
96
|
+
end
|
97
|
+
|
98
|
+
def checked_in?
|
99
|
+
!@last_checked_in_at.nil?
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
module Pantry
|
2
|
+
|
3
|
+
# Commands are where the task-specific functionality is implemented, the
|
4
|
+
# core of how Pantry works. All Commands are required to implement the #perform method,
|
5
|
+
# which receives the Pantry::Message requesting the Command.
|
6
|
+
#
|
7
|
+
# All commands must be registered with Pantry before they will be available for execution.
|
8
|
+
# Use Pantry.add_client_command or Pantry.add_server_command to register the Command.
|
9
|
+
class Command
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Expose this Command to the CLI and configure the options and information
|
13
|
+
# that this Command needs from the CLI to function.
|
14
|
+
#
|
15
|
+
# See OptParsePlus for documentation
|
16
|
+
def command(name, &block)
|
17
|
+
@command_name = name
|
18
|
+
@command_config = block
|
19
|
+
end
|
20
|
+
attr_reader :command_name
|
21
|
+
attr_reader :command_config
|
22
|
+
end
|
23
|
+
|
24
|
+
# Initialize this Command
|
25
|
+
# Due to the multiple ways a Command is instantiated (via the CLI and from the Network stack)
|
26
|
+
# any Command initializer must support being called with zero parameters if it expects some.
|
27
|
+
def initialize(*args)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Set up the Message that needs to be created to send this Command
|
31
|
+
# to the server to be processed. Used by the CLI. This method is given
|
32
|
+
# the ClientFilter of which clients should respond to this message (if any) and
|
33
|
+
# the extra arguments given on the command line.
|
34
|
+
#
|
35
|
+
# If work needs to be done prior to the network communication for CLI use,
|
36
|
+
# override method to take care of that logic.
|
37
|
+
#
|
38
|
+
# The message returned here is then passed through the network to the appropriate
|
39
|
+
# recipients (Clients, Server, or both) and used to trigger #perform on said
|
40
|
+
# recipient.
|
41
|
+
def prepare_message(options)
|
42
|
+
to_message
|
43
|
+
end
|
44
|
+
|
45
|
+
# Run whatever this command needs to do.
|
46
|
+
# All Command subclasses must implement this method.
|
47
|
+
# If the message triggering this Command expects a response, the return
|
48
|
+
# value of this method will be that response.
|
49
|
+
def perform(message)
|
50
|
+
end
|
51
|
+
|
52
|
+
# When a message comes back from the server as a response to or because of
|
53
|
+
# this command's #perform, the command object on the CLI will receive that
|
54
|
+
# message here. This method will dispatch to either #receive_server_response
|
55
|
+
# or #receive_client_response depending on the type of Command run.
|
56
|
+
# In most cases, Commands should override the server/client specific receivers.
|
57
|
+
# Only override this method to fully customize Message response handling.
|
58
|
+
def receive_response(response)
|
59
|
+
@response_tracker ||= TrackResponses.new
|
60
|
+
@response_tracker.new_response(response)
|
61
|
+
|
62
|
+
if @response_tracker.from_server?
|
63
|
+
receive_server_response(response)
|
64
|
+
finished
|
65
|
+
elsif @response_tracker.from_client?
|
66
|
+
receive_client_response(response)
|
67
|
+
finished if @response_tracker.all_response_received?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Handle a response from a Server Command. Override this for specific handling
|
72
|
+
# of Server Command responses.
|
73
|
+
def receive_server_response(response)
|
74
|
+
Pantry.ui.say("Server response:")
|
75
|
+
Pantry.ui.say(response.body.inspect)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Handle a response from a Client Command. This will be called for each Client
|
79
|
+
# executing and responding to the requested Command.
|
80
|
+
def receive_client_response(response)
|
81
|
+
Pantry.ui.say("Response from #{response.from}:")
|
82
|
+
Pantry.ui.say(response.body.inspect)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Send a request out, returning the Future which will eventually
|
86
|
+
# contain the response Message
|
87
|
+
def send_request(message)
|
88
|
+
@server_or_client.send_request(message)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Send a request out and wait for the response. Will return the response
|
92
|
+
# once it is received.
|
93
|
+
#
|
94
|
+
# This is a blocking call.
|
95
|
+
def send_request!(message)
|
96
|
+
send_request(message).value
|
97
|
+
end
|
98
|
+
|
99
|
+
# Create a new Message from the information in the current Command
|
100
|
+
def to_message
|
101
|
+
Pantry::Message.new(self.class.message_type)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Blocking call that returns when the command has completed
|
105
|
+
# Can be given a timeout (in seconds) to wait for a response
|
106
|
+
def wait_for_finish(timeout = nil)
|
107
|
+
completion_future.value(timeout)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Notify all listeners that this command has completed its tasks
|
111
|
+
def finished
|
112
|
+
completion_future.signal(OpenStruct.new(:value => nil))
|
113
|
+
end
|
114
|
+
|
115
|
+
# Is this command finished?
|
116
|
+
def finished?
|
117
|
+
completion_future.ready?
|
118
|
+
end
|
119
|
+
|
120
|
+
def completion_future
|
121
|
+
@completion_future ||= Celluloid::Future.new
|
122
|
+
end
|
123
|
+
protected :completion_future
|
124
|
+
|
125
|
+
# The Type of this command, used to differentiate Messages.
|
126
|
+
# Defaults to the full scope of the name, though with the special
|
127
|
+
# case of removing any "Pantry" related scoping such as Pantry::
|
128
|
+
# and Pantry::Commands::
|
129
|
+
#
|
130
|
+
# This value must be unique across the system or the messages will not
|
131
|
+
# be processed reliably.
|
132
|
+
#
|
133
|
+
# Override this for a custom name.
|
134
|
+
def self.message_type
|
135
|
+
self.name.gsub(/Pantry::Commands::/, '').gsub(/Pantry::/, '')
|
136
|
+
end
|
137
|
+
|
138
|
+
# Set a link back to the Server or Client that's handling
|
139
|
+
# this command. This will be set by the CommandHandler before calling
|
140
|
+
# #perform.
|
141
|
+
def server_or_client=(server_or_client)
|
142
|
+
@server_or_client = server_or_client
|
143
|
+
end
|
144
|
+
alias client= server_or_client=
|
145
|
+
alias server= server_or_client=
|
146
|
+
|
147
|
+
# Get the server or client object handling this command
|
148
|
+
def server_or_client
|
149
|
+
@server_or_client
|
150
|
+
end
|
151
|
+
alias server server_or_client
|
152
|
+
alias client server_or_client
|
153
|
+
|
154
|
+
protected
|
155
|
+
|
156
|
+
# Internal state tracking of server and client responses.
|
157
|
+
# When a Client Command is triggered, the Server first responses with a message
|
158
|
+
# containing the list of Clients who will execute the Command and respond.
|
159
|
+
# Then we need to keep track of all the Clients who have responded so we know
|
160
|
+
# when the command has fully finished across all Clients.
|
161
|
+
class TrackResponses
|
162
|
+
def initialize
|
163
|
+
@expected_clients = []
|
164
|
+
@received_from_clients = []
|
165
|
+
end
|
166
|
+
|
167
|
+
def new_response(response)
|
168
|
+
@latest_response = response
|
169
|
+
|
170
|
+
if response[:client_response_list]
|
171
|
+
@expected_clients = response.body
|
172
|
+
count = @expected_clients.length
|
173
|
+
Pantry.ui.say("Expecting response from #{count} client#{count == 1 ? "" : "s"}...")
|
174
|
+
elsif from_client?
|
175
|
+
@received_from_clients << response
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def from_server?
|
180
|
+
@latest_response.from_server? && !@latest_response[:client_response_list]
|
181
|
+
end
|
182
|
+
|
183
|
+
def from_client?
|
184
|
+
!@latest_response.from_server?
|
185
|
+
end
|
186
|
+
|
187
|
+
def all_response_received?
|
188
|
+
!@expected_clients.empty? && @expected_clients.length == @received_from_clients.length
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|