spec/arborist/manager_spec.rb in arborist-0.0.1.pre20160128152542 vs spec/arborist/manager_spec.rb in arborist-0.0.1.pre20160606141735
- old
+ new
@@ -2,14 +2,20 @@
require_relative '../spec_helper'
require 'timecop'
require 'arborist/manager'
+require 'arborist/node/host'
-
describe Arborist::Manager do
+ after( :all ) do
+ Arborist::Manager.state_file = nil
+ end
+ before( :each ) do
+ Arborist::Manager.configure
+ end
after( :each ) do
Arborist::Node::Root.reset
end
@@ -47,10 +53,152 @@
it "has an uptime of 0 if it hasn't yet been started" do
expect( manager.uptime ).to eq( 0 )
end
+ describe "state-saving" do
+
+ before( :each ) do
+ Arborist::Manager.state_file = nil
+ end
+
+ let( :router_node ) { Arborist::Host('router') }
+ let( :host_node ) { Arborist::Host( 'host-a', router_node ) }
+ let( :tree ) {[ router_node, host_node ]}
+
+ let( :manager ) do
+ instance = described_class.new
+ instance.load_tree( tree )
+ instance
+ end
+
+
+ it "saves the state of its node tree if the state file is configured" do
+ statefile = Pathname( './arborist.tree' )
+ Arborist::Manager.state_file = statefile
+
+ tempfile = instance_double( Tempfile,
+ path: './arborist20160224-31449-zevoz2.tree', unlink: nil )
+
+ expect( Tempfile ).to receive( :create ).
+ with( ['arborist', '.tree'], '.', encoding: 'binary' ).
+ and_return( tempfile )
+ expect( Marshal ).to receive( :dump ).with( manager.nodes, tempfile )
+ expect( tempfile ).to receive( :close )
+ expect( File ).to receive( :rename ).
+ with( './arborist20160224-31449-zevoz2.tree', './arborist.tree' )
+
+ manager.save_node_states
+ end
+
+
+ it "cleans up the tempfile created by checkpointing if renaming the file fails" do
+ statefile = Pathname( './arborist.tree' )
+ Arborist::Manager.state_file = statefile
+
+ tempfile = instance_double( Tempfile, path: './arborist20160224-31449-zevoz2.tree' )
+
+ expect( Tempfile ).to receive( :create ).
+ with( ['arborist', '.tree'], '.', encoding: 'binary' ).
+ and_return( tempfile )
+ expect( Marshal ).to receive( :dump ).with( manager.nodes, tempfile )
+ expect( tempfile ).to receive( :close )
+ expect( File ).to receive( :rename ).
+ and_raise( Errno::ENOENT.new("no such file or directory") )
+ expect( File ).to receive( :exist? ).with( tempfile.path ).and_return( true )
+ expect( File ).to receive( :unlink ).with( tempfile.path )
+
+ manager.save_node_states
+ end
+
+
+ it "doesn't try to save state if the state file is not configured" do
+ Arborist::Manager.state_file = nil
+
+ expect( Tempfile ).to_not receive( :create )
+ expect( Marshal ).to_not receive( :dump )
+ expect( File ).to_not receive( :rename )
+
+ manager.save_node_states
+ end
+
+
+ it "restores the state of loaded nodes if the state file is configured" do
+ _ = manager
+
+ statefile = Pathname( './arborist.tree' )
+ Arborist::Manager.state_file = statefile
+ state_file_io = instance_double( File )
+
+ saved_router_node = Marshal.load( Marshal.dump(router_node) )
+ saved_router_node.instance_variable_set( :@status, 'up' )
+ saved_host_node = Marshal.load( Marshal.dump(host_node) )
+ saved_host_node.instance_variable_set( :@status, 'down' )
+ saved_host_node.error = 'Stuff happened and it was not good.'
+
+ expect( statefile ).to receive( :readable? ).and_return( true )
+ expect( statefile ).to receive( :open ).with( 'r:binary' ).
+ and_return( state_file_io )
+ expect( Marshal ).to receive( :load ).with( state_file_io ).
+ and_return({ 'router' => saved_router_node, 'host-a' => saved_host_node })
+
+ expect( manager.restore_node_states ).to be_truthy
+
+ expect( manager.nodes['router'].status ).to eq( 'up' )
+ expect( manager.nodes['host-a'].status ).to eq( 'down' )
+ expect( manager.nodes['host-a'].error ).to eq( 'Stuff happened and it was not good.' )
+
+ end
+
+
+ it "doesn't error if the configured state file isn't readable" do
+ _ = manager
+
+ statefile = Pathname( './arborist.tree' )
+ Arborist::Manager.state_file = statefile
+
+ expect( statefile ).to receive( :readable? ).and_return( false )
+ expect( statefile ).to_not receive( :open )
+
+ expect( manager.restore_node_states ).to be_falsey
+ end
+
+
+ it "checkpoints the state file periodically if an interval is configured" do
+ described_class.configure( manager: {checkpoint_frequency: 20, state_file: 'arb.tree'} )
+
+ timer = instance_double( ZMQ::Timer, "checkpoint timer" )
+ expect( ZMQ::Timer ).to receive( :new ).with( 20, 0 ).and_return( timer )
+
+ expect( manager.start_state_checkpointing ).to eq( timer )
+ end
+
+
+ it "doesn't checkpoint if no interval is configured" do
+ described_class.configure( manager: {checkpoint_frequency: nil, state_file: 'arb.tree'} )
+
+ expect( ZMQ::Timer ).to_not receive( :new )
+
+ expect( manager.start_state_checkpointing ).to be_nil
+ end
+
+
+ it "doesn't checkpoint if no state file is configured" do
+ described_class.configure( manager: {checkpoint_frequency: 20, state_file: nil} )
+
+ expect( ZMQ::Timer ).to_not receive( :new )
+
+ expect( manager.start_state_checkpointing ).to be_nil
+ end
+
+
+ it "writes a checkpoint if it receives a SIGUSR1"
+
+
+ end
+
+
context "a new empty manager" do
let( :node ) do
testing_node 'italian_lessons'
end
@@ -204,20 +352,25 @@
# host_a host_b host_c
# www smtp imap www nfs ssh www
[
testing_node( 'router' ),
- testing_node( 'host_a', 'router' ),
- testing_node( 'host_a_www', 'host_a' ),
- testing_node( 'host_a_smtp', 'host_a' ),
- testing_node( 'host_a_imap', 'host_a' ),
- testing_node( 'host_b', 'router' ),
- testing_node( 'host_b_www', 'host_b' ),
- testing_node( 'host_b_nfs', 'host_b' ),
- testing_node( 'host_b_ssh', 'host_b' ),
- testing_node( 'host_c', 'router' ),
- testing_node( 'host_c_www', 'host_c' ),
+ testing_node( 'host-a', 'router' ),
+ testing_node( 'host-a-www', 'host-a' ),
+ testing_node( 'host-a-smtp', 'host-a' ),
+ testing_node( 'host-a-imap', 'host-a' ),
+ testing_node( 'host-b', 'router' ),
+ testing_node( 'host-b-www', 'host-b' ),
+ testing_node( 'host-b-nfs', 'host-b' ),
+ testing_node( 'host-b-ssh', 'host-b' ),
+ testing_node( 'host-c', 'router' ),
+ testing_node( 'host-c-www', 'host-c' ),
+ testing_node( 'host-d', 'router' ),
+ testing_node( 'host-d-ssh', 'host-d' ),
+ testing_node( 'host-d-amqp', 'host-d' ),
+ testing_node( 'host-d-database', 'host-d' ),
+ testing_node( 'host-d-memcached', 'host-d' ),
]
end
let( :manager ) do
instance = described_class.new
@@ -232,45 +385,50 @@
expect( iter.to_a ).to eq( [manager.root] + tree )
end
it "can traverse all nodes whose status is 'up'" do
- manager.nodes.each {|_, node| node.status = :up }
- manager.nodes[ 'host_a' ].update( error: "ping failed" )
- expect( manager.nodes[ 'host_a' ] ).to be_down
- manager.nodes[ 'host_c' ].update( error: "gamma rays" )
- expect( manager.nodes[ 'host_c' ] ).to be_down
- manager.nodes[ 'host_b_nfs' ].
- update( ack: {sender: 'nancy_kerrigan', message: 'bad case of disk rot'} )
- expect( manager.nodes[ 'host_b_nfs' ] ).to be_disabled
- expect( manager.nodes[ 'host_b_nfs' ] ).to_not be_down
+ manager.nodes.each {|_, node| node.status = 'up' }
+ manager.nodes[ 'host-a' ].status = 'down'
+ manager.nodes[ 'host-c' ].status = 'down'
+ manager.nodes[ 'host-b-nfs' ].status = 'disabled'
+ manager.nodes[ 'host-b-www' ].status = 'quieted'
iter = manager.reachable_nodes
expect( iter ).to be_a( Enumerator )
nodes = iter.map( &:identifier )
expect( nodes ).to include(
"_",
"router",
- "host_b",
- "host_b_www",
- "host_b_ssh"
+ "host-b",
+ "host-b-ssh",
+ "host-d",
+ "host-d-ssh",
+ "host-d-amqp",
+ "host-d-database",
+ "host-d-memcached",
)
expect( nodes ).to_not include(
- "host_b_nfs",
- "host_c",
- "host_c_www",
- "host_a",
- 'host_a_www',
- 'host_a_smtp',
- 'host_a_imap'
+ "host-b-www",
+ "host-b-nfs",
+ "host-c",
+ "host-c-www",
+ "host-a",
+ "host-a-www",
+ "host-a-smtp",
+ "host-a-imap",
)
end
- it "can create an Enumerator for all of a node's parents from leaf to root"
+ it "can create an Enumerator for all of its children to a specified depth" do
+ nodes = manager.depth_limited_enumerator_for( manager.nodes['_'], 2 ).to_a
+ expect( nodes.length ).to eq( 6 )
+ expect( nodes.map(&:identifier) ).to eq( %w[_ router host-a host-b host-c host-d] )
+ end
end
describe "node updates and events" do
@@ -280,20 +438,20 @@
# host_a host_b host_c
# www smtp imap www nfs ssh www
[
testing_node( 'router' ),
- testing_node( 'host_a', 'router' ),
- testing_node( 'host_a_www', 'host_a' ),
- testing_node( 'host_a_smtp', 'host_a' ),
- testing_node( 'host_a_imap', 'host_a' ),
- testing_node( 'host_b', 'router' ),
- testing_node( 'host_b_www', 'host_b' ),
- testing_node( 'host_b_nfs', 'host_b' ),
- testing_node( 'host_b_ssh', 'host_b' ),
- testing_node( 'host_c', 'router' ),
- testing_node( 'host_c_www', 'host_c' ),
+ testing_node( 'host-a', 'router' ),
+ testing_node( 'host-a-www', 'host-a' ) { tags :home, :church },
+ testing_node( 'host-a-smtp', 'host-a' ) { tags :home },
+ testing_node( 'host-a-imap', 'host-a' ),
+ testing_node( 'host-b', 'router' ),
+ testing_node( 'host-b-www', 'host-b' ) { tags :church },
+ testing_node( 'host-b-nfs', 'host-b' ),
+ testing_node( 'host-b-ssh', 'host-b' ) { tags :work },
+ testing_node( 'host-c', 'router' ),
+ testing_node( 'host-c-www', 'host-c' ) { tags :work, :home },
]
end
let( :manager ) do
instance = described_class.new
@@ -303,86 +461,112 @@
it "can fetch a Hash of node states" do
states = manager.fetch_matching_node_states( {}, [] )
expect( states.size ).to eq( manager.nodes.size )
- expect( states ).to include( 'host_b_nfs', 'host_c', 'router' )
- expect( states['host_b_nfs'] ).to be_a( Hash )
- expect( states['host_c'] ).to be_a( Hash )
+ expect( states ).to include( 'host-b-nfs', 'host-c', 'router' )
+ expect( states['host-b-nfs'] ).to be_a( Hash )
+ expect( states['host-c'] ).to be_a( Hash )
expect( states['router'] ).to be_a( Hash )
end
+ it "can fetch a Hash of node states for nodes which match specified criteria" do
+ states = manager.fetch_matching_node_states( {'identifier' => 'host-c'}, [] )
+ expect( states.size ).to eq( 1 )
+ expect( states.keys.first ).to eq( 'host-c' )
+ expect( states['host-c'] ).to be_a( Hash )
+ end
+
+
+ it "can fetch a Hash of node states for nodes which don't match specified negative criteria" do
+ states = manager.fetch_matching_node_states( {}, [], false, {'identifier' => 'host-c'} )
+ expect( states.size ).to eq( manager.nodes.size - 1 )
+ expect( states ).to_not include( 'host-c' )
+ end
+
+
+ it "can fetch a Hash of node states for nodes combining positive and negative criteria" do
+ positive = {'tag' => 'home'}
+ negative = {'identifier' => 'host-a-www'}
+
+ states = manager.fetch_matching_node_states( positive, [], false, negative )
+
+ expect( states.size ).to eq( 2 )
+ expect( states ).to_not include( 'host-a-www' )
+ end
+
+
it "can update an event by identifier" do
- manager.update_node( 'host_b_www', http: { status: 200 } )
+ manager.update_node( 'host-b-www', http: { status: 200 } )
expect(
- manager.nodes['host_b_www'].properties
+ manager.nodes['host-b-www'].properties
).to include( 'http' => { 'status' => 200 } )
end
it "ignores updates to an identifier that is not (any longer) in the tree" do
expect {
- manager.update_node( 'host_y', asset_tag: '2by-n86y7t' )
+ manager.update_node( 'host-y', asset_tag: '2by-n86y7t' )
}.to_not raise_error
end
it "propagates events from an update up the node tree" do
expect( manager.root ).to receive( :publish_events ).
at_least( :once ).
and_call_original
- expect( manager.nodes['host_c'] ).to receive( :publish_events ).
+ expect( manager.nodes['host-c'] ).to receive( :publish_events ).
at_least( :once ).
and_call_original
- manager.update_node( 'host_c_www', response_status: 504, error: 'Timeout talking to web service.' )
+ manager.update_node( 'host-c-www', response_status: 504, error: 'Timeout talking to web service.' )
end
it "only propagates events to a node's ancestors" do
expect( manager.root ).to receive( :publish_events ).
at_least( :once ).
and_call_original
- expect( manager.nodes['host_c'] ).to_not receive( :publish_events )
+ expect( manager.nodes['host-c'] ).to_not receive( :publish_events )
- manager.update_node( 'host_b_www', response_status: 504, error: 'Timeout talking to web service.' )
+ manager.update_node( 'host-b-www', response_status: 504, error: 'Timeout talking to web service.' )
end
end
describe "subscriptions" do
- let( :tree ) {[ testing_node('host_c') ]}
+ let( :tree ) {[ testing_node('host-c') ]}
let( :manager ) do
instance = described_class.new
instance.load_tree( tree )
instance
end
it "can attach subscriptions to a node by its identifier" do
sub = subid = nil
expect {
- sub = manager.create_subscription( 'host_c', 'node.update', type: 'host' )
+ sub = manager.create_subscription( 'host-c', 'node.update', type: 'host' )
}.to change { manager.subscriptions.size }.by( 1 )
node = manager.subscriptions[ sub.id ]
expect( sub ).to be_a( Arborist::Subscription )
- expect( node ).to be( manager.nodes['host_c'] )
+ expect( node ).to be( manager.nodes['host-c'] )
end
it "can detach subscriptions from a node given the subscription ID" do
- sub = manager.create_subscription( 'host_c', 'node.ack', type: 'service' )
+ sub = manager.create_subscription( 'host-c', 'node.ack', type: 'service' )
rval = nil
expect {
rval = manager.remove_subscription( sub.id )
}.to change { manager.subscriptions.size }.by( -1 ).and(
- change { manager.nodes['host_c'].subscriptions.size }.by( -1 )
+ change { manager.nodes['host-c'].subscriptions.size }.by( -1 )
)
expect( rval ).to be( sub )
end
@@ -411,10 +595,9 @@
allow( tree_pollitem ).to receive( :pollable ).and_return( tree_sock )
allow( tree_sock ).to receive( :close )
allow( event_pollitem ).to receive( :pollable ).and_return( event_sock )
allow( event_sock ).to receive( :close )
end
-
it "sets up its sockets with handlers and starts the ZMQ loop when started" do
expect( tree_sock ).to receive( :bind ).with( Arborist.tree_api_url )
expect( tree_sock ).to receive( :linger= ).with( 0 )