#!/usr/bin/env rspec -cfd require_relative '../spec_helper' require 'timecop' require 'arborist/manager' describe Arborist::Manager do after( :each ) do Arborist::Node::Root.reset end let( :manager ) { described_class.new } # # Examples # it "starts with a root node" do expect( described_class.new.root ).to be_a( Arborist::Node ) end it "starts with a node registry with the root node and itself" do result = manager.nodes expect( result ).to include( '_' ) expect( result['_'] ).to be( manager.root ) end it "knows how long it has been running" do Timecop.freeze do manager.start_time = Time.now Timecop.travel( 10 ) do expect( manager.uptime ).to be_within( 1 ).of( 10 ) end end end it "has an uptime of 0 if it hasn't yet been started" do expect( manager.uptime ).to eq( 0 ) end context "a new empty manager" do let( :node ) do testing_node 'italian_lessons' end let( :node2 ) do testing_node 'french_laundry' end let( :node3 ) do testing_node 'german_oak_cats' end it "has a nodecount of 1" do expect( manager.nodecount ).to eq( 1 ) end it "can have a node added to it" do manager.add_node( node ) expect( manager.nodes ).to include( 'italian_lessons' ) expect( manager.nodes['italian_lessons'] ).to be( node ) expect( manager.nodecount ).to eq( 2 ) expect( manager.nodelist ).to include( '_', 'italian_lessons' ) end it "can load its tree from an Enumerator that yields nodes" do manager.load_tree([ node, node2, node3 ]) expect( manager.nodes ).to include( 'italian_lessons', 'french_laundry', 'german_oak_cats' ) expect( manager.nodes['italian_lessons'] ).to be( node ) expect( manager.nodes['french_laundry'] ).to be( node2 ) expect( manager.nodes['german_oak_cats'] ).to be( node3 ) expect( manager.nodecount ).to eq( 4 ) expect( manager.nodelist ).to include( '_', 'italian_lessons', 'french_laundry', 'german_oak_cats' ) end it "can replace an existing node" do manager.add_node( node ) another_node = testing_node( 'italian_lessons' ) manager.add_node( another_node ) expect( manager.nodes ).to include( 'italian_lessons' ) expect( manager.nodes['italian_lessons'] ).to_not be( node ) expect( manager.nodes['italian_lessons'] ).to be( another_node ) expect( manager.nodecount ).to eq( 2 ) expect( manager.nodelist ).to include( '_', 'italian_lessons' ) end it "can have a node removed from it" do manager.add_node( node ) deleted_node = manager.remove_node( 'italian_lessons' ) expect( deleted_node ).to be( node ) expect( manager.nodes ).to_not include( 'italian_lessons' ) expect( manager.nodecount ).to eq( 1 ) expect( manager.nodelist ).to include( '_' ) end it "disallows removal of operational nodes" do expect { manager.remove_node('_') }.to raise_error( /can't remove an operational node/i ) end end context "a manager with some loaded nodes" do let( :trunk_node ) do testing_node( 'trunk' ) end let( :branch_node ) do testing_node( 'branch', 'trunk' ) end let( :leaf_node ) do testing_node( 'leaf', 'branch' ) end let( :manager ) do instance = described_class.new instance.load_tree([ branch_node, leaf_node, trunk_node ]) instance end it "has a tree built out of its nodes" do expect( manager.root ).to have_children end it "knows what nodes have been loaded" do expect( manager.nodelist ).to include( 'trunk', 'branch', 'leaf' ) end it "errors if any of its nodes are missing their parent" do manager = described_class.new orphan = testing_node( 'orphan' ) do parent 'daddy_warbucks' end expect { manager.load_tree([ orphan ]) }.to raise_error( /no parent 'daddy_warbucks' node loaded for/i ) end it "grafts a node into the tree when one with a previously unknown identifier is added" do new_node = testing_node( 'new' ) do parent 'branch' end manager.add_node( new_node ) expect( manager.nodes['branch'].children ).to include( 'new' ) end it "replaces a node in the tree when a node with an existing identifier is added" do updated_node = testing_node( 'leaf' ) do parent 'trunk' end manager.add_node( updated_node ) expect( manager.nodes['branch'].children ).to_not include( 'leaf' => leaf_node ) expect( manager.nodes['trunk'].children ).to include( 'leaf' => updated_node ) end it "rebuilds the tree when a node is removed from it" do manager.remove_node( 'branch' ) expect( manager.nodes['trunk'].children ).to_not include( 'branch' ) expect( manager.nodes ).to_not include( 'branch' ) expect( manager.nodes ).to_not include( 'leaf' ) end end describe "tree traversal" do let( :tree ) do # router # 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' ), ] end let( :manager ) do instance = described_class.new instance.load_tree( tree ) instance end it "can traverse all nodes in its node tree" do iter = manager.all_nodes expect( iter ).to be_a( Enumerator ) 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 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" ) expect( nodes ).to_not include( "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" end describe "node updates and events" do let( :tree ) do # router # 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' ), ] end let( :manager ) do instance = described_class.new instance.load_tree( tree ) instance end 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['router'] ).to be_a( Hash ) end it "can update an event by identifier" do manager.update_node( 'host_b_www', http: { status: 200 } ) expect( 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' ) }.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 ). at_least( :once ). and_call_original 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 ) 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( :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' ) }.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'] ) end it "can detach subscriptions from a node given the subscription ID" do 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 ) ) expect( rval ).to be( sub ) end end describe "sockets" do let( :zmq_context ) { Arborist.zmq_context } let( :zmq_loop ) { instance_double(ZMQ::Loop) } let( :tree_sock ) { instance_double(ZMQ::Socket::Rep, "tree API socket") } let( :event_sock ) { instance_double(ZMQ::Socket::Pub, "event socket") } let( :tree_pollitem ) { instance_double(ZMQ::Pollitem, "tree API pollitem") } let( :event_pollitem ) { instance_double(ZMQ::Pollitem, "event API pollitem") } let( :signal_timer ) { instance_double(ZMQ::Timer, "signal timer") } before( :each ) do allow( ZMQ::Loop ).to receive( :new ).and_return( zmq_loop ) allow( zmq_context ).to receive( :socket ).with( :REP ).and_return( tree_sock ) allow( zmq_context ).to receive( :socket ).with( :PUB ).and_return( event_sock ) allow( zmq_loop ).to receive( :remove ).with( tree_pollitem ) allow( zmq_loop ).to receive( :remove ).with( event_pollitem ) 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 ) expect( event_sock ).to receive( :bind ).with( Arborist.event_api_url ) expect( event_sock ).to receive( :linger= ).with( 0 ) expect( ZMQ::Pollitem ).to receive( :new ).with( tree_sock, ZMQ::POLLIN|ZMQ::POLLOUT ). and_return( tree_pollitem ) expect( ZMQ::Pollitem ).to receive( :new ).with( event_sock, ZMQ::POLLOUT ). and_return( event_pollitem ) expect( tree_pollitem ).to receive( :handler= ). with( an_instance_of(Arborist::Manager::TreeAPI) ) expect( zmq_loop ).to receive( :register ).with( tree_pollitem ) expect( event_pollitem ).to receive( :handler= ). with( an_instance_of(Arborist::Manager::EventPublisher) ) expect( zmq_loop ).to receive( :register ).with( event_pollitem ) expect( ZMQ::Timer ).to receive( :new ). with( described_class::SIGNAL_INTERVAL, 0, manager.method(:process_signal_queue) ). and_return( signal_timer ) expect( zmq_loop ).to receive( :register_timer ).with( signal_timer ) expect( zmq_loop ).to receive( :start ) expect( zmq_loop ).to receive( :remove ).with( tree_pollitem ) expect( zmq_loop ).to receive( :remove ).with( event_pollitem ) manager.run end end end