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