arborist 0.0.1.pre20160606141735 → 0.0.1.pre20160829140603

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/ChangeLog +81 -2
  3. data/Events.md +11 -11
  4. data/Manifest.txt +2 -4
  5. data/TODO.md +9 -29
  6. data/lib/arborist.rb +6 -2
  7. data/lib/arborist/command/watch.rb +59 -8
  8. data/lib/arborist/loader/file.rb +1 -1
  9. data/lib/arborist/manager.rb +139 -50
  10. data/lib/arborist/manager/event_publisher.rb +29 -0
  11. data/lib/arborist/manager/tree_api.rb +16 -1
  12. data/lib/arborist/mixins.rb +36 -1
  13. data/lib/arborist/monitor.rb +12 -0
  14. data/lib/arborist/monitor/socket.rb +8 -9
  15. data/lib/arborist/monitor_runner.rb +2 -2
  16. data/lib/arborist/node.rb +16 -3
  17. data/lib/arborist/node/host.rb +25 -22
  18. data/lib/arborist/node/resource.rb +28 -14
  19. data/lib/arborist/node/service.rb +21 -2
  20. data/lib/arborist/observer_runner.rb +68 -7
  21. data/spec/arborist/client_spec.rb +3 -3
  22. data/spec/arborist/manager/event_publisher_spec.rb +0 -1
  23. data/spec/arborist/manager/tree_api_spec.rb +6 -5
  24. data/spec/arborist/manager_spec.rb +53 -24
  25. data/spec/arborist/node/resource_spec.rb +9 -0
  26. data/spec/arborist/node/service_spec.rb +16 -1
  27. data/spec/arborist/node_spec.rb +22 -12
  28. data/spec/arborist/observer_runner_spec.rb +58 -0
  29. data/spec/arborist/observer_spec.rb +15 -15
  30. data/spec/arborist/subscription_spec.rb +1 -1
  31. data/spec/data/nodes/{duir.rb → sub/duir.rb} +0 -0
  32. data/spec/data/observers/auditor.rb +2 -2
  33. data/spec/spec_helper.rb +1 -0
  34. metadata +30 -27
  35. checksums.yaml.gz.sig +0 -0
  36. data.tar.gz.sig +0 -2
  37. data/lib/arborist/event/sys_node_added.rb +0 -10
  38. data/lib/arborist/event/sys_node_removed.rb +0 -10
  39. data/lib/arborist/event/sys_reloaded.rb +0 -15
  40. metadata.gz.sig +0 -0
@@ -7,11 +7,13 @@ require 'socket'
7
7
 
8
8
  require 'arborist/node'
9
9
  require 'arborist/mixins'
10
+ require 'arborist/exceptions'
10
11
 
11
12
 
12
13
  # A node type for Arborist trees that represent services running on hosts.
13
14
  class Arborist::Node::Service < Arborist::Node
14
- include Arborist::HashUtilities
15
+ include Arborist::HashUtilities,
16
+ Arborist::NetworkUtilities
15
17
 
16
18
 
17
19
  # The default transport layer protocol to use for services that don't specify
@@ -29,6 +31,7 @@ class Arborist::Node::Service < Arborist::Node
29
31
  qualified_identifier = "%s-%s" % [ host.identifier, identifier ]
30
32
 
31
33
  @host = host
34
+ @addresses = nil
32
35
  @app_protocol = nil
33
36
  @protocol = nil
34
37
  @port = nil
@@ -84,9 +87,25 @@ class Arborist::Node::Service < Arborist::Node
84
87
  end
85
88
 
86
89
 
90
+ ### Set an IP address of the service. This must be one of the addresses of its
91
+ ### containing host.
92
+ def address( new_address )
93
+ self.log.debug "Adding address %p to %p" % [ new_address, self ]
94
+ normalized_addresses = normalize_address( new_address )
95
+
96
+ unless normalized_addresses.all? {|addr| @host.addresses.include?(addr) }
97
+ raise Arborist::ConfigError, "%s is not one of %s's addresses" %
98
+ [ new_address, @host.identifier ]
99
+ end
100
+
101
+ @addresses ||= []
102
+ @addresses += normalized_addresses
103
+ end
104
+
105
+
87
106
  ### Delegate the service's address to its host.
88
107
  def addresses
89
- return @host.addresses
108
+ return @addresses || @host.addresses
90
109
  end
91
110
 
92
111
 
@@ -31,7 +31,8 @@ class Arborist::ObserverRunner
31
31
 
32
32
  ### Create a ZMQ::Handler that acts as the agent that runs the specified
33
33
  ### +observer+.
34
- def initialize( reactor )
34
+ def initialize( runner, reactor )
35
+ @runner = runner
35
36
  @client = Arborist::Client.new
36
37
  @pollitem = ZMQ::Pollitem.new( @client.event_api, ZMQ::POLLIN )
37
38
  @pollitem.handler = self
@@ -45,6 +46,9 @@ class Arborist::ObserverRunner
45
46
  public
46
47
  ######
47
48
 
49
+ # The Arborist::ObserverRunner that owns this handler.
50
+ attr_reader :runner
51
+
48
52
  # The Arborist::Client that will be used for creating and tearing down subscriptions
49
53
  attr_reader :client
50
54
 
@@ -52,6 +56,16 @@ class Arborist::ObserverRunner
52
56
  attr_reader :subscriptions
53
57
 
54
58
 
59
+ ### Unsubscribe from and clear all current subscriptions.
60
+ def reset
61
+ self.log.warn "Resetting the observer handler."
62
+ self.subscriptions.keys.each do |subid|
63
+ self.client.event_api.unsubscribe( subid )
64
+ end
65
+ self.subscriptions.clear
66
+ end
67
+
68
+
55
69
  ### Add the specified +observer+ and subscribe to the events it wishes to receive.
56
70
  def add_observer( observer )
57
71
  self.log.info "Adding observer: %s" % [ observer.description ]
@@ -85,10 +99,13 @@ class Arborist::ObserverRunner
85
99
  subid = self.recv
86
100
  raise "Partial write?!" unless self.pollitem.pollable.rcvmore?
87
101
  raw_event = self.recv
102
+ event = MessagePack.unpack( raw_event )
88
103
 
89
104
  if (( observer = self.subscriptions[subid] ))
90
- event = MessagePack.unpack( raw_event )
91
105
  observer.handle_event( subid, event )
106
+ elsif subid.start_with?( 'sys.' )
107
+ self.log.debug "System event! %p" % [ event ]
108
+ self.runner.handle_system_event( subid, event )
92
109
  else
93
110
  self.log.warn "Ignoring event %p for which we have no observer." % [ subid ]
94
111
  end
@@ -105,6 +122,7 @@ class Arborist::ObserverRunner
105
122
  @timers = []
106
123
  @handler = nil
107
124
  @reactor = ZMQ::Loop.new
125
+ @manager_last_runid = nil
108
126
  end
109
127
 
110
128
 
@@ -133,12 +151,11 @@ class Arborist::ObserverRunner
133
151
 
134
152
  ### Run the specified +observers+
135
153
  def run
136
- self.handler = Arborist::ObserverRunner::Handler.new( self.reactor )
154
+ self.handler = Arborist::ObserverRunner::Handler.new( self, self.reactor )
137
155
 
138
- self.observers.each do |observer|
139
- self.handler.add_observer( observer )
140
- self.add_timers_for( observer )
141
- end
156
+ self.register_observers
157
+ self.register_observer_timers
158
+ self.subscribe_to_system_events
142
159
 
143
160
  self.reactor.start
144
161
  rescue Interrupt
@@ -158,6 +175,28 @@ class Arborist::ObserverRunner
158
175
  end
159
176
 
160
177
 
178
+ ### Register each of the runner's Observers with its handler.
179
+ def register_observers
180
+ self.observers.each do |observer|
181
+ self.handler.add_observer( observer )
182
+ end
183
+ end
184
+
185
+
186
+ ### Register timers for each Observer.
187
+ def register_observer_timers
188
+ self.observers.each do |observer|
189
+ self.add_timers_for( observer )
190
+ end
191
+ end
192
+
193
+
194
+ ### Subscribe the runner to system events published by the Manager.
195
+ def subscribe_to_system_events
196
+ self.handler.client.event_api.subscribe( 'sys.' )
197
+ end
198
+
199
+
161
200
  ### Register a timer for the specified +observer+.
162
201
  def add_timers_for( observer )
163
202
  observer.timers.each do |interval, callback|
@@ -177,5 +216,27 @@ class Arborist::ObserverRunner
177
216
  end
178
217
  end
179
218
 
219
+
220
+ ### Handle a `sys.` event from the Manager being observed.
221
+ def handle_system_event( event_type, event )
222
+ self.log.debug "Got a %s event from the Manager: %p" % [ event_type, event ]
223
+
224
+ case event_type
225
+ when 'sys.heartbeat'
226
+ this_runid = event['run_id']
227
+ if @manager_last_runid && this_runid != @manager_last_runid
228
+ self.log.warn "Manager run ID changed: re-subscribing"
229
+ self.handler.reset
230
+ self.register_observers
231
+ end
232
+
233
+ @manager_last_runid = this_runid
234
+ when 'sys.node_added', 'sys.node_removed'
235
+ # no-op
236
+ else
237
+ # no-op
238
+ end
239
+ end
240
+
180
241
  end # class Arborist::ObserverRunner
181
242
 
@@ -12,15 +12,15 @@ describe Arborist::Client do
12
12
  describe "synchronous API", :testing_manager do
13
13
 
14
14
  before( :each ) do
15
- @manager = make_testing_manager()
16
15
  @manager_thread = Thread.new do
16
+ @manager = make_testing_manager()
17
17
  Thread.current.abort_on_exception = true
18
18
  @manager.run
19
19
  Loggability[ Arborist ].info "Stopped the test manager"
20
20
  end
21
21
 
22
22
  count = 0
23
- until @manager.running? || count > 30
23
+ until (@manager && @manager.running?) || count > 30
24
24
  sleep 0.1
25
25
  count += 1
26
26
  end
@@ -28,7 +28,7 @@ describe Arborist::Client do
28
28
  end
29
29
 
30
30
  after( :each ) do
31
- @manager.stop
31
+ @manager.simulate_signal( :TERM )
32
32
  @manager_thread.join
33
33
 
34
34
  count = 0
@@ -60,7 +60,6 @@ describe Arborist::Manager::EventPublisher do
60
60
  publisher.on_writable
61
61
  end
62
62
 
63
-
64
63
  end
65
64
 
66
65
 
@@ -6,23 +6,24 @@ require_relative '../../spec_helper'
6
6
  describe Arborist::Manager::TreeAPI, :testing_manager do
7
7
 
8
8
  before( :each ) do
9
- @manager = make_testing_manager()
9
+ @manager = nil
10
10
  @manager_thread = Thread.new do
11
+ @manager = make_testing_manager()
11
12
  Thread.current.abort_on_exception = true
12
- manager.run
13
+ @manager.run
13
14
  Loggability[ Arborist ].info "Stopped the test manager"
14
15
  end
15
16
 
16
17
  count = 0
17
- until manager.running? || count > 30
18
+ until (@manager && @manager.running?) || count > 30
18
19
  sleep 0.1
19
20
  count += 1
20
21
  end
21
- raise "Manager didn't start up" unless manager.running?
22
+ raise "Manager didn't start up" unless @manager.running?
22
23
  end
23
24
 
24
25
  after( :each ) do
25
- @manager.stop
26
+ @manager.simulate_signal( :TERM )
26
27
  unless @manager_thread.join( 5 )
27
28
  $stderr.puts "Manager thread didn't exit on its own; killing it."
28
29
  @manager_thread.kill
@@ -164,30 +164,32 @@ describe Arborist::Manager do
164
164
 
165
165
 
166
166
  it "checkpoints the state file periodically if an interval is configured" do
167
- described_class.configure( manager: {checkpoint_frequency: 20, state_file: 'arb.tree'} )
167
+ described_class.configure( manager: {checkpoint_frequency: 20_000, state_file: 'arb.tree'} )
168
168
 
169
+ zloop = instance_double( ZMQ::Loop, register: nil, :verbose= => nil )
169
170
  timer = instance_double( ZMQ::Timer, "checkpoint timer" )
170
- expect( ZMQ::Timer ).to receive( :new ).with( 20, 0 ).and_return( timer )
171
+ expect( ZMQ::Loop ).to receive( :new ).and_return( zloop )
172
+ allow( ZMQ::Timer ).to receive( :new ).and_call_original
173
+ expect( ZMQ::Timer ).to receive( :new ).with( 20.0, 0 ).and_return( timer )
171
174
 
172
- expect( manager.start_state_checkpointing ).to eq( timer )
175
+ manager = described_class.new
176
+ expect( manager.checkpoint_timer ).to eq( timer )
173
177
  end
174
178
 
175
179
 
176
180
  it "doesn't checkpoint if no interval is configured" do
177
181
  described_class.configure( manager: {checkpoint_frequency: nil, state_file: 'arb.tree'} )
178
182
 
179
- expect( ZMQ::Timer ).to_not receive( :new )
180
-
181
- expect( manager.start_state_checkpointing ).to be_nil
183
+ manager = described_class.new
184
+ expect( manager.checkpoint_timer ).to be_nil
182
185
  end
183
186
 
184
187
 
185
188
  it "doesn't checkpoint if no state file is configured" do
186
189
  described_class.configure( manager: {checkpoint_frequency: 20, state_file: nil} )
187
190
 
188
- expect( ZMQ::Timer ).to_not receive( :new )
189
-
190
- expect( manager.start_state_checkpointing ).to be_nil
191
+ manager = described_class.new
192
+ expect( manager.checkpoint_timer ).to be_nil
191
193
  end
192
194
 
193
195
 
@@ -197,6 +199,20 @@ describe Arborist::Manager do
197
199
  end
198
200
 
199
201
 
202
+ context "heartbeat event" do
203
+
204
+ it "errors if configured with a heartbeat of 0" do
205
+ expect {
206
+ described_class.configure( manager: {heartbeat_frequency: 0} )
207
+ }.to raise_error( Arborist::ConfigError, /positive non-zero/i )
208
+ end
209
+
210
+
211
+ it "is sent at the configured "
212
+
213
+ end
214
+
215
+
200
216
  context "a new empty manager" do
201
217
 
202
218
  let( :node ) do
@@ -583,12 +599,14 @@ describe Arborist::Manager do
583
599
  let( :event_pollitem ) { instance_double(ZMQ::Pollitem, "event API pollitem") }
584
600
  let( :signal_timer ) { instance_double(ZMQ::Timer, "signal timer") }
585
601
 
602
+
586
603
  before( :each ) do
587
604
  allow( ZMQ::Loop ).to receive( :new ).and_return( zmq_loop )
588
605
 
589
606
  allow( zmq_context ).to receive( :socket ).with( :REP ).and_return( tree_sock )
590
607
  allow( zmq_context ).to receive( :socket ).with( :PUB ).and_return( event_sock )
591
608
 
609
+ allow( zmq_loop ).to receive( :verbose= )
592
610
  allow( zmq_loop ).to receive( :remove ).with( tree_pollitem )
593
611
  allow( zmq_loop ).to receive( :remove ).with( event_pollitem )
594
612
 
@@ -596,41 +614,52 @@ describe Arborist::Manager do
596
614
  allow( tree_sock ).to receive( :close )
597
615
  allow( event_pollitem ).to receive( :pollable ).and_return( event_sock )
598
616
  allow( event_sock ).to receive( :close )
599
- end
600
-
601
617
 
602
- it "sets up its sockets with handlers and starts the ZMQ loop when started" do
603
- expect( tree_sock ).to receive( :bind ).with( Arborist.tree_api_url )
604
- expect( tree_sock ).to receive( :linger= ).with( 0 )
618
+ allow( tree_sock ).to receive( :bind ).with( Arborist.tree_api_url )
619
+ allow( tree_sock ).to receive( :linger= )
605
620
 
606
- expect( event_sock ).to receive( :bind ).with( Arborist.event_api_url )
607
- expect( event_sock ).to receive( :linger= ).with( 0 )
621
+ allow( event_sock ).to receive( :bind ).with( Arborist.event_api_url )
622
+ allow( event_sock ).to receive( :linger= )
608
623
 
609
- expect( ZMQ::Pollitem ).to receive( :new ).with( tree_sock, ZMQ::POLLIN|ZMQ::POLLOUT ).
624
+ allow( ZMQ::Pollitem ).to receive( :new ).with( tree_sock, ZMQ::POLLIN|ZMQ::POLLOUT ).
610
625
  and_return( tree_pollitem )
611
- expect( ZMQ::Pollitem ).to receive( :new ).with( event_sock, ZMQ::POLLOUT ).
626
+ allow( ZMQ::Pollitem ).to receive( :new ).with( event_sock, ZMQ::POLLOUT ).
612
627
  and_return( event_pollitem )
613
628
 
614
- expect( tree_pollitem ).to receive( :handler= ).
629
+ allow( tree_pollitem ).to receive( :handler= ).
615
630
  with( an_instance_of(Arborist::Manager::TreeAPI) )
616
- expect( zmq_loop ).to receive( :register ).with( tree_pollitem )
617
- expect( event_pollitem ).to receive( :handler= ).
631
+ allow( zmq_loop ).to receive( :register ).with( tree_pollitem )
632
+ allow( event_pollitem ).to receive( :handler= ).
618
633
  with( an_instance_of(Arborist::Manager::EventPublisher) )
619
- expect( zmq_loop ).to receive( :register ).with( event_pollitem )
634
+ allow( zmq_loop ).to receive( :register ).with( event_pollitem )
635
+ end
636
+
620
637
 
638
+ it "starts handling signals and events when started" do
621
639
  expect( ZMQ::Timer ).to receive( :new ).
622
640
  with( described_class::SIGNAL_INTERVAL, 0, manager.method(:process_signal_queue) ).
623
641
  and_return( signal_timer )
624
642
  expect( zmq_loop ).to receive( :register_timer ).with( signal_timer )
643
+ expect( zmq_loop ).to receive( :register_timer ).with( manager.heartbeat_timer )
625
644
  expect( zmq_loop ).to receive( :start )
626
645
 
627
646
  expect( zmq_loop ).to receive( :remove ).with( tree_pollitem )
628
647
  expect( zmq_loop ).to receive( :remove ).with( event_pollitem )
629
648
 
630
649
  manager.run
631
- end
632
- end
633
650
 
651
+ expect( manager.event_publisher.event_queue.length ).to eq( 1 )
652
+
653
+ event = manager.event_publisher.event_queue.first
654
+ expect( event.first ).to eq( 'sys.startup' )
655
+
656
+ payload = unpack_message( event.last )
657
+ expect( payload ).to include(
658
+ 'start_time' => an_instance_of(String),
659
+ 'version' => an_instance_of(String)
660
+ )
661
+ end
634
662
 
663
+ end
635
664
  end
636
665
 
@@ -19,6 +19,10 @@ describe Arborist::Node::Resource do
19
19
  expect( result.identifier ).to eq( "testhost-disk" )
20
20
  end
21
21
 
22
+ it "defaults the category to the identifier" do
23
+ result = described_class.new( 'load', host )
24
+ expect( result.category ).to eq( 'load' )
25
+ end
22
26
 
23
27
  it "raises a sensible error when created without a host" do
24
28
  expect {
@@ -51,6 +55,11 @@ describe Arborist::Node::Resource do
51
55
  expect( node ).to_not match_criteria( address: '192.168.66.64/27' )
52
56
  expect( node ).to_not match_criteria( address: '127.0.0.0/8' )
53
57
  end
58
+
59
+ it "can be matched with a category" do
60
+ expect( node ).to match_criteria( category: 'disk' )
61
+ expect( node ).to_not match_criteria( category: 'processes' )
62
+ end
54
63
  end
55
64
  end
56
65
 
@@ -122,12 +122,27 @@ describe Arborist::Node::Service do
122
122
  end
123
123
 
124
124
 
125
- it "can be matched with one of its host's addresses" do
125
+ it "inherits its host's addresses" do
126
126
  expect( node ).to match_criteria( address: '192.168.66.12' )
127
127
  expect( node ).to_not match_criteria( address: '127.0.0.1' )
128
128
  end
129
129
 
130
130
 
131
+ it "can be limited to a subset of its host's addresses" do
132
+ node.address( host.addresses.first )
133
+ expect( node ).to match_criteria( address: '192.168.66.12' )
134
+ expect( node ).to_not match_criteria( address: '10.1.33.8' )
135
+ expect( node ).to_not match_criteria( address: '127.0.0.1' )
136
+ end
137
+
138
+
139
+ it "errors if it specifies an address other than one of its host's addresses" do
140
+ expect {
141
+ node.address( '127.0.0.1' )
142
+ }.to raise_error( Arborist::ConfigError, /127.0.0.1 is not one of testhost's addresses/i )
143
+ end
144
+
145
+
131
146
  it "can be matched with a netblock that includes one of its host's addresses" do
132
147
  expect( node ).to match_criteria( address: '192.168.66.0/24' )
133
148
  expect( node ).to match_criteria( address: '10.0.0.0/8' )
@@ -356,6 +356,8 @@ describe Arborist::Node do
356
356
  any_of('webproxy', on: ['fe-host1','fe-host2','fe-host3'])
357
357
  )
358
358
 
359
+ config os: 'freebsd-10'
360
+
359
361
  update( 'song' => 'Around the World', 'artist' => 'Daft Punk', 'length' => '7:09' )
360
362
  end
361
363
  end
@@ -400,6 +402,7 @@ describe Arborist::Node do
400
402
 
401
403
  old_node.instance_variable_set( :@parent, 'foo' )
402
404
  old_node.instance_variable_set( :@description, 'Some older description' )
405
+ old_node.instance_variable_set( :@config, {'os' => 'freebsd-8'} )
403
406
  old_node.tags( :bunker, :lucky, :tickle, :trucker )
404
407
  old_node.source = '/somewhere/else'
405
408
 
@@ -410,6 +413,7 @@ describe Arborist::Node do
410
413
  expect( node.tags ).to eq( node_copy.tags )
411
414
  expect( node.source ).to eq( node_copy.source )
412
415
  expect( node.dependencies ).to eq( node_copy.dependencies )
416
+ expect( node.config ).to eq( node_copy.config )
413
417
  end
414
418
 
415
419
 
@@ -465,26 +469,25 @@ describe Arborist::Node do
465
469
  end
466
470
 
467
471
 
468
- it "an ACKed node goes back to ACKed when re-added to the tree" do
472
+ it "can be marshalled" do
473
+ data = Marshal.dump( node )
474
+ cloned_node = Marshal.load( data )
475
+
476
+ expect( cloned_node ).to eq( node )
477
+ end
478
+
469
479
 
480
+ it "an ACKed node stays ACKed when serialized and restored" do
470
481
  node.update( error: "there's a fire" )
471
482
  node.update( ack: {
472
483
  message: 'We know about the fire. It rages on.',
473
484
  sender: '1986 Labyrinth David Bowie'
474
485
  })
475
- cloned_node = concrete_class.from_hash( node.to_h )
476
- node_added_event = Arborist::Event.create( :sys_node_added, cloned_node )
477
- cloned_node.handle_event( node_added_event )
478
-
479
- expect( cloned_node ).to be_acked
480
- end
481
-
486
+ expect( node ).to be_acked
482
487
 
483
- it "can be marshalled" do
484
- data = Marshal.dump( node )
485
- cloned_node = Marshal.load( data )
488
+ restored_node = Marshal.load( Marshal.dump(node) )
486
489
 
487
- expect( cloned_node ).to eq( node )
490
+ expect( restored_node ).to be_acked
488
491
  end
489
492
 
490
493
 
@@ -639,6 +642,7 @@ describe Arborist::Node do
639
642
  parent 'bar'
640
643
  description "The prototypical node"
641
644
  tags :chunker, :hunky, :flippin, :hippo
645
+ config os: 'freebsd-10'
642
646
 
643
647
  update(
644
648
  'song' => 'Around the World',
@@ -692,6 +696,12 @@ describe Arborist::Node do
692
696
  end
693
697
 
694
698
 
699
+ it "can be matched with config values" do
700
+ expect( node ).to match_criteria( config: {os: 'freebsd-10'} )
701
+ expect( node ).to_not match_criteria( config: {os: 'macosx-10.11.3'} )
702
+ end
703
+
704
+
695
705
  it "can be matched with its user properties" do
696
706
  expect( node ).to match_criteria( song: 'Around the World' )
697
707
  expect( node ).to match_criteria( artist: 'Daft Punk' )