spec/arborist/manager_spec.rb in arborist-0.2.0.pre20170519125456 vs spec/arborist/manager_spec.rb in arborist-0.2.0

- old
+ new

@@ -3,12 +3,17 @@ require_relative '../spec_helper' require 'tmpdir' require 'timecop' require 'arborist/manager' +require 'arborist/mixins' require 'arborist/node/host' +require 'arborist/event/node_update' +using Arborist::TimeRefinements + + describe Arborist::Manager do after( :all ) do Arborist::Manager.state_file = nil end @@ -19,13 +24,13 @@ Arborist::Node::Root.reset end let( :manager ) { described_class.new } + let( :tmpfile_pathname ) { Pathname(Dir::Tmpname.create(['arb', 'tree']) {}) } - # # Examples # it "starts with a root node" do @@ -162,11 +167,11 @@ expect( manager.restore_node_states ).to be_falsey end it "checkpoints the state file periodically if an interval is configured" do - statefile = Pathname( Dir.tmpdir ) + Dir::Tmpname.make_tmpname( 'arb', 'tree' ) + statefile = tmpfile_pathname() described_class.configure( checkpoint_frequency: 20_000, state_file: statefile ) manager = described_class.new manager.register_checkpoint_timer expect( manager.checkpoint_timer ).to be_a( Timers::Timer ) @@ -210,12 +215,43 @@ described_class.configure( heartbeat_frequency: 0 ) }.to raise_error( Arborist::ConfigError, /positive and non-zero/i ) end - it "is sent at the configured interval" + it "is sent at the configured interval" do + described_class.configure( heartbeat_frequency: 11 ) + expect( manager.reactor ).to receive( :add_periodic_timer ).with( 11 ) + manager.register_heartbeat_timer + end + + + it "doesn't try to publish the heartbeat if it's not been started" do + manager.start_time = nil + + manager.publish_heartbeat_event + expect( manager.event_queue ).to be_empty + end + + + it "contains runtime information about the manager" do + time = Time.now + manager.start_time = time + + manager.publish_heartbeat_event + event = manager.event_queue.shift + + expect( event ).to be_a( CZTop::Message ) + decoded = Arborist::EventAPI.decode( event ) + + expect( decoded ).to include( + 'run_id' => manager.run_id, + 'start_time' => time.iso8601, + 'version' => Arborist::VERSION + ) + end + end context "a new empty manager" do @@ -255,21 +291,17 @@ '_', 'italian_lessons', 'french_laundry', 'german_oak_cats' ) end - it "can replace an existing node" do + it "complains if adding a node that already exists" 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' ) + expect { + manager.add_node( another_node ) + }.to raise_error( Arborist::NodeError, /already present/ ) end it "can have a node removed from it" do manager.add_node( node ) @@ -341,21 +373,10 @@ 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' ) @@ -363,11 +384,11 @@ end end - xdescribe "tree API", :testing_manager do + describe "tree API", :testing_manager do before( :each ) do @manager = nil @manager_thread = Thread.new do @manager = make_testing_manager() @@ -399,35 +420,44 @@ end raise "Manager didn't stop" if @manager.running? end - describe "status" do + let( :manager ) { @manager } + let( :sock ) do + sock = CZTop::Socket::REQ.new + sock.options.linger = 0 + sock.connect( TESTING_API_SOCK ) + sock + end + + describe "status" do + it "returns a Map describing the manager and its state" do - msg = Arborist::TreeAPI.encode( :status ) + msg = Arborist::TreeAPI.request( :status ) - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body.length ).to eq( 4 ) expect( body ).to include( 'server_version', 'state', 'uptime', 'nodecount' ) end end - describe "fetch" do + describe "search" do it "returns an array of full state maps for nodes matching specified criteria" do - msg = Arborist::TreeAPI.encode( :fetch, type: 'service', port: 22 ) + msg = Arborist::TreeAPI.request( :search, type: 'service', port: 22 ) - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to be_a( Hash ) @@ -437,14 +467,14 @@ expect( body.values ).to all( include('status', 'type') ) end it "returns an array of full state maps for nodes not matching specified negative criteria" do - msg = Arborist::TreeAPI.encode( :fetch, [ {}, {type: 'service', port: 22} ] ) + msg = Arborist::TreeAPI.request( :search, [ {}, {type: 'service', port: 22} ] ) - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to be_a( Hash ) @@ -454,61 +484,61 @@ expect( body.values ).to all( include('status', 'type') ) end it "returns an array of full state maps for nodes combining positive and negative criteria" do - msg = Arborist::TreeAPI.encode( :fetch, [ {type: 'service'}, {port: 22} ] ) + msg = Arborist::TreeAPI.request( :search, [ {type: 'service'}, {port: 22} ] ) - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to be_a( Hash ) - expect( body.length ).to eq( 16 ) + expect( body.length ).to eq( 18 ) expect( body.values ).to all( be_a(Hash) ) expect( body.values ).to all( include('status', 'type') ) end - it "doesn't return nodes beneath downed nodes by default" do + it "omits nodes beneath downed nodes if asked to" do manager.nodes['sidonie'].update( error: 'sunspots' ) - msg = Arborist::TreeAPI.encode( :fetch, type: 'service', port: 22 ) + msg = Arborist::TreeAPI.request( :search, {exclude_down: true}, type: 'service', port: 22 ) - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to be_a( Hash ) expect( body.length ).to eq( 2 ) expect( body ).to include( 'duir-ssh', 'yevaud-ssh' ) end - it "does return nodes beneath downed nodes if asked to" do + it "include nodes beneath downed nodes by default" do manager.nodes['sidonie'].update( error: 'plague of locusts' ) - msg = Arborist::TreeAPI.encode( :fetch, {include_down: true}, type: 'service', port: 22 ) + msg = Arborist::TreeAPI.request( :search, type: 'service', port: 22 ) - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to be_a( Hash ) expect( body.length ).to eq( 3 ) expect( body ).to include( 'duir-ssh', 'yevaud-ssh', 'sidonie-ssh' ) end it "returns only identifiers if the `return` header is set to `nil`" do - msg = Arborist::TreeAPI.encode( :fetch, {return: nil}, type: 'service', port: 22 ) + msg = Arborist::TreeAPI.request( :search, {return: nil}, type: 'service', port: 22 ) - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to be_a( Hash ) expect( body.length ).to eq( 3 ) @@ -516,15 +546,15 @@ expect( body.values ).to all( be_empty ) end it "returns only specified state if the `return` header is set to an Array of keys" do - msg = Arborist::TreeAPI.encode( :fetch, {return: %w[status tags addresses]}, + msg = Arborist::TreeAPI.request( :search, {return: %w[status tags addresses]}, type: 'service', port: 22 ) - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body.length ).to eq( 3 ) expect( body ).to include( 'duir-ssh', 'yevaud-ssh', 'sidonie-ssh' ) @@ -533,16 +563,16 @@ end - describe "list" do + describe "fetch" do it "returns an array of node state" do - msg = Arborist::TreeAPI.encode( :list ) - sock.send( msg ) - resmsg = sock.recv + msg = Arborist::TreeAPI.request( :fetch ) + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body.length ).to eq( manager.nodes.length ) expect( body ).to all( be_a(Hash) ) @@ -552,23 +582,69 @@ expect( body ).to include( hash_including('identifier' => 'sidonie-ssh') ) expect( body ).to include( hash_including('identifier' => 'sidonie-demon-http') ) expect( body ).to include( hash_including('identifier' => 'yevaud') ) end + + it "can start at a node other than the root" do + msg = Arborist::TreeAPI.request( :fetch, {from: 'sidonie'}, nil ) + msg.send_to( sock ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( 'success' => true ) + expect( body.length ).to eq( manager.nodes.keys.count {|id| id.include?('sidonie')} ) + expect( body ).to all( be_a(Hash) ) + expect( body ).to_not include( hash_including('identifier' => '_') ) + expect( body ).to_not include( hash_including('identifier' => 'duir') ) + expect( body ).to include( hash_including('identifier' => 'sidonie') ) + expect( body ).to include( hash_including('identifier' => 'sidonie-ssh') ) + expect( body ).to include( hash_including('identifier' => 'sidonie-demon-http') ) + expect( body ).to_not include( hash_including('identifier' => 'yevaud') ) + end + + + it "can be fetched as a tree" do + msg = Arborist::TreeAPI.request( :fetch, {tree: true}, nil ) + msg.send_to( sock ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( 'success' => true ) + expect( body.length ).to eq( 1 ) + expect( body.first ).to be_a( Hash ) + expect( body.first ).to include( 'children' ) + expect( body.first['identifier'] ).to eq( '_' ) + expect( body.first['children'].keys ).to include( 'duir', 'localhost' ) + end + + it "can be limited by depth" do - msg = Arborist::TreeAPI.encode( :list, {depth: 1}, nil ) - sock.send( msg ) - resmsg = sock.recv + msg = Arborist::TreeAPI.request( :fetch, {depth: 1}, nil ) + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body.length ).to eq( 3 ) expect( body ).to all( be_a(Hash) ) expect( body ).to include( hash_including('identifier' => '_') ) expect( body ).to include( hash_including('identifier' => 'duir') ) expect( body ).to_not include( hash_including('identifier' => 'duir-ssh') ) end + + + it "errors when fetching from a nonexistent node" do + msg = Arborist::TreeAPI.request( :fetch, {from: "nope-nope-nope"}, nil ) + msg.send_to( sock ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( 'success' => false ) + expect( hdr ).to include( "reason" => "No such node nope-nope-nope." ) + expect( body ).to be_nil + end end describe "update" do @@ -588,13 +664,13 @@ ping: { rtt: 843 } } } - msg = Arborist::TreeAPI.encode( :update, update_data ) - sock.send( msg ) - resmsg = sock.recv + msg = Arborist::TreeAPI.request( :update, update_data ) + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to be_nil @@ -603,22 +679,22 @@ expect( manager.nodes['yevaud'].properties['ping'] ).to include( 'rtt' => 843 ) end it "ignores unknown identifiers" do - msg = Arborist::TreeAPI.encode( :update, charlie_humperton: {ping: { rtt: 8 }} ) - sock.send( msg ) - resmsg = sock.recv + msg = Arborist::TreeAPI.request( :update, charlie_humperton: {ping: { rtt: 8 }} ) + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) end it "fails with a client error if the body is invalid" do - msg = Arborist::TreeAPI.encode( :update, nil ) - sock.send( msg ) - resmsg = sock.recv + msg = Arborist::TreeAPI.request( :update, nil ) + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => false ) expect( hdr['reason'] ).to match( /respond to #each/ ) end @@ -626,101 +702,105 @@ describe "subscribe" do it "adds a subscription for all event types to the root node by default" do - msg = Arborist::TreeAPI.encode( :subscribe, [{}, {}] ) + msg = Arborist::TreeAPI.request( :subscribe, [{}, {}] ) resmsg = nil expect { - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive }.to change { manager.subscriptions.length }.by( 1 ).and( change { manager.root.subscriptions.length }.by( 1 ) ) hdr, body = Arborist::TreeAPI.decode( resmsg ) sub_id = manager.subscriptions.keys.first expect( hdr ).to include( 'success' => true ) - expect( body ).to eq([ sub_id ]) + expect( body ).to be_a( Hash ) + expect( body ).to include( 'id' ) + expect( manager.subscriptions.keys ).to include( body['id'] ) end it "adds a subscription to the specified node if an identifier is specified" do - msg = Arborist::TreeAPI.encode( :subscribe, {identifier: 'sidonie'}, [{}, {}] ) + msg = Arborist::TreeAPI.request( :subscribe, {identifier: 'sidonie'}, [{}, {}] ) resmsg = nil expect { - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive }.to change { manager.subscriptions.length }.by( 1 ).and( change { manager.nodes['sidonie'].subscriptions.length }.by( 1 ) ) hdr, body = Arborist::TreeAPI.decode( resmsg ) - sub_id = manager.subscriptions.keys.first - expect( hdr ).to include( 'success' => true ) - expect( body ).to eq([ sub_id ]) + expect( body ).to be_a( Hash ) + expect( body ).to include( 'id' ) + expect( manager.subscriptions.keys ).to include( body['id'] ) end it "adds a subscription for particular event types if one is specified" do - msg = Arborist::TreeAPI.encode( :subscribe, {event_type: 'node.acked'}, [{}, {}] ) + msg = Arborist::TreeAPI.request( :subscribe, {event_type: 'node.acked'}, [{}, {}] ) resmsg = nil expect { - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive }.to change { manager.subscriptions.length }.by( 1 ).and( change { manager.root.subscriptions.length }.by( 1 ) ) hdr, body = Arborist::TreeAPI.decode( resmsg ) - node = manager.subscriptions[ body.first ] - sub = node.subscriptions[ body.first ] + node = manager.subscriptions[ body['id'] ] + sub = node.subscriptions[ body['id'] ] expect( sub.event_type ).to eq( 'node.acked' ) end it "adds a subscription for events which match a pattern if one is specified" do criteria = { type: 'host' } - msg = Arborist::TreeAPI.encode( :subscribe, [criteria, {}] ) + msg = Arborist::TreeAPI.request( :subscribe, [criteria, {}] ) resmsg = nil expect { - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive }.to change { manager.subscriptions.length }.by( 1 ).and( change { manager.root.subscriptions.length }.by( 1 ) ) hdr, body = Arborist::TreeAPI.decode( resmsg ) - node = manager.subscriptions[ body.first ] - sub = node.subscriptions[ body.first ] + expect( body ).to be_a( Hash ).and( include('id') ) + node = manager.subscriptions[ body['id'] ] + sub = node.subscriptions[ body['id'] ] expect( sub.event_type ).to be_nil expect( sub.criteria ).to eq({ 'type' => 'host' }) end it "adds a subscription for events which don't match a pattern if an exclusion pattern is given" do criteria = { type: 'host' } - msg = Arborist::TreeAPI.encode( :subscribe, [{}, criteria] ) + msg = Arborist::TreeAPI.request( :subscribe, [{}, criteria] ) resmsg = nil expect { - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive }.to change { manager.subscriptions.length }.by( 1 ).and( change { manager.root.subscriptions.length }.by( 1 ) ) hdr, body = Arborist::TreeAPI.decode( resmsg ) - node = manager.subscriptions[ body.first ] - sub = node.subscriptions[ body.first ] + expect( body ).to be_a( Hash ).and( include('id') ) + node = manager.subscriptions[ body['id'] ] + sub = node.subscriptions[ body['id'] ] expect( sub.event_type ).to be_nil expect( sub.negative_criteria ).to eq({ 'type' => 'host' }) end @@ -733,32 +813,32 @@ manager.create_subscription( nil, 'node.delta', {type: 'host'} ) end it "removes the subscription with the specified ID" do - msg = Arborist::TreeAPI.encode( :unsubscribe, {subscription_id: subscription.id}, nil ) + msg = Arborist::TreeAPI.request( :unsubscribe, {subscription_id: subscription.id}, nil ) resmsg = nil expect { - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive }.to change { manager.subscriptions.length }.by( -1 ).and( change { manager.root.subscriptions.length }.by( -1 ) ) hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( body ).to include( 'event_type' => 'node.delta', 'criteria' => {'type' => 'host'} ) end it "ignores unsubscription of a non-existant ID" do - msg = Arborist::TreeAPI.encode( :unsubscribe, {subscription_id: 'the bears!'}, nil ) + msg = Arborist::TreeAPI.request( :unsubscribe, {subscription_id: 'the bears!'}, nil ) resmsg = nil expect { - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive }.to_not change { manager.subscriptions.length } hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( body ).to be_nil end @@ -767,49 +847,51 @@ describe "prune" do it "removes a single node" do - msg = Arborist::TreeAPI.encode( :prune, {identifier: 'duir-ssh'}, nil ) - sock.send( msg ) - resmsg = sock.recv + msg = Arborist::TreeAPI.request( :prune, {identifier: 'duir-ssh'}, nil ) + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) - expect( body ).to eq( true ) + expect( body ).to be_a( Hash ) + expect( body ).to include( 'identifier' => 'duir-ssh' ) expect( manager.nodes ).to_not include( 'duir-ssh' ) end it "returns Nil without error if the node to prune didn't exist" do - msg = Arborist::TreeAPI.encode( :prune, {identifier: 'shemp-ssh'}, nil ) - sock.send( msg ) - resmsg = sock.recv + msg = Arborist::TreeAPI.request( :prune, {identifier: 'shemp-ssh'}, nil ) + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to be_nil end it "removes children nodes along with the parent" do - msg = Arborist::TreeAPI.encode( :prune, {identifier: 'duir'}, nil ) - sock.send( msg ) - resmsg = sock.recv + msg = Arborist::TreeAPI.request( :prune, {identifier: 'duir'}, nil ) + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) - expect( body ).to eq( true ) + expect( body ).to be_a( Hash ) + expect( body ).to include( 'identifier' => 'duir' ) expect( manager.nodes ).to_not include( 'duir' ) expect( manager.nodes ).to_not include( 'duir-ssh' ) end it "returns an error to the client when missing required attributes" do - msg = Arborist::TreeAPI.encode( :prune ) - sock.send( msg ) - resmsg = sock.recv + msg = Arborist::TreeAPI.request( :prune ) + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => false ) expect( hdr['reason'] ).to match( /no identifier/i ) end @@ -826,18 +908,18 @@ attributes = { description: 'The evil penguin node of doom.', addresses: ['10.2.66.8'], tags: ['internal', 'football'] } - msg = Arborist::TreeAPI.encode( :graft, header, attributes ) + msg = Arborist::TreeAPI.request( :graft, header, attributes ) - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) - expect( body ).to eq( 'guenter' ) + expect( body ).to include( 'identifier' => 'guenter' ) new_node = manager.nodes[ 'guenter' ] expect( new_node ).to be_a( Arborist::Node::Host ) expect( new_node.identifier ).to eq( header[:identifier] ) expect( new_node.description ).to eq( attributes[:description] ) @@ -855,18 +937,18 @@ attributes = { description: 'The true form of the evil penguin node of doom.', addresses: ['192.168.22.8'], tags: ['evil', 'space', 'entity'] } - msg = Arborist::TreeAPI.encode( :graft, header, attributes ) + msg = Arborist::TreeAPI.request( :graft, header, attributes ) - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) - expect( body ).to eq( 'orgalorg' ) + expect( body ).to include( 'identifier' => 'orgalorg' ) new_node = manager.nodes[ 'orgalorg' ] expect( new_node ).to be_a( Arborist::Node::Host ) expect( new_node.identifier ).to eq( header[:identifier] ) expect( new_node.parent ).to eq( header[:parent] ) @@ -883,18 +965,18 @@ parent: 'duir' } attributes = { description: 'Mmmmm AppleTalk.' } - msg = Arborist::TreeAPI.encode( :graft, header, attributes ) + msg = Arborist::TreeAPI.request( :graft, header, attributes ) - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) - expect( body ).to eq( 'duir-echo' ) + expect( body ).to eq( 'identifier' => 'duir-echo' ) new_node = manager.nodes[ 'duir-echo' ] expect( new_node ).to be_a( Arborist::Node::Service ) expect( new_node.identifier ).to eq( 'duir-echo' ) expect( new_node.parent ).to eq( header[:parent] ) @@ -911,20 +993,35 @@ type: 'service' } attributes = { description: 'Mmmmm AppleTalk.' } - msg = Arborist::TreeAPI.encode( :graft, header, attributes ) + msg = Arborist::TreeAPI.request( :graft, header, attributes ) - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => false ) expect( hdr['reason'] ).to match( /no host given/i ) end + + it "errors if adding a node that already exists" do + header = { + identifier: 'duir', + type: 'host', + } + msg = Arborist::TreeAPI.request( :graft, header, {} ) + + msg.send_to( sock ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( 'success' => false ) + expect( hdr['reason'] ).to match( /exists/i ) + end end describe "modify" do @@ -934,14 +1031,14 @@ } attributes = { parent: '_', addresses: ['192.168.32.32', '10.2.2.28'] } - msg = Arborist::TreeAPI.encode( :modify, header, attributes ) + msg = Arborist::TreeAPI.request( :modify, header, attributes ) - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) node = manager.nodes[ 'sidonie' ] @@ -957,14 +1054,14 @@ identifier: 'sidonie', } attributes = { identifier: 'somethingelse' } - msg = Arborist::TreeAPI.encode( :modify, header, attributes ) + msg = Arborist::TreeAPI.request( :modify, header, attributes ) - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => true ) expect( manager.nodes['sidonie'] ).to be_an( Arborist::Node ) @@ -977,14 +1074,14 @@ identifier: '_', } attributes = { identifier: 'somethingelse' } - msg = Arborist::TreeAPI.encode( :modify, header, attributes ) + msg = Arborist::TreeAPI.request( :modify, header, attributes ) - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => false ) expect( manager.nodes['_'].identifier ).to eq( '_' ) end @@ -995,23 +1092,403 @@ identifier: 'nopenopenope', } attributes = { identifier: 'somethingelse' } - msg = Arborist::TreeAPI.encode( :modify, header, attributes ) + msg = Arborist::TreeAPI.request( :modify, header, attributes ) - sock.send( msg ) - resmsg = sock.recv + msg.send_to( sock ) + resmsg = sock.receive hdr, body = Arborist::TreeAPI.decode( resmsg ) expect( hdr ).to include( 'success' => false ) end + + + it "reparents a node whose parent is altered" do + header = { + identifier: 'sidonie' + } + attributes = { + parent: 'yevaud' + } + + msg = Arborist::TreeAPI.request( :modify, header, attributes ) + + node = manager.nodes[ 'sidonie' ] + old_parent = manager.nodes[ 'duir' ] + expect( node.parent ).to eq( 'duir' ) + + msg.send_to( sock ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( 'success' => true ) + + new_parent = manager.nodes[ 'yevaud' ] + expect( node.parent ).to eq( 'yevaud' ) + expect( old_parent.children ).to_not include( 'sidonie' ) + expect( new_parent.children ).to include( 'sidonie' ) + end + end + + describe "deps" do + + it "returns a list of the identifiers of nodes that depend on it" do + msg = Arborist::TreeAPI.request( :deps, {from: 'sidonie'}, nil ) + msg.send_to( sock ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( 'success' => true ) + expect( body ).to be_a( Hash ) + expect( body ).to include( 'deps' ) + expect( body['deps'] ).to_not include( 'sidonie' ) + expect( body['deps'] ).to be_an( Array ). + and( include('sidonie-ssh', 'yevaud-cozy_frontend', 'sandbox01-canary') ) + end + + end + + + describe "ack" do + + it "acknowledges a single node" do + msg = Arborist::TreeAPI.request( :ack, + {identifier: 'sidonie'}, + {message: "Planned maintenance", sender: 'xerces'} + ) + msg.send_to( sock ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( 'success' => true ) + expect( body ).to be_nil + + expect( manager.nodes['sidonie'] ).to be_disabled + end + + + it "returns an error if the node to ack doesn't exist" do + msg = Arborist::TreeAPI.request( :ack, + {identifier: 'shemp-ssh'}, + {message: "Planned maintenance", sender: 'xerces'} + ) + msg.send_to( sock ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( 'success' => false ) + expect( hdr['reason'] ).to match( /no such node/i ) + end + + + it "returns an error if the node to ack isn't specified" do + msg = Arborist::TreeAPI.request( :ack, {}, {message: "", sender: ''} ) + msg.send_to( sock ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( 'success' => false ) + expect( hdr['reason'] ).to match( /no identifier/i ) + end + + + it "returns an error if one of the required arguments isn't provided" do + msg = Arborist::TreeAPI.request( :ack, + {identifier: 'sidonie'}, + {message: "Planned maintenance"} + ) + msg.send_to( sock ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( 'success' => false ) + expect( hdr['reason'] ).to match( /missing required ack sender/i ) + end + + + it "propagates events from an acknowledgement up the node tree" do + expect( manager.root ).to receive( :publish_events ). + at_least( :once ). + and_call_original + expect( manager.nodes['duir'] ).to receive( :publish_events ). + at_least( :once ). + and_call_original + + manager.handle_ack_request( { 'identifier' => 'sidonie' }, { + 'message' => 'There will also be corn served.', + 'sender' => 'Smoove-B' + }) + end + end + + + describe "unack" do + + it "removes an acknowledgement from a single node" do + msg = Arborist::TreeAPI.request( :unack, {identifier: 'sidonie'}, nil ) + msg.send_to( sock ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( 'success' => true ) + expect( body ).to be_nil + + expect( manager.nodes['sidonie'] ).to be_unknown + end + + + it "returns an error if the node to unack doesn't exist" do + msg = Arborist::TreeAPI.request( :unack, {identifier: 'shemp-ssh'}, nil ) + msg.send_to( sock ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( 'success' => false ) + expect( hdr['reason'] ).to match( /no such/i ) + end + + + it "returns an error if the node to unack isn't specified" do + msg = Arborist::TreeAPI.request( :unack, {}, nil ) + msg.send_to( sock ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( 'success' => false ) + expect( hdr['reason'] ).to match( /no identifier/i ) + end + + + it "propagates events from an unack up the node tree" do + expect( manager.root ).to receive( :publish_events ). + at_least( :once ). + and_call_original + expect( manager.nodes['duir'] ).to receive( :publish_events ). + at_least( :once ). + and_call_original + manager.handle_unack_request( { 'identifier' => 'sidonie' }, nil ) + end + end + + + + describe "malformed requests" do + + it "send an error response if the request can't be deserialized" do + sock << "whatevs, dude!" + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( + 'success' => false, + 'reason' => /invalid message/i, + 'category' => 'client' + ) + expect( body ).to be_nil + end + + + it "send an error response if the request isn't a tuple" do + sock << MessagePack.pack({ version: 1, action: 'fetch' }) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( + 'success' => false, + 'reason' => /invalid message.*not an array/i, + 'category' => 'client' + ) + expect( body ).to be_nil + end + + + it "send an error response if the request is empty" do + sock << MessagePack.pack([]) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( + 'success' => false, + 'reason' => /invalid message.*expected 1-2 parts, got 0/i, + 'category' => 'client' + ) + expect( body ).to be_nil + end + + + it "send an error response if the request is an incorrect length" do + sock << MessagePack.pack( [{}, {}, {}] ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( + 'success' => false, + 'reason' => /expected 1-2 parts, got 3/i, + 'category' => 'client' + ) + expect( body ).to be_nil + end + + + it "send an error response if the request's header is not a Map" do + sock << MessagePack.pack( [nil, {}] ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( + 'success' => false, + 'reason' => /no header/i, + 'category' => 'client' + ) + expect( body ).to be_nil + end + + + it "send an error response if the request's body is not Nil, a Map, or an Array of Maps" do + sock << MessagePack.pack( [{version: 1, action: 'fetch'}, 18] ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( + 'success' => false, + 'reason' => /invalid message.*body must be nil, a map, or an array of maps/i, + 'category' => 'client' + ) + expect( body ).to be_nil + end + + + it "send an error response if missing a version" do + sock << MessagePack.pack( [{action: 'fetch'}] ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( + 'success' => false, + 'reason' => /invalid message.*missing required header 'version'/i, + 'category' => 'client' + ) + expect( body ).to be_nil + end + + + it "send an error response if missing an action" do + sock << MessagePack.pack( [{version: 1}] ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( + 'success' => false, + 'reason' => /invalid message.*missing required header 'action'/i, + 'category' => 'client' + ) + expect( body ).to be_nil + end + + + it "send an error response for unknown actions" do + badmsg = Arborist::TreeAPI.request( :slap ) + badmsg.send_to( sock ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( + 'success' => false, + 'reason' => /invalid message.*no such action 'slap'/i, + 'category' => 'client' + ) + expect( body ).to be_nil + end + + + it "send an error response for the `tree` action" do + badmsg = Arborist::TreeAPI.request( :tree ) + badmsg.send_to( sock ) + resmsg = sock.receive + + hdr, body = Arborist::TreeAPI.decode( resmsg ) + expect( hdr ).to include( + 'success' => false, + 'reason' => /invalid message.*no such action 'tree'/i, + 'category' => 'client' + ) + expect( body ).to be_nil + end + + end + end + describe "event API" do + + before( :each ) do + @manager = nil + @manager_thread = Thread.new do + @manager = make_testing_manager() + Thread.current.abort_on_exception = true + @manager.run + Loggability[ Arborist ].info "Stopped the test manager" + end + + count = 0 + until (@manager && @manager.running?) || count > 30 + sleep 0.1 + count += 1 + end + raise "Manager didn't start up" unless @manager.running? + end + + after( :each ) do + @manager.simulate_signal( :TERM ) + unless @manager_thread.join( 5 ) + $stderr.puts "Manager thread didn't exit on its own; killing it." + @manager_thread.kill + end + + count = 0 + while @manager.running? || count > 30 + sleep 0.1 + Loggability[ Arborist ].info "Manager still running" + count += 1 + end + raise "Manager didn't stop" if @manager.running? + end + + + let( :manager ) { @manager } + + let!( :sock ) do + sock = CZTop::Socket::SUB.new + sock.options.linger = 0 + sock.subscribe( '' ) + event_uri = manager.event_socket.last_endpoint + sock.connect( event_uri ) + Loggability[ Arborist ].info "Connected subscribed socket to %p" % [ event_uri ] + sock + end + + + it "publishes messages via the event socket" do + node = Arborist::Node.create( :root ) + event = Arborist::Event.create( :node_update, node ) + manager.publish( 'identifier-00aa', event ) + + msg = nil + wait( 1.second ).for { msg = sock.receive }.to be_a( CZTop::Message ) + + expect( msg.frames.first.to_s ).to eq( 'identifier-00aa' ) + expect( msg.frames.last.to_s ).to be_a_messagepacked( Hash ) + end + + end + + describe "tree traversal" do let( :tree ) do # router # host_a host_b host_c @@ -1094,11 +1571,11 @@ end end - describe "node updates and events" do + describe "node updates" do let( :tree ) do # router # host_a host_b host_c # www smtp imap www nfs ssh www @@ -1123,40 +1600,40 @@ instance.load_tree( tree ) instance end - it "can fetch a Hash of node states" do - states = manager.fetch_matching_node_states( {}, [] ) + it "can search a Hash of node states" do + states = manager.find_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 fetch a Hash of node states for nodes which match specified criteria" do - states = manager.fetch_matching_node_states( {'identifier' => 'host-c'}, [] ) + it "can search a Hash of node states for nodes which match specified criteria" do + states = manager.find_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'} ) + it "can search a Hash of node states for nodes which don't match specified negative criteria" do + states = manager.find_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 + it "can search 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 ) + states = manager.find_matching_node_states( positive, [], false, negative ) expect( states.size ).to eq( 2 ) expect( states ).to_not include( 'host-a-www' ) end @@ -1238,204 +1715,6 @@ end end - -__END__ - - let( :socket ) { instance_double( ZMQ::Socket::Pub ) } - let( :pollitem ) { instance_double( ZMQ::Pollitem, pollable: socket ) } - let( :zloop ) { instance_double( ZMQ::Loop ) } - - let( :manager ) { Arborist::Manager.new } - let( :event ) { Arborist::Event.create(TestEvent, 'stuff') } - - let( :publisher ) { described_class.new(pollitem, manager, zloop) } - - - it "starts out registered for writing" do - expect( publisher ).to be_registered - end - - - it "unregisters itself if told to write with an empty event queue" do - expect( zloop ).to receive( :remove ).with( pollitem ) - expect { - publisher.on_writable - }.to change { publisher.registered? }.to( false ) - end - - - it "registers itself if it's not already when an event is appended" do - # Cause the socket to become unregistered - allow( zloop ).to receive( :remove ) - publisher.on_writable - - expect( zloop ).to receive( :register ).with( pollitem ) - - expect { - publisher.publish( 'identifier-00aa', event ) - }.to change { publisher.registered? }.to( true ) - end - - - it "publishes events with their identifier" do - identifier = '65b2430b-6855-4961-ab46-d742cf4456a1' - - expect( socket ).to receive( :sendm ).with( identifier ) - expect( socket ).to receive( :send ) do |raw_data| - ev = MessagePack.unpack( raw_data ) - expect( ev ).to include( 'type', 'data' ) - - expect( ev['type'] ).to eq( 'test.event' ) - expect( ev['data'] ).to eq( 'stuff' ) - end - expect( zloop ).to receive( :remove ).with( pollitem ) - - publisher.publish( identifier, event ) - publisher.on_writable - end - - - - let( :manager ) { @manager } - - let!( :sock ) do - sock = Arborist.zmq_context.socket( :REQ ) - sock.linger = 0 - sock.connect( TESTING_API_SOCK ) - sock - end - - let( :api_handler ) { described_class.new( rep_sock, manager ) } - - - describe "malformed requests" do - - it "send an error response if the request can't be deserialized" do - sock.send( "whatevs, dude!" ) - resmsg = sock.recv - - hdr, body = Arborist::TreeAPI.decode( resmsg ) - expect( hdr ).to include( - 'success' => false, - 'reason' => /invalid request/i, - 'category' => 'client' - ) - expect( body ).to be_nil - end - - - it "send an error response if the request isn't a tuple" do - sock.send( MessagePack.pack({ version: 1, action: 'list' }) ) - resmsg = sock.recv - - hdr, body = Arborist::TreeAPI.decode( resmsg ) - expect( hdr ).to include( - 'success' => false, - 'reason' => /invalid request.*not a tuple/i, - 'category' => 'client' - ) - expect( body ).to be_nil - end - - - it "send an error response if the request is empty" do - sock.send( MessagePack.pack([]) ) - resmsg = sock.recv - - hdr, body = Arborist::TreeAPI.decode( resmsg ) - expect( hdr ).to include( - 'success' => false, - 'reason' => /invalid request.*incorrect length/i, - 'category' => 'client' - ) - expect( body ).to be_nil - end - - - it "send an error response if the request is an incorrect length" do - sock.send( MessagePack.pack([{}, {}, {}]) ) - resmsg = sock.recv - - hdr, body = Arborist::TreeAPI.decode( resmsg ) - expect( hdr ).to include( - 'success' => false, - 'reason' => /invalid request.*incorrect length/i, - 'category' => 'client' - ) - expect( body ).to be_nil - end - - - it "send an error response if the request's header is not a Map" do - sock.send( MessagePack.pack([nil, {}]) ) - resmsg = sock.recv - - hdr, body = Arborist::TreeAPI.decode( resmsg ) - expect( hdr ).to include( - 'success' => false, - 'reason' => /invalid request.*header is not a map/i, - 'category' => 'client' - ) - expect( body ).to be_nil - end - - - it "send an error response if the request's body is not Nil, a Map, or an Array of Maps" do - sock.send( MessagePack.pack([{version: 1, action: 'list'}, 18]) ) - resmsg = sock.recv - - hdr, body = Arborist::TreeAPI.decode( resmsg ) - expect( hdr ).to include( - 'success' => false, - 'reason' => /invalid request.*body must be nil, a map, or an array of maps/i, - 'category' => 'client' - ) - expect( body ).to be_nil - end - - - it "send an error response if missing a version" do - sock.send( MessagePack.pack([{action: 'list'}]) ) - resmsg = sock.recv - - hdr, body = Arborist::TreeAPI.decode( resmsg ) - expect( hdr ).to include( - 'success' => false, - 'reason' => /invalid request.*missing required header 'version'/i, - 'category' => 'client' - ) - expect( body ).to be_nil - end - - - it "send an error response if missing an action" do - sock.send( MessagePack.pack([{version: 1}]) ) - resmsg = sock.recv - - hdr, body = Arborist::TreeAPI.decode( resmsg ) - expect( hdr ).to include( - 'success' => false, - 'reason' => /invalid request.*missing required header 'action'/i, - 'category' => 'client' - ) - expect( body ).to be_nil - end - - - it "send an error response for unknown actions" do - badmsg = Arborist::TreeAPI.encode( :slap ) - sock.send( badmsg ) - resmsg = sock.recv - - hdr, body = Arborist::TreeAPI.decode( resmsg ) - expect( hdr ).to include( - 'success' => false, - 'reason' => /invalid request.*no such action 'slap'/i, - 'category' => 'client' - ) - expect( body ).to be_nil - end - end