arborist 0.0.1.pre20160106113421

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 (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
+