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,53 @@
1
+ module Pantry
2
+
3
+ # Manages and processes commands as requested from the Client or the Server.
4
+ # Given a mapping of available commands, maps the incoming message to the appropriate
5
+ # command handler and returns the response. Returns nil if no command found.
6
+ class CommandHandler
7
+
8
+ def initialize(server_or_client, commands_to_register = [])
9
+ @handlers = {}
10
+ @server_or_client = server_or_client
11
+
12
+ commands_to_register.each do |command_class|
13
+ add_command(command_class)
14
+ end
15
+ end
16
+
17
+ # Install a Command class as a message handler for this process.
18
+ # The Message's +type+ for this kind of message is simply the name of the class
19
+ # without any scope information. E.g. Echo not Pantry::Command::Echo.
20
+ def add_command(command_class)
21
+ @handlers[command_class.message_type] = build_command_proc(command_class)
22
+ end
23
+
24
+ # Does this CommandHandler know how to handle the given command?
25
+ def can_handle?(message)
26
+ !@handlers[message.type].nil?
27
+ end
28
+
29
+ # Given a message, figure out which handler should be triggered and get things rolling
30
+ def process(message)
31
+ if handler = @handlers[message.type]
32
+ Pantry.logger.debug("[#{@server_or_client.identity}] Running handler on #{message.inspect}")
33
+ handler.call(message)
34
+ else
35
+ Pantry.logger.warn(
36
+ "[#{@server_or_client.identity}] No known handler for message type #{message.type}"
37
+ )
38
+ nil
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ def build_command_proc(command_class)
45
+ proc do |message|
46
+ command_obj = command_class.new
47
+ command_obj.server_or_client = @server_or_client
48
+ command_obj.perform(message)
49
+ end
50
+ end
51
+ end
52
+
53
+ end
@@ -0,0 +1,115 @@
1
+ module Pantry
2
+
3
+ class CommandLine
4
+
5
+ # The top-level set of CLI options and flags Pantry respects
6
+ BASE_OPTIONS = proc {
7
+ banner "Usage: #{File.basename($0)} [options] [command [command options]]"
8
+ option "-h", "--host HOSTNAME", String, "Hostname of the Server to connect to"
9
+ option "--curve-key-file FILE", String, "Name of the file in .pantry holding Curve keys.",
10
+ "Specifying this option will turn on Curve encryption."
11
+
12
+ option "-a", "--application APPLICATION", String, "Filter Clients by a specific APPLICATION"
13
+ option "-e", "--environment ENVIRONMENT", String, "Filter Clients by a specific ENVIRONMENT"
14
+ option "-r", "--roles ROLE1,ROLE2", Array, "Filter Clients by given ROLES"
15
+ option "-v", "--verbose", "Verbose output (INFO)"
16
+ option "-d", "--debug", "Even more Verbose output (DEBUG)"
17
+ option "-V", "--version", "Print out Pantry's version"
18
+ }
19
+
20
+ def initialize(command_line)
21
+ @command_line = command_line
22
+ @known_commands = {}
23
+
24
+ @commands = find_all_cli_enabled_commands
25
+ end
26
+
27
+ # Parse the full command line. Returns a hash containing the options found
28
+ # as well as what is still left on the command line.
29
+ # If the command line is empty, will default to --help.
30
+ #
31
+ # Returns [nil, nil] if help was requested or there was a problem.
32
+ def parse!
33
+ @command_line = merge_command_line_with_defaults(@command_line)
34
+ parser = build_parser(@commands)
35
+
36
+ begin
37
+ if @command_line.empty?
38
+ @command_line << "--help"
39
+ end
40
+
41
+ @parsed_options = parser.parse!(@command_line)
42
+
43
+ if @parsed_options['help']
44
+ # Help printed already
45
+ return [nil, nil]
46
+ end
47
+
48
+ [@parsed_options, @command_line]
49
+ rescue => ex
50
+ puts ex, ""
51
+ puts parser.help
52
+ [nil, nil]
53
+ end
54
+ end
55
+
56
+ # Returns details of the command found during parsing.
57
+ # Returns a hash with the keys +banner+ and +class+,
58
+ # or returns nil if no matching command was found
59
+ def triggered_command
60
+ [
61
+ @commands[@parsed_options.command_found],
62
+ @parsed_options[@parsed_options.command_found]
63
+ ]
64
+ end
65
+
66
+ protected
67
+
68
+ def find_all_cli_enabled_commands
69
+ commands = {}
70
+ Pantry.all_commands.each do |command_class|
71
+ if command_class.command_name
72
+ # Hmm duplicated from OptParsePlus
73
+ base_command_name = command_class.command_name.split(/\s/).first
74
+ commands[base_command_name] = {
75
+ banner: command_class.command_name,
76
+ class: command_class
77
+ }
78
+ end
79
+ end
80
+
81
+ commands
82
+ end
83
+
84
+ def merge_command_line_with_defaults(command_line)
85
+ [read_defaults_file, command_line].flatten
86
+ end
87
+
88
+ def read_defaults_file
89
+ dot_pantry_config = Pantry.root.join("config")
90
+
91
+ if File.exist?(dot_pantry_config)
92
+ # ARGV is an array of the command line seperated by white-space.
93
+ # Make sure what we read from .pantry returns the same
94
+ File.readlines(dot_pantry_config).map { |line|
95
+ line.strip.split(/\s/)
96
+ }.flatten
97
+ else
98
+ []
99
+ end
100
+ end
101
+
102
+ def build_parser(cli_commands)
103
+ parser = OptParsePlus.new
104
+ parser.add_options(&BASE_OPTIONS)
105
+
106
+ cli_commands.each do |base_command_name, command_info|
107
+ parser.add_command(command_info[:banner], &command_info[:class].command_config)
108
+ end
109
+
110
+ parser
111
+ end
112
+
113
+ end
114
+
115
+ end
@@ -0,0 +1,30 @@
1
+ module Pantry
2
+ module Commands
3
+
4
+ # Ask the server to generate a new set of keys
5
+ # Prints a yaml file that contains the required keys for a client to properly
6
+ # conenct and authenticate to the server
7
+ class CreateClient < Pantry::Command
8
+ command "client:create" do
9
+ description "Generate a new public/private encryption keypair for a client."
10
+ end
11
+
12
+ def perform(message)
13
+ server.create_client
14
+ end
15
+
16
+ def receive_server_response(message)
17
+ keys = message.body[0]
18
+ Pantry.ui.say("New Client Credentials")
19
+ Pantry.ui.say("Store this in the Client's Pantry.root/security/curve/client_keys.yml")
20
+ Pantry.ui.say(YAML.dump({
21
+ "server_public_key" => keys[:server_public_key],
22
+ "public_key" => keys[:public_key],
23
+ "private_key" => keys[:private_key]
24
+ }))
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ module Pantry
2
+ module Commands
3
+
4
+ # Download all content inside of the given directory.
5
+ #
6
+ # This command expects simple directories with a small number of files that are
7
+ # themselves small in size, as this command reads every file into memory and sends
8
+ # that raw content back to the Client. If there are more substantial files to transfer
9
+ # use #send_file and #receive_file instead.
10
+ class DownloadDirectory < Pantry::Command
11
+
12
+ def initialize(directory = nil)
13
+ @directory = directory
14
+ end
15
+
16
+ def to_message
17
+ super.tap do |message|
18
+ message << @directory.to_s
19
+ end
20
+ end
21
+
22
+ def perform(message)
23
+ directory = Pantry.root.join(message.body[0])
24
+
25
+ Dir[directory.join("**", "*")].map do |file|
26
+ next if File.directory?(file)
27
+ [Pathname.new(file).relative_path_from(directory).to_s,
28
+ File.read(file)]
29
+ end.compact
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,32 @@
1
+ module Pantry
2
+ module Commands
3
+
4
+ # Simple Echo command, returns the body of the Message given.
5
+ class Echo < Command
6
+
7
+ command "echo MESSAGE" do
8
+ description "Test Client communication with a simple Echo request"
9
+ end
10
+
11
+ def initialize(string_to_echo = "")
12
+ @string_to_echo = string_to_echo
13
+ end
14
+
15
+ def to_message
16
+ message = super
17
+ message << @string_to_echo
18
+ message
19
+ end
20
+
21
+ def perform(message)
22
+ message.body[0]
23
+ end
24
+
25
+ def receive_client_response(response)
26
+ Pantry.ui.say("#{response.from} echo's #{response.body[0].inspect}")
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,60 @@
1
+ module Pantry
2
+ module Commands
3
+
4
+ # Edit the configuration of the requested Application.
5
+ #
6
+ # Application configuration is stored on the Server under
7
+ # Pantry.root/applications/[app name]/config.yml and is where all
8
+ # configuration lives for how Pantry manages this application.
9
+ class EditApplication < Pantry::Command
10
+
11
+ command "edit" do
12
+ description "Edit an application's configuration with the text editor specified in $EDITOR.
13
+ Requires an Application."
14
+ end
15
+
16
+ def prepare_message(options)
17
+ @application = options[:application]
18
+ raise Pantry::MissingOption, 'Missing required option "application"' unless @application
19
+
20
+ # Let the EDITOR check run before we do any communication
21
+ @editor = Pantry::FileEditor.new
22
+
23
+ super.tap do |msg|
24
+ msg << @application
25
+ end
26
+ end
27
+
28
+ # Read or create a new config file for the given application
29
+ # and return the contents of this config file, which will always be
30
+ # a YAML document
31
+ def perform(message)
32
+ application = message.body[0]
33
+
34
+ config_file = Pantry.root.join("applications", application, "config.yml")
35
+ FileUtils.mkdir_p(File.dirname(config_file))
36
+
37
+ if File.exists?(config_file)
38
+ [File.read(config_file)]
39
+ else
40
+ [{"name" => application}.to_yaml]
41
+ end
42
+ end
43
+
44
+ def receive_server_response(message)
45
+ current_config = message.body[0]
46
+ new_config = @editor.edit(current_config, :yaml)
47
+
48
+ if new_config != current_config
49
+ send_request!(
50
+ Pantry::Commands::UpdateApplication.new(
51
+ @application, new_config
52
+ ).to_message
53
+ )
54
+ end
55
+ end
56
+
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,38 @@
1
+ module Pantry
2
+ module Commands
3
+
4
+ class RegisterClient < Command
5
+
6
+ def initialize(client_info = nil)
7
+ @client_info = client_info
8
+ end
9
+
10
+ def to_message
11
+ message = super
12
+ message << {
13
+ application: @client_info.application,
14
+ environment: @client_info.environment,
15
+ roles: @client_info.roles
16
+ }
17
+ message
18
+ end
19
+
20
+ # Take note that a Client has connected and registered itself
21
+ # with this Server.
22
+ def perform(message)
23
+ details = message.body[0]
24
+
25
+ @client_info = Pantry::ClientInfo.new(
26
+ identity: message.from,
27
+ application: details[:application],
28
+ environment: details[:environment],
29
+ roles: details[:roles]
30
+ )
31
+
32
+ self.server.register_client(@client_info)
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,78 @@
1
+ module Pantry
2
+ module Commands
3
+
4
+ class Status < Command
5
+
6
+ command "status" do
7
+ description "List all Clients that match the options"
8
+ end
9
+
10
+ attr_accessor :client_filter
11
+
12
+ def prepare_message(options)
13
+ @client_filter = Pantry::Communication::ClientFilter.new(
14
+ application: options[:application],
15
+ environment: options[:environment],
16
+ roles: options[:roles]
17
+ )
18
+ super
19
+ end
20
+
21
+ def to_message
22
+ message = super
23
+ message << @client_filter.to_hash
24
+ message
25
+ end
26
+
27
+ # Return information about all connected Clients that match the given filter
28
+ def perform(message)
29
+ @client_filter = Pantry::Communication::ClientFilter.new(**(message.body[0] || {}))
30
+ self.server.client_registry.all_matching(@client_filter) do |client, record|
31
+ {
32
+ identity: client.identity,
33
+ application: client.application,
34
+ environment: client.environment,
35
+ roles: client.roles,
36
+ last_checked_in: record.last_checked_in_at
37
+ }
38
+ end
39
+ end
40
+
41
+ def receive_server_response(message)
42
+ output =
43
+ clients = message.body.map do |client|
44
+ [
45
+ time_ago_in_words(client[:last_checked_in]),
46
+ client[:identity],
47
+ "|",
48
+ client[:application],
49
+ client[:environment],
50
+ [client[:roles]].flatten.join(",")
51
+ ].compact.join(" ")
52
+ end
53
+
54
+ Pantry.ui.list(output)
55
+ end
56
+
57
+ protected
58
+
59
+ def time_ago_in_words(time)
60
+ now = DateTime.now.to_time
61
+ checked_in = DateTime.parse(time).to_time
62
+
63
+ seconds_since = (now - checked_in).to_i
64
+ case seconds_since
65
+ when 0..(2*60)
66
+ Pantry.ui.color("A minute ago", :green)
67
+ when (2*60+1)..(59*60)
68
+ Pantry.ui.color("#{seconds_since / 60} minutes ago", :green)
69
+ else
70
+ hours_since = seconds_since / 60 / 60
71
+ hours_key = hours_since > 1 ? "hours" : "hour"
72
+ Pantry.ui.color("#{hours_since} #{hours_key} ago", :red)
73
+ end
74
+ end
75
+ end
76
+
77
+ end
78
+ end