arborist 0.0.1.pre20160606141735 → 0.0.1.pre20160829140603

Sign up to get free protection for your applications and to get access to all the features.
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' )