rhcp 0.1.3 → 0.1.4

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.
data/lib/rhcp.rb ADDED
@@ -0,0 +1,47 @@
1
+ require 'singleton'
2
+ require 'logger'
3
+
4
+ require 'rhcp/broker'
5
+ require 'rhcp/command'
6
+ require 'rhcp/command_param'
7
+ require 'rhcp/dispatching_broker'
8
+ require 'rhcp/http_exporter'
9
+ require 'rhcp/request'
10
+ require 'rhcp/response'
11
+ require 'rhcp/rhcp_exception'
12
+ require 'rhcp/client/http_broker'
13
+ require 'rhcp/client/command_stub'
14
+ require 'rhcp/client/command_param_stub'
15
+
16
+ module RHCP #:nodoc:
17
+
18
+ class Version
19
+
20
+ include Singleton
21
+
22
+ MAJOR = 0
23
+ MINOR = 1
24
+ TINY = 4
25
+
26
+ def Version.to_s
27
+ [ MAJOR, MINOR, TINY ].join(".")
28
+ end
29
+
30
+ end
31
+
32
+ class ModuleHelper
33
+
34
+ include Singleton
35
+
36
+ attr_accessor :logger
37
+
38
+ def initialize()
39
+ # TODO do we really want to log to STDOUT per default?
40
+ # TODO check the whole package for correct usage of loggers
41
+ @logger = Logger.new(STDOUT)
42
+ end
43
+
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,40 @@
1
+ module RHCP
2
+
3
+ # Applications register the commands they are implementing at a broker.
4
+ # The broker holds the list of commands and is the central entry point for
5
+ # all code that wants to export/publish these commands
6
+ class Broker
7
+
8
+
9
+ # TODO add a getName() thingy?
10
+
11
+ def initialize
12
+ # command_name => command
13
+ @known_commands = Hash.new()
14
+ end
15
+
16
+ # returns a list of all known commands
17
+ def get_command_list()
18
+ @known_commands
19
+ end
20
+
21
+ # returns the specified command object
22
+ def get_command(command_name)
23
+ raise RHCP::RhcpException.new("no such command : #{command_name}") unless @known_commands.has_key?(command_name)
24
+ get_command_list[command_name]
25
+ end
26
+
27
+ # registers a new command - this method should be called by the application
28
+ # providing the command
29
+ def register_command(command)
30
+ raise RHCP::RhcpException.new("duplicate command name : #{command.name}") if @known_commands.has_key?(command.name)
31
+ @known_commands[command.name] = command
32
+ end
33
+
34
+ # removes all commands that have been registered previously
35
+ def clear
36
+ @known_commands = Hash.new()
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,51 @@
1
+ require 'rhcp/command_param'
2
+
3
+ module RHCP
4
+
5
+ module Client
6
+
7
+ # This is a proxy representing a remote CommandParam - see
8
+ # RHCP::CommandParam for details
9
+ class CommandParamStub < RHCP::CommandParam
10
+
11
+ # the block that should be executed when +get_lookup_values+ is called
12
+ # on this CommandParamStub
13
+ attr_accessor :get_lookup_values_block
14
+
15
+ # when constructing a CommandParamStub, the +:lookup_method+ option is
16
+ # set to +remote_get_lookup_values+ so that a method can be injected
17
+ # from outside that will retrieve the lookup values (using the
18
+ # +get_lookup_values_block+ property).
19
+ def initialize(name, description, options)
20
+ # we don't need to stub anything if the method does not have lookup values
21
+ if (options[:has_lookup_values])
22
+ options[:lookup_method] = self.method(:remote_get_lookup_values)
23
+ end
24
+ super(name, description, options)
25
+ end
26
+
27
+ def self.reconstruct_from_json(json_data)
28
+ object = json_data.instance_of?(Hash) ? json_data : JSON.parse(json_data)
29
+ args = object.values_at('name', 'description')
30
+ args << {
31
+ :allows_multiple_values => object['allows_multiple_values'],
32
+ :has_lookup_values => object['has_lookup_values'],
33
+ :is_default_param => object['is_default_param'],
34
+ :mandatory => object['mandatory']
35
+ }
36
+ self.new(*args)
37
+ end
38
+
39
+ def remote_get_lookup_values(partial_value = "")
40
+ @get_lookup_values_block.call(partial_value)
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+
50
+
51
+
@@ -0,0 +1,64 @@
1
+ require 'rhcp/command'
2
+ require 'rhcp/client/command_param_stub'
3
+ require 'rubygems'
4
+ require 'json'
5
+
6
+
7
+ module RHCP
8
+
9
+ module Client
10
+
11
+ # This is a stub that represents a remote RHCP::Command.
12
+ # Instances of this class will live in a client that uses one of the brokers
13
+ # from the RHCP::Client package. The interesting aspect about this stub is
14
+ # that the execute method is modified so that it does not execute the
15
+ # command directly, but can be modified from outside (by setting the
16
+ # +execute_block+ property) so that broker classes can inject their
17
+ # remote invocation logic.
18
+ class CommandStub < RHCP::Command
19
+
20
+ attr_accessor :execute_block
21
+
22
+ # constructs a new instance
23
+ # should not be invoked directly, but is called from +reconstruct_from_json+
24
+ def initialize(name, description)
25
+ super(name, description, lambda {})
26
+ end
27
+
28
+ # builds a CommandStub out of some json data (either serialized as string
29
+ # or already unpacked into a ruby-hash)
30
+ # all nested params are unmarshalled as RHCP::CommandParamStub instances
31
+ # so that we are able to inject the logic for invoking get_lookup_values
32
+ # remotely
33
+ def self.reconstruct_from_json(json_data)
34
+ object = json_data.instance_of?(Hash) ? json_data : JSON.parse(json_data)
35
+ args = object.values_at('name', 'description')
36
+ instance = self.new(*args)
37
+ if object.values_at('read_only')
38
+ instance.mark_as_read_only
39
+ end
40
+ instance.result_hints = {
41
+ :display_type => object['result_hints']['display_type'],
42
+ :overview_columns => object['result_hints']['overview_columns'],
43
+ :column_titles => object['result_hints']['column_titles']
44
+ }
45
+ object['params'].each do |p|
46
+ param = RHCP::Client::CommandParamStub.reconstruct_from_json(p)
47
+ instance.add_param(param)
48
+ end
49
+ instance
50
+ end
51
+
52
+ # we don't want to execute the command block as the normal RHCP::Command
53
+ # does, but rather want to call the block injected by the registry that
54
+ # has created this stub.
55
+ def execute_request(request)
56
+ @execute_block.call(request)
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ end
63
+
64
+
@@ -0,0 +1,86 @@
1
+ #require 'rhcp/rhcp'
2
+ require 'rhcp/broker'
3
+ require 'rhcp/rhcp_exception'
4
+ require 'rhcp/client/command_stub'
5
+ require 'net/http'
6
+ require 'json'
7
+
8
+ module RHCP
9
+
10
+ module Client
11
+
12
+ # This is an implementation of a RHCP broker that retrieves it's data via
13
+ # http from a remote broker. Since it implements the same interface as RHCP::Broker,
14
+ # clients can use it exactly as if they were talking with the broker itself.
15
+ # TODO shouldn't this descend from Broker?
16
+ class HttpBroker
17
+
18
+ def initialize(url)
19
+ # TODO should this really be an URL? or just a host name?
20
+ @url = url
21
+ @logger = RHCP::ModuleHelper.instance.logger
22
+ @logger.debug "connecting to #{@url}"
23
+ res = Net::HTTP.new(@url.host, @url.port).start { |http| http.get("/rhcp/") }
24
+ if (res.code == "200")
25
+ @logger.info "connected to '#{@url}' successfully."
26
+ else
27
+ @logger.error "cannot connect to '#{@url}' : got http status code #{res.code}"
28
+ raise RHCP::RhcpException.new("could not connect to '#{@url}' : got http status code #{res.code} (#{res.message})");
29
+ end
30
+ end
31
+
32
+ # returns a list of all known commands
33
+ # TODO add caching for commands
34
+ def get_command_list()
35
+ res = Net::HTTP.new(@url.host, @url.port).start { |http| http.get("/rhcp/get_commands") }
36
+ if (res.code != "200")
37
+ raise RHCP::RhcpException.new("could not retrieve command list from remote server : http status #{res.code} (#{res.message})")
38
+ end
39
+
40
+ @logger.debug "raw response : >>#{res.body}<<"
41
+
42
+ # the commands are transferred as array => convert into hash
43
+ command_array = JSON.parse(res.body)
44
+ commands = Hash.new()
45
+ command_array.each do |command_string|
46
+ command = RHCP::Client::CommandStub.reconstruct_from_json(command_string)
47
+ commands[command.name] = command
48
+ # and inject the block for executing over http
49
+ command.execute_block = lambda {
50
+ |req|
51
+ @logger.debug("executing command #{command.name} via http using #{@url}")
52
+
53
+ res = Net::HTTP.new(@url.host, @url.port).start { |http| http.post("/rhcp/execute", req.to_json()) }
54
+ if (res.code != "200")
55
+ raise RHCP::RhcpException.new("could not execute command using remote server : http status #{res.code} (#{res.message})")
56
+ end
57
+
58
+ json_response = RHCP::Response.reconstruct_from_json(res.body)
59
+ @logger.debug "json response : #{json_response}"
60
+ json_response
61
+ }
62
+
63
+ # inject appropriate blocks for all params
64
+ command.params.each do |name, param|
65
+ param.get_lookup_values_block = lambda {
66
+ |partial_value|
67
+ @logger.debug("getting lookup values for param #{name} of command #{command.name} using #{@url}")
68
+ # TODO add caching for lookup values
69
+ res = Net::HTTP.new(@url.host, @url.port).start { |http|
70
+ http.get("/rhcp/get_lookup_values?command=#{command.name}&param=#{name}")
71
+ }
72
+ if (res.code != "200")
73
+ raise RHCP::RhcpException.new("could not retrieve lookup values from remote server : http status #{res.code} (#{res.message})")
74
+ end
75
+ JSON.parse(res.body)
76
+ }
77
+ end
78
+ end
79
+ commands
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+
86
+ end
@@ -0,0 +1,143 @@
1
+ module RHCP
2
+
3
+ require 'rubygems'
4
+ require 'json'
5
+
6
+ #require 'rhcp/rhcp'
7
+ require 'rhcp/response'
8
+
9
+ # This class represents a single RHCP command, i.e. a functionality that
10
+ # should be exposed to clients.
11
+ class Command
12
+
13
+ # TODO add metadata about the command's result (display type ("table"), column order etc.)
14
+
15
+ # a unique name for this command
16
+ attr_reader :name
17
+
18
+ # textual description of what this command is doing
19
+ attr_reader :description
20
+
21
+ # the parameters supported by this command
22
+ attr_reader :params
23
+
24
+ # the block that should be executed if this command is invoked
25
+ attr_reader :block
26
+
27
+ # the parameter that is the default param for this command; might be nil
28
+ attr_reader :default_param
29
+
30
+ # "true" if this command returns data, but does not modify anything.
31
+ attr_reader :is_read_only
32
+
33
+ # hints about how the result of this command might be displayed
34
+ attr_accessor :result_hints
35
+
36
+ def initialize(name, description, block)
37
+ @logger = RHCP::ModuleHelper.instance().logger
38
+
39
+ @name = name
40
+ @description = description
41
+ @block = block
42
+ @params = Hash.new()
43
+ @default_param = nil
44
+ @is_read_only = false
45
+ # TODO formalize this! (we need rules how clients should react, otherwise this will be a mess)
46
+ # TODO in some cases, the display_type could also be guessed from the response, I guess
47
+ @result_hints = {
48
+ :display_type => "unspecified",
49
+ :overview_columns => [],
50
+ :column_titles => []
51
+ }
52
+ end
53
+
54
+ # adds a new parameter and returns the command
55
+ def add_param(new_param)
56
+ raise "duplicate parameter name '#{new_param.name}'" if @params.has_key?(new_param.name)
57
+ @params[new_param.name] = new_param
58
+ if new_param.is_default_param
59
+ if @default_param != nil
60
+ raise RHCP::RhcpException.new("there can be only one default parameter per command, and the parameter '#{@default_param.name}' has already been marked as default")
61
+ end
62
+ @default_param = new_param
63
+ end
64
+ self
65
+ end
66
+
67
+ # marks this command as a read-only command, i.e. a command that returns data,
68
+ # but does not perform any action.
69
+ # this information might influence permissions and/or the way the command's
70
+ # result is displayed
71
+ def mark_as_read_only()
72
+ @is_read_only = true
73
+ self
74
+ end
75
+
76
+ # returns the specified CommandParam
77
+ # or throws an RhcpException if the parameter does not exist
78
+ def get_param(param_name)
79
+ raise RHCP::RhcpException.new("no such parameter : #{param_name}") unless @params.has_key?(param_name)
80
+ @params[param_name]
81
+ end
82
+
83
+ # invokes this command
84
+ def execute_request(request)
85
+ @logger.debug "gonna execute request >>#{request}<<"
86
+ response = RHCP::Response.new()
87
+ begin
88
+ # TODO redirect the block's output (both Logger and STDOUT/STDERR) and send it back with the response
89
+ result = block.call(request, response)
90
+ response.set_payload(result)
91
+ rescue => ex
92
+ response.mark_as_error(ex.to_s, ex.backtrace.join("\n"))
93
+ end
94
+
95
+ fire_post_exec_event(request, response)
96
+
97
+ response
98
+ end
99
+
100
+ def execute(param_values = {})
101
+ req = RHCP::Request.new(self, param_values)
102
+ execute_request(req)
103
+ end
104
+
105
+
106
+ # we do not serialize the block (no sense in this) nor the default param
107
+ # when the command is unmarshalled as stub, the default param will be set again
108
+ # automatically by add_param(), and the block will be replaced by the block
109
+ # that does the remote invocation
110
+ def to_json(*args)
111
+ {
112
+ 'name' => @name,
113
+ 'description' => @description,
114
+ 'params' => @params.values,
115
+ 'read_only' => @is_read_only,
116
+ 'result_hints' => @result_hints
117
+ }.to_json(*args)
118
+ end
119
+
120
+ # TODO think about moving this and the whole command thing to the broker
121
+ def Command.register_post_exec_listener(block)
122
+ @@listener = [] unless defined?(@@listener)
123
+ @@listener << block
124
+ end
125
+
126
+ def Command.clear_post_exec_listeners()
127
+ @@listener = []
128
+ end
129
+
130
+ def fire_post_exec_event(request, response)
131
+ return unless defined? @@listener
132
+ @@listener.each do |listener|
133
+ begin
134
+ listener.call(request, response)
135
+ rescue => ex
136
+ @logger.warn "a post-exec listener failed with the following message : #{ex}"
137
+ end
138
+ end
139
+ end
140
+
141
+ end
142
+
143
+ end
@@ -0,0 +1,93 @@
1
+ require 'rubygems'
2
+ require 'json'
3
+
4
+ require 'rhcp/rhcp_exception'
5
+
6
+ module RHCP
7
+
8
+ # This class represents a single parameter that can be specified for
9
+ # a certain command. It is used by the server side to define which
10
+ # commands are available for clients.
11
+ class CommandParam
12
+
13
+ # a unique name for this parameter
14
+ attr_reader :name
15
+ # a textual description of this parameter's meaning
16
+ attr_reader :description
17
+ # "true" if the command can not be invoked without this parameter
18
+ attr_reader :mandatory
19
+ # "true" if there are lookup values available for this parameter
20
+ attr_reader :has_lookup_values
21
+ # "true" if this parameter might be specified multiple times, resulting
22
+ # in a list of values for this parameter
23
+ attr_reader :allows_multiple_values
24
+ # "true" if this parameter is the default parameter for the enclosing command
25
+ attr_reader :is_default_param
26
+
27
+ # creates a new command parameter
28
+ # options is a hash, possibly holding the following values (all optional)
29
+ # :mandatory true if the parameter is necessary for this command
30
+ # :allows_multiple_values true if this parameter might be specified multiple times
31
+ # :lookup_method a block that returns an array of lookup values valid for this param
32
+ # :is_default_param true if this parameter is the default param for the enclosing command
33
+ def initialize(name, description, options = Hash.new)
34
+ @name = name
35
+ @description = description
36
+
37
+ @mandatory = options[:mandatory] || false
38
+ @lookup_value_block = options[:lookup_method]
39
+ @has_lookup_values = @lookup_value_block != nil
40
+ @allows_multiple_values = options[:allows_multiple_values] || false
41
+ @is_default_param = options[:is_default_param] || false
42
+ end
43
+
44
+ # returns lookup values for this parameter
45
+ # if "partial_value" is specified, only those values are returned that start
46
+ # with "partial_value"
47
+ def get_lookup_values(partial_value = "")
48
+ if @has_lookup_values
49
+ @lookup_value_block.call().grep(/^#{partial_value}/)
50
+ else
51
+ []
52
+ end
53
+ end
54
+
55
+ # throws an exception if +possible_value+ is not a valid value for this parameter
56
+ # returns true otherwise
57
+ # note that this methods expects +possible_value+ to be an array since all param values
58
+ # are specified as arrays
59
+ def check_param_is_valid(possible_value)
60
+
61
+ # formal check : single-value params against multi-value params
62
+ if ((! @allows_multiple_values) && (possible_value.size > 1))
63
+ raise RHCP::RhcpException.new("multiple values specified for single-value parameter '#{@name}'")
64
+ end
65
+
66
+ # check against lookup values
67
+ if @has_lookup_values
68
+ possible_value.each do |value|
69
+ if ! get_lookup_values().include?(value)
70
+ raise RHCP::RhcpException.new("invalid value '#{value}' for parameter '#{@name}'")
71
+ end
72
+ end
73
+ end
74
+
75
+ true
76
+ end
77
+
78
+ # we do not serialize the lookup_method (it couldn't be invoked remotely
79
+ # anyway)
80
+ def to_json(*args)
81
+ {
82
+ 'name' => @name,
83
+ 'description' => @description,
84
+ 'allows_multiple_values' => @allows_multiple_values,
85
+ 'has_lookup_values' => @has_lookup_values,
86
+ 'is_default_param' => @is_default_param,
87
+ 'mandatory' => @mandatory
88
+ }.to_json(*args)
89
+ end
90
+
91
+ end
92
+
93
+ end