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 )