pantry 0.0.0 → 0.1.0

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.
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