arborist 0.0.1.pre20160106113421

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.document +4 -0
  3. data/.simplecov +9 -0
  4. data/ChangeLog +417 -0
  5. data/Events.md +20 -0
  6. data/History.md +4 -0
  7. data/LICENSE +29 -0
  8. data/Manifest.txt +72 -0
  9. data/Monitors.md +141 -0
  10. data/Nodes.md +0 -0
  11. data/Observers.md +72 -0
  12. data/Protocol.md +214 -0
  13. data/README.md +75 -0
  14. data/Rakefile +81 -0
  15. data/TODO.md +24 -0
  16. data/bin/amanagerd +10 -0
  17. data/bin/amonitord +12 -0
  18. data/bin/aobserverd +12 -0
  19. data/lib/arborist.rb +182 -0
  20. data/lib/arborist/client.rb +191 -0
  21. data/lib/arborist/event.rb +61 -0
  22. data/lib/arborist/event/node_acked.rb +18 -0
  23. data/lib/arborist/event/node_delta.rb +20 -0
  24. data/lib/arborist/event/node_matching.rb +34 -0
  25. data/lib/arborist/event/node_update.rb +19 -0
  26. data/lib/arborist/event/sys_reloaded.rb +15 -0
  27. data/lib/arborist/exceptions.rb +21 -0
  28. data/lib/arborist/manager.rb +508 -0
  29. data/lib/arborist/manager/event_publisher.rb +97 -0
  30. data/lib/arborist/manager/tree_api.rb +207 -0
  31. data/lib/arborist/mixins.rb +363 -0
  32. data/lib/arborist/monitor.rb +377 -0
  33. data/lib/arborist/monitor/socket.rb +163 -0
  34. data/lib/arborist/monitor_runner.rb +217 -0
  35. data/lib/arborist/node.rb +700 -0
  36. data/lib/arborist/node/host.rb +87 -0
  37. data/lib/arborist/node/root.rb +60 -0
  38. data/lib/arborist/node/service.rb +112 -0
  39. data/lib/arborist/observer.rb +176 -0
  40. data/lib/arborist/observer/action.rb +125 -0
  41. data/lib/arborist/observer/summarize.rb +105 -0
  42. data/lib/arborist/observer_runner.rb +181 -0
  43. data/lib/arborist/subscription.rb +82 -0
  44. data/spec/arborist/client_spec.rb +282 -0
  45. data/spec/arborist/event/node_update_spec.rb +71 -0
  46. data/spec/arborist/event_spec.rb +64 -0
  47. data/spec/arborist/manager/event_publisher_spec.rb +66 -0
  48. data/spec/arborist/manager/tree_api_spec.rb +458 -0
  49. data/spec/arborist/manager_spec.rb +442 -0
  50. data/spec/arborist/mixins_spec.rb +195 -0
  51. data/spec/arborist/monitor/socket_spec.rb +195 -0
  52. data/spec/arborist/monitor_runner_spec.rb +152 -0
  53. data/spec/arborist/monitor_spec.rb +251 -0
  54. data/spec/arborist/node/host_spec.rb +104 -0
  55. data/spec/arborist/node/root_spec.rb +29 -0
  56. data/spec/arborist/node/service_spec.rb +98 -0
  57. data/spec/arborist/node_spec.rb +552 -0
  58. data/spec/arborist/observer/action_spec.rb +205 -0
  59. data/spec/arborist/observer/summarize_spec.rb +294 -0
  60. data/spec/arborist/observer_spec.rb +146 -0
  61. data/spec/arborist/subscription_spec.rb +71 -0
  62. data/spec/arborist_spec.rb +146 -0
  63. data/spec/data/monitors/pings.rb +80 -0
  64. data/spec/data/monitors/port_checks.rb +27 -0
  65. data/spec/data/monitors/system_resources.rb +30 -0
  66. data/spec/data/monitors/web_services.rb +17 -0
  67. data/spec/data/nodes/duir.rb +20 -0
  68. data/spec/data/nodes/localhost.rb +15 -0
  69. data/spec/data/nodes/sidonie.rb +29 -0
  70. data/spec/data/nodes/yevaud.rb +26 -0
  71. data/spec/data/observers/auditor.rb +23 -0
  72. data/spec/data/observers/webservices.rb +18 -0
  73. data/spec/spec_helper.rb +117 -0
  74. metadata +368 -0
@@ -0,0 +1,105 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'schedulability'
5
+ require 'schedulability/schedule'
6
+ require 'loggability'
7
+
8
+ require 'arborist/observer' unless defined?( Arborist::Observer )
9
+
10
+
11
+ # An summarization action taken by an Observer.
12
+ class Arborist::Observer::Summarize
13
+ extend Loggability
14
+
15
+
16
+ # Loggability API -- log to the Arborist logger
17
+ log_to :arborist
18
+
19
+
20
+ ### Create a new Summary that will call the specified +block+ +during+ the given schedule,
21
+ ### +every+ specified number of seconds or +count+ events, whichever is sooner.
22
+ def initialize( every: 0, count: 0, during: nil, &block )
23
+ raise ArgumentError, "Summarize requires a block" unless block
24
+ raise ArgumentError, "Summarize requires a value for `every` or `count`." if
25
+ every.zero? && count.zero?
26
+
27
+ @time_threshold = every
28
+ @count_threshold = count
29
+ @schedule = Schedulability::Schedule.parse( during ) if during
30
+ @block = block
31
+
32
+ @event_history = {}
33
+ end
34
+
35
+
36
+ ######
37
+ public
38
+ ######
39
+
40
+ ##
41
+ # The object to #call when the action is triggered.
42
+ attr_reader :block
43
+
44
+ ##
45
+ # The number of seconds between calls to the action
46
+ attr_reader :time_threshold
47
+
48
+ ##
49
+ # The number of events that cause the action to be called.
50
+ attr_reader :count_threshold
51
+
52
+ ##
53
+ # The schedule that applies to this action.
54
+ attr_reader :schedule
55
+
56
+ ##
57
+ # The Hash of recent events, keyed by their arrival time.
58
+ attr_reader :event_history
59
+
60
+
61
+ ### Call the action for the specified +event+.
62
+ def handle_event( event )
63
+ self.record_event( event )
64
+ self.call_block if self.should_run?
65
+ end
66
+
67
+
68
+ ### Handle a timing event by calling the block with any events in the history.
69
+ def on_timer
70
+ self.log.debug "Timer event: %d pending event/s" % [ self.event_history.size ]
71
+ self.call_block unless self.event_history.empty?
72
+ end
73
+
74
+
75
+ ### Execute the action block.
76
+ def call_block
77
+ self.block.call( self.event_history.dup )
78
+ ensure
79
+ self.event_history.clear
80
+ end
81
+
82
+
83
+ ### Record the specified +event+ in the event history if within the scheduled period(s).
84
+ def record_event( event )
85
+ return if self.schedule && !self.schedule.now?
86
+ self.event_history[ Time.now ] = event
87
+ end
88
+
89
+
90
+ ### Returns +true+ if the count threshold is exceeded and the current time is within the
91
+ ### action's schedule.
92
+ def should_run?
93
+ return self.count_threshold_exceeded?
94
+ end
95
+
96
+
97
+ ### Returns +true+ if the number of events in the event history meet or exceed the
98
+ ### #count_threshold.
99
+ def count_threshold_exceeded?
100
+ return false if self.count_threshold.zero?
101
+ self.log.debug "Event history has %d events" % [ self.event_history.size ]
102
+ return self.event_history.size >= self.count_threshold
103
+ end
104
+
105
+ end # class Arborist::Observer::Summarize
@@ -0,0 +1,181 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'rbczmq'
5
+ require 'loggability'
6
+
7
+ require 'arborist' unless defined?( Arborist )
8
+ require 'arborist/client'
9
+ require 'arborist/observer'
10
+
11
+
12
+ # Undo the useless scoping
13
+ class ZMQ::Loop
14
+ public_class_method :instance
15
+ end
16
+
17
+
18
+ # An event-driven runner for Arborist::Observers.
19
+ class Arborist::ObserverRunner
20
+ extend Loggability
21
+
22
+ log_to :arborist
23
+
24
+
25
+ # A ZMQ::Handler object for managing IO for all running observers.
26
+ class Handler < ZMQ::Handler
27
+ extend Loggability,
28
+ Arborist::MethodUtilities
29
+
30
+ log_to :arborist
31
+
32
+ ### Create a ZMQ::Handler that acts as the agent that runs the specified
33
+ ### +observer+.
34
+ def initialize( reactor )
35
+ @client = Arborist::Client.new
36
+ @pollitem = ZMQ::Pollitem.new( @client.event_api, ZMQ::POLLIN )
37
+ @pollitem.handler = self
38
+ @subscriptions = {}
39
+
40
+ reactor.register( @pollitem )
41
+ end
42
+
43
+
44
+ ######
45
+ public
46
+ ######
47
+
48
+ # The Arborist::Client that will be used for creating and tearing down subscriptions
49
+ attr_reader :client
50
+
51
+ # The map of subscription IDs to the Observer which it was created for.
52
+ attr_reader :subscriptions
53
+
54
+
55
+ ### Add the specified +observer+ and subscribe to the events it wishes to receive.
56
+ def add_observer( observer )
57
+ self.log.info "Adding observer: %s" % [ observer.description ]
58
+ observer.subscriptions.each do |sub|
59
+ subid = self.client.subscribe( sub )
60
+ self.subscriptions[ subid ] = observer
61
+ self.client.event_api.subscribe( subid )
62
+ self.log.debug " subscribed to %p with subscription %s" % [ sub, subid ]
63
+ end
64
+ end
65
+
66
+
67
+ ### Remove the specified +observer+ after unsubscribing from its events.
68
+ def remove_observer( observer )
69
+ self.log.info "Removing observer: %s" % [ observer.description ]
70
+
71
+ self.subscriptions.keys.each do |subid|
72
+ next unless self.subscriptions[ subid ] == observer
73
+
74
+ self.client.unsubscribe( subid )
75
+ self.subscriptions.delete( subid )
76
+ self.client.event_api.unsubscribe( subid )
77
+ self.log.debug " unsubscribed from %p" % [ subid ]
78
+ end
79
+ end
80
+
81
+
82
+ ### Read events from the event socket when it becomes readable, and dispatch them to
83
+ ### the correct observer.
84
+ def on_readable
85
+ subid = self.recv
86
+ raise "Partial write?!" unless self.pollitem.pollable.rcvmore?
87
+ raw_event = self.recv
88
+
89
+ if (( observer = self.subscriptions[subid] ))
90
+ event = MessagePack.unpack( raw_event )
91
+ observer.handle_event( subid, event )
92
+ else
93
+ self.log.warn "Ignoring event %p for which we have no observer." % [ subid ]
94
+ end
95
+
96
+ return true
97
+ end
98
+
99
+ end # class Handler
100
+
101
+
102
+ ### Create a new Arborist::ObserverRunner
103
+ def initialize
104
+ @observers = []
105
+ @timers = []
106
+ @handler = nil
107
+ @reactor = ZMQ::Loop.new
108
+ end
109
+
110
+
111
+ ######
112
+ public
113
+ ######
114
+
115
+ # The Array of loaded Arborist::Observers the runner should run.
116
+ attr_reader :observers
117
+
118
+ # The Array of registered ZMQ::Timers
119
+ attr_reader :timers
120
+
121
+ # The ZMQ::Handler subclass that handles all async IO
122
+ attr_accessor :handler
123
+
124
+ # The reactor (a ZMQ::Loop) the runner uses to drive everything
125
+ attr_accessor :reactor
126
+
127
+
128
+ ### Load observers from the specified +enumerator+.
129
+ def load_observers( enumerator )
130
+ @observers += enumerator.to_a
131
+ end
132
+
133
+
134
+ ### Run the specified +observers+
135
+ def run
136
+ self.handler = Arborist::ObserverRunner::Handler.new( self.reactor )
137
+
138
+ self.observers.each do |observer|
139
+ self.handler.add_observer( observer )
140
+ self.add_timers_for( observer )
141
+ end
142
+
143
+ self.reactor.start
144
+ rescue Interrupt
145
+ $stderr.puts "Interrupted!"
146
+ self.stop
147
+ end
148
+
149
+
150
+ ### Stop the observer
151
+ def stop
152
+ self.observers.each do |observer|
153
+ self.remove_timers
154
+ self.handler.remove_observer( observer )
155
+ end
156
+
157
+ self.reactor.stop
158
+ end
159
+
160
+
161
+ ### Register a timer for the specified +observer+.
162
+ def add_timers_for( observer )
163
+ observer.timers.each do |interval, callback|
164
+ self.log.info "Creating timer for %s observer to run %p every %ds" %
165
+ [ observer.description, callback, interval ]
166
+ timer = ZMQ::Timer.new( interval, 0, &callback )
167
+ self.reactor.register_timer( timer )
168
+ self.timers << timer
169
+ end
170
+ end
171
+
172
+
173
+ ### Remove any registered timers.
174
+ def remove_timers
175
+ self.timers.each do |timer|
176
+ self.reactor.cancel_timer( timer )
177
+ end
178
+ end
179
+
180
+ end # class Arborist::ObserverRunner
181
+
@@ -0,0 +1,82 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'loggability'
5
+ require 'securerandom'
6
+
7
+ require 'arborist' unless defined?( Arborist )
8
+ require 'arborist/mixins'
9
+
10
+
11
+ # An observer subscription to node events.
12
+ class Arborist::Subscription
13
+ extend Loggability
14
+ include Arborist::HashUtilities
15
+
16
+
17
+ # Loggability API -- log to the Arborist logger
18
+ log_to :arborist
19
+
20
+
21
+ ### Instantiate a new Subscription object given an +event+ pattern
22
+ ### and event +criteria+.
23
+ def initialize( publisher, event_type=nil, criteria={} )
24
+ @publisher = publisher
25
+ @event_type = event_type
26
+ @criteria = stringify_keys( criteria )
27
+ @id = self.generate_id
28
+ end
29
+
30
+
31
+ ######
32
+ public
33
+ ######
34
+
35
+ # The Arborist::Manager::EventPublisher the subscription will use to publish matching events.
36
+ attr_reader :publisher
37
+
38
+ # A unique identifier for this subscription request.
39
+ attr_reader :id
40
+
41
+ # The Arborist event pattern that this subscription handles.
42
+ attr_reader :event_type
43
+
44
+ # Node selection attributes to match
45
+ attr_reader :criteria
46
+
47
+
48
+ ### Create an identifier for this subscription object.
49
+ def generate_id
50
+ return SecureRandom.uuid
51
+ end
52
+
53
+
54
+ ### Publish any of the specified +events+ which match the subscription.
55
+ def on_events( *events )
56
+ events.flatten.each do |event|
57
+ self.publisher.publish( self.id, event ) if self.interested_in?( event )
58
+ end
59
+ end
60
+
61
+
62
+ ### Returns +true+ if the receiver is interested in publishing the specified +event+.
63
+ def interested_in?( event )
64
+ self.log.debug "Testing %p against type = %p and criteria = %p" %
65
+ [ event, self.event_type, self.criteria ]
66
+ return event.match( self )
67
+ end
68
+ alias_method :is_interested_in?, :interested_in?
69
+
70
+
71
+ ### Return a String representation of the object suitable for debugging.
72
+ def inspect
73
+ return "#<%p:%#x [%s] for %s events matching: %p>" % [
74
+ self.class,
75
+ self.object_id * 2,
76
+ self.id,
77
+ self.event_type,
78
+ self.criteria,
79
+ ]
80
+ end
81
+
82
+ end # class Arborist::Subscription
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'arborist/client'
6
+
7
+
8
+ describe Arborist::Client do
9
+
10
+ let( :client ) { described_class.new }
11
+
12
+ describe "synchronous API", :testing_manager do
13
+
14
+ before( :each ) do
15
+ @manager = make_testing_manager()
16
+ @manager_thread = Thread.new do
17
+ Thread.current.abort_on_exception = true
18
+ @manager.run
19
+ Loggability[ Arborist ].info "Stopped the test manager"
20
+ end
21
+
22
+ count = 0
23
+ until @manager.running? || count > 30
24
+ sleep 0.1
25
+ count += 1
26
+ end
27
+ raise "Manager didn't start up" unless @manager.running?
28
+ end
29
+
30
+ after( :each ) do
31
+ @manager.stop
32
+ @manager_thread.join
33
+
34
+ count = 0
35
+ while @manager.zmq_loop.running? || count > 30
36
+ sleep 0.1
37
+ Loggability[ Arborist ].info "ZMQ loop still running"
38
+ count += 1
39
+ end
40
+ raise "ZMQ Loop didn't stop" if @manager.zmq_loop.running?
41
+ end
42
+
43
+
44
+ let( :manager ) { @manager }
45
+
46
+
47
+ it "can fetch the status of the manager it's connected to" do
48
+ res = client.status
49
+ expect( res ).to include( 'server_version', 'state', 'uptime', 'nodecount' )
50
+ end
51
+
52
+
53
+ it "can list the nodes of the manager it's connected to" do
54
+ res = client.list
55
+ expect( res ).to be_an( Array )
56
+ expect( res.length ).to eq( manager.nodes.length )
57
+ end
58
+
59
+
60
+ it "can list a subtree of the nodes of the manager it's connected to" do
61
+ res = client.list( from: 'duir' )
62
+ expect( res ).to be_an( Array )
63
+ expect( res.length ).to be < manager.nodes.length
64
+ end
65
+
66
+
67
+ it "can fetch all node properties for all 'up' nodes" do
68
+ res = client.fetch
69
+ expect( res ).to be_a( Hash )
70
+ expect( res.length ).to be == manager.nodes.length
71
+ expect( res.values ).to all( be_a(Hash) )
72
+ end
73
+
74
+
75
+ it "can fetch identifiers for all 'up' nodes" do
76
+ res = client.fetch( {}, properties: nil )
77
+ expect( res ).to be_a( Hash )
78
+ expect( res.length ).to be == manager.nodes.length
79
+ expect( res.values ).to all( be_empty )
80
+ end
81
+
82
+
83
+ it "can fetch a subset of properties for all 'up' nodes" do
84
+ res = client.fetch( {}, properties: [:addresses, :status] )
85
+ expect( res ).to be_a( Hash )
86
+ expect( res.length ).to be == manager.nodes.length
87
+ expect( res.values ).to all( be_a(Hash) )
88
+ expect( res.values.map(&:length) ).to all( be <= 2 )
89
+ end
90
+
91
+
92
+ it "can fetch a subset of properties for all 'up' nodes matching specified criteria" do
93
+ res = client.fetch( {type: 'host'}, properties: [:addresses, :status] )
94
+ expect( res ).to be_a( Hash )
95
+ expect( res.length ).to be == manager.nodes.values.count {|n| n.type == 'host' }
96
+ expect( res.values ).to all( include('addresses', 'status') )
97
+ end
98
+
99
+
100
+ it "can fetch all properties for all nodes regardless of their status" do
101
+ # Down a node
102
+ manager.nodes['duir'].update( error: 'something happened' )
103
+
104
+ res = client.fetch( {type: 'host'}, include_down: true )
105
+
106
+ expect( res ).to be_a( Hash )
107
+ expect( res ).to include( 'duir' )
108
+ expect( res['duir']['status'] ).to eq( 'down' )
109
+ end
110
+
111
+
112
+ it "can update the properties of managed nodes" do
113
+ client.update( duir: { ping: {rtt: 24} } )
114
+
115
+ expect( manager.nodes['duir'].properties ).to include( 'ping' )
116
+ expect( manager.nodes['duir'].properties['ping'] ).to include( 'rtt' )
117
+ expect( manager.nodes['duir'].properties['ping']['rtt'] ).to eq( 24 )
118
+ end
119
+
120
+
121
+ it "can subscribe to all events" do
122
+ sub_id = client.subscribe
123
+ expect( sub_id ).to be_a( String )
124
+ expect( sub_id ).to match( /^[\w\-]{16,}/ )
125
+
126
+ node = manager.subscriptions[ sub_id ]
127
+ sub = manager.root.subscriptions[ sub_id ]
128
+
129
+ expect( sub ).to be_a( Arborist::Subscription )
130
+ expect( sub.criteria ).to be_empty
131
+ expect( sub.event_type ).to be_nil
132
+ end
133
+
134
+
135
+ it "can subscribe to a particular kind of event" do
136
+ sub_id = client.subscribe( event_type: 'node.ack' )
137
+ expect( sub_id ).to be_a( String )
138
+ expect( sub_id ).to match( /^[\w\-]{16,}/ )
139
+
140
+ node = manager.subscriptions[ sub_id ]
141
+ sub = manager.root.subscriptions[ sub_id ]
142
+
143
+ expect( sub ).to be_a( Arborist::Subscription )
144
+ expect( sub.criteria ).to be_empty
145
+ expect( sub.event_type ).to eq( 'node.ack' )
146
+ end
147
+
148
+
149
+ it "can subscribe to events for descendants of a particular node in the tree" do
150
+ sub_id = client.subscribe( identifier: 'sidonie' )
151
+ expect( sub_id ).to be_a( String )
152
+ expect( sub_id ).to match( /^[\w\-]{16,}/ )
153
+
154
+ node = manager.subscriptions[ sub_id ]
155
+ sub = node.subscriptions[ sub_id ]
156
+
157
+ expect( node.identifier ).to eq( 'sidonie' )
158
+ expect( sub ).to be_a( Arborist::Subscription )
159
+ expect( sub.criteria ).to be_empty
160
+ expect( sub.event_type ).to be_nil
161
+ end
162
+
163
+
164
+ it "can subscribe to events of a particular type for descendants of a particular node" do
165
+ sub_id = client.subscribe( identifier: 'sidonie', event_type: 'node.delta' )
166
+ expect( sub_id ).to be_a( String )
167
+ expect( sub_id ).to match( /^[\w\-]{16,}/ )
168
+
169
+ node = manager.subscriptions[ sub_id ]
170
+ sub = node.subscriptions[ sub_id ]
171
+
172
+ expect( node.identifier ).to eq( 'sidonie' )
173
+ expect( sub ).to be_a( Arborist::Subscription )
174
+ expect( sub.criteria ).to be_empty
175
+ expect( sub.event_type ).to eq( 'node.delta' )
176
+ end
177
+
178
+
179
+ it "can subscribe to events matching one or more criteria" do
180
+ sub_id = client.subscribe( criteria: {type: 'service'} )
181
+ expect( sub_id ).to be_a( String )
182
+ expect( sub_id ).to match( /^[\w\-]{16,}/ )
183
+
184
+ node = manager.subscriptions[ sub_id ]
185
+ sub = node.subscriptions[ sub_id ]
186
+
187
+ expect( node.identifier ).to eq( '_' )
188
+ expect( sub ).to be_a( Arborist::Subscription )
189
+ expect( sub.criteria ).to eq( 'type' => 'service' )
190
+ expect( sub.event_type ).to eq( nil )
191
+ end
192
+
193
+ end
194
+
195
+
196
+ describe "asynchronous API" do
197
+
198
+ it "can make a raw status request" do
199
+ req = client.make_status_request
200
+ expect( req ).to be_a( String )
201
+ expect( req.encoding ).to eq( Encoding::ASCII_8BIT )
202
+
203
+ msg = unpack_message( req )
204
+ expect( msg ).to be_an( Array )
205
+ expect( msg.first ).to be_a( Hash )
206
+ expect( msg.first ).to include( 'version', 'action' )
207
+ expect( msg.first['version'] ).to eq( Arborist::Client::API_VERSION )
208
+ expect( msg.first['action'] ).to eq( 'status' )
209
+ end
210
+
211
+
212
+ it "can make a raw list request" do
213
+ req = client.make_list_request
214
+ expect( req ).to be_a( String )
215
+ expect( req.encoding ).to eq( Encoding::ASCII_8BIT )
216
+
217
+ msg = unpack_message( req )
218
+ expect( msg ).to be_an( Array )
219
+ expect( msg.first ).to be_a( Hash )
220
+ expect( msg.first ).to include( 'version', 'action' )
221
+ expect( msg.first ).to_not include( 'from' )
222
+ expect( msg.first['version'] ).to eq( Arborist::Client::API_VERSION )
223
+ expect( msg.first['action'] ).to eq( 'list' )
224
+ end
225
+
226
+
227
+ it "can make a raw fetch request" do
228
+ req = client.make_fetch_request( {} )
229
+ expect( req ).to be_a( String )
230
+ expect( req.encoding ).to eq( Encoding::ASCII_8BIT )
231
+
232
+ msg = unpack_message( req )
233
+ expect( msg ).to be_an( Array )
234
+ expect( msg.first ).to be_a( Hash )
235
+ expect( msg.first ).to include( 'version', 'action' )
236
+ expect( msg.first['version'] ).to eq( Arborist::Client::API_VERSION )
237
+ expect( msg.first['action'] ).to eq( 'fetch' )
238
+
239
+ expect( msg.last ).to eq( {} )
240
+ end
241
+
242
+
243
+ it "can make a raw fetch request with criteria" do
244
+ req = client.make_fetch_request( {type: 'host'} )
245
+ expect( req ).to be_a( String )
246
+ expect( req.encoding ).to eq( Encoding::ASCII_8BIT )
247
+
248
+ msg = unpack_message( req )
249
+ expect( msg ).to be_an( Array )
250
+ expect( msg.first ).to be_a( Hash )
251
+ expect( msg.first ).to include( 'version', 'action' )
252
+ expect( msg.first['version'] ).to eq( Arborist::Client::API_VERSION )
253
+ expect( msg.first['action'] ).to eq( 'fetch' )
254
+
255
+ expect( msg.last ).to be_a( Hash )
256
+ expect( msg.last ).to include( 'type' )
257
+ expect( msg.last['type'] ).to eq( 'host' )
258
+ end
259
+
260
+
261
+ it "can make a raw update request" do
262
+ req = client.make_update_request( duir: {error: "Something happened."} )
263
+ expect( req ).to be_a( String )
264
+ expect( req.encoding ).to eq( Encoding::ASCII_8BIT )
265
+
266
+ msg = unpack_message( req )
267
+ expect( msg ).to be_an( Array )
268
+ expect( msg.first ).to be_a( Hash )
269
+ expect( msg.first ).to include( 'version', 'action' )
270
+ expect( msg.first['version'] ).to eq( Arborist::Client::API_VERSION )
271
+ expect( msg.first['action'] ).to eq( 'update' )
272
+
273
+ expect( msg.last ).to be_a( Hash )
274
+ expect( msg.last ).to include( 'duir' )
275
+ expect( msg.last['duir'] ).to eq( 'error' => 'Something happened.' )
276
+ end
277
+
278
+ end
279
+
280
+
281
+ end
282
+