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,50 @@
1
+ module Pantry
2
+ module Commands
3
+
4
+ # Base class for any Command that needs to sync a set of files in a directory
5
+ # from the Server down to the Client.
6
+ #
7
+ # Subclasses need to define where on the server the files live and where on
8
+ # the client the files will be written to. Both #server_directory and #client_directory
9
+ # are executed on the Client so #client is accessible for added information.
10
+ #
11
+ # This command expects simple directories with a small number of files that are
12
+ # themselves small in size, as this command reads every file into memory and sends
13
+ # that raw content back to the Client. If there are more substantial files to transfer
14
+ # use #send_file and #receive_file instead.
15
+ class SyncDirectory < Pantry::Command
16
+
17
+ def server_directory(local_root)
18
+ raise "Specify the read directory on the server"
19
+ end
20
+
21
+ def client_directory(local_root)
22
+ raise "Specify the write directory on the client"
23
+ end
24
+
25
+ def perform(message)
26
+ dir_contents = send_request!(
27
+ Pantry::Commands::DownloadDirectory.new(
28
+ server_directory(Pathname.new(""))
29
+ ).to_message
30
+ )
31
+
32
+ write_to = client_directory(Pantry.root)
33
+ FileUtils.mkdir_p(write_to)
34
+
35
+ dir_contents.body.each do |(file_name, file_contents)|
36
+ file_path = write_to.join(file_name).cleanpath
37
+ FileUtils.mkdir_p(File.dirname(file_path))
38
+
39
+ File.open(file_path, "w+") do |file|
40
+ file.write(file_contents)
41
+ end
42
+ end
43
+
44
+ true
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,45 @@
1
+ module Pantry
2
+ module Commands
3
+
4
+ # Upload and save new configuration for an Application
5
+ #
6
+ # See EditApplication for more information
7
+ class UpdateApplication < Pantry::Command
8
+
9
+ def initialize(application_name = nil, config_body = nil)
10
+ @application_name = application_name
11
+ @config_body = config_body
12
+ end
13
+
14
+ def to_message
15
+ super.tap do |msg|
16
+ msg << @application_name
17
+ msg << @config_body
18
+ end
19
+ end
20
+
21
+ def perform(message)
22
+ application_name = message.body[0]
23
+ config_body = message.body[1]
24
+
25
+ app_config_file = Pantry.root.join("applications", application_name, "config.yml")
26
+ FileUtils.mkdir_p(File.dirname(app_config_file))
27
+
28
+ begin
29
+ Psych.parse(config_body, "config.yml")
30
+ rescue => ex
31
+ # Invalid YAML, don't save!
32
+ return [false, ex.message]
33
+ end
34
+
35
+ File.open(app_config_file, "w+") do |file|
36
+ file.write(config_body)
37
+ end
38
+
39
+ true
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,68 @@
1
+ module Pantry
2
+ module Commands
3
+
4
+ # Base class for any command that needs to upload a single file where
5
+ # that file is small enough for its contents to be passed around in plain messages.
6
+ # For larger files that shouldn't be pulled entirely into memory, please use
7
+ # #send_file and #receive_file instead.
8
+ #
9
+ # This class is not used directly. Subclass to define the CLI command pattern
10
+ # and the directory where the uploaded file will end up.
11
+ class UploadFile < Pantry::Command
12
+
13
+ attr_reader :file_to_upload
14
+
15
+ def initialize(file_to_upload = nil)
16
+ @file_to_upload = file_to_upload
17
+ end
18
+
19
+ # Specify the directory this file should be written to
20
+ # When applicable, +application+ is the application that should know about this file
21
+ def upload_directory(application)
22
+ raise "Must implement #upload_directory in subclass"
23
+ end
24
+
25
+ # Specify any required options for this Command by long-name
26
+ # For example, to require the base APPLICATION option, return %i(application)
27
+ # Does not matter if the list is of strings or symbols.
28
+ def required_options
29
+ []
30
+ end
31
+
32
+ def prepare_message(options)
33
+ required_options.each do |required|
34
+ unless options[required]
35
+ raise Pantry::MissingOption, "Required option #{required} is missing"
36
+ end
37
+ end
38
+
39
+ super.tap do |message|
40
+ message << options
41
+ message << File.basename(@file_to_upload)
42
+ message << File.read(@file_to_upload)
43
+ end
44
+ end
45
+
46
+ def perform(message)
47
+ cmd_options = message.body[0]
48
+ file_name = message.body[1]
49
+ file_body = message.body[2]
50
+
51
+ upload_dir = upload_directory(cmd_options)
52
+
53
+ FileUtils.mkdir_p(upload_dir)
54
+ File.open(upload_dir.join(file_name), "w+") do |file|
55
+ file.write file_body
56
+ end
57
+
58
+ true
59
+ end
60
+
61
+ def receive_server_response(response)
62
+ # Say nothing. Finishing is enough
63
+ end
64
+
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,20 @@
1
+ module Pantry
2
+
3
+ # The Communication subsystem of Pantry is managed via 0MQ through the
4
+ # Celluloid::ZMQ library.
5
+ module Communication
6
+ Celluloid::ZMQ.init
7
+
8
+ # Configure a ZMQ socket with some common options
9
+ def self.configure_socket(socket)
10
+ # Ensure the socket doesn't spam us trying to reconnect
11
+ # after a disconnect or authentication failure
12
+ socket.set(::ZMQ::RECONNECT_IVL, 1_000)
13
+ socket.set(::ZMQ::RECONNECT_IVL_MAX, 30_000)
14
+
15
+ # Drop all messages on shutdown
16
+ socket.linger = 0
17
+ end
18
+ end
19
+
20
+ end
@@ -0,0 +1,75 @@
1
+ module Pantry
2
+ module Communication
3
+
4
+ # The communication layer of a Pantry::Client
5
+ # This class manages all of the ZeroMQ sockets and underlying
6
+ # communication systems, handling the sending and receiving of messages.
7
+ class Client
8
+ include Celluloid
9
+
10
+ def initialize(listener)
11
+ @listener = listener
12
+ @response_wait_list = Communication::WaitList.new
13
+ end
14
+
15
+ # Start up the networking layer, opening up sockets and getting
16
+ # ready for communication.
17
+ def run
18
+ @security = Communication::Security.new_client
19
+
20
+ @subscribe_socket = Communication::SubscribeSocket.new_link(
21
+ Pantry.config.server_host,
22
+ Pantry.config.pub_sub_port,
23
+ @security
24
+ )
25
+ @subscribe_socket.add_listener(self)
26
+ @subscribe_socket.filter_on(@listener.filter)
27
+ @subscribe_socket.open
28
+
29
+ @send_socket = Communication::SendSocket.new_link(
30
+ Pantry.config.server_host,
31
+ Pantry.config.receive_port,
32
+ @security
33
+ )
34
+ @send_socket.open
35
+
36
+ @file_service = Communication::FileService.new_link(
37
+ Pantry.config.server_host,
38
+ Pantry.config.file_service_port,
39
+ @security
40
+ )
41
+ @file_service.start_client
42
+ end
43
+
44
+ # Callback from the SubscribeSocket when a message is received.
45
+ def handle_message(message)
46
+ if @response_wait_list.waiting_for?(message)
47
+ @response_wait_list.received(message)
48
+ else
49
+ @listener.receive_message(message)
50
+ end
51
+ end
52
+
53
+ def send_request(message)
54
+ @response_wait_list.wait_for(message).tap do
55
+ send_message(message)
56
+ end
57
+ end
58
+
59
+ def send_message(message)
60
+ message.from = @listener
61
+ @send_socket.send_message(message)
62
+ end
63
+
64
+ def receive_file(file_size, file_checksum)
65
+ @file_service.receive_file(file_size, file_checksum)
66
+ end
67
+
68
+ def send_file(file_path, receiver_uuid, file_uuid)
69
+ @file_service.send_file(file_path, receiver_uuid, file_uuid)
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,117 @@
1
+ module Pantry
2
+ module Communication
3
+
4
+ # ClientFilter handles and manages building filters that map configuration values
5
+ # to 0MQ stream names. A message stream is a period-delimited string that works
6
+ # with 0MQ's Subscription prefix matcher, allowing Clients to choose which messages
7
+ # they want to receive. Streams are built to enable tiered delivery capability.
8
+ #
9
+ # For example, a Client with the application 'pantry' and roles 'app' and 'db' will
10
+ # subscribe to the following streams:
11
+ #
12
+ # pantry
13
+ # pantry.app
14
+ # pantry.db
15
+ #
16
+ # Similarly to differentiate between environments, the environment is added inbetween
17
+ # the application and the role:
18
+ #
19
+ # pantry
20
+ # pantry.production
21
+ # pantry.production.app
22
+ # pantry.production.db
23
+ #
24
+ # This class is also used to when sending messages, to choose which specific stream
25
+ # (e.g. the deepest buildable) to send a given message down.
26
+ #
27
+ # A client identity token can also be given via +identity+. If identity is provided
28
+ # then that stream will be chosen above all others. Use this to send a message to
29
+ # specific clients.
30
+ class ClientFilter
31
+
32
+ attr_reader :application, :environment, :roles, :identity
33
+
34
+ def initialize(application: nil, environment: nil, roles: [], identity: nil)
35
+ @application = application
36
+ @environment = environment
37
+ @roles = roles || []
38
+ @identity = identity
39
+ end
40
+
41
+ # List out all communication streams this ClientFilter is configured to know about.
42
+ def streams
43
+ list = []
44
+ base_stream = []
45
+
46
+ if @identity
47
+ list << @identity
48
+ end
49
+
50
+ if @application
51
+ list << @application
52
+ base_stream = [@application]
53
+ end
54
+
55
+ if @environment
56
+ list << [base_stream, @environment].flatten.compact.join(".")
57
+ end
58
+
59
+ @roles.each do |role|
60
+ list << [base_stream, @environment, role].flatten.compact.join(".")
61
+ end
62
+
63
+ list = list.flatten.compact
64
+ list.empty? ? [""] : list
65
+ end
66
+
67
+ # Return the most specific stream that matches this ClientFilter.
68
+ # +identity+ is chosen above all others.
69
+ def stream
70
+ if @identity
71
+ @identity
72
+ else
73
+ [@application, @environment, @roles.first].compact.join(".")
74
+ end
75
+ end
76
+
77
+ def ==(other)
78
+ return false unless other
79
+ return false unless other.is_a?(ClientFilter)
80
+
81
+ self.application == other.application &&
82
+ self.environment == other.environment &&
83
+ self.roles == other.roles &&
84
+ self.identity == other.identity
85
+ end
86
+
87
+ # Will this filter match on the given stream?
88
+ def matches?(test_stream)
89
+ self.streams.any? do |stream|
90
+ stream.start_with?(test_stream)
91
+ end
92
+ end
93
+
94
+ # A filter includes another filter if the other filter matches.
95
+ # This does not look at identities.
96
+ def includes?(filter)
97
+ return true if self == filter
98
+ return true if streams == [""]
99
+
100
+ my_stream = Set.new(streams)
101
+ other_stream = Set.new(filter.streams)
102
+
103
+ my_stream.subset?(other_stream)
104
+ end
105
+
106
+ def to_hash
107
+ {
108
+ application: @application,
109
+ environment: @environment,
110
+ roles: @roles,
111
+ identity: @identity
112
+ }
113
+ end
114
+
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,125 @@
1
+ module Pantry
2
+ module Communication
3
+
4
+ # FileService manages the sending and receiving of files that are too big
5
+ # to cleanly send as a plain ZeroMQ message.
6
+ # Every Client and Server has its own FileService handler which can manage
7
+ # both sending and receiving files from each other.
8
+ #
9
+ # Setting up a file transfer processes backwards from what may be expected.
10
+ # As the Receiver actually requests chunks from the Sender, a protocol that's
11
+ # heavily influenced by http://zguide.zeromq.org/page:all#Transferring-Files,
12
+ # a Receiver must be initiated first on the receiving end, which will then pass
13
+ # back the appropriate information (receiver_uuid and file upload UUID) a
14
+ # Sender needs to start up and run.
15
+ #
16
+ # From this the two parts complete the process automatically. A Receiver writes the
17
+ # data it receives in a tempfile, and must be configured with a completion block
18
+ # to move the uploaded file to its final location.
19
+ #
20
+ # To ensure this object has as little special-casing code as possible, the communication
21
+ # takes place in a ZeroMQ ROUTER <-> ROUTER topology.
22
+ class FileService
23
+ include Celluloid::ZMQ
24
+ finalizer :shutdown
25
+
26
+ attr_reader :identity
27
+
28
+ def initialize(server_host, port, security)
29
+ @host = server_host
30
+ @port = port
31
+
32
+ @socket = Celluloid::ZMQ::RouterSocket.new
33
+ @socket.set(::ZMQ::ROUTER_MANDATORY, 1)
34
+ @socket.identity = @identity = SecureRandom.uuid
35
+ Communication.configure_socket(@socket)
36
+
37
+ @security = security
38
+ @security.configure_socket(@socket)
39
+
40
+ @receiver = FileService::ReceiveFile.new_link(self)
41
+ @sender = FileService::SendFile.new_link(self)
42
+ end
43
+
44
+ def secure_with(security_handler)
45
+ @security = security_handler
46
+ end
47
+
48
+ def start_server
49
+ @socket.bind("tcp://#{@host}:#{@port}")
50
+ run
51
+ end
52
+
53
+ def start_client
54
+ @socket.connect("tcp://#{@host}:#{@port}")
55
+ run
56
+ end
57
+
58
+ def shutdown
59
+ @running = false
60
+ end
61
+
62
+ def run
63
+ @running = true
64
+ self.async.process_messages
65
+ end
66
+
67
+ # Inform the service that it will soon be receiving a file of the given
68
+ # size and checksum. Returns a UploadInfo struct with the information for
69
+ # the Sender.
70
+ def receive_file(size, checksum)
71
+ Pantry.logger.debug("[FileService] Receiving file of size #{size} and checksum #{checksum}")
72
+ @receiver.receive_file(size, checksum).tap do |info|
73
+ info.receiver_uuid = @socket.identity
74
+ end
75
+ end
76
+
77
+ # Inform the service that we want to start sending a file up to the receiver
78
+ # who's listening on the given UUID.
79
+ def send_file(file_path, receiver_uuid, file_uuid)
80
+ Pantry.logger.debug("[FileService] Sending file #{file_path} to #{receiver_uuid}")
81
+ @sender.send_file(file_path, receiver_uuid, file_uuid)
82
+ end
83
+
84
+ def send_message(identity, message)
85
+ @socket.write(
86
+ [
87
+ identity,
88
+ SerializeMessage.to_zeromq(message)
89
+ ].flatten
90
+ )
91
+ end
92
+
93
+ def receive_message(from_identity, message)
94
+ @sender.async.receive_message(from_identity, message)
95
+ @receiver.async.receive_message(from_identity, message)
96
+ end
97
+
98
+ protected
99
+
100
+ def process_messages
101
+ while @running
102
+ process_next_message
103
+ end
104
+
105
+ @socket.close
106
+ end
107
+
108
+ def process_next_message
109
+ next_message = []
110
+
111
+ from_identity = @socket.read
112
+
113
+ while @socket.more_parts?
114
+ next_message << @socket.read
115
+ end
116
+
117
+ async.receive_message(
118
+ from_identity, SerializeMessage.from_zeromq(next_message)
119
+ )
120
+ end
121
+
122
+ end
123
+
124
+ end
125
+ end