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