#!/usr/bin/env rspec -cfd require_relative '../../spec_helper' describe Arborist::Manager::TreeAPI, :testing_manager 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.zmq_loop.running? || count > 30 sleep 0.1 Loggability[ Arborist ].info "ZMQ loop still running" count += 1 end raise "ZMQ Loop didn't stop" if @manager.zmq_loop.running? 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 = unpack_message( 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 = unpack_message( 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 = unpack_message( 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 = unpack_message( 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 = unpack_message( 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 = unpack_message( 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 = unpack_message( 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 = unpack_message( 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 = pack_message( :slap ) sock.send( badmsg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => false, 'reason' => /invalid request.*no such action 'slap'/i, 'category' => 'client' ) expect( body ).to be_nil end end describe "status" do it "returns a Map describing the manager and its state" do msg = pack_message( :status ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( 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 it "returns an array of full state maps for nodes matching specified criteria" do msg = pack_message( :fetch, type: 'service', port: 22 ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to be_a( Hash ) expect( body.length ).to eq( 3 ) expect( body.values ).to all( be_a(Hash) ) 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 = pack_message( :fetch, [ {}, {type: 'service', port: 22} ] ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to be_a( Hash ) expect( body.length ).to eq( manager.nodes.length - 3 ) expect( body.values ).to all( be_a(Hash) ) 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 = pack_message( :fetch, [ {type: 'service'}, {port: 22} ] ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to be_a( Hash ) expect( body.length ).to eq( 16 ) 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 manager.nodes['sidonie'].update( error: 'sunspots' ) msg = pack_message( :fetch, type: 'service', port: 22 ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( 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 manager.nodes['sidonie'].update( error: 'plague of locusts' ) msg = pack_message( :fetch, {include_down: true}, type: 'service', port: 22 ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( 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 = pack_message( :fetch, {return: nil}, type: 'service', port: 22 ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( 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' ) 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 = pack_message( :fetch, {return: %w[status tags addresses]}, type: 'service', port: 22 ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body.length ).to eq( 3 ) expect( body ).to include( 'duir-ssh', 'yevaud-ssh', 'sidonie-ssh' ) expect( body.values.map(&:keys) ).to all( contain_exactly('status', 'tags', 'addresses') ) end end describe "list" do it "returns an array of node state" do msg = pack_message( :list ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body.length ).to eq( manager.nodes.length ) 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 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 include( hash_including('identifier' => 'yevaud') ) end it "can be limited by depth" do msg = pack_message( :list, {depth: 1}, nil ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( 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 end describe "update" do it "merges the properties sent with those of the targeted nodes" do update_data = { duir: { ping: { rtt: 254 } }, sidonie: { ping: { rtt: 1208 } }, yevaud: { ping: { rtt: 843 } } } msg = pack_message( :update, update_data ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to be_nil expect( manager.nodes['duir'].properties['ping'] ).to include( 'rtt' => 254 ) expect( manager.nodes['sidonie'].properties['ping'] ).to include( 'rtt' => 1208 ) expect( manager.nodes['yevaud'].properties['ping'] ).to include( 'rtt' => 843 ) end it "ignores unknown identifiers" do msg = pack_message( :update, charlie_humperton: {ping: { rtt: 8 }} ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => true ) end it "fails with a client error if the body is invalid" do msg = pack_message( :update, nil ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => false ) expect( hdr['reason'] ).to match( /respond to #each/ ) end end describe "subscribe" do it "adds a subscription for all event types to the root node by default" do msg = pack_message( :subscribe, [{}, {}] ) resmsg = nil expect { sock.send( msg ) resmsg = sock.recv }.to change { manager.subscriptions.length }.by( 1 ).and( change { manager.root.subscriptions.length }.by( 1 ) ) hdr, body = unpack_message( resmsg ) sub_id = manager.subscriptions.keys.first expect( hdr ).to include( 'success' => true ) expect( body ).to eq([ sub_id ]) end it "adds a subscription to the specified node if an identifier is specified" do msg = pack_message( :subscribe, {identifier: 'sidonie'}, [{}, {}] ) resmsg = nil expect { sock.send( msg ) resmsg = sock.recv }.to change { manager.subscriptions.length }.by( 1 ).and( change { manager.nodes['sidonie'].subscriptions.length }.by( 1 ) ) hdr, body = unpack_message( resmsg ) sub_id = manager.subscriptions.keys.first expect( hdr ).to include( 'success' => true ) expect( body ).to eq([ sub_id ]) end it "adds a subscription for particular event types if one is specified" do msg = pack_message( :subscribe, {event_type: 'node.acked'}, [{}, {}] ) resmsg = nil expect { sock.send( msg ) resmsg = sock.recv }.to change { manager.subscriptions.length }.by( 1 ).and( change { manager.root.subscriptions.length }.by( 1 ) ) hdr, body = unpack_message( resmsg ) node = manager.subscriptions[ body.first ] sub = node.subscriptions[ body.first ] 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 = pack_message( :subscribe, [criteria, {}] ) resmsg = nil expect { sock.send( msg ) resmsg = sock.recv }.to change { manager.subscriptions.length }.by( 1 ).and( change { manager.root.subscriptions.length }.by( 1 ) ) hdr, body = unpack_message( resmsg ) node = manager.subscriptions[ body.first ] sub = node.subscriptions[ body.first ] 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 = pack_message( :subscribe, [{}, criteria] ) resmsg = nil expect { sock.send( msg ) resmsg = sock.recv }.to change { manager.subscriptions.length }.by( 1 ).and( change { manager.root.subscriptions.length }.by( 1 ) ) hdr, body = unpack_message( resmsg ) node = manager.subscriptions[ body.first ] sub = node.subscriptions[ body.first ] expect( sub.event_type ).to be_nil expect( sub.negative_criteria ).to eq({ 'type' => 'host' }) end end describe "unsubscribe" do let( :subscription ) do manager.create_subscription( nil, 'node.delta', {type: 'host'} ) end it "removes the subscription with the specified ID" do msg = pack_message( :unsubscribe, {subscription_id: subscription.id}, nil ) resmsg = nil expect { sock.send( msg ) resmsg = sock.recv }.to change { manager.subscriptions.length }.by( -1 ).and( change { manager.root.subscriptions.length }.by( -1 ) ) hdr, body = unpack_message( resmsg ) expect( body ).to include( 'event_type' => 'node.delta', 'criteria' => {'type' => 'host'} ) end it "ignores unsubscription of a non-existant ID" do msg = pack_message( :unsubscribe, {subscription_id: 'the bears!'}, nil ) resmsg = nil expect { sock.send( msg ) resmsg = sock.recv }.to_not change { manager.subscriptions.length } hdr, body = unpack_message( resmsg ) expect( body ).to be_nil end end describe "prune" do it "removes a single node" do msg = pack_message( :prune, {identifier: 'duir-ssh'}, nil ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to eq( true ) expect( manager.nodes ).to_not include( 'duir-ssh' ) end it "returns Nil without error if the node to prune didn't exist" do msg = pack_message( :prune, {identifier: 'shemp-ssh'}, nil ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to be_nil end it "removes children nodes along with the parent" do msg = pack_message( :prune, {identifier: 'duir'}, nil ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to eq( true ) 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 = pack_message( :prune ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => false ) expect( hdr['reason'] ).to match( /no identifier/i ) end end describe "graft" do it "can add a node with no explicit parent" do header = { identifier: 'guenter', type: 'host', } attributes = { description: 'The evil penguin node of doom.', addresses: ['10.2.66.8'], tags: ['internal', 'football'] } msg = pack_message( :graft, header, attributes ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to eq( '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] ) expect( new_node.addresses ).to eq([ IPAddr.new(attributes[:addresses].first) ]) expect( new_node.tags ).to include( *attributes[:tags] ) end it "can add a node with a parent specified" do header = { identifier: 'orgalorg', type: 'host', parent: 'duir' } attributes = { description: 'The true form of the evil penguin node of doom.', addresses: ['192.168.22.8'], tags: ['evil', 'space', 'entity'] } msg = pack_message( :graft, header, attributes ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to eq( '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] ) expect( new_node.description ).to eq( attributes[:description] ) expect( new_node.addresses ).to eq([ IPAddr.new(attributes[:addresses].first) ]) expect( new_node.tags ).to include( *attributes[:tags] ) end it "can add a subordinate node" do header = { identifier: 'echo', type: 'service', parent: 'duir' } attributes = { description: 'Mmmmm AppleTalk.' } msg = pack_message( :graft, header, attributes ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => true ) expect( body ).to eq( '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] ) expect( new_node.description ).to eq( attributes[:description] ) expect( new_node.port ).to eq( 7 ) expect( new_node.protocol ).to eq( 'tcp' ) expect( new_node.app_protocol ).to eq( 'echo' ) end it "errors if adding a subordinate node with no parent" do header = { identifier: 'echo', type: 'service' } attributes = { description: 'Mmmmm AppleTalk.' } msg = pack_message( :graft, header, attributes ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => false ) expect( hdr['reason'] ).to match( /no host given/i ) end end describe "modify" do it "can change operational attributes of a node" do header = { identifier: 'sidonie', } attributes = { parent: '_', addresses: ['192.168.32.32', '10.2.2.28'] } msg = pack_message( :modify, header, attributes ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => true ) node = manager.nodes[ 'sidonie' ] expect( node.addresses ).to eq( [IPAddr.new('192.168.32.32'), IPAddr.new('10.2.2.28')] ) expect( node.parent ).to eq( '_' ) end it "ignores modifications to unsupported attributes" do header = { identifier: 'sidonie', } attributes = { identifier: 'somethingelse' } msg = pack_message( :modify, header, attributes ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => true ) expect( manager.nodes['sidonie'] ).to be_an( Arborist::Node ) expect( manager.nodes['sidonie'].identifier ).to eq( 'sidonie' ) end it "errors on modifications to the root node" do header = { identifier: '_', } attributes = { identifier: 'somethingelse' } msg = pack_message( :modify, header, attributes ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => false ) expect( manager.nodes['_'].identifier ).to eq( '_' ) end it "errors on modifications to nonexistent nodes" do header = { identifier: 'nopenopenope', } attributes = { identifier: 'somethingelse' } msg = pack_message( :modify, header, attributes ) sock.send( msg ) resmsg = sock.recv hdr, body = unpack_message( resmsg ) expect( hdr ).to include( 'success' => false ) end end end