pantry 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +9 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +19 -0
  5. data/Gemfile +15 -0
  6. data/Guardfile +16 -0
  7. data/LICENSE +20 -0
  8. data/README.md +53 -0
  9. data/Rakefile +18 -0
  10. data/Vagrantfile +86 -0
  11. data/bin/pantry +11 -0
  12. data/bin/pantry-client +38 -0
  13. data/bin/pantry-server +33 -0
  14. data/dist/client.yml +79 -0
  15. data/dist/server.yml +56 -0
  16. data/dist/upstart/pantry-client.conf +12 -0
  17. data/dist/upstart/pantry-server.conf +12 -0
  18. data/doc/message_packet.dot +19 -0
  19. data/doc/message_packet.dot.png +0 -0
  20. data/doc/network_topology.dot +42 -0
  21. data/doc/network_topology.dot.png +0 -0
  22. data/lib/celluloid_zmq_patches.rb +16 -0
  23. data/lib/opt_parse_plus.rb +184 -0
  24. data/lib/pantry.rb +197 -0
  25. data/lib/pantry/cli.rb +154 -0
  26. data/lib/pantry/client.rb +131 -0
  27. data/lib/pantry/client_info.rb +34 -0
  28. data/lib/pantry/client_registry.rb +104 -0
  29. data/lib/pantry/command.rb +194 -0
  30. data/lib/pantry/command_handler.rb +53 -0
  31. data/lib/pantry/command_line.rb +115 -0
  32. data/lib/pantry/commands/create_client.rb +30 -0
  33. data/lib/pantry/commands/download_directory.rb +35 -0
  34. data/lib/pantry/commands/echo.rb +32 -0
  35. data/lib/pantry/commands/edit_application.rb +60 -0
  36. data/lib/pantry/commands/register_client.rb +38 -0
  37. data/lib/pantry/commands/status.rb +78 -0
  38. data/lib/pantry/commands/sync_directory.rb +50 -0
  39. data/lib/pantry/commands/update_application.rb +45 -0
  40. data/lib/pantry/commands/upload_file.rb +68 -0
  41. data/lib/pantry/communication.rb +20 -0
  42. data/lib/pantry/communication/client.rb +75 -0
  43. data/lib/pantry/communication/client_filter.rb +117 -0
  44. data/lib/pantry/communication/file_service.rb +125 -0
  45. data/lib/pantry/communication/file_service/file_progress.rb +164 -0
  46. data/lib/pantry/communication/file_service/receive_file.rb +97 -0
  47. data/lib/pantry/communication/file_service/send_file.rb +74 -0
  48. data/lib/pantry/communication/publish_socket.rb +20 -0
  49. data/lib/pantry/communication/reading_socket.rb +89 -0
  50. data/lib/pantry/communication/receive_socket.rb +23 -0
  51. data/lib/pantry/communication/security.rb +44 -0
  52. data/lib/pantry/communication/security/authentication.rb +98 -0
  53. data/lib/pantry/communication/security/curve_key_store.rb +120 -0
  54. data/lib/pantry/communication/security/curve_security.rb +70 -0
  55. data/lib/pantry/communication/security/null_security.rb +32 -0
  56. data/lib/pantry/communication/send_socket.rb +19 -0
  57. data/lib/pantry/communication/serialize_message.rb +84 -0
  58. data/lib/pantry/communication/server.rb +97 -0
  59. data/lib/pantry/communication/subscribe_socket.rb +33 -0
  60. data/lib/pantry/communication/wait_list.rb +45 -0
  61. data/lib/pantry/communication/writing_socket.rb +46 -0
  62. data/lib/pantry/config.rb +182 -0
  63. data/lib/pantry/file_editor.rb +67 -0
  64. data/lib/pantry/logger.rb +78 -0
  65. data/lib/pantry/message.rb +134 -0
  66. data/lib/pantry/multi_command.rb +36 -0
  67. data/lib/pantry/server.rb +132 -0
  68. data/lib/pantry/test/acceptance.rb +83 -0
  69. data/lib/pantry/test/support/fake_fs.rb +31 -0
  70. data/lib/pantry/test/support/matchers.rb +13 -0
  71. data/lib/pantry/test/support/minitest.rb +13 -0
  72. data/lib/pantry/test/support/mock_ui.rb +23 -0
  73. data/lib/pantry/test/unit.rb +13 -0
  74. data/lib/pantry/ui.rb +68 -0
  75. data/lib/pantry/version.rb +3 -0
  76. data/pantry.gemspec +40 -0
  77. data/test/acceptance/cli/error_handling_test.rb +7 -0
  78. data/test/acceptance/cli/execute_command_on_clients_test.rb +32 -0
  79. data/test/acceptance/cli/request_info_from_server_test.rb +44 -0
  80. data/test/acceptance/communication/client_requests_info_from_server_test.rb +28 -0
  81. data/test/acceptance/communication/heartbeat_test.rb +19 -0
  82. data/test/acceptance/communication/pub_sub_communication_test.rb +53 -0
  83. data/test/acceptance/communication/security_test.rb +117 -0
  84. data/test/acceptance/communication/server_requests_info_from_client_test.rb +41 -0
  85. data/test/acceptance/test_helper.rb +25 -0
  86. data/test/fixtures/config.yml +22 -0
  87. data/test/fixtures/empty.yml +2 -0
  88. data/test/fixtures/file_to_upload +3 -0
  89. data/test/root_dir/.gitkeep +0 -0
  90. data/test/unit/cli_test.rb +173 -0
  91. data/test/unit/client_registry_test.rb +61 -0
  92. data/test/unit/client_test.rb +128 -0
  93. data/test/unit/command_handler_test.rb +79 -0
  94. data/test/unit/command_line_test.rb +5 -0
  95. data/test/unit/command_test.rb +206 -0
  96. data/test/unit/commands/create_client_test.rb +25 -0
  97. data/test/unit/commands/download_directory_test.rb +58 -0
  98. data/test/unit/commands/echo_test.rb +22 -0
  99. data/test/unit/commands/edit_application_test.rb +84 -0
  100. data/test/unit/commands/register_client_test.rb +41 -0
  101. data/test/unit/commands/status_test.rb +81 -0
  102. data/test/unit/commands/sync_directory_test.rb +75 -0
  103. data/test/unit/commands/update_application_test.rb +35 -0
  104. data/test/unit/commands/upload_file_test.rb +51 -0
  105. data/test/unit/communication/client_filter_test.rb +262 -0
  106. data/test/unit/communication/client_test.rb +99 -0
  107. data/test/unit/communication/file_service/receive_file_test.rb +214 -0
  108. data/test/unit/communication/file_service/send_file_test.rb +110 -0
  109. data/test/unit/communication/file_service_test.rb +56 -0
  110. data/test/unit/communication/publish_socket_test.rb +19 -0
  111. data/test/unit/communication/reading_socket_test.rb +110 -0
  112. data/test/unit/communication/receive_socket_test.rb +20 -0
  113. data/test/unit/communication/security/authentication_test.rb +97 -0
  114. data/test/unit/communication/security/curve_key_store_test.rb +110 -0
  115. data/test/unit/communication/security/curve_security_test.rb +44 -0
  116. data/test/unit/communication/security/null_security_test.rb +15 -0
  117. data/test/unit/communication/security_test.rb +49 -0
  118. data/test/unit/communication/send_socket_test.rb +19 -0
  119. data/test/unit/communication/serialize_message_test.rb +128 -0
  120. data/test/unit/communication/server_test.rb +106 -0
  121. data/test/unit/communication/subscribe_socket_test.rb +46 -0
  122. data/test/unit/communication/wait_list_test.rb +60 -0
  123. data/test/unit/communication/writing_socket_test.rb +46 -0
  124. data/test/unit/config_test.rb +150 -0
  125. data/test/unit/logger_test.rb +79 -0
  126. data/test/unit/message_test.rb +179 -0
  127. data/test/unit/multi_command_test.rb +45 -0
  128. data/test/unit/opt_parse_plus_test.rb +218 -0
  129. data/test/unit/pantry_test.rb +82 -0
  130. data/test/unit/server_test.rb +166 -0
  131. data/test/unit/test_helper.rb +25 -0
  132. data/test/unit/ui_test.rb +58 -0
  133. metadata +389 -13
@@ -0,0 +1,78 @@
1
+ module Pantry
2
+
3
+ # Access Pantry's logger.
4
+ def self.logger
5
+ @@logger ||= Pantry::Logger.new
6
+ end
7
+
8
+ def self.logger=(logger)
9
+ @@logger = logger
10
+ end
11
+
12
+ # Wrapper around the Celluloid's logging system. Depending on the passed in
13
+ # config, will send to STDOUT, Syslog, or a given file.
14
+ # See Celluloid::Logger for API (should be the same as Ruby's Logger API)
15
+ class Logger
16
+
17
+ def initialize(config = Pantry.config)
18
+ logger =
19
+ if config.log_to.nil? || config.log_to == "stdout"
20
+ ::Logger.new(STDOUT)
21
+ elsif config.log_to == "syslog"
22
+ ::Syslog::Logger.new(config.syslog_program_name)
23
+ else
24
+ ::Logger.new(config.log_to)
25
+ end
26
+
27
+ logger.level = log_level(config.log_level)
28
+ Celluloid.logger = logger
29
+ end
30
+
31
+ # Turn off all logging entirely
32
+ def disable!
33
+ Celluloid.logger = NullLogger.new
34
+ end
35
+
36
+ # Forward all methods on to the internal Celluloid Logger.
37
+ def method_missing(*args)
38
+ Celluloid.logger.send(*args) if Celluloid.logger
39
+ end
40
+
41
+ protected
42
+
43
+ def log_level(log_level_string)
44
+ case log_level_string.to_s
45
+ when "debug"
46
+ ::Logger::DEBUG
47
+ when "info"
48
+ ::Logger::INFO
49
+ when "warn"
50
+ ::Logger::WARN
51
+ when "error"
52
+ ::Logger::ERROR
53
+ when "fatal"
54
+ ::Logger::FATAL
55
+ else
56
+ raise "Unknown log level given: #{log_level_string}"
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ # Because Celluloid tries to log things on shut-down that throw
63
+ # tons of exceptions if the logger is nil
64
+ class NullLogger
65
+ def debug(*args)
66
+ end
67
+
68
+ def info(*args)
69
+ end
70
+
71
+ def warn(*args)
72
+ end
73
+
74
+ def error(*args)
75
+ end
76
+ end
77
+
78
+ end
@@ -0,0 +1,134 @@
1
+ module Pantry
2
+
3
+ # A Message is the container for all network communication between Clients and Servers.
4
+ # Messages know what stream they've been sent down, have a type to differentiate them
5
+ # from each other, and an arbitrarily large body.
6
+ #
7
+ # Every message has three sections, the stream, metadata, and body. The stream defines
8
+ # where the message needs to go. The metadata defines information about the message, its
9
+ # type, if it needs a response, and anything else that doesn't go in the body.
10
+ # The body is the request message itself and can be one or many parts.
11
+ class Message
12
+
13
+ # Unique identifier for this Message. Automatically generated
14
+ attr_reader :uuid
15
+
16
+ # Where or who is this message intended for (Can be an identity or a stream)
17
+ # Defaults to the catch-all stream `""`
18
+ attr_accessor :to
19
+
20
+ # Who is this message coming from (Should be an identity)
21
+ attr_accessor :from
22
+
23
+ # What type of message are we?
24
+ attr_accessor :type
25
+
26
+ # The full, raw body of the message.
27
+ attr_accessor :body
28
+
29
+ attr_accessor :custom_metadata
30
+
31
+ attr_writer :requires_response
32
+
33
+ def initialize(message_type = nil)
34
+ @type = message_type
35
+ @body = []
36
+ @to = ""
37
+
38
+ @requires_response = false
39
+ @forwarded = false
40
+
41
+ @custom_metadata = {}
42
+
43
+ @uuid = SecureRandom.uuid
44
+ end
45
+
46
+ # Set the source of this message either by an object that responds to #identity
47
+ # or a string.
48
+ def from=(source)
49
+ if source.respond_to?(:identity)
50
+ @from = source.identity
51
+ else
52
+ @from = source
53
+ end
54
+ end
55
+
56
+ def from_server?
57
+ @from == Pantry::SERVER_IDENTITY
58
+ end
59
+
60
+ # Flag this message as requiring a response
61
+ def requires_response!
62
+ @requires_response = true
63
+ end
64
+
65
+ # Does this message require a response message?
66
+ def requires_response?
67
+ @requires_response
68
+ end
69
+
70
+ # Has this message been forwarded through the Server?
71
+ # This flag is checked when the message comes back through the Server,
72
+ # which lets it know if the message needs to continue back to another Client.
73
+ def forwarded!
74
+ @forwarded = true
75
+ end
76
+
77
+ def forwarded?
78
+ @forwarded
79
+ end
80
+
81
+ # Set custom metadata on this message.
82
+ def []=(key, val)
83
+ @custom_metadata[key] = val
84
+ end
85
+
86
+ # Access value from the custom metadata
87
+ def [](key)
88
+ @custom_metadata[key]
89
+ end
90
+
91
+ # Build a copy of this message to use when responding
92
+ # to the message
93
+ def build_response
94
+ response = self.clone
95
+ response.body = []
96
+ response.to = self.from
97
+ response.from = self.to
98
+ response.requires_response = false
99
+ response.custom_metadata = self.custom_metadata.clone
100
+ response
101
+ end
102
+
103
+ # Add a message part to this Message's body
104
+ def <<(part)
105
+ @body << part
106
+ end
107
+
108
+ # Return all of this message's metadata as a hash
109
+ def metadata
110
+ {
111
+ :uuid => self.uuid,
112
+ :type => self.type,
113
+ :from => self.from,
114
+ :to => self.to || "",
115
+ :requires_response => self.requires_response?,
116
+ :forwarded => self.forwarded?,
117
+ :custom => @custom_metadata
118
+ }
119
+ end
120
+
121
+ # Given a hash, pull out the parts into local variables
122
+ def metadata=(hash)
123
+ @uuid = hash[:uuid]
124
+ @type = hash[:type]
125
+ @from = hash[:from]
126
+ @to = hash[:to] || ""
127
+ @requires_response = hash[:requires_response]
128
+ @forwarded = hash[:forwarded]
129
+ @custom_metadata = hash[:custom]
130
+ end
131
+
132
+ end
133
+
134
+ end
@@ -0,0 +1,36 @@
1
+ module Pantry
2
+
3
+ # A MultiCommand allows specifying multiple Commands to be run in succession.
4
+ # Each command class given in .performs will have it's #perform executed and
5
+ # the return values will be grouped together in a single return Message.
6
+ #
7
+ # It's currently expected that each Command executed is done when it's #perform
8
+ # returns.
9
+ class MultiCommand < Command
10
+
11
+ # MultiCommand.performs takes a list of Command class constants.
12
+ def self.performs(command_classes = [])
13
+ @command_classes = command_classes
14
+ end
15
+
16
+ def self.command_classes
17
+ @command_classes
18
+ end
19
+
20
+ # Iterate through each Command class and run that Command with the
21
+ # given message. The results of each Command will be combined into a single
22
+ # array return value and thus a single response Message back to the requester.
23
+ def perform(message)
24
+ Pantry.logger.debug("[#{client.identity}] Running MultiCommands")
25
+
26
+ self.class.command_classes.map do |command_class|
27
+ Pantry.logger.debug("[#{client.identity}] Running #{command_class}")
28
+ command = command_class.new
29
+ command.server_or_client = server_or_client
30
+ command.perform(message)
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,132 @@
1
+ module Pantry
2
+
3
+ # The Pantry Server.
4
+ class Server
5
+ include Celluloid
6
+ finalizer :shutdown
7
+
8
+ attr_accessor :identity
9
+
10
+ attr_reader :client_registry
11
+
12
+ def initialize(network_stack_class = Communication::Server)
13
+ @commands = CommandHandler.new(self, Pantry.server_commands)
14
+ @identity = current_hostname
15
+ @clients = []
16
+
17
+ @client_registry = ClientRegistry.new
18
+
19
+ @networking = network_stack_class.new_link(self)
20
+ end
21
+
22
+ # Start up the networking stack and start the server
23
+ def run
24
+ Pantry.set_proc_title("pantry server #{Pantry::VERSION}")
25
+ @networking.run
26
+ Pantry.logger.info("[#{@identity}] Server running")
27
+ end
28
+
29
+ # Close down networking and clean up resources
30
+ def shutdown
31
+ Pantry.logger.info("[#{@identity}] Server Shutting Down")
32
+ end
33
+
34
+ # Mark an authenticated client as checked-in
35
+ def register_client(client)
36
+ Pantry.logger.info("[#{@identity}] Received client registration :: #{client.identity}")
37
+ @client_registry.check_in(client)
38
+ end
39
+
40
+ # Generate new authentication credentials for a client.
41
+ # Returns a Hash containing the credentials required for the client to
42
+ # connect and authenticate with this Server
43
+ def create_client
44
+ @networking.create_client
45
+ end
46
+
47
+ # Return ClientInfo on which Client sent the given Message
48
+ def client_who_sent(message)
49
+ @client_registry.find(message.from)
50
+ end
51
+
52
+ # Broadcast a +message+ to all clients who match the given +filter+.
53
+ # By default all clients will be notified.
54
+ def publish_message(message, filter = Communication::ClientFilter.new)
55
+ Pantry.logger.debug("[#{@identity}] Publishing #{message.inspect} to #{filter.stream.inspect}")
56
+ message.to = filter.stream
57
+ @networking.publish_message(message)
58
+ end
59
+
60
+ # Callback from the network when a message is received unsolicited from a client.
61
+ # If the message received is unhandleable by this Server, the message is forwarded
62
+ # on down to the clients who match the message's +to+.
63
+ def receive_message(message)
64
+ Pantry.logger.debug("[#{@identity}] Received message #{message.inspect}")
65
+ if @commands.can_handle?(message)
66
+ results = @commands.process(message)
67
+
68
+ if message.requires_response?
69
+ Pantry.logger.debug("[#{@identity}] Returning results #{results.inspect}")
70
+ send_results_back_to_requester(message, results)
71
+ end
72
+ else
73
+ matched_clients = @client_registry.all_matching(message.to).map(&:identity)
74
+
75
+ Pantry.logger.debug("[#{@identity}] Forwarding message on. Expect response from #{matched_clients.inspect}")
76
+ send_results_back_to_requester(message, matched_clients, true)
77
+ forward_message(message)
78
+ end
79
+ end
80
+
81
+ # Send a Pantry::Message but mark it as requiring a response.
82
+ # This will set up and return a Celluloid::Future that will contain the
83
+ # response once it is available.
84
+ def send_request(client, message)
85
+ message.requires_response!
86
+ message.to = client.identity
87
+
88
+ Pantry.logger.debug("[#{@identity}] Sending request #{message.inspect}")
89
+
90
+ @networking.send_request(message)
91
+ end
92
+
93
+ # Start a FileService::SendFile worker to upload the contents of the
94
+ # file at +file_path+ to the equivalent ReceiveFile at +receiver_uuid+.
95
+ # Using this method requires asking the receiving end (Server or Client) to receive
96
+ # a file first, which will return the +receiver_uuid+ and +file_uuid+ to use here.
97
+ def send_file(file_path, receiver_uuid, file_uuid)
98
+ @networking.send_file(file_path, receiver_uuid, file_uuid)
99
+ end
100
+
101
+ # Set up a FileService::ReceiveFile worker to begin receiving a file with
102
+ # the given size and checksum. This returns an informational object with
103
+ # the new receiver's identity and the file UUID so a SendFile worker knows who
104
+ # to send the file contents to.
105
+ def receive_file(file_size, file_checksum)
106
+ @networking.receive_file(file_size, file_checksum)
107
+ end
108
+
109
+ protected
110
+
111
+ def current_hostname
112
+ Socket.gethostname
113
+ end
114
+
115
+ def send_results_back_to_requester(message, results, client_response_list = false)
116
+ response_message = message.build_response
117
+ response_message.from = Pantry::SERVER_IDENTITY
118
+ response_message[:client_response_list] = client_response_list
119
+
120
+ [results].flatten(1).each do |entry|
121
+ response_message << entry
122
+ end
123
+
124
+ @networking.publish_message(response_message)
125
+ end
126
+
127
+ def forward_message(message)
128
+ @networking.forward_message(message)
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,83 @@
1
+ #
2
+ # Require this file to grab the Pantry acceptance test environment.
3
+ # Acceptance tests run a full network of server and multiple clients,
4
+ # and are normally run via a CLI.
5
+ #
6
+
7
+ require 'pantry'
8
+ require 'celluloid/test'
9
+ require 'pantry/test/support/minitest'
10
+ require 'pantry/test/support/matchers'
11
+ require 'pantry/test/support/mock_ui'
12
+ require 'pantry/test/support/fake_fs'
13
+
14
+ # Catch and show all exceptions thrown during a test run
15
+ $all_exceptions = []
16
+ Celluloid.exception_handler do |exception|
17
+ $all_exceptions << exception
18
+ end
19
+
20
+ Minitest.after_run do
21
+ $all_exceptions.each do |exception|
22
+ puts exception
23
+ puts exception.backtrace.join("\n")
24
+ puts ""
25
+ end
26
+ end
27
+
28
+ # Set up a Server-only echo command so we can differentiate between
29
+ # Client requests and Server requests in the acceptance tests.
30
+ class ServerEchoCommand < Pantry::Commands::Echo
31
+ end
32
+
33
+ module PantryAcceptanceHelpers
34
+ def configure_pantry(ports_start_at: 10101, heartbeat: 300, security: nil)
35
+ Pantry.config.server_host = "127.0.0.1"
36
+ Pantry.config.pub_sub_port = ports_start_at
37
+ Pantry.config.receive_port = ports_start_at + 1
38
+ Pantry.config.file_service_port = ports_start_at + 2
39
+ Pantry.config.client_heartbeat_interval = heartbeat
40
+ Pantry.config.response_timeout = 5
41
+ Pantry.config.security = security
42
+
43
+ begin
44
+ Pantry.add_server_command(ServerEchoCommand)
45
+ rescue Pantry::DuplicateCommandError
46
+ # Already registered
47
+ end
48
+ end
49
+
50
+ # Set up a fully functional Server + 2 Client environment on the given ports
51
+ # Make sure that the ports given are different for each test or port-conflict
52
+ # errors will happen. Tests should also have a wide enough range between their ports,
53
+ # to ensure there's room for the current setup and any later expansion (10 is a good number).
54
+ #
55
+ # This helper exposes @server, @client1, and @client2 for use in tests
56
+ def set_up_environment(ports_start_at: 10101, heartbeat: 300, security: nil)
57
+ Celluloid.boot
58
+
59
+ configure_pantry(ports_start_at: ports_start_at, heartbeat: heartbeat, security: security)
60
+
61
+ @server = Pantry::Server.new
62
+ @server.identity = "Test Server"
63
+ @server.run
64
+
65
+ @client1 = Pantry::Client.new identity: "client1", application: "pantry", environment: "test", roles: ["app1"]
66
+ @client1.run
67
+
68
+ @client2 = Pantry::Client.new identity: "client2", application: "pantry", environment: "test", roles: ["app2"]
69
+ @client2.run
70
+ end
71
+
72
+ def after_teardown
73
+ @client1.shutdown if @client1
74
+ @client2.shutdown if @client2
75
+ @server.shutdown if @server
76
+
77
+ Celluloid.shutdown rescue nil
78
+ end
79
+ end
80
+
81
+ class Minitest::Test
82
+ include PantryAcceptanceHelpers
83
+ end