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,97 @@
1
+ module Pantry
2
+ module Communication
3
+
4
+ # The communication layer of a Pantry::Server
5
+ # This class manages all of the ZeroMQ sockets and underlying
6
+ # communication systems, handling the sending and receiving of messages.
7
+ class Server
8
+ include Celluloid
9
+
10
+ #
11
+ # +listener+ must respond to the #receive_message method
12
+ def initialize(listener)
13
+ @listener = listener
14
+ @response_wait_list = Communication::WaitList.new
15
+ end
16
+
17
+ # Start up the networking layer, opening up sockets and getting
18
+ # ready for client communication.
19
+ def run
20
+ @security = Communication::Security.new_server
21
+ @security.link_to(self)
22
+
23
+ @publish_socket = Communication::PublishSocket.new_link(
24
+ Pantry.config.server_host,
25
+ Pantry.config.pub_sub_port,
26
+ @security
27
+ )
28
+ @publish_socket.open
29
+
30
+ @receive_socket = Communication::ReceiveSocket.new_link(
31
+ Pantry.config.server_host,
32
+ Pantry.config.receive_port,
33
+ @security
34
+ )
35
+ @receive_socket.add_listener(self)
36
+ @receive_socket.open
37
+
38
+ @file_service = Communication::FileService.new_link(
39
+ Pantry.config.server_host,
40
+ Pantry.config.file_service_port,
41
+ @security
42
+ )
43
+ @file_service.start_server
44
+ end
45
+
46
+ # Ask Security to generate a new set of credentials as necessary
47
+ # for a new Client to connect to this Server
48
+ def create_client
49
+ @security.create_client
50
+ end
51
+
52
+ # Send a request to all clients, expecting a result. Returns a Future
53
+ # which can be queried later for the client response.
54
+ def send_request(message)
55
+ @response_wait_list.wait_for(message).tap do
56
+ publish_message(message)
57
+ end
58
+ end
59
+
60
+ # Send a message to all connected subscribers without modifying the package.
61
+ # Used when handling requests meant for other clients (say from the CLI). The source
62
+ # is untouched so the Client(s) handling know how to respond.
63
+ def forward_message(message)
64
+ message.forwarded!
65
+ publish_message(message)
66
+ end
67
+
68
+ # Send a message to all clients who match the given filter.
69
+ def publish_message(message)
70
+ message.from ||= @listener
71
+ @publish_socket.send_message(message)
72
+ end
73
+
74
+ # Listener callback from ReceiveSocket. See if we need to match this response
75
+ # with a previous request or if it's a new message entirely.
76
+ def handle_message(message)
77
+ if message.forwarded?
78
+ forward_message(message)
79
+ elsif @response_wait_list.waiting_for?(message)
80
+ @response_wait_list.received(message)
81
+ else
82
+ @listener.receive_message(message)
83
+ end
84
+ end
85
+
86
+ def receive_file(file_size, file_checksum)
87
+ @file_service.receive_file(file_size, file_checksum)
88
+ end
89
+
90
+ def send_file(file_path, receiver_uuid, file_uuid)
91
+ @file_service.send_file(file_path, receiver_uuid, file_uuid)
92
+ end
93
+
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,33 @@
1
+ module Pantry
2
+ module Communication
3
+
4
+ # The SubscribeSocket manages the Subscription side of the Pub/Sub channel,
5
+ # using a 0MQ PUB socket. This socket can subscribe to any number of streams
6
+ # depending on the filtering given. Messages received by this socket are passed
7
+ # to the configured listener as Messages.
8
+ class SubscribeSocket < ReadingSocket
9
+
10
+ def initialize(host, port, security)
11
+ super
12
+ @filter = ClientFilter.new
13
+ end
14
+
15
+ def filter_on(client_filter)
16
+ @filter = client_filter
17
+ end
18
+
19
+ def build_socket
20
+ Celluloid::ZMQ::SubSocket.new
21
+ end
22
+
23
+ def open_socket(socket)
24
+ socket.connect("tcp://#{host}:#{port}")
25
+
26
+ @filter.streams.each do |stream|
27
+ socket.subscribe(stream)
28
+ end
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,45 @@
1
+ module Pantry
2
+ module Communication
3
+
4
+ # The WaitList manages futures for asynchronously waiting for responses
5
+ # from either the Client or the Server. Given an identity and a message,
6
+ # WaitList returns a Future that will be filled when the handler in question
7
+ # receives a message of the same Message type from that identity.
8
+ class WaitList
9
+
10
+ def initialize
11
+ @futures = Hash.new {|hash, key| hash[key] = []}
12
+ end
13
+
14
+ # Given a +message+ being sent, create a Future for a response to this message.
15
+ # This keys off of the Message's UUID, which must be kept in tact as it
16
+ # passes through the system.
17
+ def wait_for(message)
18
+ future = Celluloid::Future.new
19
+ @futures[ message.uuid ] << future
20
+ future
21
+ end
22
+
23
+ # Is there a future waiting for this response message?
24
+ def waiting_for?(message)
25
+ !@futures[ message.uuid ].empty?
26
+ end
27
+
28
+ # Internal to Celluloid::Future, using #signal ends up in a Result object
29
+ # in which calling #value then calls #value on the saved data which in our
30
+ # case is Message. We just want the Message so wrap up our messages in this
31
+ # object to work around this oddity.
32
+ #
33
+ # https://github.com/celluloid/celluloid/blob/master/lib/celluloid/future.rb#L89
34
+ FutureResultWrapper = Struct.new(:value)
35
+
36
+ def received(message)
37
+ if future = @futures[ message.uuid ].shift
38
+ future.signal(FutureResultWrapper.new(message))
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,46 @@
1
+ module Pantry
2
+ module Communication
3
+
4
+ # Base class of all sockets that write messages through ZMQ.
5
+ # Not meant for direct use, please use one of the subclasses for specific
6
+ # functionality.
7
+ class WritingSocket
8
+ include Celluloid::ZMQ
9
+
10
+ attr_reader :host, :port
11
+
12
+ def initialize(host, port, security)
13
+ @host = host
14
+ @port = port
15
+ @security = security
16
+ end
17
+
18
+ def open
19
+ @socket = build_socket
20
+ Communication.configure_socket(@socket)
21
+ @security.configure_socket(@socket)
22
+
23
+ open_socket(@socket)
24
+ end
25
+
26
+ def build_socket
27
+ raise "Implement the socket setup."
28
+ end
29
+
30
+ def open_socket(socket)
31
+ raise "Connect / Bind the socket built in #build_socket"
32
+ end
33
+
34
+ def close
35
+ @socket.close if @socket
36
+ end
37
+
38
+ def send_message(message)
39
+ @socket.write(
40
+ SerializeMessage.to_zeromq(message)
41
+ )
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,182 @@
1
+ module Pantry
2
+
3
+ # Retrieve the current configuration set
4
+ def self.config
5
+ @@config ||= Config.new
6
+ end
7
+
8
+ def self.reset_config!
9
+ @@config = nil
10
+ end
11
+
12
+ # Global configuration values for running all of Pantry.
13
+ class Config
14
+ ##
15
+ # Global Configuration
16
+ ##
17
+
18
+ # Where does Pantry log to?
19
+ # Can be "stdout", "syslog", or a file system path
20
+ # Defaults to STDOUT
21
+ # When using syslog, program name will be "pantry"
22
+ attr_accessor :log_to
23
+
24
+ # After what level are logs dropped and ignored?
25
+ # Can be any of: "fatal", "error", "warn", "info", "debug"
26
+ # Each level will include the logs of all levels above it.
27
+ # Defaults to "info"
28
+ attr_accessor :log_level
29
+
30
+ # If logging to Syslog, set the program-name Pantry will
31
+ # use when sending logs to syslog.
32
+ # Defaults to "pantry"
33
+ attr_accessor :syslog_program_name
34
+
35
+ # Location on the file system Pantry will store any persistent data
36
+ # Default: /var/lib/pantry
37
+ attr_accessor :root_dir
38
+
39
+ ##
40
+ # Communication Configuration
41
+ ##
42
+
43
+ # Host name of the Pantry Server
44
+ attr_accessor :server_host
45
+
46
+ # Port used for Pub/Sub communication
47
+ attr_accessor :pub_sub_port
48
+
49
+ # Port clients use to send information to the Server
50
+ attr_accessor :receive_port
51
+
52
+ # Port through which files are sent and received
53
+ attr_accessor :file_service_port
54
+
55
+ # How often, in seconds, the client pings the Server
56
+ attr_accessor :client_heartbeat_interval
57
+
58
+ # What type of security will Pantry be employing?
59
+ # Available types are nil (no security) and "curve" (ZMQ4 Curve security)
60
+ #
61
+ # Defaults to nil because curve security has not yet been fully
62
+ # vetted by the crypto-community
63
+ attr_accessor :security
64
+
65
+ ##
66
+ # Client Identification
67
+ ##
68
+
69
+ # Unique identification of this Client
70
+ attr_accessor :client_identity
71
+
72
+ # Application this Client serves
73
+ attr_accessor :client_application
74
+
75
+ # Environment of the Application this Client runs
76
+ attr_accessor :client_environment
77
+
78
+ # Roles this Client serves
79
+ attr_accessor :client_roles
80
+
81
+ ##
82
+ # Testing configuration helpers
83
+ ##
84
+
85
+ # Time in seconds the CLI will wait for a response from the server
86
+ # By default this is nil, meaning unlimited timeout. Used mainly in tests.
87
+ attr_accessor :response_timeout
88
+
89
+ def initialize
90
+
91
+ # Logging defaults
92
+ @log_level = "info"
93
+ @syslog_program_name = "pantry"
94
+
95
+ # Default connectivity settings
96
+ @server_host = "127.0.0.1"
97
+ @pub_sub_port = 23001
98
+ @receive_port = 23002
99
+ @file_service_port = 23003
100
+
101
+ # Default client heartbeat to every 5 minutes
102
+ @client_heartbeat_interval = 300
103
+
104
+ # Default Client identificiation values
105
+ @client_identity = nil
106
+ @client_application = nil
107
+ @client_environment = nil
108
+ @client_roles = []
109
+
110
+ end
111
+
112
+ # Given a YAML config file, read in config values
113
+ def load_file(config_file)
114
+ configs = SafeYAML.load_file(config_file)
115
+ load_global_configs(configs)
116
+ load_networking_configs(configs["networking"])
117
+ load_client_configs(configs["client"])
118
+ refresh
119
+ end
120
+
121
+ def refresh
122
+ apply_configuration
123
+ end
124
+
125
+ protected
126
+
127
+ def load_global_configs(configs)
128
+ @log_to = configs["log_to"]
129
+
130
+ if configs["log_level"]
131
+ @log_level = configs["log_level"]
132
+ end
133
+
134
+ if configs["syslog_program_name"]
135
+ @syslog_program_name = configs["syslog_program_name"]
136
+ end
137
+
138
+ @root_dir = configs["root_dir"]
139
+ end
140
+
141
+ def load_networking_configs(configs)
142
+ return unless configs
143
+
144
+ if configs["server_host"]
145
+ @server_host = configs["server_host"]
146
+ end
147
+
148
+ if configs["pub_sub_port"]
149
+ @pub_sub_port = configs["pub_sub_port"]
150
+ end
151
+
152
+ if configs["receive_port"]
153
+ @receive_port = configs["receive_port"]
154
+ end
155
+
156
+ if configs["file_service_port"]
157
+ @file_service_port = configs["file_service_port"]
158
+ end
159
+
160
+ @security = configs["security"]
161
+ end
162
+
163
+ def load_client_configs(configs)
164
+ return unless configs
165
+
166
+ @client_identity = configs["identity"]
167
+ @client_application = configs["application"]
168
+ @client_environment = configs["environment"]
169
+ @client_roles = configs["roles"]
170
+
171
+ if configs["heartbeat_interval"]
172
+ @client_heartbeat_interval = configs["heartbeat_interval"]
173
+ end
174
+ end
175
+
176
+ def apply_configuration
177
+ # Reset our logger knowledge so the next call picks up the
178
+ # new configs
179
+ Pantry.logger = nil
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,67 @@
1
+ module Pantry
2
+
3
+ # Use EDITOR to edit the contents of a remote file locally
4
+ # The editor can validate the updated content to be YAML (more to be added as needed)
5
+ # and will show errors and re-edit the file if validation fails.
6
+ #
7
+ # If the user chooses to cancel editing, #edit will return the original
8
+ # content given to it.
9
+ #
10
+ # Usage is simple:
11
+ #
12
+ # editor = FileEditor.new
13
+ # updated_content = editor.edit(file_contents, file_type)
14
+ #
15
+ class FileEditor
16
+
17
+ def initialize
18
+ @editor = ENV['EDITOR']
19
+ raise "Please set EDITOR environment variable to a text editor." unless @editor
20
+ end
21
+
22
+ def edit(original_content, file_type)
23
+ file = create_temp_file(original_content, file_type)
24
+ new_content = ""
25
+
26
+ loop do
27
+ new_content = edit_file(file)
28
+
29
+ is_valid, message = validate_content(new_content, file_type)
30
+ break if is_valid
31
+
32
+ Pantry.ui.say(message)
33
+ if !Pantry.ui.continue?("Continue editing?")
34
+ new_content = original_content
35
+ break
36
+ end
37
+ end
38
+
39
+ file.unlink
40
+ new_content
41
+ end
42
+
43
+ protected
44
+
45
+ def create_temp_file(file_contents, file_type)
46
+ tempfile = Tempfile.new(["edit-in-line", ".#{file_type}"])
47
+ tempfile.write(file_contents)
48
+ tempfile.close
49
+ tempfile
50
+ end
51
+
52
+ def edit_file(tempfile)
53
+ system("#{@editor} #{tempfile.path}")
54
+ File.read(tempfile.path)
55
+ end
56
+
57
+ def validate_content(content, file_type)
58
+ begin
59
+ Psych.parse(content, "config.yml")
60
+ return true, nil
61
+ rescue => ex
62
+ return false, ex.message
63
+ end
64
+ end
65
+ end
66
+
67
+ end